diff --git a/accessible/jsat/AccessFu.jsm b/accessible/jsat/AccessFu.jsm
index d87962ac3d85..cba9ee07ce95 100644
--- a/accessible/jsat/AccessFu.jsm
+++ b/accessible/jsat/AccessFu.jsm
@@ -37,6 +37,16 @@ this.AccessFu = { // jshint ignore:line
Services.obs.addObserver(this, 'Accessibility:Settings', false);
} catch (x) {
// Not on Android
+ if (aWindow.navigator.mozSettings) {
+ let lock = aWindow.navigator.mozSettings.createLock();
+ let req = lock.get(SCREENREADER_SETTING);
+ req.addEventListener('success', () => {
+ this._systemPref = req.result[SCREENREADER_SETTING];
+ this._enableOrDisable();
+ });
+ aWindow.navigator.mozSettings.addObserver(
+ SCREENREADER_SETTING, this.handleEvent);
+ }
}
this._activatePref = new PrefCache(
@@ -55,6 +65,9 @@ this.AccessFu = { // jshint ignore:line
}
if (Utils.MozBuildApp === 'mobile/android') {
Services.obs.removeObserver(this, 'Accessibility:Settings');
+ } else if (Utils.win.navigator.mozSettings) {
+ Utils.win.navigator.mozSettings.removeObserver(
+ SCREENREADER_SETTING, this.handleEvent);
}
delete this._activatePref;
Utils.uninit();
diff --git a/accessible/jsat/Utils.jsm b/accessible/jsat/Utils.jsm
index 61a68eb28b67..4e478cab0d85 100644
--- a/accessible/jsat/Utils.jsm
+++ b/accessible/jsat/Utils.jsm
@@ -4,7 +4,7 @@
/* global Components, XPCOMUtils, Services, PluralForm, Logger, Rect, Utils,
States, Relations, Roles, dump, Events, PivotContext, PrefCache */
-/* exported Utils, Logger, PivotContext, PrefCache */
+/* exported Utils, Logger, PivotContext, PrefCache, SettingCache */
'use strict';
@@ -26,7 +26,8 @@ XPCOMUtils.defineLazyModuleGetter(this, 'States', // jshint ignore:line
XPCOMUtils.defineLazyModuleGetter(this, 'PluralForm', // jshint ignore:line
'resource://gre/modules/PluralForm.jsm');
-this.EXPORTED_SYMBOLS = ['Utils', 'Logger', 'PivotContext', 'PrefCache']; // jshint ignore:line
+this.EXPORTED_SYMBOLS = ['Utils', 'Logger', 'PivotContext', 'PrefCache', // jshint ignore:line
+ 'SettingCache'];
this.Utils = { // jshint ignore:line
_buildAppMap: {
@@ -1073,3 +1074,41 @@ PrefCache.prototype = {
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();
+ });
+};
diff --git a/accessible/tests/mochitest/jsat/jsatcommon.js b/accessible/tests/mochitest/jsat/jsatcommon.js
index 43d754afa915..aa7ee74e400b 100644
--- a/accessible/tests/mochitest/jsat/jsatcommon.js
+++ b/accessible/tests/mochitest/jsat/jsatcommon.js
@@ -149,7 +149,8 @@ var AccessFuTest = {
Logger.logLevel = Logger.DEBUG;
};
- var prefs = [['accessibility.accessfu.notify_output', 1]];
+ var prefs = [['accessibility.accessfu.notify_output', 1],
+ ['dom.mozSettings.enabled', true]];
prefs.push.apply(prefs, aAdditionalPrefs);
this.originalDwellThreshold = GestureSettings.dwellThreshold;
diff --git a/addon-sdk/source/python-lib/cuddlefish/prefs.py b/addon-sdk/source/python-lib/cuddlefish/prefs.py
index c7208b7d732d..bb087b7f5eaf 100644
--- a/addon-sdk/source/python-lib/cuddlefish/prefs.py
+++ b/addon-sdk/source/python-lib/cuddlefish/prefs.py
@@ -216,6 +216,7 @@ DEFAULT_TEST_PREFS = {
'layout.css.report_errors': True,
'layout.css.grid.enabled': True,
'layout.spammy_warnings.enabled': False,
+ 'dom.mozSettings.enabled': True,
# Make sure the disk cache doesn't get auto disabled
'network.http.bypass-cachelock-threshold': 200000,
# Always use network provider for geolocation tests
diff --git a/addon-sdk/source/test/preferences/test.json b/addon-sdk/source/test/preferences/test.json
index be96488b374a..d34061fb88b4 100644
--- a/addon-sdk/source/test/preferences/test.json
+++ b/addon-sdk/source/test/preferences/test.json
@@ -31,6 +31,7 @@
"layout.css.report_errors": true,
"layout.css.grid.enabled": true,
"layout.spammy_warnings.enabled": false,
+ "dom.mozSettings.enabled": true,
"network.http.bypass-cachelock-threshold": 200000,
"geo.provider.testing": true,
"browser.pagethumbnails.capturing_disabled": true,
diff --git a/b2g/app/b2g.js b/b2g/app/b2g.js
index 375abb5c4720..ba79ad2713a1 100644
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -278,6 +278,7 @@ pref("ui.threedlightshadow", "#ece7e2");
pref("ui.threedshadow", "#aea194");
pref("ui.windowframe", "#efebe7");
+// Themable via mozSettings
pref("ui.menu", "#f97c17");
pref("ui.menutext", "#ffffff");
pref("ui.infobackground", "#343e40");
@@ -408,6 +409,9 @@ pref("dom.webapps.firstRunWithSIM", true);
pref("dom.mozApps.single_variant_sourcedir", "/persist/svoperapps");
#endif
+// WebSettings
+pref("dom.mozSettings.enabled", true);
+
// controls if we want camera support
pref("device.camera.enabled", true);
pref("media.realtime_decoder.enabled", true);
@@ -912,6 +916,27 @@ pref("dom.mapped_arraybuffer.enabled", true);
// UDPSocket API
pref("dom.udpsocket.enabled", true);
+// Enable TV Manager API
+pref("dom.tv.enabled", true);
+
+// Enable Inputport Manager API
+pref("dom.inputport.enabled", true);
+
+pref("dom.mozSettings.SettingsDB.debug.enabled", true);
+pref("dom.mozSettings.SettingsManager.debug.enabled", true);
+pref("dom.mozSettings.SettingsRequestManager.debug.enabled", true);
+pref("dom.mozSettings.SettingsService.debug.enabled", true);
+
+pref("dom.mozSettings.SettingsDB.verbose.enabled", false);
+pref("dom.mozSettings.SettingsManager.verbose.enabled", false);
+pref("dom.mozSettings.SettingsRequestManager.verbose.enabled", false);
+pref("dom.mozSettings.SettingsService.verbose.enabled", false);
+
+// Controlling whether we want to allow forcing some Settings
+// IndexedDB transactions to be opened as readonly or keep everything as
+// readwrite.
+pref("dom.mozSettings.allowForceReadOnly", false);
+
// Comma separated list of activity names that can only be provided by
// the system app in dev mode.
pref("dom.activities.developer_mode_only", "import-app");
diff --git a/b2g/chrome/content/desktop.js b/b2g/chrome/content/desktop.js
index 83b443e21936..5a1e7ff04727 100644
--- a/b2g/chrome/content/desktop.js
+++ b/b2g/chrome/content/desktop.js
@@ -111,6 +111,8 @@ function checkDebuggerPort() {
if (dbgport) {
dump('Opening debugger server on ' + dbgport + '\n');
Services.prefs.setCharPref('devtools.debugger.unix-domain-socket', dbgport);
+ navigator.mozSettings.createLock().set(
+ {'debugger.remote-mode': 'adb-devtools'});
}
}
diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in
index ac23cceee7a4..2282de2b20bf 100644
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -207,6 +207,7 @@
@RESPATH@/components/dom_quota.xpt
@RESPATH@/components/dom_range.xpt
@RESPATH@/components/dom_security.xpt
+@RESPATH@/components/dom_settings.xpt
@RESPATH@/components/dom_sidebar.xpt
@RESPATH@/components/dom_storage.xpt
@RESPATH@/components/dom_stylesheets.xpt
@@ -501,6 +502,8 @@
@RESPATH@/components/XULStore.manifest
@RESPATH@/components/messageWakeupService.js
@RESPATH@/components/messageWakeupService.manifest
+@RESPATH@/components/SettingsManager.js
+@RESPATH@/components/SettingsManager.manifest
@RESPATH@/components/recording-cmdline.js
@RESPATH@/components/recording-cmdline.manifest
@RESPATH@/components/htmlMenuBuilder.js
diff --git a/devtools/client/webide/content/devicesettings.js b/devtools/client/webide/content/devicesettings.js
new file mode 100644
index 000000000000..987df5995b26
--- /dev/null
+++ b/devtools/client/webide/content/devicesettings.js
@@ -0,0 +1,81 @@
+/* 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/. */
+
+var Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {AppManager} = require("devtools/client/webide/modules/app-manager");
+const {Connection} = require("devtools/shared/client/connection-manager");
+const ConfigView = require("devtools/client/webide/modules/config-view");
+
+var configView = new ConfigView(window);
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad);
+ AppManager.on("app-manager-update", OnAppManagerUpdate);
+ document.getElementById("close").onclick = CloseUI;
+ document.getElementById("device-fields").onchange = UpdateField;
+ document.getElementById("device-fields").onclick = CheckReset;
+ document.getElementById("search-bar").onkeyup = document.getElementById("search-bar").onclick = SearchField;
+ document.getElementById("custom-value").onclick = UpdateNewField;
+ document.getElementById("custom-value-type").onchange = ClearNewFields;
+ document.getElementById("add-custom-field").onkeyup = CheckNewFieldSubmit;
+ BuildUI();
+}, true);
+
+window.addEventListener("unload", function onUnload() {
+ window.removeEventListener("unload", onUnload);
+ AppManager.off("app-manager-update", OnAppManagerUpdate);
+});
+
+function CloseUI() {
+ window.parent.UI.openProject();
+}
+
+function OnAppManagerUpdate(event, what) {
+ if (what == "connection" || what == "runtime-global-actors") {
+ BuildUI();
+ }
+}
+
+function CheckNewFieldSubmit(event) {
+ configView.checkNewFieldSubmit(event);
+}
+
+function UpdateNewField() {
+ configView.updateNewField();
+}
+
+function ClearNewFields() {
+ configView.clearNewFields();
+}
+
+function CheckReset(event) {
+ configView.checkReset(event);
+}
+
+function UpdateField(event) {
+ configView.updateField(event);
+}
+
+function SearchField(event) {
+ configView.search(event);
+}
+
+var getAllSettings; // Used by tests
+function BuildUI() {
+ configView.resetTable();
+
+ if (AppManager.connection &&
+ AppManager.connection.status == Connection.Status.CONNECTED &&
+ AppManager.settingsFront) {
+ configView.front = AppManager.settingsFront;
+ configView.kind = "Setting";
+ configView.includeTypeName = false;
+
+ getAllSettings = AppManager.settingsFront.getAllSettings()
+ .then(json => configView.generateDisplay(json));
+ } else {
+ CloseUI();
+ }
+}
diff --git a/devtools/client/webide/content/devicesettings.xhtml b/devtools/client/webide/content/devicesettings.xhtml
new file mode 100644
index 000000000000..0406c6f076fa
--- /dev/null
+++ b/devtools/client/webide/content/devicesettings.xhtml
@@ -0,0 +1,50 @@
+
+
+
+
+
+ %webideDTD;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/devtools/client/webide/content/jar.mn b/devtools/client/webide/content/jar.mn
index 81da906f7859..73b7b62097a2 100644
--- a/devtools/client/webide/content/jar.mn
+++ b/devtools/client/webide/content/jar.mn
@@ -20,6 +20,8 @@ webide.jar:
content/monitor.js (monitor.js)
content/devicepreferences.js (devicepreferences.js)
content/devicepreferences.xhtml (devicepreferences.xhtml)
+ content/devicesettings.js (devicesettings.js)
+ content/devicesettings.xhtml (devicesettings.xhtml)
content/wifi-auth.js (wifi-auth.js)
content/wifi-auth.xhtml (wifi-auth.xhtml)
content/logs.xhtml (logs.xhtml)
diff --git a/devtools/client/webide/content/webide.js b/devtools/client/webide/content/webide.js
index 384c82e60d37..5df2d09a9f30 100644
--- a/devtools/client/webide/content/webide.js
+++ b/devtools/client/webide/content/webide.js
@@ -1052,6 +1052,10 @@ var Cmds = {
UI.selectDeckPanel("devicepreferences");
},
+ showSettings: function () {
+ UI.selectDeckPanel("devicesettings");
+ },
+
showMonitor: function () {
UI.selectDeckPanel("monitor");
},
diff --git a/devtools/client/webide/content/webide.xul b/devtools/client/webide/content/webide.xul
index 4c6ca53b3319..8c9f5d31b10a 100644
--- a/devtools/client/webide/content/webide.xul
+++ b/devtools/client/webide/content/webide.xul
@@ -157,6 +157,7 @@
+
diff --git a/devtools/client/webide/test/chrome.ini b/devtools/client/webide/test/chrome.ini
index e673b4bfaf23..ec289edccf69 100644
--- a/devtools/client/webide/test/chrome.ini
+++ b/devtools/client/webide/test/chrome.ini
@@ -60,6 +60,7 @@ skip-if = true # Bug 1201392 - Update add-ons after migration
[test_telemetry.html]
skip-if = true # Bug 1201392 - Update add-ons after migration
[test_device_preferences.html]
+[test_device_settings.html]
[test_fullscreenToolbox.html]
[test_zoom.html]
[test_build.html]
diff --git a/devtools/client/webide/test/test_device_settings.html b/devtools/client/webide/test/test_device_settings.html
new file mode 100644
index 000000000000..ec8e7943b47d
--- /dev/null
+++ b/devtools/client/webide/test/test_device_settings.html
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/devtools/server/actors/settings.js b/devtools/server/actors/settings.js
new file mode 100644
index 000000000000..179c82aa5fd9
--- /dev/null
+++ b/devtools/server/actors/settings.js
@@ -0,0 +1,146 @@
+/* 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/. */
+
+"use strict";
+
+const {Cc, Ci} = require("chrome");
+const protocol = require("devtools/shared/protocol");
+const {DebuggerServer} = require("devtools/server/main");
+const promise = require("promise");
+const Services = require("Services");
+const { settingsSpec } = require("devtools/shared/specs/settings");
+const { FileUtils} = require("resource://gre/modules/FileUtils.jsm");
+const { NetUtil} = require("resource://gre/modules/NetUtil.jsm");
+
+var defaultSettings = {};
+var settingsFile;
+
+exports.register = function (handle) {
+ handle.addGlobalActor(SettingsActor, "settingsActor");
+};
+
+exports.unregister = function (handle) {
+};
+
+function getDefaultSettings() {
+ let chan = NetUtil.newChannel({
+ uri: NetUtil.newURI(settingsFile),
+ loadUsingSystemPrincipal: true});
+ let stream = chan.open2();
+ // Obtain a converter to read from a UTF-8 encoded input stream.
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ let rawstr = converter.ConvertToUnicode(NetUtil.readInputStreamToString(
+ stream,
+ stream.available()) || "");
+
+ try {
+ defaultSettings = JSON.parse(rawstr);
+ } catch (e) { }
+ stream.close();
+}
+
+function loadSettingsFile() {
+ // Loading resource://app/defaults/settings.json doesn't work because
+ // settings.json is not in the omnijar.
+ // So we look for the app dir instead and go from here...
+ if (settingsFile) {
+ return;
+ }
+ settingsFile = FileUtils.getFile("DefRt", ["settings.json"], false);
+ if (!settingsFile || (settingsFile && !settingsFile.exists())) {
+ // On b2g desktop builds the settings.json file is moved in the
+ // profile directory by the build system.
+ settingsFile = FileUtils.getFile("ProfD", ["settings.json"], false);
+ if (!settingsFile || (settingsFile && !settingsFile.exists())) {
+ console.log("settings.json file does not exist");
+ }
+ }
+
+ if (settingsFile.exists()) {
+ getDefaultSettings();
+ }
+}
+
+var SettingsActor = exports.SettingsActor = protocol.ActorClassWithSpec(settingsSpec, {
+ _getSettingsService: function () {
+ let win = Services.wm.getMostRecentWindow(DebuggerServer.chromeWindowType);
+ return win.navigator.mozSettings;
+ },
+
+ getSetting: function (name) {
+ let deferred = promise.defer();
+ let lock = this._getSettingsService().createLock();
+ let req = lock.get(name);
+ req.onsuccess = function () {
+ deferred.resolve(req.result[name]);
+ };
+ req.onerror = function () {
+ deferred.reject(req.error);
+ };
+ return deferred.promise;
+ },
+
+ setSetting: function (name, value) {
+ let deferred = promise.defer();
+ let data = {};
+ data[name] = value;
+ let lock = this._getSettingsService().createLock();
+ let req = lock.set(data);
+ req.onsuccess = function () {
+ deferred.resolve(true);
+ };
+ req.onerror = function () {
+ deferred.reject(req.error);
+ };
+ return deferred.promise;
+ },
+
+ _hasUserSetting: function (name, value) {
+ if (typeof value === "object") {
+ return JSON.stringify(defaultSettings[name]) !== JSON.stringify(value);
+ }
+ return (defaultSettings[name] !== value);
+ },
+
+ getAllSettings: function () {
+ loadSettingsFile();
+ let settings = {};
+ let self = this;
+
+ let deferred = promise.defer();
+ let lock = this._getSettingsService().createLock();
+ let req = lock.get("*");
+
+ req.onsuccess = function () {
+ for (var name in req.result) {
+ settings[name] = {
+ value: req.result[name],
+ hasUserValue: self._hasUserSetting(name, req.result[name])
+ };
+ }
+ deferred.resolve(settings);
+ };
+ req.onfailure = function () {
+ deferred.reject(req.error);
+ };
+
+ return deferred.promise;
+ },
+
+ clearUserSetting: function (name) {
+ loadSettingsFile();
+ try {
+ this.setSetting(name, defaultSettings[name]);
+ } catch (e) {
+ console.log(e);
+ }
+ }
+});
+
+// For tests
+exports._setDefaultSettings = function (settings) {
+ defaultSettings = settings || {};
+};
diff --git a/devtools/server/main.js b/devtools/server/main.js
index 5df2d4cbb717..55f2153506e0 100644
--- a/devtools/server/main.js
+++ b/devtools/server/main.js
@@ -433,6 +433,13 @@ var DebuggerServer = {
type: { global: true }
});
}
+ if (Services.prefs.getBoolPref("dom.mozSettings.enabled")) {
+ this.registerModule("devtools/server/actors/settings", {
+ prefix: "settings",
+ constructor: "SettingsActor",
+ type: { global: true }
+ });
+ }
this.registerModule("devtools/server/actors/addons", {
prefix: "addons",
constructor: "AddonsActor",
diff --git a/devtools/server/tests/mochitest/chrome.ini b/devtools/server/tests/mochitest/chrome.ini
index 3f097bb5ecc4..85cc75611b9c 100644
--- a/devtools/server/tests/mochitest/chrome.ini
+++ b/devtools/server/tests/mochitest/chrome.ini
@@ -89,6 +89,7 @@ support-files =
[test_memory_gc_01.html]
[test_memory_gc_events.html]
[test_preference.html]
+[test_settings.html]
[test_setupInParentChild.html]
[test_styles-applied.html]
[test_styles-computed.html]
diff --git a/devtools/server/tests/mochitest/test_settings.html b/devtools/server/tests/mochitest/test_settings.html
new file mode 100644
index 000000000000..5665b46b3cd6
--- /dev/null
+++ b/devtools/server/tests/mochitest/test_settings.html
@@ -0,0 +1,130 @@
+
+
+
+
+
+ Test Settings Actor
+
+
+
+
+
+
+
+
+
+
diff --git a/devtools/shared/discovery/discovery.js b/devtools/shared/discovery/discovery.js
index eb3893138546..a07f21d445e7 100644
--- a/devtools/shared/discovery/discovery.js
+++ b/devtools/shared/discovery/discovery.js
@@ -137,22 +137,45 @@ Transport.prototype = {
/**
* Manages the local device's name. The name can be generated in serveral
* platform-specific ways (see |_generate|). The aim is for each device on the
- * same local network to have a unique name.
+ * same local network to have a unique name. If the Settings API is available,
+ * the name is saved there to persist across reboots.
*/
function LocalDevice() {
this._name = LocalDevice.UNKNOWN;
+ if ("@mozilla.org/settingsService;1" in Cc) {
+ this._settings =
+ Cc["@mozilla.org/settingsService;1"].getService(Ci.nsISettingsService);
+ Services.obs.addObserver(this, "mozsettings-changed", false);
+ }
// Trigger |_get| to load name eagerly
this._get();
}
+LocalDevice.SETTING = "devtools.discovery.device";
LocalDevice.UNKNOWN = "unknown";
LocalDevice.prototype = {
_get: function () {
- // Without Settings API, just generate a name and stop, since the value
- // can't be persisted.
- this._generate();
+ if (!this._settings) {
+ // Without Settings API, just generate a name and stop, since the value
+ // can't be persisted.
+ this._generate();
+ return;
+ }
+ // Initial read of setting value
+ this._settings.createLock().get(LocalDevice.SETTING, {
+ handle: (_, name) => {
+ if (name && name !== LocalDevice.UNKNOWN) {
+ this._name = name;
+ log("Device: " + this._name);
+ return;
+ }
+ // No existing name saved, so generate one.
+ this._generate();
+ },
+ handleError: () => log("Failed to get device name setting")
+ });
},
/**
@@ -180,13 +203,39 @@ LocalDevice.prototype = {
}
},
+ /**
+ * Observe any changes that might be made via the Settings app
+ */
+ observe: function (subject, topic, data) {
+ if (topic !== "mozsettings-changed") {
+ return;
+ }
+ if ("wrappedJSObject" in subject) {
+ subject = subject.wrappedJSObject;
+ }
+ if (subject.key !== LocalDevice.SETTING) {
+ return;
+ }
+ this._name = subject.value;
+ log("Device: " + this._name);
+ },
+
get name() {
return this._name;
},
set name(name) {
- this._name = name;
- log("Device: " + this._name);
+ if (!this._settings) {
+ this._name = name;
+ log("Device: " + this._name);
+ return;
+ }
+ // Persist to Settings API
+ // The new value will be seen and stored by the observer above
+ this._settings.createLock().set(LocalDevice.SETTING, name, {
+ handle: () => {},
+ handleError: () => log("Failed to set device name setting")
+ });
}
};
diff --git a/devtools/shared/fronts/settings.js b/devtools/shared/fronts/settings.js
new file mode 100644
index 000000000000..158425364e55
--- /dev/null
+++ b/devtools/shared/fronts/settings.js
@@ -0,0 +1,29 @@
+/* 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/. */
+"use strict";
+
+const {settingsSpec} = require("devtools/shared/specs/settings");
+const protocol = require("devtools/shared/protocol");
+
+const SettingsFront = protocol.FrontClassWithSpec(settingsSpec, {
+ initialize: function (client, form) {
+ protocol.Front.prototype.initialize.call(this, client);
+ this.actorID = form.settingsActor;
+ this.manage(this);
+ },
+});
+
+const _knownSettingsFronts = new WeakMap();
+
+exports.getSettingsFront = function (client, form) {
+ if (!form.settingsActor) {
+ return null;
+ }
+ if (_knownSettingsFronts.has(client)) {
+ return _knownSettingsFronts.get(client);
+ }
+ let front = new SettingsFront(client, form);
+ _knownSettingsFronts.set(client, front);
+ return front;
+};
diff --git a/devtools/shared/specs/settings.js b/devtools/shared/specs/settings.js
new file mode 100644
index 000000000000..482c8f473c06
--- /dev/null
+++ b/devtools/shared/specs/settings.js
@@ -0,0 +1,31 @@
+/* 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/. */
+"use strict";
+
+const {Arg, RetVal, generateActorSpec} = require("devtools/shared/protocol");
+
+const settingsSpec = generateActorSpec({
+ typeName: "settings",
+
+ methods: {
+ getSetting: {
+ request: { value: Arg(0) },
+ response: { value: RetVal("json") }
+ },
+ setSetting: {
+ request: { name: Arg(0), value: Arg(1) },
+ response: {}
+ },
+ getAllSettings: {
+ request: {},
+ response: { value: RetVal("json") }
+ },
+ clearUserSetting: {
+ request: { name: Arg(0) },
+ response: {}
+ }
+ },
+});
+
+exports.settingsSpec = settingsSpec;
diff --git a/dom/audiochannel/AudioChannelService.cpp b/dom/audiochannel/AudioChannelService.cpp
index aa29228cdee2..8f964453b533 100644
--- a/dom/audiochannel/AudioChannelService.cpp
+++ b/dom/audiochannel/AudioChannelService.cpp
@@ -25,6 +25,7 @@
#include "nsGlobalWindow.h"
#include "nsPIDOMWindow.h"
#include "nsServiceManagerUtils.h"
+#include "mozilla/dom/SettingChangeNotificationBinding.h"
#ifdef MOZ_WIDGET_GONK
#include "nsJSUtils.h"
@@ -234,6 +235,11 @@ AudioChannelService::Shutdown()
if (IsParentProcess()) {
obs->RemoveObserver(gAudioChannelService, "ipc:content-shutdown");
+
+#ifdef MOZ_WIDGET_GONK
+ // To monitor the volume settings based on audio channel.
+ obs->RemoveObserver(gAudioChannelService, "mozsettings-changed");
+#endif
}
}
@@ -273,6 +279,11 @@ AudioChannelService::AudioChannelService()
obs->AddObserver(this, "outer-window-destroyed", false);
if (IsParentProcess()) {
obs->AddObserver(this, "ipc:content-shutdown", false);
+
+#ifdef MOZ_WIDGET_GONK
+ // To monitor the volume settings based on audio channel.
+ obs->AddObserver(this, "mozsettings-changed", false);
+#endif
}
}
diff --git a/dom/events/moz.build b/dom/events/moz.build
index 685837c25b37..329cfae0c870 100644
--- a/dom/events/moz.build
+++ b/dom/events/moz.build
@@ -139,6 +139,7 @@ LOCAL_INCLUDES += [
'/docshell/base',
'/dom/base',
'/dom/html',
+ '/dom/settings',
'/dom/storage',
'/dom/svg',
'/dom/workers',
diff --git a/dom/events/test/test_all_synthetic_events.html b/dom/events/test/test_all_synthetic_events.html
index 6cf5a7123fca..c64d71778f21 100644
--- a/dom/events/test/test_all_synthetic_events.html
+++ b/dom/events/test/test_all_synthetic_events.html
@@ -259,6 +259,14 @@ const kEventConstructors = {
return new MozOtaStatusEvent(aName, aProps);
},
},
+ MozSettingsEvent: { create: function (aName, aProps) {
+ return new MozSettingsEvent(aName, aProps);
+ },
+ },
+ MozSettingsTransactionEvent: { create: function (aName, aProps) {
+ return new MozSettingsTransactionEvent(aName, aProps);
+ },
+ },
MozSmsEvent: { create: function (aName, aProps) {
return new MozSmsEvent(aName, aProps);
},
diff --git a/dom/media/tests/mochitest/NetworkPreparationChromeScript.js b/dom/media/tests/mochitest/NetworkPreparationChromeScript.js
index 5d2a653d1dc3..1de778778c6b 100644
--- a/dom/media/tests/mochitest/NetworkPreparationChromeScript.js
+++ b/dom/media/tests/mochitest/NetworkPreparationChromeScript.js
@@ -8,6 +8,21 @@ var browser = Services.wm.getMostRecentWindow('navigator:browser');
var connection = browser.navigator.mozMobileConnections[0];
// provide a fake APN and enable data connection.
+function enableDataConnection() {
+ let setLock = browser.navigator.mozSettings.createLock();
+ setLock.set({
+ 'ril.data.enabled': true,
+ 'ril.data.apnSettings': [
+ [
+ {'carrier':'T-Mobile US',
+ 'apn':'epc.tmobile.com',
+ 'mmsc':'http://mms.msg.eng.t-mobile.com/mms/wapenc',
+ 'types':['default','supl','mms']}
+ ]
+ ]
+ });
+}
+
// enable 3G radio
function enableRadio() {
if (connection.radioState !== 'enabled') {
@@ -32,6 +47,7 @@ addMessageListener('prepare-network', function(message) {
});
enableRadio();
+ enableDataConnection();
});
addMessageListener('network-cleanup', function(message) {
diff --git a/dom/moz.build b/dom/moz.build
index c2b0311ef319..c558d717812d 100644
--- a/dom/moz.build
+++ b/dom/moz.build
@@ -12,6 +12,7 @@ interfaces = [
'core',
'html',
'events',
+ 'settings',
'stylesheets',
'sidebar',
'css',
@@ -66,6 +67,7 @@ DIRS += [
'push',
'quota',
'security',
+ 'settings',
'storage',
'svg',
'time',
diff --git a/dom/settings/SettingsDB.jsm b/dom/settings/SettingsDB.jsm
new file mode 100644
index 000000000000..a1ea5e72b2c1
--- /dev/null
+++ b/dom/settings/SettingsDB.jsm
@@ -0,0 +1,249 @@
+/* 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/. */
+
+"use strict";
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+
+Cu.importGlobalProperties(['Blob', 'File']);
+Cu.import("resource://gre/modules/Services.jsm");
+
+this.EXPORTED_SYMBOLS = ["SettingsDB", "SETTINGSDB_NAME", "SETTINGSSTORE_NAME"];
+
+var DEBUG = false;
+var VERBOSE = false;
+
+try {
+ DEBUG =
+ Services.prefs.getBoolPref("dom.mozSettings.SettingsDB.debug.enabled");
+ VERBOSE =
+ Services.prefs.getBoolPref("dom.mozSettings.SettingsDB.verbose.enabled");
+} catch (ex) { }
+
+function debug(s) {
+ dump("-*- SettingsDB: " + s + "\n");
+}
+
+const TYPED_ARRAY_THINGS = new Set([
+ "Int8Array",
+ "Uint8Array",
+ "Uint8ClampedArray",
+ "Int16Array",
+ "Uint16Array",
+ "Int32Array",
+ "Uint32Array",
+ "Float32Array",
+ "Float64Array",
+]);
+
+this.SETTINGSDB_NAME = "settings";
+this.SETTINGSDB_VERSION = 8;
+this.SETTINGSSTORE_NAME = "settings";
+
+Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
+this.SettingsDB = function SettingsDB() {}
+
+SettingsDB.prototype = {
+
+ __proto__: IndexedDBHelper.prototype,
+
+ upgradeSchema: function upgradeSchema(aTransaction, aDb, aOldVersion, aNewVersion) {
+ let objectStore;
+ if (aOldVersion == 0) {
+ objectStore = aDb.createObjectStore(SETTINGSSTORE_NAME, { keyPath: "settingName" });
+ if (VERBOSE) debug("Created object stores");
+ } else if (aOldVersion == 1) {
+ if (VERBOSE) debug("Get object store for upgrade and remove old index");
+ objectStore = aTransaction.objectStore(SETTINGSSTORE_NAME);
+ objectStore.deleteIndex("settingValue");
+ } else {
+ if (VERBOSE) debug("Get object store for upgrade");
+ objectStore = aTransaction.objectStore(SETTINGSSTORE_NAME);
+ }
+
+ // Loading resource://app/defaults/settings.json doesn't work because
+ // settings.json is not in the omnijar.
+ // So we look for the app dir instead and go from here...
+ let settingsFile = FileUtils.getFile("DefRt", ["settings.json"], false);
+ if (!settingsFile || (settingsFile && !settingsFile.exists())) {
+ // On b2g desktop builds the settings.json file is moved in the
+ // profile directory by the build system.
+ settingsFile = FileUtils.getFile("ProfD", ["settings.json"], false);
+ if (!settingsFile || (settingsFile && !settingsFile.exists())) {
+ return;
+ }
+ }
+
+ let chan = NetUtil.newChannel({
+ uri: NetUtil.newURI(settingsFile),
+ loadUsingSystemPrincipal: true});
+ let stream = chan.open2();
+ // Obtain a converter to read from a UTF-8 encoded input stream.
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ let rawstr = converter.ConvertToUnicode(NetUtil.readInputStreamToString(
+ stream,
+ stream.available()) || "");
+ let settings;
+ try {
+ settings = JSON.parse(rawstr);
+ } catch(e) {
+ if (DEBUG) debug("Error parsing " + settingsFile.path + " : " + e);
+ return;
+ }
+ stream.close();
+
+ objectStore.openCursor().onsuccess = function(event) {
+ let cursor = event.target.result;
+ if (cursor) {
+ let value = cursor.value;
+ if (value.settingName in settings) {
+ if (VERBOSE) debug("Upgrade " +settings[value.settingName]);
+ value.defaultValue = this.prepareValue(settings[value.settingName]);
+ delete settings[value.settingName];
+ if ("settingValue" in value) {
+ value.userValue = this.prepareValue(value.settingValue);
+ delete value.settingValue;
+ }
+ cursor.update(value);
+ } else if ("userValue" in value || "settingValue" in value) {
+ value.defaultValue = undefined;
+ if (aOldVersion == 1 && value.settingValue) {
+ value.userValue = this.prepareValue(value.settingValue);
+ delete value.settingValue;
+ }
+ cursor.update(value);
+ } else {
+ cursor.delete();
+ }
+ cursor.continue();
+ } else {
+ for (let name in settings) {
+ let value = this.prepareValue(settings[name]);
+ if (VERBOSE) debug("Set new:" + name +", " + value);
+ objectStore.add({ settingName: name, defaultValue: value, userValue: undefined });
+ }
+ }
+ }.bind(this);
+ },
+
+ // If the value is a data: uri, convert it to a Blob.
+ convertDataURIToBlob: function(aValue) {
+ /* base64 to ArrayBuffer decoding, from
+ https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding
+ */
+ function b64ToUint6 (nChr) {
+ return nChr > 64 && nChr < 91 ?
+ nChr - 65
+ : nChr > 96 && nChr < 123 ?
+ nChr - 71
+ : nChr > 47 && nChr < 58 ?
+ nChr + 4
+ : nChr === 43 ?
+ 62
+ : nChr === 47 ?
+ 63
+ :
+ 0;
+ }
+
+ function base64DecToArr(sBase64, nBlocksSize) {
+ let sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""),
+ nInLen = sB64Enc.length,
+ nOutLen = nBlocksSize ? Math.ceil((nInLen * 3 + 1 >> 2) / nBlocksSize) * nBlocksSize
+ : nInLen * 3 + 1 >> 2,
+ taBytes = new Uint8Array(nOutLen);
+
+ for (let nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) {
+ nMod4 = nInIdx & 3;
+ nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4;
+ if (nMod4 === 3 || nInLen - nInIdx === 1) {
+ for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) {
+ taBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255;
+ }
+ nUint24 = 0;
+ }
+ }
+ return taBytes;
+ }
+
+ // Check if we have a data: uri, and if it's base64 encoded.
+ // data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEA...
+ if (typeof aValue == "string" && aValue.startsWith("data:")) {
+ try {
+ let uri = Services.io.newURI(aValue);
+ // XXX: that would be nice to reuse the c++ bits of the data:
+ // protocol handler instead.
+ let mimeType = "application/octet-stream";
+ let mimeDelim = aValue.indexOf(";");
+ if (mimeDelim !== -1) {
+ mimeType = aValue.substring(5, mimeDelim);
+ }
+ let start = aValue.indexOf(",") + 1;
+ let isBase64 = ((aValue.indexOf("base64") + 7) == start);
+ let payload = aValue.substring(start);
+
+ return new Blob([isBase64 ? base64DecToArr(payload) : payload],
+ { type: mimeType });
+ } catch(e) {
+ dump(e);
+ }
+ }
+ return aValue
+ },
+
+ getObjectKind: function(aObject) {
+ if (aObject === null || aObject === undefined) {
+ return "primitive";
+ } else if (Array.isArray(aObject)) {
+ return "array";
+ } else if (aObject instanceof File) {
+ return "file";
+ } else if (aObject instanceof Ci.nsIDOMBlob) {
+ return "blob";
+ } else if (aObject.constructor.name == "Date") {
+ return "date";
+ } else if (TYPED_ARRAY_THINGS.has(aObject.constructor.name)) {
+ return aObject.constructor.name;
+ } else if (typeof aObject == "object") {
+ return "object";
+ } else {
+ return "primitive";
+ }
+ },
+
+ // Makes sure any property that is a data: uri gets converted to a Blob.
+ prepareValue: function(aObject) {
+ let kind = this.getObjectKind(aObject);
+ if (kind == "array") {
+ let res = [];
+ aObject.forEach(function(aObj) {
+ res.push(this.prepareValue(aObj));
+ }, this);
+ return res;
+ } else if (kind == "file" || kind == "blob" || kind == "date") {
+ return aObject;
+ } else if (kind == "primitive") {
+ return this.convertDataURIToBlob(aObject);
+ }
+
+ // Fall-through, we now have a dictionary object.
+ let res = {};
+ for (let prop in aObject) {
+ res[prop] = this.prepareValue(aObject[prop]);
+ }
+ return res;
+ },
+
+ init: function init() {
+ this.initDBHelper(SETTINGSDB_NAME, SETTINGSDB_VERSION,
+ [SETTINGSSTORE_NAME]);
+ }
+}
diff --git a/dom/settings/SettingsManager.js b/dom/settings/SettingsManager.js
new file mode 100644
index 000000000000..a73e5f3127b8
--- /dev/null
+++ b/dom/settings/SettingsManager.js
@@ -0,0 +1,506 @@
+/* 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/. */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
+
+var DEBUG = false;
+var VERBOSE = false;
+
+try {
+ DEBUG =
+ Services.prefs.getBoolPref("dom.mozSettings.SettingsManager.debug.enabled");
+ VERBOSE =
+ Services.prefs.getBoolPref("dom.mozSettings.SettingsManager.verbose.enabled");
+} catch (ex) { }
+
+function debug(s) {
+ dump("-*- SettingsManager: " + s + "\n");
+}
+
+XPCOMUtils.defineLazyServiceGetter(Services, "DOMRequest",
+ "@mozilla.org/dom/dom-request-service;1",
+ "nsIDOMRequestService");
+XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
+ "@mozilla.org/childprocessmessagemanager;1",
+ "nsIMessageSender");
+XPCOMUtils.defineLazyServiceGetter(this, "mrm",
+ "@mozilla.org/memory-reporter-manager;1",
+ "nsIMemoryReporterManager");
+XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
+ "@mozilla.org/uuid-generator;1",
+ "nsIUUIDGenerator");
+
+const kObserverSoftLimit = 10;
+
+/**
+ * In order to make SettingsManager work with Privileged Apps, we need the lock
+ * to be OOP. However, the lock state needs to be managed on the child process,
+ * while the IDB functions now happen on the parent process so we don't have to
+ * expose IDB permissions at the child process level. We use the
+ * DOMRequestHelper mechanism to deal with DOMRequests/promises across the
+ * processes.
+ *
+ * However, due to the nature of the IDBTransaction lifetime, we need to relay
+ * to the parent when to finalize the transaction once the child is done with the
+ * lock. We keep a list of all open requests for a lock, and once the lock
+ * reaches the end of its receiveMessage function with no more queued requests,
+ * we consider it dead. At that point, we send a message to the parent to notify
+ * it to finalize the transaction.
+ */
+
+function SettingsLock(aSettingsManager) {
+ if (VERBOSE) debug("settings lock init");
+ this._open = true;
+ this._settingsManager = aSettingsManager;
+ this._id = uuidgen.generateUUID().toString();
+
+ // DOMRequestIpcHelper.initHelper sets this._window
+ this.initDOMRequestHelper(this._settingsManager._window, ["Settings:Get:OK", "Settings:Get:KO",
+ "Settings:Clear:OK", "Settings:Clear:KO",
+ "Settings:Set:OK", "Settings:Set:KO",
+ "Settings:Finalize:OK", "Settings:Finalize:KO"]);
+ let createLockPayload = {
+ lockID: this._id,
+ isServiceLock: false,
+ windowID: this._settingsManager.innerWindowID,
+ lockStack: (new Error).stack
+ };
+ this.sendMessage("Settings:CreateLock", createLockPayload);
+ Services.tm.currentThread.dispatch(this._closeHelper.bind(this), Ci.nsIThread.DISPATCH_NORMAL);
+
+ // We only want to file closeHelper once per set of receiveMessage calls.
+ this._closeCalled = true;
+}
+
+SettingsLock.prototype = {
+ __proto__: DOMRequestIpcHelper.prototype,
+ set onsettingstransactionsuccess(aHandler) {
+ this.__DOM_IMPL__.setEventHandler("onsettingstransactionsuccess", aHandler);
+ },
+
+ get onsettingstransactionsuccess() {
+ return this.__DOM_IMPL__.getEventHandler("onsettingstransactionsuccess");
+ },
+
+ set onsettingstransactionfailure(aHandler) {
+ this.__DOM_IMPL__.setEventHandler("onsettingstransactionfailure", aHandler);
+ },
+
+ get onsettingstransactionfailure() {
+ return this.__DOM_IMPL__.getEventHandler("onsettingstransactionfailure");
+ },
+
+ get closed() {
+ return !this._open;
+ },
+
+ _closeHelper: function() {
+ if (VERBOSE) debug("closing lock " + this._id);
+ this._open = false;
+ this._closeCalled = false;
+ if (!this._requests || Object.keys(this._requests).length == 0) {
+ if (VERBOSE) debug("Requests exhausted, finalizing " + this._id);
+ this._settingsManager.unregisterLock(this._id);
+ this.sendMessage("Settings:Finalize", {lockID: this._id});
+ } else {
+ if (VERBOSE) debug("Requests left: " + Object.keys(this._requests).length);
+ this.sendMessage("Settings:Run", {lockID: this._id});
+ }
+ },
+
+
+ _wrap: function _wrap(obj) {
+ return Cu.cloneInto(obj, this._settingsManager._window);
+ },
+
+ sendMessage: function(aMessageName, aData) {
+ // sendMessage can be called after our window has died, or get
+ // queued to run later in a thread via _closeHelper, but the
+ // SettingsManager may have died in between the time it was
+ // scheduled and the time it runs. Make sure our window is valid
+ // before sending, otherwise just ignore.
+ if (!this._settingsManager._window) {
+ Cu.reportError(
+ "SettingsManager window died, cannot run settings transaction." +
+ " SettingsMessage: " + aMessageName +
+ " SettingsData: " + JSON.stringify(aData));
+ return;
+ }
+ cpmm.sendAsyncMessage(aMessageName,
+ aData,
+ undefined,
+ this._settingsManager._window.document.nodePrincipal);
+ },
+
+ receiveMessage: function(aMessage) {
+ let msg = aMessage.data;
+
+ // SettingsRequestManager broadcasts changes to all locks in the child. If
+ // our lock isn't being addressed, just return.
+ if (msg.lockID != this._id) {
+ return;
+ }
+ if (VERBOSE) debug("receiveMessage (" + this._id + "): " + aMessage.name);
+
+ // Finalizing a transaction does not return a request ID since we are
+ // supposed to fire callbacks.
+ //
+ // We also destroy the DOMRequestHelper after we've received the
+ // finalize message. At this point, we will be guarenteed no more
+ // request returns are coming from the SettingsRequestManager.
+
+ if (!msg.requestID) {
+ let event;
+ switch (aMessage.name) {
+ case "Settings:Finalize:OK":
+ if (VERBOSE) debug("Lock finalize ok: " + this._id);
+ event = new this._window.MozSettingsTransactionEvent("settingstransactionsuccess", {});
+ this.__DOM_IMPL__.dispatchEvent(event);
+ this.destroyDOMRequestHelper();
+ break;
+ case "Settings:Finalize:KO":
+ if (DEBUG) debug("Lock finalize failed: " + this._id);
+ event = new this._window.MozSettingsTransactionEvent("settingstransactionfailure", {
+ error: msg.errorMsg
+ });
+ this.__DOM_IMPL__.dispatchEvent(event);
+ this.destroyDOMRequestHelper();
+ break;
+ default:
+ if (DEBUG) debug("Message type " + aMessage.name + " is missing a requestID");
+ }
+ return;
+ }
+
+
+ let req = this.getRequest(msg.requestID);
+ if (!req) {
+ if (DEBUG) debug("Matching request not found.");
+ return;
+ }
+ this.removeRequest(msg.requestID);
+ // DOMRequest callbacks called from here can die due to having
+ // things like marionetteScriptFinished in them. Make sure we file
+ // our call to run/finalize BEFORE opening the lock and fulfilling
+ // DOMRequests.
+ if (!this._closeCalled) {
+ // We only want to file closeHelper once per set of receiveMessage calls.
+ Services.tm.currentThread.dispatch(this._closeHelper.bind(this), Ci.nsIThread.DISPATCH_NORMAL);
+ this._closeCalled = true;
+ }
+ if (VERBOSE) debug("receiveMessage: " + aMessage.name);
+ switch (aMessage.name) {
+ case "Settings:Get:OK":
+ for (let i in msg.settings) {
+ msg.settings[i] = this._wrap(msg.settings[i]);
+ }
+ this._open = true;
+ Services.DOMRequest.fireSuccess(req.request, this._wrap(msg.settings));
+ this._open = false;
+ break;
+ case "Settings:Set:OK":
+ case "Settings:Clear:OK":
+ this._open = true;
+ Services.DOMRequest.fireSuccess(req.request, 0);
+ this._open = false;
+ break;
+ case "Settings:Get:KO":
+ case "Settings:Set:KO":
+ case "Settings:Clear:KO":
+ if (DEBUG) debug("error:" + msg.errorMsg);
+ Services.DOMRequest.fireError(req.request, msg.errorMsg);
+ break;
+ default:
+ if (DEBUG) debug("Wrong message: " + aMessage.name);
+ }
+ },
+
+ get: function get(aName) {
+ if (VERBOSE) debug("get (" + this._id + "): " + aName);
+ if (!this._open) {
+ dump("Settings lock not open!\n");
+ throw Components.results.NS_ERROR_ABORT;
+ }
+ let req = this.createRequest();
+ let reqID = this.getRequestId({request: req});
+ this.sendMessage("Settings:Get", {requestID: reqID,
+ lockID: this._id,
+ name: aName});
+ return req;
+ },
+
+ set: function set(aSettings) {
+ if (VERBOSE) debug("send: " + JSON.stringify(aSettings));
+ if (!this._open) {
+ throw "Settings lock not open";
+ }
+ let req = this.createRequest();
+ let reqID = this.getRequestId({request: req});
+ this.sendMessage("Settings:Set", {requestID: reqID,
+ lockID: this._id,
+ settings: aSettings});
+ return req;
+ },
+
+ clear: function clear() {
+ if (VERBOSE) debug("clear");
+ if (!this._open) {
+ throw "Settings lock not open";
+ }
+ let req = this.createRequest();
+ let reqID = this.getRequestId({request: req});
+ this.sendMessage("Settings:Clear", {requestID: reqID,
+ lockID: this._id});
+ return req;
+ },
+
+ classID: Components.ID("{60c9357c-3ae0-4222-8f55-da01428470d5}"),
+ contractID: "@mozilla.org/settingsLock;1",
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference])
+};
+
+function SettingsManager() {
+ this._callbacks = null;
+ this._isRegistered = false;
+ this._locks = [];
+ this._createdLocks = 0;
+ this._unregisteredLocks = 0;
+}
+
+SettingsManager.prototype = {
+ _wrap: function _wrap(obj) {
+ return Cu.cloneInto(obj, this._window);
+ },
+
+ set onsettingchange(aHandler) {
+ this.__DOM_IMPL__.setEventHandler("onsettingchange", aHandler);
+ this.checkMessageRegistration();
+ },
+
+ get onsettingchange() {
+ return this.__DOM_IMPL__.getEventHandler("onsettingchange");
+ },
+
+ createLock: function() {
+ let lock = new SettingsLock(this);
+ if (VERBOSE) debug("creating lock " + lock._id);
+ this._locks.push(lock._id);
+ this._createdLocks++;
+ return lock;
+ },
+
+ unregisterLock: function(aLockID) {
+ let lock_index = this._locks.indexOf(aLockID);
+ if (lock_index != -1) {
+ if (VERBOSE) debug("Unregistering lock " + aLockID);
+ this._locks.splice(lock_index, 1);
+ this._unregisteredLocks++;
+ }
+ },
+
+ receiveMessage: function(aMessage) {
+ if (VERBOSE) debug("Settings::receiveMessage: " + aMessage.name);
+ let msg = aMessage.json;
+
+ switch (aMessage.name) {
+ case "Settings:Change:Return:OK":
+ if (VERBOSE) debug('data:' + msg.key + ':' + msg.value + '\n');
+
+ let event = new this._window.MozSettingsEvent("settingchange", this._wrap({
+ settingName: msg.key,
+ settingValue: msg.value
+ }));
+ this.__DOM_IMPL__.dispatchEvent(event);
+
+ if (this._callbacks && this._callbacks[msg.key]) {
+ if (VERBOSE) debug("observe callback called! " + msg.key + " " + this._callbacks[msg.key].length);
+ this._callbacks[msg.key].forEach(function(cb) {
+ cb(this._wrap({settingName: msg.key, settingValue: msg.value}));
+ }.bind(this));
+ } else {
+ if (VERBOSE) debug("no observers stored!");
+ }
+ break;
+ default:
+ if (DEBUG) debug("Wrong message: " + aMessage.name);
+ }
+ },
+
+ // If we have either observer callbacks or an event handler,
+ // register for messages from the main thread. Otherwise, if no one
+ // is listening, unregister to reduce parent load.
+ checkMessageRegistration: function checkRegistration() {
+ let handler = this.__DOM_IMPL__.getEventHandler("onsettingchange");
+ if (!this._isRegistered) {
+ if (VERBOSE) debug("Registering for messages");
+ cpmm.sendAsyncMessage("Settings:RegisterForMessages",
+ undefined,
+ undefined,
+ this._window.document.nodePrincipal);
+ this._isRegistered = true;
+ } else {
+ if ((!this._callbacks || Object.keys(this._callbacks).length == 0) &&
+ !handler) {
+ if (VERBOSE) debug("Unregistering for messages");
+ cpmm.sendAsyncMessage("Settings:UnregisterForMessages",
+ undefined,
+ undefined,
+ this._window.document.nodePrincipal);
+ this._isRegistered = false;
+ this._callbacks = null;
+ }
+ }
+ },
+
+ addObserver: function addObserver(aName, aCallback) {
+ if (VERBOSE) debug("addObserver " + aName);
+
+ if (!this._callbacks) {
+ this._callbacks = {};
+ }
+
+ if (!this._callbacks[aName]) {
+ this._callbacks[aName] = [aCallback];
+ } else {
+ this._callbacks[aName].push(aCallback);
+ }
+
+ let length = this._callbacks[aName].length;
+ if (length >= kObserverSoftLimit) {
+ debug("WARNING: MORE THAN " + kObserverSoftLimit + " OBSERVERS FOR " +
+ aName + ": " + length + " FROM" + (new Error).stack);
+#ifdef DEBUG
+ debug("JS STOPS EXECUTING AT THIS POINT IN DEBUG BUILDS!");
+ throw Components.results.NS_ERROR_ABORT;
+#endif
+ }
+
+ this.checkMessageRegistration();
+ },
+
+ removeObserver: function removeObserver(aName, aCallback) {
+ if (VERBOSE) debug("deleteObserver " + aName);
+ if (this._callbacks && this._callbacks[aName]) {
+ let index = this._callbacks[aName].indexOf(aCallback);
+ if (index != -1) {
+ this._callbacks[aName].splice(index, 1);
+ if (this._callbacks[aName].length == 0) {
+ delete this._callbacks[aName];
+ }
+ } else {
+ if (VERBOSE) debug("Callback not found for: " + aName);
+ }
+ } else {
+ if (VERBOSE) debug("No observers stored for " + aName);
+ }
+ this.checkMessageRegistration();
+ },
+
+ init: function(aWindow) {
+ if (VERBOSE) debug("SettingsManager init");
+ mrm.registerStrongReporter(this);
+ cpmm.addMessageListener("Settings:Change:Return:OK", this);
+ Services.obs.addObserver(this, "inner-window-destroyed", false);
+ let util = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ this.innerWindowID = util.currentInnerWindowID;
+ this._window = aWindow;
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ if (VERBOSE) debug("Topic: " + aTopic);
+ if (aTopic === "inner-window-destroyed") {
+ let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data;
+ if (wId === this.innerWindowID) {
+ if (DEBUG) debug("Received: inner-window-destroyed for valid innerWindowID=" + wId + ", cleanup.");
+ this.cleanup();
+ }
+ }
+ },
+
+ collectReports: function(aCallback, aData, aAnonymize) {
+ for (let topic in this._callbacks) {
+ let length = this._callbacks[topic].length;
+ if (length == 0) {
+ continue;
+ }
+
+ let path;
+ if (length < kObserverSoftLimit) {
+ path = "settings-observers";
+ } else {
+ path = "settings-observers-suspect/referent(topic=" +
+ (aAnonymize ? "" : topic) + ")";
+ }
+
+ aCallback.callback("", path,
+ Ci.nsIMemoryReporter.KIND_OTHER,
+ Ci.nsIMemoryReporter.UNITS_COUNT,
+ length,
+ "The number of settings observers for this topic.",
+ aData);
+ }
+
+ aCallback.callback("",
+ "settings-locks/alive",
+ Ci.nsIMemoryReporter.KIND_OTHER,
+ Ci.nsIMemoryReporter.UNITS_COUNT,
+ this._locks.length,
+ "The number of locks that are currently alives.",
+ aData);
+
+ aCallback.callback("",
+ "settings-locks/created",
+ Ci.nsIMemoryReporter.KIND_OTHER,
+ Ci.nsIMemoryReporter.UNITS_COUNT,
+ this._createdLocks,
+ "The number of locks that were created.",
+ aData);
+
+ aCallback.callback("",
+ "settings-locks/deleted",
+ Ci.nsIMemoryReporter.KIND_OTHER,
+ Ci.nsIMemoryReporter.UNITS_COUNT,
+ this._unregisteredLocks,
+ "The number of locks that were deleted.",
+ aData);
+ },
+
+ cleanup: function() {
+ Services.obs.removeObserver(this, "inner-window-destroyed");
+ // At this point, the window is dying, so there's nothing left
+ // that we could do with our lock. Go ahead and run finalize on
+ // it to make sure changes are commited.
+ for (let i = 0; i < this._locks.length; ++i) {
+ if (DEBUG) debug("Lock alive at destroy, finalizing: " + this._locks[i]);
+ // Due to bug 1105511 we should be able to send this without
+ // cached principals. However, this is scary because any iframe
+ // in the process could run this?
+ cpmm.sendAsyncMessage("Settings:Finalize",
+ {lockID: this._locks[i]});
+ }
+ cpmm.removeMessageListener("Settings:Change:Return:OK", this);
+ mrm.unregisterStrongReporter(this);
+ this.innerWindowID = null;
+ this._window = null;
+ },
+
+ classID: Components.ID("{c40b1c70-00fb-11e2-a21f-0800200c9a66}"),
+ contractID: "@mozilla.org/settingsManager;1",
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
+ Ci.nsIDOMGlobalPropertyInitializer,
+ Ci.nsIObserver,
+ Ci.nsIMemoryReporter]),
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SettingsManager, SettingsLock]);
diff --git a/dom/settings/SettingsManager.manifest b/dom/settings/SettingsManager.manifest
new file mode 100644
index 000000000000..fc9612928241
--- /dev/null
+++ b/dom/settings/SettingsManager.manifest
@@ -0,0 +1,5 @@
+component {c40b1c70-00fb-11e2-a21f-0800200c9a66} SettingsManager.js
+contract @mozilla.org/settingsManager;1 {c40b1c70-00fb-11e2-a21f-0800200c9a66}
+
+component {60c9357c-3ae0-4222-8f55-da01428470d5} SettingsManager.js
+contract @mozilla.org/settingsLock;1 {60c9357c-3ae0-4222-8f55-da01428470d5}
diff --git a/dom/settings/SettingsRequestManager.jsm b/dom/settings/SettingsRequestManager.jsm
new file mode 100644
index 000000000000..d7616319af2e
--- /dev/null
+++ b/dom/settings/SettingsRequestManager.jsm
@@ -0,0 +1,1213 @@
+/* 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/. */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.importGlobalProperties(['File']);
+
+this.EXPORTED_SYMBOLS = ["SettingsRequestManager"];
+
+Cu.import("resource://gre/modules/SettingsDB.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+var DEBUG = false;
+var VERBOSE = false;
+var TRACK = false;
+
+try {
+ DEBUG =
+ Services.prefs.getBoolPref("dom.mozSettings.SettingsRequestManager.debug.enabled");
+ VERBOSE =
+ Services.prefs.getBoolPref("dom.mozSettings.SettingsRequestManager.verbose.enabled");
+ TRACK =
+ Services.prefs.getBoolPref("dom.mozSettings.trackTasksUsage");
+} catch (ex) { }
+
+var allowForceReadOnly = false;
+try {
+ allowForceReadOnly = Services.prefs.getBoolPref("dom.mozSettings.allowForceReadOnly");
+} catch (ex) { }
+
+function debug(s) {
+ dump("-*- SettingsRequestManager: " + s + "\n");
+}
+
+var inParent = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime)
+ .processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+
+const kXpcomShutdownObserverTopic = "xpcom-shutdown";
+const kInnerWindowDestroyed = "inner-window-destroyed";
+const kMozSettingsChangedObserverTopic = "mozsettings-changed";
+const kSettingsReadSuffix = "-read";
+const kSettingsWriteSuffix = "-write";
+const kSettingsClearPermission = "settings-clear";
+const kAllSettingsReadPermission = "settings" + kSettingsReadSuffix;
+const kAllSettingsWritePermission = "settings" + kSettingsWriteSuffix;
+// Any application with settings permissions, be it for all settings
+// or a single one, will need to be able to access the settings API.
+// The settings-api permission allows an app to see the mozSettings
+// API in order to create locks and queue tasks. Whether these tasks
+// will be allowed depends on the exact permissions the app has.
+const kSomeSettingsReadPermission = "settings-api" + kSettingsReadSuffix;
+const kSomeSettingsWritePermission = "settings-api" + kSettingsWriteSuffix;
+
+// Time, in seconds, to consider the API is starting to jam
+var kSoftLockupDelta = 30;
+try {
+ kSoftLockupDelta = Services.prefs.getIntPref("dom.mozSettings.softLockupDelta");
+} catch (ex) { }
+
+XPCOMUtils.defineLazyServiceGetter(this, "mrm",
+ "@mozilla.org/memory-reporter-manager;1",
+ "nsIMemoryReporterManager");
+XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
+ "@mozilla.org/parentprocessmessagemanager;1",
+ "nsIMessageBroadcaster");
+XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
+ "@mozilla.org/uuid-generator;1",
+ "nsIUUIDGenerator");
+XPCOMUtils.defineLazyServiceGetter(this, "gSettingsService",
+ "@mozilla.org/settingsService;1",
+ "nsISettingsService");
+
+var SettingsPermissions = {
+ checkPermission: function(aPrincipal, aPerm) {
+ if (!aPrincipal) {
+ Cu.reportError("SettingsPermissions.checkPermission was passed a null principal. Denying all permissions.");
+ return false;
+ }
+ if (aPrincipal.origin == "[System Principal]" ||
+ Services.perms.testExactPermissionFromPrincipal(aPrincipal, aPerm) == Ci.nsIPermissionManager.ALLOW_ACTION) {
+ return true;
+ }
+ return false;
+ },
+ hasAllReadPermission: function(aPrincipal) {
+ return this.checkPermission(aPrincipal, kAllSettingsReadPermission);
+ },
+ hasAllWritePermission: function(aPrincipal) {
+ return this.checkPermission(aPrincipal, kAllSettingsWritePermission);
+ },
+ hasSomeReadPermission: function(aPrincipal) {
+ return this.checkPermission(aPrincipal, kSomeSettingsReadPermission);
+ },
+ hasSomeWritePermission: function(aPrincipal) {
+ return this.checkPermission(aPrincipal, kSomeSettingsWritePermission);
+ },
+ hasClearPermission: function(aPrincipal) {
+ return this.checkPermission(aPrincipal, kSettingsClearPermission);
+ },
+ hasReadPermission: function(aPrincipal, aSettingsName) {
+ return this.hasAllReadPermission(aPrincipal) || this.checkPermission(aPrincipal, "settings:" + aSettingsName + kSettingsReadSuffix);
+ },
+ hasWritePermission: function(aPrincipal, aSettingsName) {
+ return this.hasAllWritePermission(aPrincipal) || this.checkPermission(aPrincipal, "settings:" + aSettingsName + kSettingsWriteSuffix);
+ }
+};
+
+
+function SettingsLockInfo(aDB, aMsgMgr, aPrincipal, aLockID, aIsServiceLock, aWindowID, aLockStack) {
+ return {
+ // ID Shared with the object on the child side
+ lockID: aLockID,
+ // Is this a content lock or a settings service lock?
+ isServiceLock: aIsServiceLock,
+ // Which inner window ID
+ windowID: aWindowID,
+ // Where does this lock comes from
+ lockStack: aLockStack,
+ // Tasks to be run once the lock is at the head of the queue
+ tasks: [],
+ // This is set to true once a transaction is ready to run, but is not at the
+ // head of the lock queue.
+ consumable: false,
+ // Holds values that are requested to be set until the lock lifetime ends,
+ // then commits them to the DB.
+ queuedSets: {},
+ // Internal transaction object
+ _transaction: undefined,
+ // Message manager that controls the lock
+ _mm: aMsgMgr,
+ // If true, it means a permissions check failed, so just fail everything now
+ _failed: false,
+ // If we're slated to run finalize, set this to make sure we don't
+ // somehow run other events afterward.
+ finalizing: false,
+ // Lets us know if we can use this lock for a clear command
+ canClear: true,
+ // Lets us know if this lock has been used to clear at any point.
+ hasCleared: false,
+ // forceReadOnly sets whether we want to do a read only transaction. Define
+ // true by default, and let queueTask() set this to false if we queue any
+ // "set" task. Since users of settings locks will queue all tasks before
+ // any idb transaction is created, we know we will have all needed
+ // information to set this before creating a transaction.
+ forceReadOnly: true,
+ // Principal the lock was created under. We assume that the lock
+ // will continue to exist under this principal for the duration of
+ // its lifetime.
+ principal: aPrincipal,
+ getObjectStore: function() {
+ if (VERBOSE) debug("Getting transaction for " + this.lockID);
+ let store;
+ // Test for transaction validity via trying to get the
+ // datastore. If it doesn't work, assume the transaction is
+ // closed, create a new transaction and try again.
+ if (this._transaction) {
+ try {
+ store = this._transaction.objectStore(SETTINGSSTORE_NAME);
+ } catch (e) {
+ if (e.name == "InvalidStateError") {
+ if (VERBOSE) debug("Current transaction for " + this.lockID + " closed, trying to create new one.");
+ } else {
+ if (DEBUG) debug("Unexpected exception, throwing: " + e);
+ throw e;
+ }
+ }
+ }
+ // Create one transaction with a global permission. This may be
+ // slightly slower on apps with full settings permissions, but
+ // it means we don't have to do our own transaction order
+ // bookkeeping.
+ let canReadOnly = allowForceReadOnly && this.forceReadOnly;
+ if (canReadOnly || !SettingsPermissions.hasSomeWritePermission(this.principal)) {
+ if (VERBOSE) debug("Making READONLY transaction for " + this.lockID);
+ this._transaction = aDB._db.transaction(SETTINGSSTORE_NAME, "readonly");
+ } else {
+ if (VERBOSE) debug("Making READWRITE transaction for " + this.lockID);
+ this._transaction = aDB._db.transaction(SETTINGSSTORE_NAME, "readwrite");
+ }
+ this._transaction.oncomplete = function() {
+ if (VERBOSE) debug("Transaction for lock " + this.lockID + " closed");
+ }.bind(this);
+ this._transaction.onabort = function () {
+ if (DEBUG) debug("Transaction for lock " + this.lockID + " aborted");
+ this._failed = true;
+ }.bind(this);
+ try {
+ store = this._transaction.objectStore(SETTINGSSTORE_NAME);
+ } catch (e) {
+ if (e.name == "InvalidStateError") {
+ if (DEBUG) debug("Cannot create objectstore on transaction for " + this.lockID);
+ return null;
+ } else {
+ if (DEBUG) debug("Unexpected exception, throwing: " + e);
+ throw e;
+ }
+ }
+ return store;
+ }
+ };
+}
+
+var SettingsRequestManager = {
+ // Access to the settings DB
+ settingsDB: new SettingsDB(),
+ // Remote messages to listen for from child
+ messages: ["child-process-shutdown", "Settings:Get", "Settings:Set",
+ "Settings:Clear", "Settings:Run", "Settings:Finalize",
+ "Settings:CreateLock", "Settings:RegisterForMessages"],
+ // Map of LockID to SettingsLockInfo objects
+ lockInfo: {},
+ // Storing soft lockup detection infos
+ softLockup: {
+ lockId: null, // last lock dealt with
+ lockTs: null // last time of dealing with
+ },
+ // Queue of LockIDs. The LockID on the front of the queue is the only lock
+ // that will have requests processed, all other locks will queue requests
+ // until they hit the front of the queue.
+ settingsLockQueue: [],
+ children: [],
+ // Since we need to call observers at times when we may not have
+ // just received a message from a child process, we cache principals
+ // for message managers and check permissions on them before we send
+ // settings notifications to child processes.
+ observerPrincipalCache: new Map(),
+ totalProcessed: 0,
+ tasksConsumed: {},
+ totalSetProcessed: 0,
+ tasksSetConsumed: {},
+ totalGetProcessed: 0,
+ tasksGetConsumed: {},
+
+ init: function() {
+ if (VERBOSE) debug("init");
+ this.settingsDB.init();
+ this.messages.forEach((function(msgName) {
+ ppmm.addMessageListener(msgName, this);
+ }).bind(this));
+ Services.obs.addObserver(this, kXpcomShutdownObserverTopic, false);
+ Services.obs.addObserver(this, kInnerWindowDestroyed, false);
+ mrm.registerStrongReporter(this);
+ },
+
+ _serializePreservingBinaries: function _serializePreservingBinaries(aObject) {
+ function needsUUID(aValue) {
+ if (!aValue || !aValue.constructor) {
+ return false;
+ }
+ return (aValue.constructor.name == "Date") || (aValue instanceof File) ||
+ (aValue instanceof Ci.nsIDOMBlob);
+ }
+ // We need to serialize settings objects, otherwise they can change between
+ // the set() call and the enqueued request being processed. We can't simply
+ // parse(stringify(obj)) because that breaks things like Blobs, Files and
+ // Dates, so we use stringify's replacer and parse's reviver parameters to
+ // preserve binaries.
+ let binaries = Object.create(null);
+ let stringified = JSON.stringify(aObject, function(key, value) {
+ value = this.settingsDB.prepareValue(value);
+ if (needsUUID(value)) {
+ let uuid = uuidgen.generateUUID().toString();
+ binaries[uuid] = value;
+ return uuid;
+ }
+ return value;
+ }.bind(this));
+ return JSON.parse(stringified, function(key, value) {
+ if (value in binaries) {
+ return binaries[value];
+ }
+ return value;
+ });
+ },
+
+ queueTask: function(aOperation, aData) {
+ if (VERBOSE) debug("Queueing task: " + aOperation);
+
+ let defer = {};
+
+ let lock = this.lockInfo[aData.lockID];
+
+ if (!lock) {
+ return Promise.reject({error: "Lock already dead, cannot queue task"});
+ }
+
+ if (aOperation == "set") {
+ aData.settings = this._serializePreservingBinaries(aData.settings);
+ }
+
+ if (aOperation === "set" || aOperation === "clear") {
+ lock.forceReadOnly = false;
+ }
+
+ lock.tasks.push({
+ operation: aOperation,
+ data: aData,
+ defer: defer
+ });
+
+ let promise = new Promise(function(resolve, reject) {
+ defer.resolve = resolve;
+ defer.reject = reject;
+ });
+
+ return promise;
+ },
+
+ // Due to the fact that we're skipping the database in some places
+ // by keeping a local "set" value cache, resolving some calls
+ // without a call to the database would mean we could potentially
+ // receive promise responses out of expected order if a get is
+ // called before a set. Therefore, we wrap our resolve in a null
+ // get, which means it will resolves afer the rest of the calls
+ // queued to the DB.
+ queueTaskReturn: function(aTask, aReturnValue) {
+ if (VERBOSE) debug("Making task queuing transaction request.");
+ let data = aTask.data;
+ let lock = this.lockInfo[data.lockID];
+ let store = lock.getObjectStore(lock.principal);
+ if (!store) {
+ if (DEBUG) debug("Rejecting task queue on lock " + aTask.data.lockID);
+ return Promise.reject({task: aTask, error: "Cannot get object store"});
+ }
+ // Due to the fact that we're skipping the database, resolving
+ // this without a call to the database would mean we could
+ // potentially receive promise responses out of expected order if
+ // a get is called before a set. Therefore, we wrap our resolve in
+ // a null get, which means it will resolves afer the rest of the
+ // calls queued to the DB.
+ let getReq = store.get(0);
+
+ let defer = {};
+ let promiseWrapper = new Promise(function(resolve, reject) {
+ defer.resolve = resolve;
+ defer.reject = reject;
+ });
+
+ getReq.onsuccess = function(event) {
+ return defer.resolve(aReturnValue);
+ };
+ getReq.onerror = function() {
+ return defer.reject({task: aTask, error: getReq.error.name});
+ };
+ return promiseWrapper;
+ },
+
+ taskGet: function(aTask) {
+ if (VERBOSE) debug("Running Get task on lock " + aTask.data.lockID);
+
+ // Check that we have permissions for getting the value
+ let data = aTask.data;
+ let lock = this.lockInfo[data.lockID];
+
+ if (!lock) {
+ return Promise.reject({task: aTask, error: "Lock died, can't finalize"});
+ }
+ if (lock._failed) {
+ if (DEBUG) debug("Lock failed. All subsequent requests will fail.");
+ return Promise.reject({task: aTask, error: "Lock failed, all requests now failing."});
+ }
+
+ if (lock.hasCleared) {
+ if (DEBUG) debug("Lock was used for a clear command. All subsequent requests will fail.");
+ return Promise.reject({task: aTask, error: "Lock was used for a clear command. All subsequent requests will fail."});
+ }
+
+ lock.canClear = false;
+
+ if (!SettingsPermissions.hasReadPermission(lock.principal, data.name)) {
+ if (DEBUG) debug("get not allowed for " + data.name);
+ lock._failed = true;
+ return Promise.reject({task: aTask, error: "No permission to get " + data.name});
+ }
+
+ // If the value was set during this transaction, use the cached value
+ if (data.name in lock.queuedSets) {
+ if (VERBOSE) debug("Returning cached set value " + lock.queuedSets[data.name] + " for " + data.name);
+ let local_results = {};
+ local_results[data.name] = lock.queuedSets[data.name];
+ return this.queueTaskReturn(aTask, {task: aTask, results: local_results});
+ }
+
+ // Create/Get transaction and make request
+ if (VERBOSE) debug("Making get transaction request for " + data.name);
+ let store = lock.getObjectStore(lock.principal);
+ if (!store) {
+ if (DEBUG) debug("Rejecting Get task on lock " + aTask.data.lockID);
+ return Promise.reject({task: aTask, error: "Cannot get object store"});
+ }
+
+ if (VERBOSE) debug("Making get request for " + data.name);
+ let getReq = (data.name === "*") ? store.mozGetAll() : store.mozGetAll(data.name);
+
+ let defer = {};
+ let promiseWrapper = new Promise(function(resolve, reject) {
+ defer.resolve = resolve;
+ defer.reject = reject;
+ });
+
+ getReq.onsuccess = function(event) {
+ if (VERBOSE) debug("Request for '" + data.name + "' successful. " +
+ "Record count: " + event.target.result.length);
+
+ if (event.target.result.length == 0) {
+ if (VERBOSE) debug("MOZSETTINGS-GET-WARNING: " + data.name + " is not in the database.\n");
+ }
+
+ let results = {};
+
+ for (let i in event.target.result) {
+ let result = event.target.result[i];
+ let name = result.settingName;
+ if (VERBOSE) debug(name + ": " + result.userValue +", " + result.defaultValue);
+ let value = result.userValue !== undefined ? result.userValue : result.defaultValue;
+ results[name] = value;
+ }
+ return defer.resolve({task: aTask, results: results});
+ };
+ getReq.onerror = function() {
+ return defer.reject({task: aTask, error: getReq.error.name});
+ };
+ return promiseWrapper;
+ },
+
+ taskSet: function(aTask) {
+ let data = aTask.data;
+ let lock = this.lockInfo[data.lockID];
+ let keys = Object.getOwnPropertyNames(data.settings);
+
+ if (!lock) {
+ return Promise.reject({task: aTask, error: "Lock died, can't finalize"});
+ }
+ if (lock._failed) {
+ if (DEBUG) debug("Lock failed. All subsequent requests will fail.");
+ return Promise.reject({task: aTask, error: "Lock failed a permissions check, all requests now failing."});
+ }
+
+ if (lock.hasCleared) {
+ if (DEBUG) debug("Lock was used for a clear command. All subsequent requests will fail.");
+ return Promise.reject({task: aTask, error: "Lock was used for a clear command. All other requests will fail."});
+ }
+
+ lock.canClear = false;
+
+ // If we have no keys, resolve
+ if (keys.length === 0) {
+ if (DEBUG) debug("No keys to change entered!");
+ return Promise.resolve({task: aTask});
+ }
+
+ for (let i = 0; i < keys.length; i++) {
+ if (!SettingsPermissions.hasWritePermission(lock.principal, keys[i])) {
+ if (DEBUG) debug("set not allowed on " + keys[i]);
+ lock._failed = true;
+ return Promise.reject({task: aTask, error: "No permission to set " + keys[i]});
+ }
+ }
+
+ for (let i = 0; i < keys.length; i++) {
+ let key = keys[i];
+ if (VERBOSE) debug("key: " + key + ", val: " + JSON.stringify(data.settings[key]) + ", type: " + typeof(data.settings[key]));
+ lock.queuedSets[key] = data.settings[key];
+ }
+
+ return this.queueTaskReturn(aTask, {task: aTask});
+ },
+
+ startRunning: function(aLockID) {
+ let lock = this.lockInfo[aLockID];
+
+ if (!lock) {
+ if (DEBUG) debug("Lock no longer alive, cannot start running");
+ return;
+ }
+
+ lock.consumable = true;
+ if (aLockID == this.settingsLockQueue[0] || this.settingsLockQueue.length == 0) {
+ // If a lock is currently at the head of the queue, run all tasks for
+ // it.
+ if (VERBOSE) debug("Start running tasks for " + aLockID);
+ this.queueConsume();
+ } else {
+ // If a lock isn't at the head of the queue, but requests to be run,
+ // simply mark it as consumable, which means it will automatically run
+ // once it comes to the head of the queue.
+ if (VERBOSE) debug("Queuing tasks for " + aLockID + " while waiting for " + this.settingsLockQueue[0]);
+ }
+ },
+
+ queueConsume: function() {
+ if (this.settingsLockQueue.length > 0 && this.lockInfo[this.settingsLockQueue[0]].consumable) {
+ Services.tm.currentThread.dispatch(SettingsRequestManager.consumeTasks.bind(this), Ci.nsIThread.DISPATCH_NORMAL);
+ }
+ },
+
+ finalizeSets: function(aTask) {
+ let data = aTask.data;
+ if (VERBOSE) debug("Finalizing tasks for lock " + data.lockID);
+ let lock = this.lockInfo[data.lockID];
+
+ if (!lock) {
+ return Promise.reject({task: aTask, error: "Lock died, can't finalize"});
+ }
+ lock.finalizing = true;
+ if (lock._failed) {
+ this.removeLock(data.lockID);
+ return Promise.reject({task: aTask, error: "Lock failed a permissions check, all requests now failing."});
+ }
+ // If we have cleared, there is no reason to continue finalizing
+ // this lock. Just resolve promise with task and move on.
+ if (lock.hasCleared) {
+ if (VERBOSE) debug("Clear was called on lock, skipping finalize");
+ this.removeLock(data.lockID);
+ return Promise.resolve({task: aTask});
+ }
+ let keys = Object.getOwnPropertyNames(lock.queuedSets);
+ if (keys.length === 0) {
+ if (VERBOSE) debug("Nothing to finalize. Exiting.");
+ this.removeLock(data.lockID);
+ return Promise.resolve({task: aTask});
+ }
+
+ let store = lock.getObjectStore(lock.principal);
+ if (!store) {
+ if (DEBUG) debug("Rejecting Set task on lock " + aTask.data.lockID);
+ this.removeLock(data.lockID);
+ return Promise.reject({task: aTask, error: "Cannot get object store"});
+ }
+
+ // Due to the fact there may have multiple set operations to clear, and
+ // they're all async, callbacks are gathered into promises, and the promises
+ // are processed with Promises.all().
+ let checkPromises = [];
+ let finalValues = {};
+ for (let i = 0; i < keys.length; i++) {
+ let key = keys[i];
+ if (VERBOSE) debug("key: " + key + ", val: " + lock.queuedSets[key] + ", type: " + typeof(lock.queuedSets[key]));
+ let checkDefer = {};
+ let checkPromise = new Promise(function(resolve, reject) {
+ checkDefer.resolve = resolve;
+ checkDefer.reject = reject;
+ });
+
+ // Get operation is used to fill in the default value, assuming there is
+ // one. For the moment, if a value doesn't exist in the settings DB, we
+ // allow the user to add it, and just pass back a null default value.
+ let checkKeyRequest = store.get(key);
+ checkKeyRequest.onsuccess = function (event) {
+ let userValue = lock.queuedSets[key];
+ let defaultValue;
+ if (!event.target.result) {
+ defaultValue = null;
+ if (VERBOSE) debug("MOZSETTINGS-GET-WARNING: " + key + " is not in the database.\n");
+ } else {
+ defaultValue = event.target.result.defaultValue;
+ }
+ let obj = {settingName: key, defaultValue: defaultValue, userValue: userValue};
+ finalValues[key] = {defaultValue: defaultValue, userValue: userValue};
+ let setReq = store.put(obj);
+ setReq.onsuccess = function() {
+ if (VERBOSE) debug("Set successful!");
+ if (VERBOSE) debug("key: " + key + ", val: " + finalValues[key] + ", type: " + typeof(finalValues[key]));
+ return checkDefer.resolve({task: aTask});
+ };
+ setReq.onerror = function() {
+ return checkDefer.reject({task: aTask, error: setReq.error.name});
+ };
+ }.bind(this);
+ checkKeyRequest.onerror = function(event) {
+ return checkDefer.reject({task: aTask, error: checkKeyRequest.error.name});
+ };
+ checkPromises.push(checkPromise);
+ }
+
+ let defer = {};
+ let promiseWrapper = new Promise(function(resolve, reject) {
+ defer.resolve = resolve;
+ defer.reject = reject;
+ });
+
+ // Once all transactions are done, or any have failed, remove the lock and
+ // start processing the tasks from the next lock in the queue.
+ Promise.all(checkPromises).then(function() {
+ // If all commits were successful, notify observers
+ for (let i = 0; i < keys.length; i++) {
+ this.sendSettingsChange(keys[i], finalValues[keys[i]].userValue, lock.isServiceLock);
+ }
+ this.removeLock(data.lockID);
+ defer.resolve({task: aTask});
+ }.bind(this), function(ret) {
+ this.removeLock(data.lockID);
+ defer.reject({task: aTask, error: "Set transaction failure"});
+ }.bind(this));
+ return promiseWrapper;
+ },
+
+ // Clear is only expected to be called via tests, and if a lock
+ // calls clear, it should be the only thing the lock does. This
+ // allows us to not have to deal with the possibility of query
+ // integrity checking. Clear should never be called in the wild,
+ // even by certified apps, which is why it has its own permission
+ // (settings-clear).
+ taskClear: function(aTask) {
+ if (VERBOSE) debug("Clearing");
+ let data = aTask.data;
+ let lock = this.lockInfo[data.lockID];
+
+ if (lock._failed) {
+ if (DEBUG) debug("Lock failed, all requests now failing.");
+ return Promise.reject({task: aTask, error: "Lock failed, all requests now failing."});
+ }
+
+ if (!lock.canClear) {
+ if (DEBUG) debug("Lock tried to clear after queuing other tasks. Failing.");
+ lock._failed = true;
+ return Promise.reject({task: aTask, error: "Cannot call clear after queuing other tasks, all requests now failing."});
+ }
+
+ if (!SettingsPermissions.hasClearPermission(lock.principal)) {
+ if (DEBUG) debug("clear not allowed");
+ lock._failed = true;
+ return Promise.reject({task: aTask, error: "No permission to clear DB"});
+ }
+
+ lock.hasCleared = true;
+
+ let store = lock.getObjectStore(lock.principal);
+ if (!store) {
+ if (DEBUG) debug("Rejecting Clear task on lock " + aTask.data.lockID);
+ return Promise.reject({task: aTask, error: "Cannot get object store"});
+ }
+ let defer = {};
+ let promiseWrapper = new Promise(function(resolve, reject) {
+ defer.resolve = resolve;
+ defer.reject = reject;
+ });
+
+ let clearReq = store.clear();
+ clearReq.onsuccess = function() {
+ return defer.resolve({task: aTask});
+ };
+ clearReq.onerror = function() {
+ return defer.reject({task: aTask});
+ };
+ return promiseWrapper;
+ },
+
+ ensureConnection : function() {
+ if (VERBOSE) debug("Ensuring Connection");
+ let defer = {};
+ let promiseWrapper = new Promise(function(resolve, reject) {
+ defer.resolve = resolve;
+ defer.reject = reject;
+ });
+ this.settingsDB.ensureDB(
+ function() { defer.resolve(); },
+ function(error) {
+ if (DEBUG) debug("Cannot open Settings DB. Trying to open an old version?\n");
+ defer.reject(error);
+ }
+ );
+ return promiseWrapper;
+ },
+
+ runTasks: function(aLockID) {
+ if (VERBOSE) debug("Running tasks for " + aLockID);
+ let lock = this.lockInfo[aLockID];
+ if (!lock) {
+ if (DEBUG) debug("Lock no longer alive, cannot run tasks");
+ return;
+ }
+ let currentTask = lock.tasks.shift();
+ let promises = [];
+ if (TRACK) {
+ if (this.tasksConsumed[aLockID] === undefined) {
+ this.tasksConsumed[aLockID] = 0;
+ this.tasksGetConsumed[aLockID] = 0;
+ this.tasksSetConsumed[aLockID] = 0;
+ }
+ }
+ while (currentTask) {
+ if (VERBOSE) debug("Running Operation " + currentTask.operation);
+ if (lock.finalizing) {
+ // We should really never get to this point, but if we do,
+ // fail every task that happens.
+ Cu.reportError("Settings lock trying to run more tasks after finalizing. Ignoring tasks, but this is bad. Lock: " + aLockID);
+ currentTask.defer.reject("Cannot call new task after finalizing");
+ } else {
+ let p;
+ this.totalProcessed++;
+ if (TRACK) {
+ this.tasksConsumed[aLockID]++;
+ }
+ switch (currentTask.operation) {
+ case "get":
+ this.totalGetProcessed++;
+ if (TRACK) {
+ this.tasksGetConsumed[aLockID]++;
+ }
+ p = this.taskGet(currentTask);
+ break;
+ case "set":
+ this.totalSetProcessed++;
+ if (TRACK) {
+ this.tasksSetConsumed[aLockID]++;
+ }
+ p = this.taskSet(currentTask);
+ break;
+ case "clear":
+ p = this.taskClear(currentTask);
+ break;
+ case "finalize":
+ p = this.finalizeSets(currentTask);
+ break;
+ default:
+ if (DEBUG) debug("Invalid operation: " + currentTask.operation);
+ p.reject("Invalid operation: " + currentTask.operation);
+ }
+ p.then(function(ret) {
+ ret.task.defer.resolve("results" in ret ? ret.results : null);
+ }.bind(currentTask), function(ret) {
+ ret.task.defer.reject(ret.error);
+ });
+ promises.push(p);
+ }
+ currentTask = lock.tasks.shift();
+ }
+ },
+
+ consumeTasks: function() {
+ if (this.settingsLockQueue.length == 0) {
+ if (VERBOSE) debug("Nothing to run!");
+ return;
+ }
+
+ let lockID = this.settingsLockQueue[0];
+ if (VERBOSE) debug("Consuming tasks for " + lockID);
+ let lock = this.lockInfo[lockID];
+
+ // If a process dies, we should clean up after it via the
+ // child-process-shutdown event. But just in case we don't, we want to make
+ // sure we never block on consuming.
+ if (!lock) {
+ if (DEBUG) debug("Lock no longer alive, cannot consume tasks");
+ this.queueConsume();
+ return;
+ }
+
+ if (!lock.consumable || lock.tasks.length === 0) {
+ if (VERBOSE) debug("No more tasks to run or not yet consuamble.");
+ return;
+ }
+
+ lock.consumable = false;
+ this.ensureConnection().then(
+ function(task) {
+ this.runTasks(lockID);
+ this.updateSoftLockup(lockID);
+ }.bind(this), function(ret) {
+ dump("-*- SettingsRequestManager: SETTINGS DATABASE ERROR: Cannot make DB connection!\n");
+ });
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ if (VERBOSE) debug("observe: " + aTopic);
+ switch (aTopic) {
+ case kXpcomShutdownObserverTopic:
+ this.messages.forEach((function(msgName) {
+ ppmm.removeMessageListener(msgName, this);
+ }).bind(this));
+ Services.obs.removeObserver(this, kXpcomShutdownObserverTopic);
+ ppmm = null;
+ mrm.unregisterStrongReporter(this);
+ break;
+
+ case kInnerWindowDestroyed:
+ let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data;
+ this.forceFinalizeChildLocksNonOOP(wId);
+ break;
+
+ default:
+ if (DEBUG) debug("Wrong observer topic: " + aTopic);
+ break;
+ }
+ },
+
+ collectReports: function(aCallback, aData, aAnonymize) {
+ for (let lockId of Object.keys(this.lockInfo)) {
+ let lock = this.lockInfo[lockId];
+ let length = lock.tasks.length;
+
+ if (length === 0) {
+ continue;
+ }
+
+ let path = "settings-locks/tasks/lock(id=" + lockId + ")/";
+
+ aCallback.callback("", path + "alive",
+ Ci.nsIMemoryReporter.KIND_OTHER,
+ Ci.nsIMemoryReporter.UNITS_COUNT,
+ length,
+ "Alive tasks for this lock",
+ aData);
+ }
+
+ aCallback.callback("",
+ "settings-locks/tasks-total/processed",
+ Ci.nsIMemoryReporter.KIND_OTHER,
+ Ci.nsIMemoryReporter.UNITS_COUNT,
+ this.totalProcessed,
+ "The total number of tasks that were executed.",
+ aData);
+
+ aCallback.callback("",
+ "settings-locks/tasks-total/set",
+ Ci.nsIMemoryReporter.KIND_OTHER,
+ Ci.nsIMemoryReporter.UNITS_COUNT,
+ this.totalSetProcessed,
+ "The total number of set tasks that were executed.",
+ aData);
+
+ aCallback.callback("",
+ "settings-locks/tasks-total/get",
+ Ci.nsIMemoryReporter.KIND_OTHER,
+ Ci.nsIMemoryReporter.UNITS_COUNT,
+ this.totalGetProcessed,
+ "The total number of get tasks that were executed.",
+ aData);
+
+ // if TRACK is not enabled, then, no details are available
+ if (!TRACK) {
+ return;
+ }
+
+ for (let lockId of Object.keys(this.tasksConsumed)) {
+ let lock = this.lockInfo[lockId];
+ let length = 0;
+ if (lock) {
+ length = lock.tasks.length;
+ }
+
+ let path = "settings-locks/tasks/lock(id=" + lockId + ")/";
+
+ aCallback.callback("", path + "set",
+ Ci.nsIMemoryReporter.KIND_OTHER,
+ Ci.nsIMemoryReporter.UNITS_COUNT,
+ this.tasksSetConsumed[lockId],
+ "Set tasks for this lock.",
+ aData);
+
+ aCallback.callback("", path + "get",
+ Ci.nsIMemoryReporter.KIND_OTHER,
+ Ci.nsIMemoryReporter.UNITS_COUNT,
+ this.tasksGetConsumed[lockId],
+ "Get tasks for this lock.",
+ aData);
+
+ aCallback.callback("", path + "processed",
+ Ci.nsIMemoryReporter.KIND_OTHER,
+ Ci.nsIMemoryReporter.UNITS_COUNT,
+ this.tasksConsumed[lockId],
+ "Number of tasks that were executed.",
+ aData);
+ }
+ },
+
+ sendSettingsChange: function(aKey, aValue, aIsServiceLock) {
+ this.broadcastMessage("Settings:Change:Return:OK",
+ { key: aKey, value: aValue });
+ var setting = {
+ key: aKey,
+ value: aValue,
+ isInternalChange: aIsServiceLock
+ };
+ setting.wrappedJSObject = setting;
+ Services.obs.notifyObservers(setting, kMozSettingsChangedObserverTopic, "");
+ },
+
+ broadcastMessage: function broadcastMessage(aMsgName, aContent) {
+ if (VERBOSE) debug("Broadcast");
+ this.children.forEach(function(msgMgr) {
+ let principal = this.observerPrincipalCache.get(msgMgr);
+ if (!principal) {
+ if (DEBUG) debug("Cannot find principal for message manager to check permissions");
+ }
+ else if (SettingsPermissions.hasReadPermission(principal, aContent.key)) {
+ try {
+ msgMgr.sendAsyncMessage(aMsgName, aContent);
+ } catch (e) {
+ if (DEBUG) debug("Failed sending message: " + aMsgName);
+ }
+ }
+ }.bind(this));
+ if (VERBOSE) debug("Finished Broadcasting");
+ },
+
+ addObserver: function(aMsgMgr, aPrincipal) {
+ if (VERBOSE) debug("Add observer for " + aPrincipal.origin);
+ if (this.children.indexOf(aMsgMgr) == -1) {
+ this.children.push(aMsgMgr);
+ this.observerPrincipalCache.set(aMsgMgr, aPrincipal);
+ }
+ },
+
+ removeObserver: function(aMsgMgr) {
+ if (VERBOSE) {
+ let principal = this.observerPrincipalCache.get(aMsgMgr);
+ if (principal) {
+ debug("Remove observer for " + principal.origin);
+ }
+ }
+ let index = this.children.indexOf(aMsgMgr);
+ if (index != -1) {
+ this.children.splice(index, 1);
+ this.observerPrincipalCache.delete(aMsgMgr);
+ }
+ if (VERBOSE) debug("Principal/MessageManager pairs left in observer cache: " + this.observerPrincipalCache.size);
+ },
+
+ removeLock: function(aLockID) {
+ if (VERBOSE) debug("Removing lock " + aLockID);
+ if (this.lockInfo[aLockID]) {
+ let transaction = this.lockInfo[aLockID]._transaction;
+ if (transaction) {
+ try {
+ transaction.abort();
+ } catch (e) {
+ if (e.name == "InvalidStateError") {
+ if (VERBOSE) debug("Transaction for " + aLockID + " closed already");
+ } else {
+ if (DEBUG) debug("Unexpected exception, throwing: " + e);
+ throw e;
+ }
+ }
+ }
+ delete this.lockInfo[aLockID];
+ }
+ let index = this.settingsLockQueue.indexOf(aLockID);
+ if (index > -1) {
+ this.settingsLockQueue.splice(index, 1);
+ }
+ // If index is 0, the lock we just removed was at the head of
+ // the queue, so possibly queue the next lock if it's
+ // consumable.
+ if (index == 0) {
+ this.queueConsume();
+ }
+ },
+
+ hasLockFinalizeTask: function(lock) {
+ // Go in reverse order because finalize should be the last one
+ for (let task_index = lock.tasks.length; task_index >= 0; task_index--) {
+ if (lock.tasks[task_index]
+ && lock.tasks[task_index].operation === "finalize") {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ enqueueForceFinalize: function(lock) {
+ if (!this.hasLockFinalizeTask(lock)) {
+ if (VERBOSE) debug("Alive lock has pending tasks: " + lock.lockID);
+ this.queueTask("finalize", {lockID: lock.lockID}).then(
+ function() {
+ if (VERBOSE) debug("Alive lock " + lock.lockID + " succeeded to force-finalize");
+ },
+ function(error) {
+ if (DEBUG) debug("Alive lock " + lock.lockID + " failed to force-finalize due to error: " + error);
+ }
+ );
+ // Finalize is considered a task running situation, but it also needs to
+ // queue a task.
+ this.startRunning(lock.lockID);
+ }
+ },
+
+ forceFinalizeChildLocksNonOOP: function(windowId) {
+ if (VERBOSE) debug("Forcing finalize on child locks, non OOP");
+
+ for (let lockId of Object.keys(this.lockInfo)) {
+ let lock = this.lockInfo[lockId];
+ if (lock.windowID === windowId) {
+ this.enqueueForceFinalize(lock);
+ }
+ }
+ },
+
+ forceFinalizeChildLocksOOP: function(aMsgMgr) {
+ if (VERBOSE) debug("Forcing finalize on child locks, OOP");
+
+ for (let lockId of Object.keys(this.lockInfo)) {
+ let lock = this.lockInfo[lockId];
+ if (lock._mm === aMsgMgr) {
+ this.enqueueForceFinalize(lock);
+ }
+ }
+ },
+
+ updateSoftLockup: function(aLockId) {
+ if (VERBOSE) debug("Treating lock " + aLockId + ", so updating soft lockup infos ...");
+
+ this.softLockup = {
+ lockId: aLockId,
+ lockTs: new Date()
+ };
+ },
+
+ checkSoftLockup: function() {
+ if (VERBOSE) debug("Checking for soft lockup ...");
+
+ if (this.settingsLockQueue.length === 0) {
+ if (VERBOSE) debug("Empty settings lock queue, no soft lockup ...");
+ return;
+ }
+
+ let head = this.settingsLockQueue[0];
+ if (head !== this.softLockup.lockId) {
+ if (VERBOSE) debug("Non matching head of settings lock queue, no soft lockup ...");
+ return;
+ }
+
+ let delta = (new Date() - this.softLockup.lockTs) / 1000;
+ if (delta < kSoftLockupDelta) {
+ if (VERBOSE) debug("Matching head of settings lock queue, but delta (" + delta + ") < 30 secs, no soft lockup ...");
+ return;
+ }
+
+ let msgBlocked = "Settings queue head blocked at " + head +
+ " for " + delta + " secs, Settings API may be soft lockup. Lock from: " +
+ this.lockInfo[head].lockStack;
+ Cu.reportError(msgBlocked);
+ if (DEBUG) debug(msgBlocked);
+ },
+
+ receiveMessage: function(aMessage) {
+ if (VERBOSE) debug("receiveMessage " + aMessage.name + ": " + JSON.stringify(aMessage.data));
+
+ let msg = aMessage.data;
+ let mm = aMessage.target;
+
+ function returnMessage(name, data) {
+ if (mm) {
+ try {
+ mm.sendAsyncMessage(name, data);
+ } catch (e) {
+ if (DEBUG) debug("Return message failed, " + name + ": " + e);
+ }
+ } else {
+ try {
+ gSettingsService.receiveMessage({ name: name, data: data });
+ } catch (e) {
+ if (DEBUG) debug("Direct return message failed, " + name + ": " + e);
+ }
+ }
+ }
+
+ // For all message types that expect a lockID, we check to make
+ // sure that we're accessing a lock that's part of our process. If
+ // not, consider it a security violation and kill the app. Killing
+ // based on creating a colliding lock ID happens as part of
+ // CreateLock check below.
+ switch (aMessage.name) {
+ case "Settings:Get":
+ case "Settings:Set":
+ case "Settings:Clear":
+ case "Settings:Run":
+ case "Settings:Finalize":
+ this.checkSoftLockup();
+ if (!msg.lockID) {
+ Cu.reportError("Process sending request for lock that does not exist. Killing.");
+ }
+ else if (!this.lockInfo[msg.lockID]) {
+ if (DEBUG) debug("Cannot find lock ID " + msg.lockID);
+ // This doesn't kill, because we can have things that file
+ // finalize, then die, and we may get the observer
+ // notification before we get the IPC messages.
+ return;
+ }
+ else if (mm != this.lockInfo[msg.lockID]._mm) {
+ Cu.reportError("Process trying to access settings lock from another process. Killing.");
+ }
+ default:
+ break;
+ }
+
+ switch (aMessage.name) {
+ case "child-process-shutdown":
+ if (VERBOSE) debug("Child process shutdown received.");
+ this.forceFinalizeChildLocksOOP(mm);
+ this.removeObserver(mm);
+ break;
+ case "Settings:RegisterForMessages":
+ if (!SettingsPermissions.hasSomeReadPermission(aMessage.principal)) {
+ Cu.reportError("Settings message " + aMessage.name +
+ " from a content process with no 'settings-api-read' privileges.");
+ return;
+ }
+ this.addObserver(mm, aMessage.principal);
+ break;
+ case "Settings:UnregisterForMessages":
+ this.removeObserver(mm);
+ break;
+ case "Settings:CreateLock":
+ if (VERBOSE) debug("Received CreateLock for " + msg.lockID + " from " + aMessage.principal.origin + " window: " + msg.windowID);
+ // If we try to create a lock ID that collides with one
+ // already in the system, consider it a security violation and
+ // kill.
+ if (msg.lockID in this.settingsLockQueue) {
+ Cu.reportError("Trying to queue a lock with the same ID as an already queued lock. Killing app.");
+ return;
+ }
+
+ if (this.softLockup.lockId === null) {
+ this.updateSoftLockup(msg.lockID);
+ }
+
+ this.settingsLockQueue.push(msg.lockID);
+ this.lockInfo[msg.lockID] = SettingsLockInfo(this.settingsDB,
+ mm,
+ aMessage.principal,
+ msg.lockID,
+ msg.isServiceLock,
+ msg.windowID,
+ msg.lockStack);
+ break;
+ case "Settings:Get":
+ if (VERBOSE) debug("Received getRequest from " + msg.lockID);
+ this.queueTask("get", msg).then(function(settings) {
+ returnMessage("Settings:Get:OK", {
+ lockID: msg.lockID,
+ requestID: msg.requestID,
+ settings: settings
+ });
+ }, function(error) {
+ if (DEBUG) debug("getRequest FAILED " + msg.name);
+ returnMessage("Settings:Get:KO", {
+ lockID: msg.lockID,
+ requestID: msg.requestID,
+ errorMsg: error
+ });
+ });
+ break;
+ case "Settings:Set":
+ if (VERBOSE) debug("Received Set Request from " + msg.lockID);
+ this.queueTask("set", msg).then(function(settings) {
+ returnMessage("Settings:Set:OK", {
+ lockID: msg.lockID,
+ requestID: msg.requestID
+ });
+ }, function(error) {
+ returnMessage("Settings:Set:KO", {
+ lockID: msg.lockID,
+ requestID: msg.requestID,
+ errorMsg: error
+ });
+ });
+ break;
+ case "Settings:Clear":
+ if (VERBOSE) debug("Received Clear Request from " + msg.lockID);
+ this.queueTask("clear", msg).then(function() {
+ returnMessage("Settings:Clear:OK", {
+ lockID: msg.lockID,
+ requestID: msg.requestID
+ });
+ }, function(error) {
+ returnMessage("Settings:Clear:KO", {
+ lockID: msg.lockID,
+ requestID: msg.requestID,
+ errorMsg: error
+ });
+ });
+ break;
+ case "Settings:Finalize":
+ if (VERBOSE) debug("Received Finalize");
+ this.queueTask("finalize", msg).then(function() {
+ returnMessage("Settings:Finalize:OK", {
+ lockID: msg.lockID
+ });
+ }, function(error) {
+ returnMessage("Settings:Finalize:KO", {
+ lockID: msg.lockID,
+ errorMsg: error
+ });
+ });
+ // YES THIS IS SUPPOSED TO FALL THROUGH. Finalize is considered a task
+ // running situation, but it also needs to queue a task.
+ case "Settings:Run":
+ if (VERBOSE) debug("Received Run");
+ this.startRunning(msg.lockID);
+ break;
+ default:
+ if (DEBUG) debug("Wrong message: " + aMessage.name);
+ }
+ }
+};
+
+// This code should ALWAYS be living only on the parent side.
+if (!inParent) {
+ debug("SettingsRequestManager should be living on parent side.");
+ throw Cr.NS_ERROR_ABORT;
+} else {
+ this.SettingsRequestManager = SettingsRequestManager;
+ SettingsRequestManager.init();
+}
diff --git a/dom/settings/SettingsService.js b/dom/settings/SettingsService.js
new file mode 100644
index 000000000000..09bd3ca72264
--- /dev/null
+++ b/dom/settings/SettingsService.js
@@ -0,0 +1,358 @@
+/* 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/. */
+
+"use strict";
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import('resource://gre/modules/SettingsRequestManager.jsm');
+
+/* static functions */
+var DEBUG = false;
+var VERBOSE = false;
+
+try {
+ DEBUG =
+ Services.prefs.getBoolPref("dom.mozSettings.SettingsService.debug.enabled");
+ VERBOSE =
+ Services.prefs.getBoolPref("dom.mozSettings.SettingsService.verbose.enabled");
+} catch (ex) { }
+
+function debug(s) {
+ dump("-*- SettingsService: " + s + "\n");
+}
+
+XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
+ "@mozilla.org/uuid-generator;1",
+ "nsIUUIDGenerator");
+XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
+ "@mozilla.org/childprocessmessagemanager;1",
+ "nsIMessageSender");
+XPCOMUtils.defineLazyServiceGetter(this, "mrm",
+ "@mozilla.org/memory-reporter-manager;1",
+ "nsIMemoryReporterManager");
+
+const nsIClassInfo = Ci.nsIClassInfo;
+const kXpcomShutdownObserverTopic = "xpcom-shutdown";
+
+const SETTINGSSERVICELOCK_CONTRACTID = "@mozilla.org/settingsServiceLock;1";
+const SETTINGSSERVICELOCK_CID = Components.ID("{d7a395a0-e292-11e1-834e-1761d57f5f99}");
+const nsISettingsServiceLock = Ci.nsISettingsServiceLock;
+
+function makeSettingsServiceRequest(aCallback, aName, aValue) {
+ return {
+ callback: aCallback,
+ name: aName,
+ value: aValue
+ };
+};
+
+const kLockListeners = ["Settings:Get:OK", "Settings:Get:KO",
+ "Settings:Clear:OK", "Settings:Clear:KO",
+ "Settings:Set:OK", "Settings:Set:KO",
+ "Settings:Finalize:OK", "Settings:Finalize:KO"];
+
+function SettingsServiceLock(aSettingsService, aTransactionCallback) {
+ if (VERBOSE) debug("settingsServiceLock constr!");
+ this._open = true;
+ this._settingsService = aSettingsService;
+ this._id = uuidgen.generateUUID().toString();
+ this._transactionCallback = aTransactionCallback;
+ this._requests = {};
+ let closeHelper = function() {
+ if (VERBOSE) debug("closing lock " + this._id);
+ this._open = false;
+ this.runOrFinalizeQueries();
+ }.bind(this);
+
+ this.addListeners();
+
+ let createLockPayload = {
+ lockID: this._id,
+ isServiceLock: true,
+ windowID: undefined,
+ lockStack: (new Error).stack
+ };
+
+ this.returnMessage("Settings:CreateLock", createLockPayload);
+ Services.tm.currentThread.dispatch(closeHelper, Ci.nsIThread.DISPATCH_NORMAL);
+}
+
+SettingsServiceLock.prototype = {
+ get closed() {
+ return !this._open;
+ },
+
+ addListeners: function() {
+ for (let msg of kLockListeners) {
+ cpmm.addMessageListener(msg, this);
+ }
+ },
+
+ removeListeners: function() {
+ for (let msg of kLockListeners) {
+ cpmm.removeMessageListener(msg, this);
+ }
+ },
+
+ returnMessage: function(aMessage, aData) {
+ SettingsRequestManager.receiveMessage({
+ name: aMessage,
+ data: aData,
+ target: undefined,
+ principal: Services.scriptSecurityManager.getSystemPrincipal()
+ });
+ },
+
+ runOrFinalizeQueries: function() {
+ if (!this._requests || Object.keys(this._requests).length == 0) {
+ this.returnMessage("Settings:Finalize", {lockID: this._id});
+ } else {
+ this.returnMessage("Settings:Run", {lockID: this._id});
+ }
+ },
+
+ receiveMessage: function(aMessage) {
+
+ let msg = aMessage.data;
+ // SettingsRequestManager broadcasts changes to all locks in the child. If
+ // our lock isn't being addressed, just return.
+ if(msg.lockID != this._id) {
+ return;
+ }
+ if (VERBOSE) debug("receiveMessage (" + this._id + "): " + aMessage.name);
+ // Finalizing a transaction does not return a request ID since we are
+ // supposed to fire callbacks.
+ if (!msg.requestID) {
+ switch (aMessage.name) {
+ case "Settings:Finalize:OK":
+ if (VERBOSE) debug("Lock finalize ok!");
+ this.callTransactionHandle();
+ break;
+ case "Settings:Finalize:KO":
+ if (DEBUG) debug("Lock finalize failed!");
+ this.callAbort();
+ break;
+ default:
+ if (DEBUG) debug("Message type " + aMessage.name + " is missing a requestID");
+ }
+
+ this._settingsService.unregisterLock(this._id);
+ return;
+ }
+
+ let req = this._requests[msg.requestID];
+ if (!req) {
+ if (DEBUG) debug("Matching request not found.");
+ return;
+ }
+ delete this._requests[msg.requestID];
+ switch (aMessage.name) {
+ case "Settings:Get:OK":
+ this._open = true;
+ let settings_names = Object.keys(msg.settings);
+ if (settings_names.length > 0) {
+ let name = settings_names[0];
+ if (DEBUG && settings_names.length > 1) {
+ debug("Warning: overloaded setting:" + name);
+ }
+ let result = msg.settings[name];
+ this.callHandle(req.callback, name, result);
+ } else {
+ this.callHandle(req.callback, req.name, null);
+ }
+ this._open = false;
+ break;
+ case "Settings:Set:OK":
+ this._open = true;
+ // We don't pass values back from sets in SettingsManager...
+ this.callHandle(req.callback, req.name, req.value);
+ this._open = false;
+ break;
+ case "Settings:Get:KO":
+ case "Settings:Set:KO":
+ if (DEBUG) debug("error:" + msg.errorMsg);
+ this.callError(req.callback, msg.error);
+ break;
+ default:
+ if (DEBUG) debug("Wrong message: " + aMessage.name);
+ }
+ this.runOrFinalizeQueries();
+ },
+
+ get: function get(aName, aCallback) {
+ if (VERBOSE) debug("get (" + this._id + "): " + aName);
+ if (!this._open) {
+ if (DEBUG) debug("Settings lock not open!\n");
+ throw Components.results.NS_ERROR_ABORT;
+ }
+ let reqID = uuidgen.generateUUID().toString();
+ this._requests[reqID] = makeSettingsServiceRequest(aCallback, aName);
+ this.returnMessage("Settings:Get", {requestID: reqID,
+ lockID: this._id,
+ name: aName});
+ },
+
+ set: function set(aName, aValue, aCallback) {
+ if (VERBOSE) debug("set: " + aName + " " + aValue);
+ if (!this._open) {
+ throw "Settings lock not open";
+ }
+ let reqID = uuidgen.generateUUID().toString();
+ this._requests[reqID] = makeSettingsServiceRequest(aCallback, aName, aValue);
+ let settings = {};
+ settings[aName] = aValue;
+ this.returnMessage("Settings:Set", {requestID: reqID,
+ lockID: this._id,
+ settings: settings});
+ },
+
+ callHandle: function callHandle(aCallback, aName, aValue) {
+ try {
+ aCallback && aCallback.handle ? aCallback.handle(aName, aValue) : null;
+ } catch (e) {
+ if (DEBUG) debug("settings 'handle' for " + aName + " callback threw an exception, dropping: " + e + "\n");
+ }
+ },
+
+ callAbort: function callAbort(aCallback, aMessage) {
+ try {
+ aCallback && aCallback.handleAbort ? aCallback.handleAbort(aMessage) : null;
+ } catch (e) {
+ if (DEBUG) debug("settings 'abort' callback threw an exception, dropping: " + e + "\n");
+ }
+ },
+
+ callError: function callError(aCallback, aMessage) {
+ try {
+ aCallback && aCallback.handleError ? aCallback.handleError(aMessage) : null;
+ } catch (e) {
+ if (DEBUG) debug("settings 'error' callback threw an exception, dropping: " + e + "\n");
+ }
+ },
+
+ callTransactionHandle: function callTransactionHandle() {
+ try {
+ this._transactionCallback && this._transactionCallback.handle ? this._transactionCallback.handle() : null;
+ } catch (e) {
+ if (DEBUG) debug("settings 'Transaction handle' callback threw an exception, dropping: " + e + "\n");
+ }
+ },
+
+ classID : SETTINGSSERVICELOCK_CID,
+ QueryInterface : XPCOMUtils.generateQI([nsISettingsServiceLock])
+};
+
+const SETTINGSSERVICE_CID = Components.ID("{f656f0c0-f776-11e1-a21f-0800200c9a66}");
+
+function SettingsService()
+{
+ if (VERBOSE) debug("settingsService Constructor");
+ this._locks = [];
+ this._serviceLocks = {};
+ this._createdLocks = 0;
+ this._unregisteredLocks = 0;
+ this.init();
+}
+
+SettingsService.prototype = {
+
+ init: function() {
+ Services.obs.addObserver(this, kXpcomShutdownObserverTopic, false);
+ mrm.registerStrongReporter(this);
+ },
+
+ uninit: function() {
+ Services.obs.removeObserver(this, kXpcomShutdownObserverTopic);
+ mrm.unregisterStrongReporter(this);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ if (VERBOSE) debug("observe: " + aTopic);
+ if (aTopic === kXpcomShutdownObserverTopic) {
+ this.uninit();
+ }
+ },
+
+ receiveMessage: function(aMessage) {
+ if (VERBOSE) debug("Entering receiveMessage");
+
+ let lockID = aMessage.data.lockID;
+ if (!lockID) {
+ if (DEBUG) debug("No lock ID");
+ return;
+ }
+
+ if (!(lockID in this._serviceLocks)) {
+ if (DEBUG) debug("Received message for lock " + lockID + " but no lock");
+ return;
+ }
+
+ if (VERBOSE) debug("Delivering message");
+ this._serviceLocks[lockID].receiveMessage(aMessage);
+ },
+
+ createLock: function createLock(aCallback) {
+ if (VERBOSE) debug("Calling createLock");
+ var lock = new SettingsServiceLock(this, aCallback);
+ if (VERBOSE) debug("Created lock " + lock._id);
+ this.registerLock(lock);
+ return lock;
+ },
+
+ registerLock: function(aLock) {
+ if (VERBOSE) debug("Registering lock " + aLock._id);
+ this._locks.push(aLock._id);
+ this._serviceLocks[aLock._id] = aLock;
+ this._createdLocks++;
+ },
+
+ unregisterLock: function(aLockID) {
+ let lock_index = this._locks.indexOf(aLockID);
+ if (lock_index != -1) {
+ if (VERBOSE) debug("Unregistering lock " + aLockID);
+ this._locks.splice(lock_index, 1);
+ this._serviceLocks[aLockID].removeListeners();
+ this._serviceLocks[aLockID] = null;
+ delete this._serviceLocks[aLockID];
+ this._unregisteredLocks++;
+ }
+ },
+
+ collectReports: function(aCallback, aData, aAnonymize) {
+ aCallback.callback("",
+ "settings-service-locks/alive",
+ Ci.nsIMemoryReporter.KIND_OTHER,
+ Ci.nsIMemoryReporter.UNITS_COUNT,
+ this._locks.length,
+ "The number of service locks that are currently alives.",
+ aData);
+
+ aCallback.callback("",
+ "settings-service-locks/created",
+ Ci.nsIMemoryReporter.KIND_OTHER,
+ Ci.nsIMemoryReporter.UNITS_COUNT,
+ this._createdLocks,
+ "The number of service locks that were created.",
+ aData);
+
+ aCallback.callback("",
+ "settings-service-locks/deleted",
+ Ci.nsIMemoryReporter.KIND_OTHER,
+ Ci.nsIMemoryReporter.UNITS_COUNT,
+ this._unregisteredLocks,
+ "The number of service locks that were deleted.",
+ aData);
+ },
+
+ classID : SETTINGSSERVICE_CID,
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsISettingsService,
+ Ci.nsIObserver,
+ Ci.nsIMemoryReporter])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SettingsService, SettingsServiceLock]);
diff --git a/dom/settings/SettingsService.manifest b/dom/settings/SettingsService.manifest
new file mode 100644
index 000000000000..ae464f921b16
--- /dev/null
+++ b/dom/settings/SettingsService.manifest
@@ -0,0 +1,5 @@
+component {d7a395a0-e292-11e1-834e-1761d57f5f99} SettingsService.js
+contract @mozilla.org/settingsServiceLock;1 {d7a395a0-e292-11e1-834e-1761d57f5f99}
+
+component {f656f0c0-f776-11e1-a21f-0800200c9a66} SettingsService.js
+contract @mozilla.org/settingsService;1 {f656f0c0-f776-11e1-a21f-0800200c9a66}
diff --git a/dom/settings/moz.build b/dom/settings/moz.build
new file mode 100644
index 000000000000..426edcbce856
--- /dev/null
+++ b/dom/settings/moz.build
@@ -0,0 +1,28 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+EXTRA_COMPONENTS += [
+ 'SettingsManager.manifest'
+]
+
+EXTRA_PP_COMPONENTS += [
+ 'SettingsManager.js'
+]
+
+if CONFIG['MOZ_B2G']:
+ EXTRA_COMPONENTS += [
+ 'SettingsService.js',
+ 'SettingsService.manifest',
+ ]
+
+EXTRA_JS_MODULES += [
+ 'SettingsDB.jsm',
+ 'SettingsRequestManager.jsm'
+]
+
+MOCHITEST_CHROME_MANIFESTS += ['tests/chrome.ini']
+
+XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
diff --git a/dom/settings/tests/chrome.ini b/dom/settings/tests/chrome.ini
new file mode 100644
index 000000000000..92b1554a00aa
--- /dev/null
+++ b/dom/settings/tests/chrome.ini
@@ -0,0 +1,24 @@
+[DEFAULT]
+skip-if = toolkit == 'android' # Bug 1287455: takes too long to complete on Android
+support-files =
+ file_loadserver.js
+ file_bug1110872.js
+ file_bug1110872.html
+ test_settings_service.js
+ test_settings_service_callback.js
+
+[test_settings_service.xul]
+run-if = buildapp == 'b2g' || buildapp == 'mulet'
+[test_settings_service_callback.xul]
+run-if = buildapp == 'b2g' || buildapp == 'mulet'
+[test_settings_basics.html]
+[test_settings_permissions.html]
+[test_settings_blobs.html]
+[test_settings_data_uris.html]
+[test_settings_events.html]
+[test_settings_navigator_object.html]
+[test_settings_onsettingchange.html]
+[test_settings_bug1110872.html]
+skip-if = !e10s
+[test_settings_observer_killer.html]
+skip-if = !debug
diff --git a/dom/settings/tests/file_bug1110872.html b/dom/settings/tests/file_bug1110872.html
new file mode 100644
index 000000000000..3dcc45b822f3
--- /dev/null
+++ b/dom/settings/tests/file_bug1110872.html
@@ -0,0 +1,47 @@
+
+
+
+ Test for Bug {1110872} Settings API Reloads
+
+
+
+ Mozilla Bug {1110872} Inner Window for Reload Test
+
+
+
+
+
diff --git a/dom/settings/tests/file_bug1110872.js b/dom/settings/tests/file_bug1110872.js
new file mode 100644
index 000000000000..d31b6c2f3252
--- /dev/null
+++ b/dom/settings/tests/file_bug1110872.js
@@ -0,0 +1,47 @@
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+
+var iframe;
+var loadedEvents = 0;
+
+function loadServer() {
+ var url = SimpleTest.getTestFileURL("file_loadserver.js");
+ var script = SpecialPowers.loadChromeScript(url);
+}
+
+function runTest() {
+ iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ iframe.addEventListener('load', mozbrowserLoaded);
+ iframe.src = 'file_bug1110872.html';
+}
+
+function iframeBodyRecv(msg) {
+ switch (loadedEvents) {
+ case 1:
+ // If we get a message back before we've seen 2 loads, that means
+ // something went wrong with the test. Fail immediately.
+ ok(true, 'got response from first test!');
+ break;
+ case 2:
+ // If we get a message back after 2 loads (initial load, reload),
+ // it means the callback for the last lock fired, which means the
+ // SettingsRequestManager queue has to have been cleared
+ // correctly.
+ ok(true, 'further queries returned ok after SettingsManager death');
+ SimpleTest.finish();
+ break;
+ }
+}
+
+function mozbrowserLoaded() {
+ loadedEvents++;
+ iframe.contentWindow.postMessage({name: "start", step: loadedEvents}, '*');
+ window.addEventListener('message', iframeBodyRecv);
+}
+
+window.addEventListener("load", function() {
+ loadServer();
+ runTest();
+});
diff --git a/dom/settings/tests/file_loadserver.js b/dom/settings/tests/file_loadserver.js
new file mode 100644
index 000000000000..a919d9690ad0
--- /dev/null
+++ b/dom/settings/tests/file_loadserver.js
@@ -0,0 +1,17 @@
+var Ci = Components.interfaces;
+var Cc = Components.classes;
+var Cu = Components.utils;
+
+// Stolen from SpecialPowers, since at this point we don't know we're in a test.
+var isMainProcess = function() {
+ try {
+ return Cc["@mozilla.org/xre/app-info;1"].
+ getService(Ci.nsIXULRuntime).
+ processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+ } catch (e) { }
+ return true;
+};
+
+if (isMainProcess()) {
+ Components.utils.import("resource://gre/modules/SettingsRequestManager.jsm");
+}
diff --git a/dom/settings/tests/test_settings_basics.html b/dom/settings/tests/test_settings_basics.html
new file mode 100644
index 000000000000..a14650390c8d
--- /dev/null
+++ b/dom/settings/tests/test_settings_basics.html
@@ -0,0 +1,816 @@
+
+
+
+
+ Test for Bug {678695} Settings API
+
+
+
+
+
+
+Mozilla Bug {678695}
+
+
+
+
+
+
+
+
+
diff --git a/dom/settings/tests/test_settings_blobs.html b/dom/settings/tests/test_settings_blobs.html
new file mode 100644
index 000000000000..6d24111cf8cc
--- /dev/null
+++ b/dom/settings/tests/test_settings_blobs.html
@@ -0,0 +1,148 @@
+
+
+
+
+ Test for Bug 821630 Settings API
+
+
+
+
+
+
+Mozilla Bug 821630
+
+
+
+
+
+
+
+
+
diff --git a/dom/settings/tests/test_settings_bug1110872.html b/dom/settings/tests/test_settings_bug1110872.html
new file mode 100644
index 000000000000..296c71a8c6f2
--- /dev/null
+++ b/dom/settings/tests/test_settings_bug1110872.html
@@ -0,0 +1,17 @@
+
+
+
+ Test for Bug {1110872} Settings API
+
+
+
+
+
+
+ Mozilla Bug {1110872}
+
+
+
+
+
diff --git a/dom/settings/tests/test_settings_data_uris.html b/dom/settings/tests/test_settings_data_uris.html
new file mode 100644
index 000000000000..7d9165fb33ba
--- /dev/null
+++ b/dom/settings/tests/test_settings_data_uris.html
@@ -0,0 +1,149 @@
+
+
+
+
+ Test for Bug 806374 Settings API
+
+
+
+
+
+
+Mozilla Bug 821630
+
+
+
+
+
+
+
+
+
diff --git a/dom/settings/tests/test_settings_events.html b/dom/settings/tests/test_settings_events.html
new file mode 100644
index 000000000000..a8bf851be309
--- /dev/null
+++ b/dom/settings/tests/test_settings_events.html
@@ -0,0 +1,47 @@
+
+
+
+
+ Test for Bug 678695
+
+
+
+
+Mozilla Bug 678695
+
+
+
+
+
+
+
+
+
diff --git a/dom/settings/tests/test_settings_navigator_object.html b/dom/settings/tests/test_settings_navigator_object.html
new file mode 100644
index 000000000000..2f666aee0844
--- /dev/null
+++ b/dom/settings/tests/test_settings_navigator_object.html
@@ -0,0 +1,37 @@
+
+
+
+
+ Test for Bug 898512 Settings API
+
+
+
+
+
+
+Mozilla Bug 898512
+
+
+
+
+
+
+
+
+
diff --git a/dom/settings/tests/test_settings_observer_killer.html b/dom/settings/tests/test_settings_observer_killer.html
new file mode 100644
index 000000000000..8e7ed973c887
--- /dev/null
+++ b/dom/settings/tests/test_settings_observer_killer.html
@@ -0,0 +1,60 @@
+
+
+
+
+ Test for Bug 1193469 Settings API
+
+
+
+
+
+
+Mozilla Bug 1193469
+
+
+
+
+
+
+
+
+
diff --git a/dom/settings/tests/test_settings_onsettingchange.html b/dom/settings/tests/test_settings_onsettingchange.html
new file mode 100644
index 000000000000..974da0c636bc
--- /dev/null
+++ b/dom/settings/tests/test_settings_onsettingchange.html
@@ -0,0 +1,306 @@
+
+
+
+
+ Test for Bug 678695 Settings API
+
+
+
+
+
+
+Mozilla Bug 678695
+
+
+
+
+
+
+
+
+
diff --git a/dom/settings/tests/test_settings_permissions.html b/dom/settings/tests/test_settings_permissions.html
new file mode 100644
index 000000000000..4cc02385aa51
--- /dev/null
+++ b/dom/settings/tests/test_settings_permissions.html
@@ -0,0 +1,184 @@
+
+
+
+
+ Test for Bug {678695} Settings API
+
+
+
+
+
+
+Mozilla Bug {900551}
+
+
+
+
+
+
+
+
+
diff --git a/dom/settings/tests/test_settings_service.js b/dom/settings/tests/test_settings_service.js
new file mode 100644
index 000000000000..132877a5d4b1
--- /dev/null
+++ b/dom/settings/tests/test_settings_service.js
@@ -0,0 +1,138 @@
+"use strict";
+
+var Cu = Components.utils;
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+
+if (SpecialPowers.isMainProcess()) {
+ SpecialPowers.Cu.import("resource://gre/modules/SettingsRequestManager.jsm");
+}
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+SimpleTest.waitForExplicitFinish();
+
+XPCOMUtils.defineLazyServiceGetter(this, "SettingsService",
+ "@mozilla.org/settingsService;1",
+ "nsISettingsService");
+
+var tests = [
+ /* Callback tests */
+ function() {
+ let callbackCount = 10;
+
+ let callback = {
+ handle: function(name, result) {
+ switch (callbackCount) {
+ case 10:
+ case 9:
+ is(result, true, "result is true");
+ break;
+ case 8:
+ case 7:
+ is(result, false, "result is false");
+ break;
+ case 6:
+ case 5:
+ is(result, 9, "result is 9");
+ break;
+ case 4:
+ case 3:
+ is(result, 9.4, "result is 9.4");
+ break;
+ case 2:
+ is(result, false, "result is false");
+ break;
+ case 1:
+ is(result, null, "result is null");
+ break;
+ default:
+ ok(false, "Unexpected call: " + callbackCount);
+ }
+
+ --callbackCount;
+ if (callbackCount === 0) {
+ next();
+ }
+ },
+
+ handleError: function(name) {
+ ok(false, "error: " + name);
+ }
+ };
+
+ let lock = SettingsService.createLock();
+ let lock1 = SettingsService.createLock();
+
+ lock.set("asdf", true, callback, null);
+ lock1.get("asdf", callback);
+ lock.get("asdf", callback);
+ lock.set("asdf", false, callback, null);
+ lock.get("asdf", callback);
+ lock.set("int", 9, callback, null);
+ lock.get("int", callback);
+ lock.set("doub", 9.4, callback, null);
+ lock.get("doub", callback);
+ lock1.get("asdfxxx", callback);
+ },
+
+ /* Observer tests */
+ function() {
+ const MOZSETTINGS_CHANGED = "mozsettings-changed";
+ const TEST_OBSERVER_KEY = "test.observer.key";
+ const TEST_OBSERVER_VALUE = true;
+ const TEST_OBSERVER_MESSAGE = "test.observer.message";
+
+ var obs = {
+ observe: function (subject, topic, data) {
+
+ if (topic !== MOZSETTINGS_CHANGED) {
+ ok(false, "Event is not mozsettings-changed.");
+ return;
+ }
+ // Data is now stored in subject
+ if ("wrappedJSObject" in subject) {
+ ok(true, "JS object wrapped into subject");
+ subject = subject.wrappedJSObject;
+ }
+ if (subject["key"] != TEST_OBSERVER_KEY) {
+ return;
+ }
+
+ function checkProp(name, type, value) {
+ ok(name in subject, "subject." + name + " is present");
+ is(typeof subject[name], type, "subject." + name + " is " + type);
+ is(subject[name], value, "subject." + name + " is " + value);
+ }
+
+ checkProp("key", "string", TEST_OBSERVER_KEY);
+ checkProp("value", "boolean", TEST_OBSERVER_VALUE);
+ checkProp("isInternalChange", "boolean", true);
+
+ Services.obs.removeObserver(this, MOZSETTINGS_CHANGED);
+ next();
+ }
+ };
+
+ Services.obs.addObserver(obs, MOZSETTINGS_CHANGED, false);
+
+ let lock = SettingsService.createLock();
+ lock.set(TEST_OBSERVER_KEY, TEST_OBSERVER_VALUE, null);
+ }
+];
+
+function next() {
+ let step = tests.shift();
+ if (step) {
+ try {
+ step();
+ } catch(e) {
+ ok(false, "Test threw: " + e);
+ }
+ } else {
+ SimpleTest.finish();
+ }
+}
+
+next();
diff --git a/dom/settings/tests/test_settings_service.xul b/dom/settings/tests/test_settings_service.xul
new file mode 100644
index 000000000000..58a9efad96d2
--- /dev/null
+++ b/dom/settings/tests/test_settings_service.xul
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+ Mozilla Bug 678695
+
+
+
+
diff --git a/dom/settings/tests/test_settings_service_callback.js b/dom/settings/tests/test_settings_service_callback.js
new file mode 100644
index 000000000000..a780bb9c329d
--- /dev/null
+++ b/dom/settings/tests/test_settings_service_callback.js
@@ -0,0 +1,47 @@
+"use strict";
+
+var Cu = Components.utils;
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+SimpleTest.waitForExplicitFinish();
+
+XPCOMUtils.defineLazyServiceGetter(this, "SettingsService",
+ "@mozilla.org/settingsService;1",
+ "nsISettingsService");
+
+var tests = [
+ function () {
+ let callback = {
+ handle: function() {
+ ok(true, "handle called!");
+ next();
+ },
+
+ handleAbort: function(name) {
+ ok(false, "error: " + name);
+ next();
+ }
+ }
+ let lock = SettingsService.createLock(callback);
+ lock.set("xasdf", true, null, null);
+ }
+];
+
+function next() {
+ let step = tests.shift();
+ if (step) {
+ try {
+ step();
+ } catch(e) {
+ ok(false, "Test threw: " + e);
+ }
+ } else {
+ SimpleTest.finish();
+ }
+}
+
+next();
diff --git a/dom/settings/tests/test_settings_service_callback.xul b/dom/settings/tests/test_settings_service_callback.xul
new file mode 100644
index 000000000000..3e4d27751501
--- /dev/null
+++ b/dom/settings/tests/test_settings_service_callback.xul
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+ Mozilla Bug 1012214
+
+
+
+
diff --git a/dom/settings/tests/unit/test_settingsrequestmanager_messages.js b/dom/settings/tests/unit/test_settingsrequestmanager_messages.js
new file mode 100644
index 000000000000..e5fb0847558d
--- /dev/null
+++ b/dom/settings/tests/unit/test_settingsrequestmanager_messages.js
@@ -0,0 +1,174 @@
+"use strict";
+
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
+ "@mozilla.org/childprocessmessagemanager;1",
+ "nsIMessageSender");
+
+var principal = Services.scriptSecurityManager.getSystemPrincipal();
+var lockID = "{435d2192-4f21-48d4-90b7-285f147a56be}";
+
+// Helper to start the Settings Request Manager
+function startSettingsRequestManager() {
+ Cu.import("resource://gre/modules/SettingsRequestManager.jsm");
+}
+
+function handlerHelper(reply, callback, runNext = true) {
+ let handler = {
+ receiveMessage: function(message) {
+ if (message.name === reply) {
+ cpmm.removeMessageListener(reply, handler);
+ callback(message);
+ if (runNext) {
+ run_next_test();
+ }
+ }
+ }
+ };
+ cpmm.addMessageListener(reply, handler);
+}
+
+// Helper function to add a listener, send message and treat the reply
+function addAndSend(msg, reply, callback, payload, runNext = true) {
+ handlerHelper(reply, callback, runNext);
+ cpmm.sendAsyncMessage(msg, payload, undefined, principal);
+}
+
+function errorHandler(reply, str) {
+ let errHandler = function(message) {
+ ok(true, str);
+ };
+
+ handlerHelper(reply, errHandler);
+}
+
+// We need to trigger a Settings:Run message to make the queue progress
+function send_settingsRun() {
+ let msg = {lockID: lockID, isServiceLock: true};
+ cpmm.sendAsyncMessage("Settings:Run", msg, undefined, principal);
+}
+
+function kill_child() {
+ let msg = {lockID: lockID, isServiceLock: true};
+ cpmm.sendAsyncMessage("child-process-shutdown", msg, undefined, principal);
+}
+
+function run_test() {
+ do_get_profile();
+ startSettingsRequestManager();
+ run_next_test();
+}
+
+add_test(function test_createLock() {
+ let msg = {lockID: lockID, isServiceLock: true};
+ cpmm.sendAsyncMessage("Settings:CreateLock", msg, undefined, principal);
+ cpmm.sendAsyncMessage(
+ "Settings:RegisterForMessages", undefined, undefined, principal);
+ ok(true);
+ run_next_test();
+});
+
+add_test(function test_get_empty() {
+ let requestID = 10;
+ let msgReply = "Settings:Get:OK";
+ let msgHandler = function(message) {
+ equal(requestID, message.data.requestID);
+ equal(lockID, message.data.lockID);
+ ok(Object.keys(message.data.settings).length >= 0);
+ };
+
+ errorHandler("Settings:Get:KO", "Settings GET failed");
+
+ addAndSend("Settings:Get", msgReply, msgHandler, {
+ requestID: requestID,
+ lockID: lockID,
+ name: "language.current"
+ });
+
+ send_settingsRun();
+});
+
+add_test(function test_set_get_nonempty() {
+ let settings = { "language.current": "fr-FR:XPC" };
+ let requestIDSet = 20;
+ let msgReplySet = "Settings:Set:OK";
+ let msgHandlerSet = function(message) {
+ equal(requestIDSet, message.data.requestID);
+ equal(lockID, message.data.lockID);
+ };
+
+ errorHandler("Settings:Set:KO", "Settings SET failed");
+
+ addAndSend("Settings:Set", msgReplySet, msgHandlerSet, {
+ requestID: requestIDSet,
+ lockID: lockID,
+ settings: settings
+ }, false);
+
+ let requestIDGet = 25;
+ let msgReplyGet = "Settings:Get:OK";
+ let msgHandlerGet = function(message) {
+ equal(requestIDGet, message.data.requestID);
+ equal(lockID, message.data.lockID);
+ for(let p in settings) {
+ equal(settings[p], message.data.settings[p]);
+ }
+ };
+
+ addAndSend("Settings:Get", msgReplyGet, msgHandlerGet, {
+ requestID: requestIDGet,
+ lockID: lockID,
+ name: Object.keys(settings)[0]
+ });
+
+ // Set and Get have been push into the queue, let's run
+ send_settingsRun();
+});
+
+// This test exposes bug 1076597 behavior
+add_test(function test_wait_for_finalize() {
+ let settings = { "language.current": "en-US:XPC" };
+ let requestIDSet = 30;
+ let msgReplySet = "Settings:Set:OK";
+ let msgHandlerSet = function(message) {
+ equal(requestIDSet, message.data.requestID);
+ equal(lockID, message.data.lockID);
+ };
+
+ errorHandler("Settings:Set:KO", "Settings SET failed");
+
+ addAndSend("Settings:Set", msgReplySet, msgHandlerSet, {
+ requestID: requestIDSet,
+ lockID: lockID,
+ settings: settings
+ }, false);
+
+ let requestIDGet = 35;
+ let msgReplyGet = "Settings:Get:OK";
+ let msgHandlerGet = function(message) {
+ equal(requestIDGet, message.data.requestID);
+ equal(lockID, message.data.lockID);
+ for(let p in settings) {
+ equal(settings[p], message.data.settings[p]);
+ }
+ };
+
+ errorHandler("Settings:Get:KO", "Settings GET failed");
+
+ addAndSend("Settings:Get", msgReplyGet, msgHandlerGet, {
+ requestID: requestIDGet,
+ lockID: lockID,
+ name: Object.keys(settings)[0]
+ });
+
+ // We simulate a child death, which will force previous requests to be set
+ // into finalize state
+ kill_child();
+
+ // Then when we issue Settings:Run, those finalized should be triggered
+ send_settingsRun();
+});
diff --git a/dom/settings/tests/unit/xpcshell.ini b/dom/settings/tests/unit/xpcshell.ini
new file mode 100644
index 000000000000..9669a1ed0911
--- /dev/null
+++ b/dom/settings/tests/unit/xpcshell.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+head =
+tail =
+
+[test_settingsrequestmanager_messages.js]
+skip-if = (buildapp != 'b2g')
diff --git a/dom/system/NetworkGeolocationProvider.js b/dom/system/NetworkGeolocationProvider.js
index 2e78f6425da4..ea2abe55f34b 100644
--- a/dom/system/NetworkGeolocationProvider.js
+++ b/dom/system/NetworkGeolocationProvider.js
@@ -12,6 +12,9 @@ const Cc = Components.classes;
const Cu = Components.utils;
const POSITION_UNAVAILABLE = Ci.nsIDOMGeoPositionError.POSITION_UNAVAILABLE;
+const SETTINGS_DEBUG_ENABLED = "geolocation.debugging.enabled";
+const SETTINGS_CHANGED_TOPIC = "mozsettings-changed";
+const SETTINGS_WIFI_ENABLED = "wifi.enabled";
var gLoggingEnabled = false;
@@ -252,6 +255,24 @@ WifiGeoPositionProvider.prototype = {
Ci.nsIObserver]),
listener: null,
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic != SETTINGS_CHANGED_TOPIC) {
+ return;
+ }
+
+ try {
+ if ("wrappedJSObject" in aSubject) {
+ aSubject = aSubject.wrappedJSObject;
+ }
+ if (aSubject.key == SETTINGS_DEBUG_ENABLED) {
+ gLoggingEnabled = aSubject.value;
+ } else if (aSubject.key == SETTINGS_WIFI_ENABLED) {
+ gWifiScanningEnabled = aSubject.value;
+ }
+ } catch (e) {
+ }
+ },
+
resetTimer: function() {
if (this.timer) {
this.timer.cancel();
@@ -270,6 +291,37 @@ WifiGeoPositionProvider.prototype = {
this.started = true;
let self = this;
+ let settingsCallback = {
+ handle: function(name, result) {
+ // Stop the B2G UI setting from overriding the js prefs setting, and turning off logging
+ // If gLoggingEnabled is already on during startup, that means it was set in js prefs.
+ if (name == SETTINGS_DEBUG_ENABLED && !gLoggingEnabled) {
+ gLoggingEnabled = result;
+ } else if (name == SETTINGS_WIFI_ENABLED) {
+ gWifiScanningEnabled = result;
+ if (self.wifiService) {
+ self.wifiService.stopWatching(self);
+ }
+ if (gWifiScanningEnabled) {
+ self.wifiService = Cc["@mozilla.org/wifi/monitor;1"].getService(Ci.nsIWifiMonitor);
+ self.wifiService.startWatching(self);
+ }
+ }
+ },
+
+ handleError: function(message) {
+ gLoggingEnabled = false;
+ LOG("settings callback threw an exception, dropping");
+ }
+ };
+
+ Services.obs.addObserver(this, SETTINGS_CHANGED_TOPIC, false);
+ let settingsService = Cc["@mozilla.org/settingsService;1"];
+ if (settingsService) {
+ let settings = settingsService.getService(Ci.nsISettingsService);
+ settings.createLock().get(SETTINGS_WIFI_ENABLED, settingsCallback);
+ settings.createLock().get(SETTINGS_DEBUG_ENABLED, settingsCallback);
+ }
if (gWifiScanningEnabled && Cc["@mozilla.org/wifi/monitor;1"]) {
if (this.wifiService) {
@@ -307,6 +359,8 @@ WifiGeoPositionProvider.prototype = {
this.wifiService = null;
}
+ Services.obs.removeObserver(this, SETTINGS_CHANGED_TOPIC);
+
this.listener = null;
this.started = false;
},
diff --git a/dom/tests/mochitest/general/test_interfaces.html b/dom/tests/mochitest/general/test_interfaces.html
index 5c296e646109..0d51a2e628d2 100644
--- a/dom/tests/mochitest/general/test_interfaces.html
+++ b/dom/tests/mochitest/general/test_interfaces.html
@@ -867,6 +867,8 @@ var interfaceNamesInGlobalScope =
"ServiceWorkerMessageEvent",
// IMPORTANT: Do not change this list without review from a DOM peer!
"ServiceWorkerRegistration",
+// IMPORTANT: Do not change this list without review from a DOM peer!
+ "SettingsLock",
// IMPORTANT: Do not change this list without review from a DOM peer!
"ShadowRoot", // Bogus, but the test harness forces it on. See bug 1159768.
// IMPORTANT: Do not change this list without review from a DOM peer!
diff --git a/dom/webidl/MozSettingsEvent.webidl b/dom/webidl/MozSettingsEvent.webidl
new file mode 100644
index 000000000000..6ca596a4c748
--- /dev/null
+++ b/dom/webidl/MozSettingsEvent.webidl
@@ -0,0 +1,19 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/.
+ */
+
+[Constructor(DOMString type, optional MozSettingsEventInit eventInitDict),
+ ChromeOnly]
+interface MozSettingsEvent : Event
+{
+ readonly attribute DOMString? settingName;
+ readonly attribute any settingValue;
+};
+
+dictionary MozSettingsEventInit : EventInit
+{
+ DOMString settingName = "";
+ any settingValue = null;
+};
diff --git a/dom/webidl/MozSettingsTransactionEvent.webidl b/dom/webidl/MozSettingsTransactionEvent.webidl
new file mode 100644
index 000000000000..95aa83f433c9
--- /dev/null
+++ b/dom/webidl/MozSettingsTransactionEvent.webidl
@@ -0,0 +1,17 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/.
+ */
+
+[Constructor(DOMString type, optional MozSettingsTransactionEventInit eventInitDict),
+ ChromeOnly]
+interface MozSettingsTransactionEvent : Event
+{
+ readonly attribute DOMString? error;
+};
+
+dictionary MozSettingsTransactionEventInit : EventInit
+{
+ DOMString error = "";
+};
diff --git a/dom/webidl/SettingsManager.webidl b/dom/webidl/SettingsManager.webidl
new file mode 100644
index 000000000000..ad707a2588a5
--- /dev/null
+++ b/dom/webidl/SettingsManager.webidl
@@ -0,0 +1,42 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/.
+ */
+
+[JSImplementation="@mozilla.org/settingsLock;1",
+ Pref="dom.mozSettings.enabled"]
+interface SettingsLock : EventTarget {
+ // Whether this lock is invalid
+ readonly attribute boolean closed;
+
+ // Contains a JSON object with name/value pairs to be set.
+ DOMRequest set(object settings);
+
+ // Result contains the value of the setting.
+ DOMRequest get(DOMString name);
+
+ DOMRequest clear();
+ attribute EventHandler onsettingstransactionsuccess;
+ attribute EventHandler onsettingstransactionfailure;
+};
+
+dictionary SettingChange {
+ DOMString settingName;
+ DOMString settingValue;
+};
+
+callback SettingChangeCallback = void (SettingChange setting);
+
+[JSImplementation="@mozilla.org/settingsManager;1",
+ NavigatorProperty="mozSettings",
+ Pref="dom.mozSettings.enabled",
+ ChromeOnly]
+interface SettingsManager : EventTarget {
+ SettingsLock createLock();
+
+ void addObserver(DOMString name, SettingChangeCallback callback);
+ void removeObserver(DOMString name, SettingChangeCallback callback);
+
+ attribute EventHandler onsettingchange;
+};
diff --git a/dom/webidl/moz.build b/dom/webidl/moz.build
index 1340cc0bfd9e..4238276a6ab3 100644
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -398,6 +398,7 @@ WEBIDL_FILES = [
'ServiceWorkerGlobalScope.webidl',
'ServiceWorkerRegistration.webidl',
'SettingChangeNotification.webidl',
+ 'SettingsManager.webidl',
'ShadowRoot.webidl',
'SharedWorker.webidl',
'SharedWorkerGlobalScope.webidl',
@@ -645,6 +646,7 @@ WEBIDL_FILES += [
'DeviceOrientationEvent.webidl',
'DeviceStorageChangeEvent.webidl',
'HashChangeEvent.webidl',
+ 'MozSettingsEvent.webidl',
'PageTransitionEvent.webidl',
'PopStateEvent.webidl',
'PopupBlockedEvent.webidl',
@@ -697,6 +699,8 @@ GENERATED_EVENTS_WEBIDL_FILES = [
'ImageCaptureErrorEvent.webidl',
'MediaStreamEvent.webidl',
'MediaStreamTrackEvent.webidl',
+ 'MozSettingsEvent.webidl',
+ 'MozSettingsTransactionEvent.webidl',
'OfflineAudioCompletionEvent.webidl',
'PageTransitionEvent.webidl',
'PerformanceEntryEvent.webidl',
diff --git a/layout/tools/reftest/b2g_start_script.js b/layout/tools/reftest/b2g_start_script.js
index 7036671a22b7..36510a47f28f 100644
--- a/layout/tools/reftest/b2g_start_script.js
+++ b/layout/tools/reftest/b2g_start_script.js
@@ -41,6 +41,10 @@ Cu.import("chrome://reftest/content/reftest.jsm", reftest);
// Prevent display off during testing.
navigator.mozPower.screenEnabled = true;
+var settingLock = navigator.mozSettings.createLock();
+var settingResult = settingLock.set({
+ 'screen.timeout': 0
+});
settingResult.onsuccess = function () {
dump("Set screen.time to 0\n");
// Start the reftests
diff --git a/mobile/android/installer/package-manifest.in b/mobile/android/installer/package-manifest.in
index d2cebd24eaa8..26397308fe66 100644
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -132,6 +132,7 @@
@BINPATH@/components/dom_quota.xpt
@BINPATH@/components/dom_range.xpt
@BINPATH@/components/dom_security.xpt
+@BINPATH@/components/dom_settings.xpt
@BINPATH@/components/dom_sidebar.xpt
@BINPATH@/components/dom_mobilemessage.xpt
@BINPATH@/components/dom_storage.xpt
@@ -270,6 +271,8 @@
@BINPATH@/components/Push.manifest
@BINPATH@/components/PushComponents.js
#endif
+@BINPATH@/components/SettingsManager.js
+@BINPATH@/components/SettingsManager.manifest
@BINPATH@/components/BrowserElementParent.manifest
@BINPATH@/components/BrowserElementParent.js
@BINPATH@/components/FeedProcessor.manifest
diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js
index a2af7c373144..0425c1f39482 100644
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -4846,6 +4846,9 @@ pref("dom.push.http2.reset_retry_count_after_ms", 60000);
pref("dom.push.http2.maxRetries", 2);
pref("dom.push.http2.retryInterval", 5000);
+// WebSettings
+pref("dom.mozSettings.enabled", false);
+
// W3C touch events
// 0 - disabled, 1 - enabled, 2 - autodetect
// Autodetection is currently only supported on Windows and GTK3
@@ -5284,6 +5287,23 @@ pref("intl.allow-insecure-text-input", false);
// Enable meta-viewport support in remote APZ-enabled frames.
pref("dom.meta-viewport.enabled", false);
+// MozSettings debugging prefs for each component
+pref("dom.mozSettings.SettingsDB.debug.enabled", false);
+pref("dom.mozSettings.SettingsManager.debug.enabled", false);
+pref("dom.mozSettings.SettingsRequestManager.debug.enabled", false);
+pref("dom.mozSettings.SettingsService.debug.enabled", false);
+
+// MozSettings verbose mode to track everything
+pref("dom.mozSettings.SettingsDB.verbose.enabled", false);
+pref("dom.mozSettings.SettingsManager.verbose.enabled", false);
+pref("dom.mozSettings.SettingsRequestManager.verbose.enabled", false);
+pref("dom.mozSettings.SettingsService.verbose.enabled", false);
+
+// Controlling whether we want to allow forcing some Settings
+// IndexedDB transactions to be opened as readonly or keep everything as
+// readwrite.
+pref("dom.mozSettings.allowForceReadOnly", false);
+
// The interval at which to check for slow running addons
#ifdef NIGHTLY_BUILD
pref("browser.addon-watch.interval", 15000);
diff --git a/testing/profiles/prefs_general.js b/testing/profiles/prefs_general.js
index b8322fe95ee8..b267beb468ea 100644
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -193,6 +193,12 @@ user_pref("layout.spammy_warnings.enabled", false);
user_pref("media.mediasource.mp4.enabled", true);
user_pref("media.mediasource.webm.enabled", true);
+// Enable mozContacts
+user_pref("dom.mozContacts.enabled", true);
+
+// Enable mozSettings
+user_pref("dom.mozSettings.enabled", true);
+
// Make sure the disk cache doesn't get auto disabled
user_pref("network.http.bypass-cachelock-threshold", 200000);
diff --git a/tools/lint/eslint/modules.json b/tools/lint/eslint/modules.json
index cd5e69569c5d..19ff25aec6de 100644
--- a/tools/lint/eslint/modules.json
+++ b/tools/lint/eslint/modules.json
@@ -195,6 +195,7 @@
"scratchpad-manager.jsm": ["ScratchpadManager"],
"server.js": ["MarionetteServer"],
"service.js": ["Service"],
+ "SettingsDB.jsm": ["SettingsDB", "SETTINGSDB_NAME", "SETTINGSSTORE_NAME"],
"SharedPromptUtils.jsm": ["PromptUtils", "EnableDelayHelper"],
"ShutdownLeaksCollector.jsm": ["ContentCollector"],
"SignInToWebsite.jsm": ["SignInToWebsiteController"],
@@ -229,7 +230,7 @@
"userapi.js": ["UserAPI10Client"],
"util.js": ["getChromeWindow", "XPCOMUtils", "Services", "Utils", "Async", "Svc", "Str"],
"utils.js": ["applicationName", "assert", "Copy", "getBrowserObject", "getChromeWindow", "getWindows", "getWindowByTitle", "getWindowByType", "getWindowId", "getMethodInWindows", "getPreference", "saveDataURL", "setPreference", "sleep", "startTimer", "stopTimer", "takeScreenshot", "unwrapNode", "waitFor", "btoa", "encryptPayload", "isConfiguredWithLegacyIdentity", "ensureLegacyIdentityManager", "setBasicCredentials", "makeIdentityConfig", "makeFxAccountsInternalMock", "configureFxAccountIdentity", "configureIdentity", "SyncTestingInfrastructure", "waitForZeroTimer", "Promise", "add_identity_test", "MockFxaStorageManager", "AccountState", "sumHistogram", "CommonUtils", "CryptoUtils", "TestingUtils"],
- "Utils.jsm": ["Utils", "Logger", "PivotContext", "PrefCache"],
+ "Utils.jsm": ["Utils", "Logger", "PivotContext", "PrefCache", "SettingCache"],
"VariablesView.jsm": ["VariablesView", "escapeHTML"],
"VariablesViewController.jsm": ["VariablesViewController", "StackFrameUtils"],
"version.jsm": ["VERSION"],