--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -517,16 +517,20 @@ class Tab extends TabBase {
} else if (nativeTab.muteReason) {
mutedInfo.reason = "extension";
mutedInfo.extensionId = nativeTab.muteReason;
}
return mutedInfo;
}
+ get lastAccessed() {
+ return this.nativeTab.lastAccessed;
+ }
+
get pinned() {
return this.nativeTab.pinned;
}
get active() {
return this.nativeTab.selected;
}
@@ -573,16 +577,17 @@ class Tab extends TabBase {
let result = {
sessionId: String(tabData.closedId),
index: tabData.pos ? tabData.pos : 0,
windowId: window && windowTracker.getId(window),
highlighted: false,
active: false,
pinned: false,
incognito: Boolean(tabData.state && tabData.state.isPrivate),
+ lastAccessed: tabData.state ? tabData.state.lastAccessed : tabData.lastAccessed,
};
if (extension.tabManager.hasTabPermission(tabData)) {
let entries = tabData.state ? tabData.state.entries : tabData.entries;
let entry = entries[entries.length - 1];
result.url = entry.url;
result.title = entry.title;
if (tabData.image) {
--- a/browser/components/extensions/schemas/tabs.json
+++ b/browser/components/extensions/schemas/tabs.json
@@ -59,16 +59,17 @@
"id": {"type": "integer", "minimum": -1, "optional": true, "description": "The ID of the tab. Tab IDs are unique within a browser session. Under some circumstances a Tab may not be assigned an ID, for example when querying foreign tabs using the $(ref:sessions) API, in which case a session ID may be present. Tab ID can also be set to $(ref:tabs.TAB_ID_NONE) for apps and devtools windows."},
"index": {"type": "integer", "minimum": -1, "description": "The zero-based index of the tab within its window."},
"windowId": {"type": "integer", "minimum": 0, "description": "The ID of the window the tab is contained within."},
"openerTabId": {"unsupported": true, "type": "integer", "minimum": 0, "optional": true, "description": "The ID of the tab that opened this tab, if any. This property is only present if the opener tab still exists."},
"selected": {"type": "boolean", "description": "Whether the tab is selected.", "deprecated": "Please use $(ref:tabs.Tab.highlighted).", "unsupported": true},
"highlighted": {"type": "boolean", "description": "Whether the tab is highlighted. Works as an alias of active"},
"active": {"type": "boolean", "description": "Whether the tab is active in its window. (Does not necessarily mean the window is focused.)"},
"pinned": {"type": "boolean", "description": "Whether the tab is pinned."},
+ "lastAccessed": {"type": "integer", "optional": true, "description": "The last time the tab was accessed as the number of milliseconds since epoch."},
"audible": {"type": "boolean", "optional": true, "description": "Whether the tab has produced sound over the past couple of seconds (but it might not be heard if also muted). Equivalent to whether the speaker audio indicator is showing."},
"mutedInfo": {"$ref": "MutedInfo", "optional": true, "description": "Current tab muted state and the reason for the last state change."},
"url": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The URL the tab is displaying. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission."},
"title": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The title of the tab. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission."},
"favIconUrl": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The URL of the tab's favicon. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission. It may also be an empty string if the tab is loading."},
"status": {"type": "string", "optional": true, "description": "Either <em>loading</em> or <em>complete</em>."},
"incognito": {"type": "boolean", "description": "Whether the tab is in an incognito window."},
"width": {"type": "integer", "optional": true, "description": "The width of the tab in pixels."},
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -118,16 +118,17 @@ skip-if = debug || asan # Bug 1354681
[browser_ext_tabs_executeScript.js]
[browser_ext_tabs_executeScript_good.js]
[browser_ext_tabs_executeScript_bad.js]
[browser_ext_tabs_executeScript_multiple.js]
[browser_ext_tabs_executeScript_no_create.js]
[browser_ext_tabs_executeScript_runAt.js]
[browser_ext_tabs_getCurrent.js]
[browser_ext_tabs_insertCSS.js]
+[browser_ext_tabs_lastAccessed.js]
[browser_ext_tabs_removeCSS.js]
[browser_ext_tabs_move_array.js]
[browser_ext_tabs_move_window.js]
[browser_ext_tabs_move_window_multiple.js]
[browser_ext_tabs_move_window_pinned.js]
[browser_ext_tabs_onHighlighted.js]
[browser_ext_tabs_onUpdated.js]
[browser_ext_tabs_query.js]
--- a/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_tabs.js
+++ b/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_tabs.js
@@ -38,42 +38,49 @@ add_task(async function test_sessions_ge
});
let win = await BrowserTestUtils.openNewBrowserWindow();
await BrowserTestUtils.loadURI(win.gBrowser.selectedBrowser, "about:mozilla");
await BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
let expectedTabs = [];
let tab = win.gBrowser.selectedTab;
expectedTabs.push(expectedTabInfo(tab, win));
+ let lastAccessedTimes = new Map();
+ lastAccessedTimes.set("about:mozilla", tab.lastAccessed);
for (let url of ["about:robots", "about:buildconfig"]) {
tab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser, url);
expectedTabs.push(expectedTabInfo(tab, win));
+ lastAccessedTimes.set(url, tab.lastAccessed);
}
await extension.startup();
// Test with a closed tab.
await BrowserTestUtils.removeTab(tab);
extension.sendMessage("check-sessions");
let recentlyClosed = await extension.awaitMessage("recentlyClosed");
let tabInfo = recentlyClosed[0].tab;
let expectedTab = expectedTabs.pop();
checkTabInfo(expectedTab, tabInfo);
+ ok(tabInfo.lastAccessed > lastAccessedTimes.get(tabInfo.url),
+ "lastAccessed has been updated");
// Test with a closed window containing tabs.
await BrowserTestUtils.closeWindow(win);
extension.sendMessage("check-sessions");
recentlyClosed = await extension.awaitMessage("recentlyClosed");
let tabInfos = recentlyClosed[0].window.tabs;
is(tabInfos.length, 2, "Expected number of tabs in closed window.");
for (let x = 0; x < tabInfos.length; x++) {
checkTabInfo(expectedTabs[x], tabInfos[x]);
+ ok(tabInfos[x].lastAccessed > lastAccessedTimes.get(tabInfos[x].url),
+ "lastAccessed has been updated");
}
await extension.unload();
// Test without tabs permission.
extension = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["sessions"],
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_lastAccessed.js
@@ -0,0 +1,42 @@
+"use strict";
+
+add_task(async function testLastAccessed() {
+ let past = Date.now();
+
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/?1");
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/?2");
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ async background() {
+ browser.test.onMessage.addListener(async function(msg, past) {
+ if (msg !== "past") {
+ return;
+ }
+
+ let [tab1] = await browser.tabs.query({url: "https://example.com/?1"});
+ let [tab2] = await browser.tabs.query({url: "https://example.com/?2"});
+
+ browser.test.assertTrue(tab1 && tab2, "Expected tabs were found");
+
+ let now = Date.now();
+
+ browser.test.assertTrue(past < tab1.lastAccessed &&
+ tab1.lastAccessed < tab2.lastAccessed &&
+ tab2.lastAccessed <= now,
+ "lastAccessed timestamps are recent and in the right order");
+
+ await browser.tabs.remove([tab1.id, tab2.id]);
+
+ browser.test.notifyPass("tabs.lastAccessed");
+ });
+ },
+ });
+
+ await extension.startup();
+ await extension.sendMessage("past", past);
+ await extension.awaitFinish("tabs.lastAccessed");
+ await extension.unload();
+});
--- a/mobile/android/components/extensions/ext-utils.js
+++ b/mobile/android/components/extensions/ext-utils.js
@@ -393,16 +393,20 @@ class Tab extends TabBase {
get index() {
return this.window.BrowserApp.tabs.indexOf(this.nativeTab);
}
get mutedInfo() {
return {muted: false};
}
+ get lastAccessed() {
+ return this.nativeTab.lastTouchedAt;
+ }
+
get pinned() {
return false;
}
get active() {
return this.nativeTab.getActive();
}
--- a/mobile/android/components/extensions/schemas/tabs.json
+++ b/mobile/android/components/extensions/schemas/tabs.json
@@ -59,16 +59,17 @@
"id": {"type": "integer", "minimum": -1, "optional": true, "description": "The ID of the tab. Tab IDs are unique within a browser session. Under some circumstances a Tab may not be assigned an ID, for example when querying foreign tabs using the $(ref:sessions) API, in which case a session ID may be present. Tab ID can also be set to $(ref:tabs.TAB_ID_NONE) for apps and devtools windows."},
"index": {"type": "integer", "minimum": -1, "description": "The zero-based index of the tab within its window."},
"windowId": {"type": "integer", "minimum": 0, "description": "The ID of the window the tab is contained within."},
"openerTabId": {"unsupported": true, "type": "integer", "minimum": 0, "optional": true, "description": "The ID of the tab that opened this tab, if any. This property is only present if the opener tab still exists."},
"selected": {"type": "boolean", "description": "Whether the tab is selected.", "deprecated": "Please use $(ref:tabs.Tab.highlighted).", "unsupported": true},
"highlighted": {"type": "boolean", "description": "Whether the tab is highlighted. Works as an alias of active."},
"active": {"type": "boolean", "description": "Whether the tab is active in its window. (Does not necessarily mean the window is focused.)"},
"pinned": {"type": "boolean", "description": "Whether the tab is pinned."},
+ "lastAccessed": {"type": "integer", "optional": true, "description": "The last time the tab was accessed as the number of milliseconds since epoch."},
"audible": {"type": "boolean", "optional": true, "description": "Whether the tab has produced sound over the past couple of seconds (but it might not be heard if also muted). Equivalent to whether the speaker audio indicator is showing."},
"mutedInfo": {"$ref": "MutedInfo", "optional": true, "description": "Current tab muted state and the reason for the last state change."},
"url": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The URL the tab is displaying. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission."},
"title": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The title of the tab. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission."},
"favIconUrl": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The URL of the tab's favicon. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission. It may also be an empty string if the tab is loading."},
"status": {"type": "string", "optional": true, "description": "Either <em>loading</em> or <em>complete</em>."},
"incognito": {"type": "boolean", "description": "Whether the tab is in an incognito window."},
"width": {"type": "integer", "optional": true, "description": "The width of the tab in pixels."},
--- a/mobile/android/components/extensions/test/mochitest/mochitest.ini
+++ b/mobile/android/components/extensions/test/mochitest/mochitest.ini
@@ -21,13 +21,14 @@ tags = webextensions
[test_ext_tabs_executeScript.html]
[test_ext_tabs_executeScript_bad.html]
skip-if = true # Currently fails in emulator runs
[test_ext_tabs_executeScript_good.html]
[test_ext_tabs_executeScript_no_create.html]
[test_ext_tabs_executeScript_runAt.html]
[test_ext_tabs_getCurrent.html]
[test_ext_tabs_insertCSS.html]
+[test_ext_tabs_lastAccessed.html]
[test_ext_tabs_reload.html]
[test_ext_tabs_reload_bypass_cache.html]
[test_ext_tabs_onUpdated.html]
[test_ext_tabs_sendMessage.html]
[test_ext_tabs_update_url.html]
new file mode 100644
--- /dev/null
+++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_lastAccessed.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Tabs lastAccessed Test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function testLastAccessed() {
+ let past = Date.now();
+
+ window.open("https://example.com/?1");
+ window.open("https://example.com/?2");
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ async background() {
+ browser.test.onMessage.addListener(async function(msg, past) {
+ if (msg !== "past") {
+ return;
+ }
+
+ let [tab1] = await browser.tabs.query({url: "https://example.com/?1"});
+ let [tab2] = await browser.tabs.query({url: "https://example.com/?2"});
+
+ browser.test.assertTrue(tab1 && tab2, "Expected tabs were found");
+
+ let now = Date.now();
+
+ browser.test.assertTrue(past < tab1.lastAccessed &&
+ tab1.lastAccessed < tab2.lastAccessed &&
+ tab2.lastAccessed <= now,
+ "lastAccessed timestamps are recent and in the right order");
+
+ await browser.tabs.remove([tab1.id, tab2.id]);
+
+ browser.test.notifyPass("tabs.lastAccessed");
+ });
+ },
+ });
+
+ await extension.startup();
+ await extension.sendMessage("past", past);
+ await extension.awaitFinish("tabs.lastAccessed");
+ await extension.unload();
+});
+</script>
+
+</body>
--- a/toolkit/components/extensions/ExtensionTabs.jsm
+++ b/toolkit/components/extensions/ExtensionTabs.jsm
@@ -261,16 +261,27 @@ class TabBase {
*/
get favIconUrl() {
if (this.hasTabPermission) {
return this._favIconUrl;
}
}
/**
+ * @property {integer} lastAccessed
+ * Returns the last time the tab was accessed as the number of
+ * milliseconds since epoch.
+ * @readonly
+ * @abstract
+ */
+ get lastAccessed() {
+ throw new Error("Not implemented");
+ }
+
+ /**
* @property {boolean} audible
* Returns true if the tab is currently playing audio, false otherwise.
* @readonly
* @abstract
*/
get audible() {
throw new Error("Not implemented");
}
@@ -476,16 +487,17 @@ class TabBase {
windowId: this.windowId,
highlighted: this.selected,
active: this.selected,
pinned: this.pinned,
status: this.status,
incognito: this.incognito,
width: this.width,
height: this.height,
+ lastAccessed: this.lastAccessed,
audible: this.audible,
mutedInfo: this.mutedInfo,
};
// If the tab has not been fully layed-out yet, fallback to the geometry
// from a different tab (usually the currently active tab).
if (fallbackTab && (!result.width || !result.height)) {
result.width = fallbackTab.width;
--- a/toolkit/components/extensions/test/mochitest/mochitest-common.ini
+++ b/toolkit/components/extensions/test/mochitest/mochitest-common.ini
@@ -122,9 +122,9 @@ skip-if = os == 'android'
[test_ext_webrequest_upload.html]
skip-if = os == 'android' # Currently fails in emulator tests
[test_ext_webrequest_permission.html]
[test_ext_webrequest_websocket.html]
[test_ext_webnavigation.html]
[test_ext_webnavigation_filters.html]
[test_ext_window_postMessage.html]
[test_ext_subframes_privileges.html]
-[test_ext_xhr_capabilities.html]
\ No newline at end of file
+[test_ext_xhr_capabilities.html]