Bug 1299775 - Implement chrome.idle.onStateChanged, r?aswan
MozReview-Commit-ID: 2M5prJYDdqe
--- a/toolkit/components/extensions/ext-idle.js
+++ b/toolkit/components/extensions/ext-idle.js
@@ -1,18 +1,94 @@
"use strict";
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
+ "resource://devtools/shared/event-emitter.js");
XPCOMUtils.defineLazyServiceGetter(this, "idleService",
"@mozilla.org/widget/idleservice;1",
"nsIIdleService");
+const {
+ SingletonEventManager,
+} = ExtensionUtils;
+
+// WeakMap[Extension -> Object]
+var observersMap = new WeakMap();
+
+function getObserverInfo(extension, context) {
+ let observerInfo = observersMap.get(extension);
+ if (!observerInfo) {
+ observerInfo = {
+ observer: null,
+ detectionInterval: 60,
+ };
+ observersMap.set(extension, observerInfo);
+ context.callOnClose({
+ close: () => {
+ let {observer, detectionInterval} = observersMap.get(extension);
+ if (observer) {
+ idleService.removeIdleObserver(observer, detectionInterval);
+ }
+ observersMap.delete(extension);
+ },
+ });
+ }
+ return observerInfo;
+}
+
+function getObserver(extension, context) {
+ let observerInfo = getObserverInfo(extension, context);
+ let {observer, detectionInterval} = observerInfo;
+ if (!observer) {
+ observer = {
+ observe: function(subject, topic, data) {
+ if (topic == "idle" || topic == "active") {
+ this.emit("stateChanged", topic);
+ }
+ },
+ };
+ EventEmitter.decorate(observer);
+ idleService.addIdleObserver(observer, detectionInterval);
+ observerInfo.observer = observer;
+ observerInfo.detectionInterval = detectionInterval;
+ }
+ return observer;
+}
+
+function setDetectionInterval(extension, context, newInterval) {
+ let observerInfo = getObserverInfo(extension, context);
+ let {observer, detectionInterval} = observerInfo;
+ if (observer) {
+ idleService.removeIdleObserver(observer, detectionInterval);
+ idleService.addIdleObserver(observer, newInterval);
+ }
+ observerInfo.detectionInterval = newInterval;
+}
extensions.registerSchemaAPI("idle", "addon_parent", context => {
+ let {extension} = context;
return {
idle: {
queryState: function(detectionIntervalInSeconds) {
if (idleService.idleTime < detectionIntervalInSeconds * 1000) {
return Promise.resolve("active");
}
return Promise.resolve("idle");
},
+ setDetectionInterval: function(detectionIntervalInSeconds) {
+ setDetectionInterval(extension, context, detectionIntervalInSeconds);
+ },
+ onStateChanged: new SingletonEventManager(context, "idle.onStateChanged", fire => {
+ let listener = (event, data) => {
+ context.runSafe(fire, data);
+ };
+
+ getObserver(extension, context).on("stateChanged", listener);
+ return () => {
+ getObserver(extension, context).off("stateChanged", listener);
+ };
+ }).api(),
},
};
});
--- a/toolkit/components/extensions/schemas/idle.json
+++ b/toolkit/components/extensions/schemas/idle.json
@@ -6,24 +6,24 @@
{
"namespace": "idle",
"description": "Use the <code>browser.idle</code> API to detect when the machine's idle state changes.",
"permissions": ["idle"],
"types": [
{
"id": "IdleState",
"type": "string",
- "enum": ["active", "idle", "locked"]
+ "enum": ["active", "idle"]
}
],
"functions": [
{
"name": "queryState",
"type": "function",
- "description": "Returns \"locked\" if the system is locked, \"idle\" if the user has not generated any input for a specified number of seconds, or \"active\" otherwise.",
+ "description": "Returns \"idle\" if the user has not generated any input for a specified number of seconds, or \"active\" otherwise.",
"async": "callback",
"parameters": [
{
"name": "detectionIntervalInSeconds",
"type": "integer",
"minimum": 15,
"description": "The system is considered idle if detectionIntervalInSeconds seconds have elapsed since the last user input detected."
},
@@ -36,35 +36,33 @@
"$ref": "IdleState"
}
]
}
]
},
{
"name": "setDetectionInterval",
- "unsupported": true,
"type": "function",
"description": "Sets the interval, in seconds, used to determine when the system is in an idle state for onStateChanged events. The default interval is 60 seconds.",
"parameters": [
{
"name": "intervalInSeconds",
"type": "integer",
"minimum": 15,
"description": "Threshold, in seconds, used to determine when the system is in an idle state."
}
]
}
],
"events": [
{
"name": "onStateChanged",
- "unsupported": true,
"type": "function",
- "description": "Fired when the system changes to an active, idle or locked state. The event fires with \"locked\" if the screen is locked or the screensaver activates, \"idle\" if the system is unlocked and the user has not generated any input for a specified number of seconds, and \"active\" when the user generates input on an idle system.",
+ "description": "Fired when the system changes to an active or idle state. The event fires with \"idle\" if the the user has not generated any input for a specified number of seconds, and \"active\" when the user generates input on an idle system.",
"parameters": [
{
"name": "newState",
"$ref": "IdleState"
}
]
}
]
--- a/toolkit/components/extensions/test/mochitest/chrome.ini
+++ b/toolkit/components/extensions/test/mochitest/chrome.ini
@@ -21,8 +21,9 @@ skip-if = os != "mac" && os != "linux"
[test_ext_cookies_expiry.html]
skip-if = buildapp == 'b2g'
[test_ext_cookies_permissions.html]
skip-if = buildapp == 'b2g'
[test_ext_jsversion.html]
skip-if = buildapp == 'b2g'
[test_ext_schema.html]
[test_chrome_ext_storage_cleanup.html]
+[test_chrome_ext_idle.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_idle.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="chrome_head.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+const idleService = Cc["@mozilla.org/widget/idleservice;1"].getService(Ci.nsIIdleService);
+
+add_task(function* testWithRealIdleService() {
+ function background() {
+ browser.test.onMessage.addListener((msg, ...args) => {
+ let detectionInterval = args[0];
+ if (msg == "addListener") {
+ browser.idle.queryState(detectionInterval).then(status => {
+ browser.test.assertEq("active", status, "Idle status is active");
+ });
+ browser.idle.setDetectionInterval(detectionInterval);
+ browser.idle.onStateChanged.addListener(newState => {
+ browser.test.assertEq("idle", newState, "listener fired with the expected state");
+ browser.test.sendMessage("listenerFired");
+ });
+ } else if (msg == "checkState") {
+ browser.idle.queryState(detectionInterval).then(status => {
+ browser.test.assertEq("idle", status, "Idle status is idle");
+ browser.test.notifyPass("idle");
+ });
+ }
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["idle"],
+ },
+ });
+
+ yield extension.startup();
+ let idleTime = idleService.idleTime;
+ let detectionInterval = Math.max(Math.ceil(idleTime / 1000) + 2, 15);
+ info(`idleTime: ${idleTime}, detectionInterval: ${detectionInterval}`);
+ extension.sendMessage("addListener", detectionInterval);
+ info("Listener added");
+ yield extension.awaitMessage("listenerFired");
+ info("Listener fired");
+ extension.sendMessage("checkState", detectionInterval);
+ yield extension.awaitFinish("idle");
+ yield extension.unload();
+});
+
+</script>
+
+</body>
+</html>
--- a/toolkit/components/extensions/test/xpcshell/test_ext_idle.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_idle.js
@@ -1,27 +1,64 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
Cu.import("resource://testing-common/MockRegistrar.jsm");
let idleService = {
+ _observers: new Set(),
+ _activity: {
+ addCalls: [],
+ removeCalls: [],
+ observerFires: [],
+ },
+ _reset: function() {
+ this._observers.clear();
+ this._activity.addCalls = [];
+ this._activity.removeCalls = [];
+ this._activity.observerFires = [];
+ },
+ _fireObservers: function(state) {
+ for (let observer of this._observers.values()) {
+ observer.observe(observer, state, null);
+ this._activity.observerFires.push(state);
+ }
+ },
QueryInterface: XPCOMUtils.generateQI([Ci.nsIIdleService]),
idleTime: 19999,
+ addIdleObserver: function(observer, time) {
+ this._observers.add(observer);
+ this._activity.addCalls.push(time);
+ },
+ removeIdleObserver: function(observer, time) {
+ this._observers.delete(observer);
+ this._activity.removeCalls.push(time);
+ },
};
+function checkActivity(expectedActivity) {
+ let {expectedAdd, expectedRemove, expectedFires} = expectedActivity;
+ let {addCalls, removeCalls, observerFires} = idleService._activity;
+ equal(expectedAdd.length, addCalls.length, "idleService.addIdleObserver was called the expected number of times");
+ equal(expectedRemove.length, removeCalls.length, "idleService.removeIdleObserver was called the expected number of times");
+ equal(expectedFires.length, observerFires.length, "idle observer was fired the expected number of times");
+ deepEqual(addCalls, expectedAdd, "expected interval passed to idleService.addIdleObserver");
+ deepEqual(removeCalls, expectedRemove, "expected interval passed to idleService.removeIdleObserver");
+ deepEqual(observerFires, expectedFires, "expected topic passed to idle observer");
+}
+
add_task(function* setup() {
let fakeIdleService = MockRegistrar.register("@mozilla.org/widget/idleservice;1", idleService);
do_register_cleanup(() => {
MockRegistrar.unregister(fakeIdleService);
});
});
-add_task(function* testIdleActive() {
+add_task(function* testQueryStateActive() {
function background() {
browser.idle.queryState(20).then(status => {
browser.test.assertEq("active", status, "Idle status is active");
browser.test.notifyPass("idle");
},
err => {
browser.test.fail(`Error: ${err} :: ${err.stack}`);
browser.test.notifyFail("idle");
@@ -35,17 +72,17 @@ add_task(function* testIdleActive() {
},
});
yield extension.startup();
yield extension.awaitFinish("idle");
yield extension.unload();
});
-add_task(function* testIdleIdle() {
+add_task(function* testQueryStateIdle() {
function background() {
browser.idle.queryState(15).then(status => {
browser.test.assertEq("idle", status, "Idle status is idle");
browser.test.notifyPass("idle");
},
err => {
browser.test.fail(`Error: ${err} :: ${err.stack}`);
browser.test.notifyFail("idle");
@@ -58,8 +95,108 @@ add_task(function* testIdleIdle() {
permissions: ["idle"],
},
});
yield extension.startup();
yield extension.awaitFinish("idle");
yield extension.unload();
});
+
+add_task(function* testOnlySetDetectionInterval() {
+ function background() {
+ browser.idle.setDetectionInterval(99);
+ browser.test.sendMessage("detectionIntervalSet");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["idle"],
+ },
+ });
+
+ idleService._reset();
+ yield extension.startup();
+ yield extension.awaitMessage("detectionIntervalSet");
+ idleService._fireObservers("idle");
+ checkActivity({expectedAdd: [], expectedRemove: [], expectedFires: []});
+ yield extension.unload();
+});
+
+add_task(function* testSetDetectionIntervalBeforeAddingListener() {
+ function background() {
+ browser.idle.setDetectionInterval(99);
+ browser.idle.onStateChanged.addListener(newState => {
+ browser.test.assertEq("idle", newState, "listener fired with the expected state");
+ browser.test.sendMessage("listenerFired");
+ });
+ browser.test.sendMessage("listenerAdded");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["idle"],
+ },
+ });
+
+ idleService._reset();
+ yield extension.startup();
+ yield extension.awaitMessage("listenerAdded");
+ idleService._fireObservers("idle");
+ yield extension.awaitMessage("listenerFired");
+ checkActivity({expectedAdd: [99], expectedRemove: [], expectedFires: ["idle"]});
+ yield extension.unload();
+});
+
+add_task(function* testSetDetectionIntervalAfterAddingListener() {
+ function background() {
+ browser.idle.onStateChanged.addListener(newState => {
+ browser.test.assertEq("idle", newState, "listener fired with the expected state");
+ browser.test.sendMessage("listenerFired");
+ });
+ browser.idle.setDetectionInterval(99);
+ browser.test.sendMessage("detectionIntervalSet");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["idle"],
+ },
+ });
+
+ idleService._reset();
+ yield extension.startup();
+ yield extension.awaitMessage("detectionIntervalSet");
+ idleService._fireObservers("idle");
+ yield extension.awaitMessage("listenerFired");
+ checkActivity({expectedAdd: [60, 99], expectedRemove: [60], expectedFires: ["idle"]});
+ yield extension.unload();
+});
+
+add_task(function* testOnlyAddingListener() {
+ function background() {
+ browser.idle.onStateChanged.addListener(newState => {
+ browser.test.assertEq("active", newState, "listener fired with the expected state");
+ browser.test.sendMessage("listenerFired");
+ });
+ browser.test.sendMessage("listenerAdded");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["idle"],
+ },
+ });
+
+ idleService._reset();
+ yield extension.startup();
+ yield extension.awaitMessage("listenerAdded");
+ idleService._fireObservers("active");
+ yield extension.awaitMessage("listenerFired");
+ // check that "idle-daily" topic does not cause a listener to fire
+ idleService._fireObservers("idle-daily");
+ checkActivity({expectedAdd: [60], expectedRemove: [], expectedFires: ["active", "idle-daily"]});
+ yield extension.unload();
+});