Bug 1210616 - have remote (sync) tabs appear in awesomebar suggestions. r=mak

This commit is contained in:
Mark Hammond 2015-11-12 11:02:44 +11:00
parent 587e99a62e
commit 225e172a8e
11 changed files with 390 additions and 10 deletions

View File

@ -157,6 +157,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
if (action) {
switch (action.type) {
case "switchtab": // Fall through.
case "remotetab": // Fall through.
case "visiturl": {
returnValue = action.params.url;
break;
@ -1490,6 +1491,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
];
break;
case "switchtab":
case "remotetab":
parts = [
item.getAttribute("title"),
action.params.url,

View File

@ -575,6 +575,7 @@ BrowserGlue.prototype = {
switchtab: 6,
tag: 7,
visiturl: 8,
remotetab: 9,
};
if (actionType in buckets) {
Services.telemetry

View File

@ -1176,7 +1176,7 @@ richlistitem[type~="action"][actiontype="searchengine"][selected="true"] > .ac-t
font-size: 0.9em;
}
richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-icon {
richlistitem[type~="action"][actiontype$="tab"] > .ac-url-box > .ac-action-icon {
list-style-image: url("chrome://browser/skin/actionicon-tab.png");
padding: 0 3px;
}

View File

@ -1854,13 +1854,13 @@ richlistitem[type~="action"][actiontype="searchengine"][selected="true"] > .ac-t
color: -moz-nativehyperlinktext;
}
richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-icon {
richlistitem[type~="action"][actiontype$="tab"] > .ac-url-box > .ac-action-icon {
list-style-image: url("chrome://browser/skin/actionicon-tab.png");
-moz-image-region: rect(0, 16px, 11px, 0);
padding: 0 3px;
}
richlistitem[type~="action"][actiontype="switchtab"][selected="true"] > .ac-url-box > .ac-action-icon {
richlistitem[type~="action"][actiontype$="tab"][selected="true"] > .ac-url-box > .ac-action-icon {
-moz-image-region: rect(11px, 16px, 22px, 0);
}
@ -1879,13 +1879,13 @@ richlistitem[type~="action"][actiontype="switchtab"][selected="true"] > .ac-url-
list-style-image: url("chrome://browser/skin/places/tag@2x.png");
}
richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-icon {
richlistitem[type~="action"][actiontype$="tab"] > .ac-url-box > .ac-action-icon {
list-style-image: url("chrome://browser/skin/actionicon-tab@2x.png");
-moz-image-region: rect(0, 32px, 22px, 0);
width: 22px;
}
richlistitem[type~="action"][actiontype="switchtab"][selected="true"] > .ac-url-box > .ac-action-icon {
richlistitem[type~="action"][actiontype$="tab"][selected="true"] > .ac-url-box > .ac-action-icon {
-moz-image-region: rect(22px, 32px, 44px, 0);
}
}

View File

@ -1537,7 +1537,7 @@ richlistitem[type~="action"][actiontype="searchengine"] > .ac-title-box > .ac-si
}
}
richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-icon {
richlistitem[type~="action"][actiontype$="tab"] > .ac-url-box > .ac-action-icon {
list-style-image: url("chrome://browser/skin/actionicon-tab.png");
-moz-image-region: rect(0, 16px, 11px, 0);
padding: 0 3px;
@ -1546,7 +1546,7 @@ richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-
}
@media (min-resolution: 1.1dppx) {
richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-icon {
richlistitem[type~="action"][actiontype$="tab"] > .ac-url-box > .ac-action-icon {
list-style-image: url("chrome://browser/skin/actionicon-tab@2x.png");
-moz-image-region: rect(0, 32px, 22px, 0);
}
@ -1556,12 +1556,12 @@ richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-
not all and (-moz-windows-default-theme) {
@media not all and (-moz-os-version: windows-win7),
not all and (-moz-windows-default-theme) {
richlistitem[type~="action"][actiontype="switchtab"][selected="true"] > .ac-url-box > .ac-action-icon {
richlistitem[type~="action"][actiontype$="tab"][selected="true"] > .ac-url-box > .ac-action-icon {
-moz-image-region: rect(11px, 16px, 22px, 0);
}
@media (min-resolution: 1.1dppx) {
richlistitem[type~="action"][actiontype="switchtab"][selected="true"] > .ac-url-box > .ac-action-icon {
richlistitem[type~="action"][actiontype$="tab"][selected="true"] > .ac-url-box > .ac-action-icon {
-moz-image-region: rect(22px, 32px, 44px, 0);
}
}

View File

@ -0,0 +1,120 @@
/* 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/. */
/*
* Provides functions to handle remote tabs (ie, tabs known by Sync) in
* the awesomebar.
*/
"use strict";
this.EXPORTED_SYMBOLS = ["PlacesRemoteTabsAutocompleteProvider"];
const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://services-sync/main.js");
XPCOMUtils.defineLazyGetter(this, "weaveXPCService", function() {
return Cc["@mozilla.org/weave/service;1"]
.getService(Ci.nsISupports)
.wrappedJSObject;
});
// from MDN...
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
// Build the in-memory structure we use.
function buildItems() {
let clients = new Map(); // keyed by client guid, value is client
let tabs = new Map(); // keyed by string URL, value is {clientId, tab}
// If Sync isn't initialized (either due to lag at startup or due to no user
// being signed in), don't reach in to Weave.Service as that may initialize
// Sync unnecessarily - we'll get an observer notification later when it
// becomes ready and has synced a list of tabs.
if (weaveXPCService.ready) {
let engine = Weave.Service.engineManager.get("tabs");
for (let [guid, client] in Iterator(engine.getAllClients())) {
clients.set(guid, client);
for (let tab of client.tabs) {
let url = tab.urlHistory[0];
tabs.set(url, { clientId: guid, tab });
}
}
}
return { clients, tabs };
}
// Manage the cache of the items we use.
// The cache itself.
let _items = null;
// Ensure the cache is good.
function ensureItems() {
if (!_items) {
_items = buildItems();
}
return _items;
}
// An observer to invalidate _items.
function observe(subject, topic, data) {
switch (topic) {
case "weave:engine:sync:finish":
if (data == "tabs") {
// The tabs engine just finished syncing, so may have a different list
// of tabs then we previously cached.
_items = null;
}
break;
case "weave:service:start-over":
// Sync is being reset due to the user disconnecting - we must invalidate
// the cache so we don't supply tabs from a different user.
_items = null;
break;
default:
break;
}
}
Services.obs.addObserver(observe, "weave:engine:sync:finish", false);
Services.obs.addObserver(observe, "weave:service:start-over", false);
// This public object is a static singleton.
this.PlacesRemoteTabsAutocompleteProvider = {
// a promise that resolves with an array of matching remote tabs.
getMatches(searchString) {
// If Sync isn't configured we bail early.
if (!Services.prefs.prefHasUserValue("services.sync.username")) {
return Promise.resolve([]);
}
let re = new RegExp(escapeRegExp(searchString), "i");
let matches = [];
let { tabs, clients } = ensureItems();
for (let [url, { clientId, tab }] of tabs) {
let title = tab.title;
if (url.match(re) || (title && title.match(re))) {
// lookup the client record.
let client = clients.get(clientId);
// create the record we return for auto-complete.
let record = {
url, title,
icon: tab.icon,
deviceClass: Weave.Service.clientsEngine.isMobile(clientId) ? "mobile" : "desktop",
deviceName: client.clientName,
};
matches.push(record);
}
}
return Promise.resolve(matches);
},
}

View File

@ -263,6 +263,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesSearchAutocompleteProvider",
"resource://gre/modules/PlacesSearchAutocompleteProvider.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesRemoteTabsAutocompleteProvider",
"resource://gre/modules/PlacesRemoteTabsAutocompleteProvider.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "textURIService",
"@mozilla.org/intl/texttosuburi;1",
@ -585,6 +587,42 @@ function makeActionURL(action, params) {
return NetUtil.newURI(url).spec;
}
/**
* Returns the key to be used for a URL in a map for the purposes of removing
* duplicate entries - any 2 URLs that should be considered the same should
* return the same key. For some moz-action URLs this will unwrap the params
* and return a key based on the wrapped URL.
*/
function makeKeyForURL(actionUrl) {
// At this stage we only consider moz-action URLs.
if (!actionUrl.startsWith("moz-action:")) {
return stripHttpAndTrim(actionUrl);
}
let [, type, params] = actionUrl.match(/^moz-action:([^,]+),(.*)$/);
try {
params = JSON.parse(params);
} catch (ex) {
// This is unexpected in this context, so just return the input.
return stripHttpAndTrim(actionUrl);
}
// For now we only handle these 2 action types and treat them as the same.
switch (type) {
case "remotetab":
case "switchtab":
if (params.url) {
return "moz-action:tab:" + stripHttpAndTrim(params.url);
}
break;
// TODO (bug 1222435) - "switchtab" should be handled as an "autofill"
// entry.
default:
// do nothing.
// TODO (bug 1222436) - extend this method so it can be used instead of
// the |placeId| that's also used to remove duplicate entries.
}
return stripHttpAndTrim(actionUrl);
}
/**
* Returns whether the passed in string looks like a url.
*/
@ -864,6 +902,9 @@ Search.prototype = {
yield this._matchFirstHeuristicResult(conn);
this._addingHeuristicFirstMatch = false;
// We sleep a little between adding the heuristicFirstMatch and matching
// any other searches so we aren't kicking off potentially expensive
// searches on every keystroke.
yield this._sleep(Prefs.delay);
if (!this.pending)
return;
@ -878,6 +919,12 @@ Search.prototype = {
return;
}
if (this._enableActions && this.hasBehavior("openpage")) {
yield this._matchRemoteTabs();
if (!this.pending)
return;
}
// If we do not have enough results, and our match type is
// MATCH_BOUNDARY_ANYWHERE, search again with MATCH_ANYWHERE to get more
// results.
@ -1188,6 +1235,35 @@ Search.prototype = {
});
},
*_matchRemoteTabs() {
let matches = yield PlacesRemoteTabsAutocompleteProvider.getMatches(this._originalSearchString);
for (let {url, title, icon, deviceClass, deviceName} of matches) {
// It's rare that Sync supplies the icon for the page (but if it does, it
// is a string URL)
if (!icon) {
try {
let favicon = yield PlacesUtils.promiseFaviconLinkUrl(url);
if (favicon) {
icon = favicon.spec;
}
} catch (ex) {} // no favicon for this URL.
}
let match = {
// We include the deviceName in the action URL so we can render it in
// the URLBar.
value: makeActionURL("remotetab", { url, deviceName }),
comment: title || url,
style: "action",
// we want frecency > FRECENCY_DEFAULT so it doesn't get pushed out
// by "remote" matches.
frecency: FRECENCY_DEFAULT + 1,
icon,
}
this._addMatch(match);
}
},
// TODO (bug 1054814): Use visited URLs to inform which scheme to use, if the
// scheme isn't specificed.
_matchUnknownUrl: function* () {
@ -1307,7 +1383,7 @@ Search.prototype = {
return;
// Must check both id and url, cause keywords dynamically modify the url.
let urlMapKey = stripHttpAndTrim(match.value);
let urlMapKey = makeKeyForURL(match.value);
if ((match.placeId && this._usedPlaceIds.has(match.placeId)) ||
this._usedURLs.has(urlMapKey)) {
return;

View File

@ -65,6 +65,7 @@ if CONFIG['MOZ_PLACES']:
'History.jsm',
'PlacesBackups.jsm',
'PlacesDBUtils.jsm',
'PlacesRemoteTabsAutocompleteProvider.jsm',
'PlacesSearchAutocompleteProvider.jsm',
'PlacesTransactions.jsm',
'PlacesUtils.jsm',

View File

@ -0,0 +1,175 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
* vim:set ts=2 sw=2 sts=2 et:
*/
"use strict";
Cu.import("resource://services-sync/main.js");
Services.prefs.setCharPref("services.sync.username", "someone@somewhere.com");
// A mock "Tabs" engine which autocomplete will use instead of the real
// engine. We pass a constructor that Sync creates.
function MockTabsEngine() {
this.clients = null; // We'll set this dynamically
}
MockTabsEngine.prototype = {
name: "tabs",
getAllClients() {
return this.clients;
},
}
// A clients engine that doesn't need to be a constructor.
let MockClientsEngine = {
isMobile(guid) {
Assert.ok(guid.endsWith("desktop") || guid.endsWith("mobile"));
return guid.endsWith("mobile");
},
}
// Tell Sync about the mocks.
Weave.Service.engineManager.register(MockTabsEngine);
Weave.Service.clientsEngine = MockClientsEngine;
// Tell the Sync XPCOM service it is initialized.
let weaveXPCService = Cc["@mozilla.org/weave/service;1"]
.getService(Ci.nsISupports)
.wrappedJSObject;
weaveXPCService.ready = true;
// Configure the singleton engine for a test.
function configureEngine(clients) {
// Configure the instance Sync created.
let engine = Weave.Service.engineManager.get("tabs");
engine.clients = clients;
// Send an observer that pretends the engine just finished a sync.
Services.obs.notifyObservers(null, "weave:engine:sync:finish", "tabs");
}
// Make a match object suitable for passing to check_autocomplete.
function makeRemoteTabMatch(url, deviceName, extra = {}) {
return {
uri: makeActionURI("remotetab", {url, deviceName}),
title: extra.title || url,
style: [ "action" ],
}
}
// The tests.
add_task(function* test_nomatch() {
// Nothing matches.
configureEngine({
guid_desktop: {
clientName: "My Desktop",
tabs: [{
urlHistory: ["http://foo.com/"],
}],
}
});
// No remote tabs match here, so we only expect search results.
yield check_autocomplete({
search: "ex",
searchParam: "enable-actions",
matches: [ makeSearchMatch("ex", { heuristic: true }) ],
});
});
add_task(function* test_minimal() {
// The minimal client and tabs info we can get away with.
configureEngine({
guid_desktop: {
clientName: "My Desktop",
tabs: [{
urlHistory: ["http://example.com/"],
}],
}
});
yield check_autocomplete({
search: "ex",
searchParam: "enable-actions",
matches: [ makeSearchMatch("ex", { heuristic: true }),
makeRemoteTabMatch("http://example.com/", "My Desktop") ],
});
});
add_task(function* test_maximal() {
// Every field that could possibly exist on a remote record.
configureEngine({
guid_mobile: {
clientName: "My Phone",
tabs: [{
urlHistory: ["http://example.com/"],
title: "An Example",
icon: "http://favicon",
}],
}
});
yield check_autocomplete({
search: "ex",
searchParam: "enable-actions",
matches: [ makeSearchMatch("ex", { heuristic: true }),
makeRemoteTabMatch("http://example.com/", "My Phone",
{ title: "An Example",
icon: "http://favicon"
}),
],
});
});
add_task(function* test_matches_title() {
// URL doesn't match search expression, should still match the title.
configureEngine({
guid_mobile: {
clientName: "My Phone",
tabs: [{
urlHistory: ["http://foo.com/"],
title: "An Example",
}],
}
});
yield check_autocomplete({
search: "ex",
searchParam: "enable-actions",
matches: [ makeSearchMatch("ex", { heuristic: true }),
makeRemoteTabMatch("http://foo.com/", "My Phone",
{ title: "An Example" }),
],
});
});
add_task(function* test_localtab_matches_override() {
// We have an open tab to the same page on a remote device, only "switch to
// tab" should appear as duplicate detection removed the remote one.
// First setup Sync to have the page as a remote tab.
configureEngine({
guid_mobile: {
clientName: "My Phone",
tabs: [{
urlHistory: ["http://foo.com/"],
title: "An Example",
}],
}
});
// Setup Places to think the tab is open locally.
let uri = NetUtil.newURI("http://foo.com/");
yield PlacesTestUtils.addVisits([
{ uri: uri, title: "An Example" },
]);
addOpenPages(uri, 1);
yield check_autocomplete({
search: "ex",
searchParam: "enable-actions",
matches: [ makeSearchMatch("ex", { heuristic: true }),
makeSwitchToTabMatch("http://foo.com/", { title: "An Example" }),
],
});
});

View File

@ -30,6 +30,7 @@ support-files =
[test_match_beginning.js]
[test_multi_word_search.js]
[test_queryurl.js]
[test_remotetabmatches.js]
[test_searchEngine_alias.js]
[test_searchEngine_current.js]
[test_searchEngine_host.js]

View File

@ -1719,6 +1719,10 @@ extends="chrome://global/content/bindings/popup.xml#popup">
displayUrl = action.params.url;
let desc = this._stringBundle.GetStringFromName("switchToTab");
this._setUpDescription(this._action, desc, true);
} else if (action.type == "remotetab") {
displayUrl = action.params.url;
let desc = action.params.deviceName;
this._setUpDescription(this._action, desc, true);
} else if (action.type == "searchengine") {
emphasiseUrl = false;