Merge fx-team to m-c. a=merge

This commit is contained in:
Ryan VanderMeulen 2014-10-02 13:00:31 -04:00
commit 93b3ed4688
121 changed files with 2609 additions and 588 deletions

View File

@ -1616,6 +1616,8 @@ pref("loop.debug.loglevel", "Error");
pref("loop.debug.dispatcher", false);
pref("loop.debug.websocket", false);
pref("loop.debug.sdk", false);
pref("loop.oauth.google.redirect_uri", "urn:ietf:wg:oauth:2.0:oob:auto");
pref("loop.oauth.google.scope", "https://www.google.com/m8/feeds");
// serverURL to be assigned by services team
pref("services.push.serverURL", "wss://push.services.mozilla.com/");

View File

@ -694,7 +694,7 @@ function gKeywordURIFixup({ target: browser, data: fixupInfo }) {
// whether the original input would be vaguely interpretable as a URL,
// so figure that out first.
let alternativeURI = deserializeURI(fixupInfo.fixedURI);
if (!fixupInfo.fixupUsedKeyword || !alternativeURI || !alternativeURI.host) {
if (!fixupInfo.keywordProviderName || !alternativeURI || !alternativeURI.host) {
return;
}
@ -2400,13 +2400,13 @@ let BrowserOnClick = {
receiveMessage: function (msg) {
switch (msg.name) {
case "Browser:CertExceptionError":
this.onAboutCertError(msg.target, msg.json.elementId,
msg.json.isTopFrame, msg.json.location,
msg.objects.failedChannel);
this.onAboutCertError(msg.target, msg.data.elementId,
msg.data.isTopFrame, msg.data.location,
msg.data.sslStatusAsString);
break;
case "Browser:SiteBlockedError":
this.onAboutBlocked(msg.json.elementId, msg.json.isMalware,
msg.json.isTopFrame, msg.json.location);
this.onAboutBlocked(msg.data.elementId, msg.data.isMalware,
msg.data.isTopFrame, msg.data.location);
break;
case "Browser:NetworkError":
// Reset network state, the error page will refresh on its own.
@ -2415,7 +2415,7 @@ let BrowserOnClick = {
}
},
onAboutCertError: function (browser, elementId, isTopFrame, location, failedChannel) {
onAboutCertError: function (browser, elementId, isTopFrame, location, sslStatusAsString) {
let secHistogram = Services.telemetry.getHistogramById("SECURITY_UI");
switch (elementId) {
@ -2423,8 +2423,11 @@ let BrowserOnClick = {
if (isTopFrame) {
secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_BAD_CERT_TOP_CLICK_ADD_EXCEPTION);
}
let sslStatus = failedChannel.securityInfo.QueryInterface(Ci.nsISSLStatusProvider)
.SSLStatus;
let serhelper = Cc["@mozilla.org/network/serialization-helper;1"]
.getService(Ci.nsISerializationHelper);
let sslStatus = serhelper.deserializeObject(sslStatusAsString);
sslStatus.QueryInterface(Components.interfaces.nsISSLStatus);
let params = { exceptionAdded : false,
sslStatus : sslStatus };

View File

@ -424,12 +424,23 @@ let ClickEventHandler = {
let docshell = ownerDoc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell);
let serhelper = Cc["@mozilla.org/network/serialization-helper;1"]
.getService(Ci.nsISerializationHelper);
let serializedSSLStatus = "";
try {
let serializable = docShell.failedChannel.securityInfo
.QueryInterface(Ci.nsISSLStatusProvider)
.SSLStatus
.QueryInterface(Ci.nsISerializable);
serializedSSLStatus = serhelper.serializeToString(serializable);
} catch (e) { }
sendAsyncMessage("Browser:CertExceptionError", {
location: ownerDoc.location.href,
elementId: targetElement.getAttribute("id"),
isTopFrame: (ownerDoc.defaultView.parent === ownerDoc.defaultView),
}, {
failedChannel: docshell.failedChannel
sslStatusAsString: serializedSSLStatus
});
},

View File

@ -104,15 +104,15 @@ skip-if = os == "linux" # Bug 924307
skip-if = e10s # Bug ?????? - no about:home support yet
[browser_aboutSyncProgress.js]
[browser_action_keyword.js]
skip-if = os == "linux" # Bug 1073339 - Investigate autocomplete test unreliability on Linux
skip-if = os == "linux" || e10s # Bug 1073339 - Investigate autocomplete test unreliability on Linux/e10s
[browser_action_searchengine.js]
skip-if = os == "linux" # Bug 1073339 - Investigate autocomplete test unreliability on Linux
skip-if = os == "linux" || e10s # Bug 1073339 - Investigate autocomplete test unreliability on Linux/e10s
[browser_action_searchengine_alias.js]
skip-if = os == "linux" # Bug 1073339 - Investigate autocomplete test unreliability on Linux
skip-if = os == "linux" || e10s # Bug 1073339 - Investigate autocomplete test unreliability on Linux/e10s
[browser_addKeywordSearch.js]
skip-if = e10s
[browser_search_favicon.js]
skip-if = os == "linux" # Bug 1073339 - Investigate autocomplete test unreliability on Linux
skip-if = os == "linux" || e10s # Bug 1073339 - Investigate autocomplete test unreliability on Linux/e10s
[browser_alltabslistener.js]
skip-if = os == "linux" || e10s # Linux: Intermittent failures, bug 951680; e10s: Bug ?????? - notifications don't work correctly.
[browser_autocomplete_a11y_label.js]

View File

@ -0,0 +1,541 @@
/* 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 { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Timer.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Log",
"resource://gre/modules/Log.jsm");
this.EXPORTED_SYMBOLS = ["GoogleImporter"];
let log = Log.repository.getLogger("Loop.Importer.Google");
log.level = Log.Level.Debug;
log.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter()));
/**
* Helper function that reads and maps the respective node value from specific
* XML DOMNodes to fields on a `target` object.
* Example: the value for field 'fullName' can be read from the XML DOMNode
* 'name', so that's the mapping we need to make; get the nodeValue of
* the node called 'name' and tack it to the target objects' 'fullName'
* property.
*
* @param {Map} fieldMap Map object containing the field name -> node
* name mapping
* @param {XMLDOMNode} node DOM node to fetch the values from for each field
* @param {String} ns XML namespace for the DOM nodes to retrieve. Optional.
* @param {Object} target Object to store the values found. Optional.
* Defaults to a new object.
* @param {Boolean} wrapInArray Indicates whether to map the field values in
* an Array. Optional. Defaults to `false`.
* @returns The `target` object with the node values mapped to the appropriate fields.
*/
const extractFieldsFromNode = function(fieldMap, node, ns = null, target = {}, wrapInArray = false) {
for (let [field, nodeName] of fieldMap) {
let nodeList = ns ? node.getElementsByTagNameNS(ns, nodeName) :
node.getElementsByTagName(nodeName);
if (nodeList.length) {
if (!nodeList[0].firstChild) {
continue;
}
let value = nodeList[0].firstChild.nodeValue;
target[field] = wrapInArray ? [value] : value;
}
}
return target;
};
/**
* Helper function that reads the type of (email-)address or phone number from an
* XMLDOMNode.
*
* @param {XMLDOMNode} node
* @returns String that depicts the type of field value.
*/
const getFieldType = function(node) {
if (node.hasAttribute("rel")) {
let rel = node.getAttribute("rel");
// The 'rel' attribute is formatted like: http://schemas.google.com/g/2005#work.
return rel.substr(rel.lastIndexOf("#") + 1);
}
if (node.hasAttribute("label")) {
return node.getAttribute("label");
}
return "other";
};
/**
* Fetch the preferred entry of a contact. Returns the first entry when no
* preferred flag is set.
*
* @param {Object} contact The contact object to check for preferred entries
* @param {String} which Type of entry to check. Optional, defaults to 'email'
* @throws An Error when no (preferred) entries are listed for this contact.
*/
const getPreferred = function(contact, which = "email") {
if (!(which in contact) || !contact[which].length) {
throw new Error("No " + which + " entry available.");
}
let preferred = contact[which][0];
contact[which].some(function(entry) {
if (entry.pref) {
preferred = entry;
return true;
}
return false;
});
return preferred;
};
/**
* Fetch an auth token (clientID or client secret), which may be overridden by
* a pref if it's set.
*
* @param {String} paramValue Initial, default, value of the parameter
* @param {String} prefName Fully qualified name of the pref to check for
* @param {Boolean} encode Whether to URLEncode the param string
*/
const getUrlParam = function(paramValue, prefName, encode = true) {
if (Services.prefs.getPrefType(prefName))
paramValue = Services.prefs.getCharPref(prefName);
paramValue = Services.urlFormatter.formatURL(paramValue);
return encode ? encodeURIComponent(paramValue) : paramValue;
};
let gAuthWindow, gProfileId;
const kAuthWindowSize = {
width: 420,
height: 460
};
const kContactsMaxResults = 10000000;
const kContactsChunkSize = 100;
const kTitlebarPollTimeout = 200;
const kNS_GD = "http://schemas.google.com/g/2005";
/**
* GoogleImporter class.
*
* Main entrypoint is the `startImport` method which calls several tasks necessary
* to import contacts from Google.
* Authentication is performed using an OAuth strategy which is loaded in a popup
* window.
*/
this.GoogleImporter = function() {};
this.GoogleImporter.prototype = {
/**
* Start the import process of contacts from the Google service, using its Contacts
* API - https://developers.google.com/google-apps/contacts/v3/.
* The import consists of four tasks:
* 1. Get the authentication code which can be used to retrieve an OAuth token
* pair. This is the bulk of the authentication flow that will be handled in
* a popup window by Google. The user will need to login to the Google service
* with his or her account and grant permission to our app to manage their
* contacts.
* 2. Get the tokenset from the Google service, using the authentication code
* that was retrieved in task 1.
* 3. Fetch all the contacts from the Google service, using the OAuth tokenset
* that was retrieved in task 2.
* 4. Process the contacts, map them to the MozContact format and store each
* contact in the database, if it doesn't exist yet.
*
* @param {Object} options Options to control the behavior of the import.
* Not used by this importer class.
* @param {Function} callback Function to invoke when the import process
* is done or when an error occurs that halts
* the import process. The first argument passed
* in an Error object or `null` and the second
* argument is an object with import statistics.
* @param {LoopContacts} db Instance of the LoopContacts database object,
* which will store the newly found contacts
* @param {nsIDomWindow} windowRef Reference to the ChromeWindow the import is
* invoked from. It will be used to be able to
* open a window for the OAuth process with chrome
* privileges.
*/
startImport: function(options, callback, db, windowRef) {
Task.spawn(function* () {
let code = yield this._promiseAuthCode(windowRef);
let tokenSet = yield this._promiseTokenSet(code);
let contactEntries = yield this._promiseContactEntries(tokenSet);
let {total, success, ids} = yield this._processContacts(contactEntries, db);
yield this._purgeContacts(ids, db);
return {
total: total,
success: success
};
}.bind(this)).then(stats => callback(null, stats),
error => callback(error))
.then(null, ex => log.error(ex.fileName + ":" + ex.lineNumber + ": " + ex.message));
},
/**
* Task that yields an authentication code that is returned after the user signs
* in to the Google service. This code can be used by this class to retrieve an
* OAuth tokenset.
*
* @param {nsIDOMWindow} windowRef Reference to the ChromeWindow the import is
* invoked from. It will be used to be able to
* open a window for the OAuth process with chrome
* privileges.
* @throws An `Error` object when authentication fails, or the authentication
* code as a String.
*/
_promiseAuthCode: Task.async(function* (windowRef) {
// Close a window that got lost in a previous login attempt.
if (gAuthWindow && !gAuthWindow.closed) {
gAuthWindow.close();
gAuthWindow = null;
}
let url = getUrlParam("https://accounts.google.com/o/oauth2/",
"loop.oauth.google.URL", false) +
"auth?response_type=code&client_id=" +
getUrlParam("%GOOGLE_OAUTH_API_CLIENTID%", "loop.oauth.google.clientIdOverride");
for (let param of ["redirect_uri", "scope"]) {
url += "&" + param + "=" + encodeURIComponent(
Services.prefs.getCharPref("loop.oauth.google." + param));
}
const features = "centerscreen,resizable=yes,toolbar=no,menubar=no,status=no,directories=no," +
"width=" + kAuthWindowSize.width + ",height=" + kAuthWindowSize.height;
gAuthWindow = windowRef.openDialog(windowRef.getBrowserURL(), "_blank", features, url);
gAuthWindow.focus();
let code;
// The following loops runs as long as the OAuth windows' titlebar doesn't
// yield a response from the Google service. If an error occurs, the loop
// will terminate early.
while (!code) {
if (!gAuthWindow || gAuthWindow.closed) {
throw new Error("Popup window was closed before authentication succeeded");
}
let matches = gAuthWindow.document.title.match(/(error|code)=(.*)$/);
if (matches && matches.length) {
let [, type, message] = matches;
gAuthWindow.close();
gAuthWindow = null;
if (type == "error") {
throw new Error("Google authentication failed with error: " + message.trim());
} else if (type == "code") {
code = message.trim();
} else {
throw new Error("Unknown response from Google");
}
} else {
yield new Promise(resolve => setTimeout(resolve, kTitlebarPollTimeout));
}
}
return code;
}),
/**
* Fetch an OAuth tokenset, that will be used to authenticate Google API calls,
* using the authentication token retrieved in `_promiseAuthCode`.
*
* @param {String} code The authentication code.
* @returns an `Error` object upon failure or an object containing OAuth tokens.
*/
_promiseTokenSet: function(code) {
return new Promise(function(resolve, reject) {
let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance(Ci.nsIXMLHttpRequest);
request.open("POST", getUrlParam("https://accounts.google.com/o/oauth2/",
"loop.oauth.google.URL",
false) + "token");
request.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
request.onload = function() {
if (request.status < 400) {
let tokenSet = JSON.parse(request.responseText);
tokenSet.date = Date.now();
resolve(tokenSet);
} else {
reject(new Error(request.status + " " + request.statusText));
}
};
request.onerror = function(error) {
reject(error);
};
let body = "grant_type=authorization_code&code=" + encodeURIComponent(code) +
"&client_id=" + getUrlParam("%GOOGLE_OAUTH_API_CLIENTID%",
"loop.oauth.google.clientIdOverride") +
"&client_secret=" + getUrlParam("%GOOGLE_OAUTH_API_KEY%",
"loop.oauth.google.clientSecretOverride") +
"&redirect_uri=" + encodeURIComponent(Services.prefs.getCharPref(
"loop.oauth.google.redirect_uri"));
request.send(body);
});
},
/**
* Fetches all the contacts in a users' address book.
*
* @see https://developers.google.com/google-apps/contacts/v3/#retrieving_all_contacts
*
* @param {Object} tokenSet OAuth tokenset used to authenticate the request
* @returns An `Error` object upon failure or an Array of contact XML nodes.
*/
_promiseContactEntries: function(tokenSet) {
return new Promise(function(resolve, reject) {
let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance(Ci.nsIXMLHttpRequest);
request.open("GET", getUrlParam("https://www.google.com/m8/feeds/contacts/default/full",
"loop.oauth.google.getContactsURL",
false) + "?max-results=" + kContactsMaxResults);
request.setRequestHeader("Content-Type", "application/xml; charset=utf-8");
request.setRequestHeader("GData-Version", "3.0");
request.setRequestHeader("Authorization", "Bearer " + tokenSet.access_token);
request.onload = function() {
if (request.status < 400) {
let doc = request.responseXML;
// First get the profile id.
let currNode = doc.documentElement.firstChild;
while (currNode) {
if (currNode.nodeType == 1 && currNode.localName == "id") {
gProfileId = currNode.firstChild.nodeValue;
break;
}
currNode = currNode.nextSibling;
}
// Then kick of the importing of contact entries.
let entries = Array.prototype.slice.call(doc.querySelectorAll("entry"));
resolve(entries);
} else {
reject(new Error(request.status + " " + request.statusText));
}
};
request.onerror = function(error) {
reject(error);
}
request.send();
});
},
/**
* Process the contact XML nodes that Google provides, convert them to the MozContact
* format, check if the contact already exists in the database and when it doesn't,
* store it permanently.
* During this process statistics are collected about the amount of successful
* imports. The consumer of this class may use these statistics to inform the
* user.
*
* @param {Array} contactEntries List of XML DOMNodes contact entries.
* @param {LoopContacts} db Instance of the LoopContacts database
* object, which will store the newly found
* contacts.
* @returns An `Error` object upon failure or an Object with statistics in the
* following format: `{ total: 25, success: 13, ids: {} }`.
*/
_processContacts: Task.async(function* (contactEntries, db) {
let stats = {
total: contactEntries.length,
success: 0,
ids: {}
};
for (let entry of contactEntries) {
let contact = this._processContactFields(entry);
stats.ids[contact.id] = 1;
let existing = yield db.promise("getByServiceId", contact.id);
if (existing) {
yield db.promise("remove", existing._guid);
}
// If the contact contains neither email nor phone number, then it is not
// useful in the Loop address book: do not add.
if (!("email" in contact) && !("tel" in contact)) {
continue;
}
yield db.promise("add", contact);
stats.success++;
}
return stats;
}),
/**
* Parse an XML node to map the appropriate data to MozContact field equivalents.
*
* @param {XMLDOMNode} entry The contact XML node in Google format to process.
* @returns `null` if the contact entry appears to be invalid or an Object containing
* all the contact data found in the XML.
*/
_processContactFields: function(entry) {
// Basic fields in the main 'atom' namespace.
let contact = extractFieldsFromNode(new Map([
["id", "id"],
// published: n/a
["updated", "updated"]
// bday: n/a
]), entry);
// Fields that need to wrapped in an Array.
extractFieldsFromNode(new Map([
["name", "fullName"],
["givenName", "givenName"],
["familyName", "familyName"],
["additionalName", "additionalName"]
]), entry, kNS_GD, contact, true);
// The 'note' field needs to wrapped in an array, but its source node is not
// namespaced.
extractFieldsFromNode(new Map([
["note", "content"]
]), entry, null, contact, true);
// Process physical, earthly addresses.
let addressNodes = entry.getElementsByTagNameNS(kNS_GD, "structuredPostalAddress");
if (addressNodes.length) {
contact.adr = [];
for (let [,addressNode] of Iterator(addressNodes)) {
let adr = extractFieldsFromNode(new Map([
["countryName", "country"],
["locality", "city"],
["postalCode", "postcode"],
["region", "region"],
["streetAddress", "street"]
]), addressNode, kNS_GD);
if (Object.keys(adr).length) {
adr.pref = (addressNode.getAttribute("primary") == "true");
adr.type = [getFieldType(addressNode)];
contacts.adr.push(adr);
}
}
}
// Process email addresses.
let emailNodes = entry.getElementsByTagNameNS(kNS_GD, "email");
if (emailNodes.length) {
contact.email = [];
for (let [,emailNode] of Iterator(emailNodes)) {
contact.email.push({
pref: (emailNode.getAttribute("primary") == "true"),
type: [getFieldType(emailNode)],
value: emailNode.getAttribute("address")
});
}
}
// Process telephone numbers.
let phoneNodes = entry.getElementsByTagNameNS(kNS_GD, "phoneNumber");
if (phoneNodes.length) {
contact.tel = [];
for (let [,phoneNode] of Iterator(phoneNodes)) {
contact.tel.push({
pref: (phoneNode.getAttribute("primary") == "true"),
type: [getFieldType(phoneNode)],
value: phoneNode.firstChild.nodeValue
});
}
}
let orgNodes = entry.getElementsByTagNameNS(kNS_GD, "organization");
if (orgNodes.length) {
contact.org = [];
contact.jobTitle = [];
for (let [,orgNode] of Iterator(orgNodes)) {
contact.org.push(orgNode.getElementsByTagNameNS(kNS_GD, "orgName")[0].firstChild.nodeValue);
contact.jobTitle.push(orgNode.getElementsByTagNameNS(kNS_GD, "orgTitle")[0].firstChild.nodeValue);
}
}
contact.category = ["google"];
// Basic sanity checking: make sure the name field isn't empty
if (!("name" in contact) || contact.name[0].length == 0) {
if (("familyName" in contact) && ("givenName" in contact)) {
// First, try to synthesize a full name from the name fields.
// Ordering is culturally sensitive, but we don't have
// cultural origin information available here. The best we
// can really do is "family, given additional"
contact.name = [contact.familyName[0] + ", " + contact.givenName[0]];
if (("additionalName" in contact)) {
contact.name[0] += " " + contact.additionalName[0];
}
} else {
let profileTitle = extractFieldsFromNode(new Map([["title", "title"]]), entry);
if (("title" in profileTitle)) {
contact.name = [profileTitle.title];
} else if ("familyName" in contact) {
contact.name = [contact.familyName[0]];
} else if ("givenName" in contact) {
contact.name = [contact.givenName[0]];
} else if ("org" in contact) {
contact.name = [contact.org[0]];
} else {
let email;
try {
email = getPreferred(contact);
} catch (ex) {}
if (email) {
contact.name = [email.value];
} else {
let tel;
try {
tel = getPreferred(contact, "phone");
} catch (ex) {}
if (tel) {
contact.name = [tel.value];
}
}
}
}
}
return contact;
},
/**
* Remove all contacts from the database that are not present anymore in the
* remote data-source.
*
* @param {Object} ids Map of IDs collected earlier of all the contacts
* that are available on the remote data-source
* @param {LoopContacts} db Instance of the LoopContacts database object, which
* will store the newly found contacts
*/
_purgeContacts: Task.async(function* (ids, db) {
let contacts = yield db.promise("getAll");
let profileId = "https://www.google.com/m8/feeds/contacts/" + encodeURIComponent(gProfileId);
let processed = 0;
for (let [guid, contact] of Iterator(contacts)) {
if (++processed % kContactsChunkSize === 0) {
// Skip a beat every time we processed a chunk.
yield new Promise(resolve => Services.tm.currentThread.dispatch(resolve,
Ci.nsIThread.DISPATCH_NORMAL));
}
if (contact.id.indexOf(profileId) >= 0 && !ids[contact.id]) {
yield db.promise("remove", guid);
}
}
})
};

View File

@ -10,8 +10,12 @@ XPCOMUtils.defineLazyModuleGetter(this, "console",
"resource://gre/modules/devtools/Console.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LoopStorage",
"resource:///modules/loop/LoopStorage.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "CardDavImporter",
"resource:///modules/loop/CardDavImporter.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "GoogleImporter",
"resource:///modules/loop/GoogleImporter.jsm");
XPCOMUtils.defineLazyGetter(this, "eventEmitter", function() {
const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {});
return new EventEmitter();
@ -324,7 +328,8 @@ let LoopContactsInternal = Object.freeze({
* Map of contact importer names to instances
*/
_importServices: {
"carddav": new CardDavImporter()
"carddav": new CardDavImporter(),
"google": new GoogleImporter()
},
/**
@ -770,7 +775,7 @@ let LoopContactsInternal = Object.freeze({
* `Error` object or `null`. The second argument will
* be the result of the operation, if successfull.
*/
startImport: function(options, callback) {
startImport: function(options, windowRef, callback) {
if (!("service" in options)) {
callback(new Error("No import service specified in options"));
return;
@ -779,7 +784,8 @@ let LoopContactsInternal = Object.freeze({
callback(new Error("Unknown import service specified: " + options.service));
return;
}
this._importServices[options.service].startImport(options, callback, this);
this._importServices[options.service].startImport(options, callback,
LoopContacts, windowRef);
},
/**
@ -858,14 +864,26 @@ this.LoopContacts = Object.freeze({
return LoopContactsInternal.unblock(guid, callback);
},
startImport: function(options, callback) {
return LoopContactsInternal.startImport(options, callback);
startImport: function(options, windowRef, callback) {
return LoopContactsInternal.startImport(options, windowRef, callback);
},
search: function(query, callback) {
return LoopContactsInternal.search(query, callback);
},
promise: function(method, ...params) {
return new Promise((resolve, reject) => {
this[method](...params, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
},
on: (...params) => eventEmitter.on(...params),
once: (...params) => eventEmitter.once(...params),

View File

@ -97,15 +97,6 @@ const injectObjectAPI = function(api, targetWindow) {
return contentObj;
};
/**
* Get the two-digit hexadecimal code for a byte
*
* @param {byte} charCode
*/
const toHexString = function(charCode) {
return ("0" + charCode.toString(16)).slice(-2);
};
/**
* Inject the loop API into the given window. The caller must be sure the
* window is a loop content window (eg, a panel, chatwindow, or similar).
@ -212,6 +203,25 @@ function injectLoopAPI(targetWindow) {
}
},
/**
* Import a list of (new) contacts from an external data source.
*
* @param {Object} options Property bag of options for the importer
* @param {Function} callback Function that will be invoked once the operation
* finished. The first argument passed will be an
* `Error` object or `null`. The second argument will
* be the result of the operation, if successfull.
*/
startImport: {
enumerable: true,
writable: true,
value: function(options, callback) {
LoopContacts.startImport(options, getChromeWindow(targetWindow), function(...results) {
callback(...[cloneValueInto(r, targetWindow) for (r of results)]);
});
}
},
/**
* Returns translated strings associated with an element. Designed
* for use with l10n.js
@ -553,42 +563,6 @@ function injectLoopAPI(targetWindow) {
return MozLoopService.generateUUID();
}
},
/**
* Compose a URL pointing to the location of an avatar by email address.
* At the moment we use the Gravatar service to match email addresses with
* avatars. This might change in the future as avatars might come from another
* source.
*
* @param {String} emailAddress Users' email address
* @param {Number} size Size of the avatar image to return in pixels.
* Optional. Default value: 40.
* @return the URL pointing to an avatar matching the provided email address.
*/
getUserAvatar: {
enumerable: true,
writable: true,
value: function(emailAddress, size = 40) {
if (!emailAddress) {
return "";
}
// Do the MD5 dance.
let hasher = Cc["@mozilla.org/security/hash;1"]
.createInstance(Ci.nsICryptoHash);
hasher.init(Ci.nsICryptoHash.MD5);
let stringStream = Cc["@mozilla.org/io/string-input-stream;1"]
.createInstance(Ci.nsIStringInputStream);
stringStream.data = emailAddress.trim().toLowerCase();
hasher.updateFromStream(stringStream, -1);
let hash = hasher.finish(false);
// Convert the binary hash data to a hex string.
let md5Email = [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
// Compose the Gravatar URL.
return "http://www.gravatar.com/avatar/" + md5Email + ".jpg?default=blank&s=" + size;
}
},
};
function onStatusChanged(aSubject, aTopic, aData) {

View File

@ -131,6 +131,16 @@ loop.contacts = (function(_, mozL10n) {
document.body.removeEventListener("click", this._onBodyClick);
},
componentShouldUpdate: function(nextProps, nextState) {
let currContact = this.props.contact;
let nextContact = nextProps.contact;
return (
currContact.name[0] !== nextContact.name[0] ||
currContact.blocked !== nextContact.blocked ||
this.getPreferredEmail(currContact).value !== this.getPreferredEmail(nextContact).value
);
},
handleAction: function(actionName) {
if (this.props.handleContactAction) {
this.props.handleContactAction(this.props.contact, actionName);
@ -149,19 +159,20 @@ loop.contacts = (function(_, mozL10n) {
};
},
getPreferredEmail: function() {
// The model currently does not enforce a name to be present, but we're
// going to assume it is awaiting more advanced validation of required fields
// by the model. (See bug 1069918)
let email = this.props.contact.email[0];
this.props.contact.email.some(function(address) {
getPreferredEmail: function(contact = this.props.contact) {
let email;
// A contact may not contain email addresses, but only a phone number instead.
if (contact.email) {
email = contact.email[0];
contact.email.some(function(address) {
if (address.pref) {
email = address;
return true;
}
return false;
});
return email;
}
return email || { value: "" };
},
canEdit: function() {
@ -181,9 +192,7 @@ loop.contacts = (function(_, mozL10n) {
return (
React.DOM.li({className: contactCSSClass, onMouseLeave: this.hideDropdownMenu},
React.DOM.div({className: "avatar"},
React.DOM.img({src: navigator.mozLoop.getUserAvatar(email.value)})
),
React.DOM.div({className: "avatar"}),
React.DOM.div({className: "details"},
React.DOM.div({className: "username"}, React.DOM.strong(null, names.firstName), " ", names.lastName,
React.DOM.i({className: cx({"icon icon-google": this.props.contact.category[0] == "google"})}),
@ -211,7 +220,8 @@ loop.contacts = (function(_, mozL10n) {
const ContactsList = React.createClass({displayName: 'ContactsList',
getInitialState: function() {
return {
contacts: {}
contacts: {},
importBusy: false
};
},
@ -227,11 +237,12 @@ loop.contacts = (function(_, mozL10n) {
// circumvent blocking the main event loop.
let addContactsInChunks = () => {
contacts.splice(0, CONTACTS_CHUNK_SIZE).forEach(contact => {
this.handleContactAddOrUpdate(contact);
this.handleContactAddOrUpdate(contact, false);
});
if (contacts.length) {
setTimeout(addContactsInChunks, 0);
}
this.forceUpdate();
};
addContactsInChunks(contacts);
@ -252,11 +263,13 @@ loop.contacts = (function(_, mozL10n) {
});
},
handleContactAddOrUpdate: function(contact) {
handleContactAddOrUpdate: function(contact, render = true) {
let contacts = this.state.contacts;
let guid = String(contact._guid);
contacts[guid] = contact;
this.setState({});
if (render) {
this.forceUpdate();
}
},
handleContactRemove: function(contact) {
@ -266,7 +279,7 @@ loop.contacts = (function(_, mozL10n) {
return;
}
delete contacts[guid];
this.setState({});
this.forceUpdate();
},
handleContactRemoveAll: function() {
@ -274,6 +287,16 @@ loop.contacts = (function(_, mozL10n) {
},
handleImportButtonClick: function() {
this.setState({ importBusy: true });
navigator.mozLoop.startImport({
service: "google"
}, (err, stats) => {
this.setState({ importBusy: false });
// TODO: bug 1076764 - proper error and success reporting.
if (err) {
throw err;
}
});
},
handleAddContactButtonClick: function() {
@ -321,12 +344,15 @@ loop.contacts = (function(_, mozL10n) {
return contact.blocked ? "blocked" : "available";
});
// TODO: bug 1076767 - add a spinner whilst importing contacts.
return (
React.DOM.div(null,
React.DOM.div({className: "content-area"},
ButtonGroup(null,
Button({caption: mozL10n.get("import_contacts_button"),
disabled: true,
Button({caption: this.state.importBusy
? mozL10n.get("importing_contacts_progress_button")
: mozL10n.get("import_contacts_button"),
disabled: this.state.importBusy,
onClick: this.handleImportButtonClick}),
Button({caption: mozL10n.get("new_contact_button"),
onClick: this.handleAddContactButtonClick})

View File

@ -131,6 +131,16 @@ loop.contacts = (function(_, mozL10n) {
document.body.removeEventListener("click", this._onBodyClick);
},
componentShouldUpdate: function(nextProps, nextState) {
let currContact = this.props.contact;
let nextContact = nextProps.contact;
return (
currContact.name[0] !== nextContact.name[0] ||
currContact.blocked !== nextContact.blocked ||
this.getPreferredEmail(currContact).value !== this.getPreferredEmail(nextContact).value
);
},
handleAction: function(actionName) {
if (this.props.handleContactAction) {
this.props.handleContactAction(this.props.contact, actionName);
@ -149,19 +159,20 @@ loop.contacts = (function(_, mozL10n) {
};
},
getPreferredEmail: function() {
// The model currently does not enforce a name to be present, but we're
// going to assume it is awaiting more advanced validation of required fields
// by the model. (See bug 1069918)
let email = this.props.contact.email[0];
this.props.contact.email.some(function(address) {
getPreferredEmail: function(contact = this.props.contact) {
let email;
// A contact may not contain email addresses, but only a phone number instead.
if (contact.email) {
email = contact.email[0];
contact.email.some(function(address) {
if (address.pref) {
email = address;
return true;
}
return false;
});
return email;
}
return email || { value: "" };
},
canEdit: function() {
@ -181,9 +192,7 @@ loop.contacts = (function(_, mozL10n) {
return (
<li className={contactCSSClass} onMouseLeave={this.hideDropdownMenu}>
<div className="avatar">
<img src={navigator.mozLoop.getUserAvatar(email.value)} />
</div>
<div className="avatar" />
<div className="details">
<div className="username"><strong>{names.firstName}</strong> {names.lastName}
<i className={cx({"icon icon-google": this.props.contact.category[0] == "google"})} />
@ -211,7 +220,8 @@ loop.contacts = (function(_, mozL10n) {
const ContactsList = React.createClass({
getInitialState: function() {
return {
contacts: {}
contacts: {},
importBusy: false
};
},
@ -227,11 +237,12 @@ loop.contacts = (function(_, mozL10n) {
// circumvent blocking the main event loop.
let addContactsInChunks = () => {
contacts.splice(0, CONTACTS_CHUNK_SIZE).forEach(contact => {
this.handleContactAddOrUpdate(contact);
this.handleContactAddOrUpdate(contact, false);
});
if (contacts.length) {
setTimeout(addContactsInChunks, 0);
}
this.forceUpdate();
};
addContactsInChunks(contacts);
@ -252,11 +263,13 @@ loop.contacts = (function(_, mozL10n) {
});
},
handleContactAddOrUpdate: function(contact) {
handleContactAddOrUpdate: function(contact, render = true) {
let contacts = this.state.contacts;
let guid = String(contact._guid);
contacts[guid] = contact;
this.setState({});
if (render) {
this.forceUpdate();
}
},
handleContactRemove: function(contact) {
@ -266,7 +279,7 @@ loop.contacts = (function(_, mozL10n) {
return;
}
delete contacts[guid];
this.setState({});
this.forceUpdate();
},
handleContactRemoveAll: function() {
@ -274,6 +287,16 @@ loop.contacts = (function(_, mozL10n) {
},
handleImportButtonClick: function() {
this.setState({ importBusy: true });
navigator.mozLoop.startImport({
service: "google"
}, (err, stats) => {
this.setState({ importBusy: false });
// TODO: bug 1076764 - proper error and success reporting.
if (err) {
throw err;
}
});
},
handleAddContactButtonClick: function() {
@ -321,12 +344,15 @@ loop.contacts = (function(_, mozL10n) {
return contact.blocked ? "blocked" : "available";
});
// TODO: bug 1076767 - add a spinner whilst importing contacts.
return (
<div>
<div className="content-area">
<ButtonGroup>
<Button caption={mozL10n.get("import_contacts_button")}
disabled
<Button caption={this.state.importBusy
? mozL10n.get("importing_contacts_progress_button")
: mozL10n.get("import_contacts_button")}
disabled={this.state.importBusy}
onClick={this.handleImportButtonClick} />
<Button caption={mozL10n.get("new_contact_button")}
onClick={this.handleAddContactButtonClick} />

View File

@ -12,10 +12,12 @@ loop.conversation = (function(mozL10n) {
"use strict";
var sharedViews = loop.shared.views;
var sharedMixins = loop.shared.mixins;
var sharedModels = loop.shared.models;
var OutgoingConversationView = loop.conversationViews.OutgoingConversationView;
var IncomingCallView = React.createClass({displayName: 'IncomingCallView',
mixins: [sharedMixins.DropdownMenuMixin],
propTypes: {
model: React.PropTypes.object.isRequired,
@ -24,25 +26,11 @@ loop.conversation = (function(mozL10n) {
getDefaultProps: function() {
return {
showDeclineMenu: false,
showMenu: false,
video: true
};
},
getInitialState: function() {
return {showDeclineMenu: this.props.showDeclineMenu};
},
componentDidMount: function() {
window.addEventListener("click", this.clickHandler);
window.addEventListener("blur", this._hideDeclineMenu);
},
componentWillUnmount: function() {
window.removeEventListener("click", this.clickHandler);
window.removeEventListener("blur", this._hideDeclineMenu);
},
clickHandler: function(e) {
var target = e.target;
if (!target.classList.contains('btn-chevron')) {
@ -104,7 +92,7 @@ loop.conversation = (function(mozL10n) {
var dropdownMenuClassesDecline = React.addons.classSet({
"native-dropdown-menu": true,
"conversation-window-dropdown": true,
"visually-hidden": !this.state.showDeclineMenu
"visually-hidden": !this.state.showMenu
});
return (
React.DOM.div({className: "call-window"},
@ -117,13 +105,11 @@ loop.conversation = (function(mozL10n) {
React.DOM.div({className: "btn-group-chevron"},
React.DOM.div({className: "btn-group"},
React.DOM.button({className: "btn btn-error btn-decline",
React.DOM.button({className: "btn btn-decline",
onClick: this._handleDecline},
mozL10n.get("incoming_call_cancel_button")
),
React.DOM.div({className: "btn-chevron",
onClick: this._toggleDeclineMenu}
)
React.DOM.div({className: "btn-chevron", onClick: this.toggleDropdownMenu})
),
React.DOM.ul({className: dropdownMenuClassesDecline},

View File

@ -49,6 +49,20 @@
z-index: 1;
}
.contact > .details {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.contact:hover > .details {
/* Hovering the contact shows the icons/ buttons, which takes up horizontal
* space. This causes the fixed-size avatar to resize horizontally, so we assign
* a flex value equivalent to the maximum pixel value to avoid the resizing
* to happen. Consider this a hack. */
flex: 190;
}
.contact > .avatar {
width: 40px;
height: 40px;

View File

@ -14,6 +14,7 @@ BROWSER_CHROME_MANIFESTS += [
EXTRA_JS_MODULES.loop += [
'CardDavImporter.jsm',
'GoogleImporter.jsm',
'LoopContacts.jsm',
'LoopStorage.jsm',
'MozLoopAPI.jsm',

View File

@ -1,11 +1,16 @@
[DEFAULT]
support-files =
fixtures/google_auth.txt
fixtures/google_contacts.txt
fixtures/google_token.txt
google_service.sjs
head.js
loop_fxa.sjs
../../../../base/content/test/general/browser_fxa_oauth.html
[browser_CardDavImporter.js]
[browser_fxa_login.js]
[browser_GoogleImporter.js]
skip-if = e10s
[browser_loop_fxa_server.js]
[browser_LoopContacts.js]

View File

@ -3,46 +3,6 @@
const {CardDavImporter} = Cu.import("resource:///modules/loop/CardDavImporter.jsm", {});
const mockDb = {
_store: { },
_next_guid: 1,
add: function(details, callback) {
if (!("id" in details)) {
callback(new Error("No 'id' field present"));
return;
}
details._guid = this._next_guid++;
this._store[details._guid] = details;
callback(null, details);
},
remove: function(guid, callback) {
if (!guid in this._store) {
callback(new Error("Could not find _guid '" + guid + "' in database"));
return;
}
delete this._store[guid];
callback(null);
},
get: function(guid, callback) {
callback(null, this._store[guid]);
},
getByServiceId: function(serviceId, callback) {
for (let guid in this._store) {
if (serviceId === this._store[guid].id) {
callback(null, this._store[guid]);
return;
}
}
callback(null, null);
},
removeAll: function(callback) {
this._store = {};
this._next_guid = 1;
callback(null);
}
};
const kAuth = {
"method": "basic",
"user": "username",

View File

@ -0,0 +1,77 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const {GoogleImporter} = Cu.import("resource:///modules/loop/GoogleImporter.jsm", {});
let importer = new GoogleImporter();
function promiseImport() {
return new Promise(function(resolve, reject) {
importer.startImport({}, function(err, stats) {
if (err) {
reject(err);
} else {
resolve(stats);
}
}, mockDb, window);
});
}
add_task(function* test_GoogleImport() {
let stats;
// An error may throw and the test will fail when that happens.
stats = yield promiseImport();
// Assert the world.
Assert.equal(stats.total, 5, "Five contacts should get processed");
Assert.equal(stats.success, 5, "Five contacts should be imported");
yield promiseImport();
Assert.equal(Object.keys(mockDb._store).length, 5, "Database should contain only five contact after reimport");
let c = mockDb._store[mockDb._next_guid - 5];
Assert.equal(c.name[0], "John Smith", "Full name should match");
Assert.equal(c.givenName[0], "John", "Given name should match");
Assert.equal(c.familyName[0], "Smith", "Family name should match");
Assert.equal(c.email[0].type, "other", "Email type should match");
Assert.equal(c.email[0].value, "john.smith@example.com", "Email should match");
Assert.equal(c.email[0].pref, true, "Pref should match");
Assert.equal(c.category[0], "google", "Category should match");
Assert.equal(c.id, "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/0", "UID should match and be scoped to provider");
c = mockDb._store[mockDb._next_guid - 4];
Assert.equal(c.name[0], "Jane Smith", "Full name should match");
Assert.equal(c.givenName[0], "Jane", "Given name should match");
Assert.equal(c.familyName[0], "Smith", "Family name should match");
Assert.equal(c.email[0].type, "other", "Email type should match");
Assert.equal(c.email[0].value, "jane.smith@example.com", "Email should match");
Assert.equal(c.email[0].pref, true, "Pref should match");
Assert.equal(c.category[0], "google", "Category should match");
Assert.equal(c.id, "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/1", "UID should match and be scoped to provider");
c = mockDb._store[mockDb._next_guid - 3];
Assert.equal(c.name[0], "Davy Randall Jones", "Full name should match");
Assert.equal(c.givenName[0], "Davy Randall", "Given name should match");
Assert.equal(c.familyName[0], "Jones", "Family name should match");
Assert.equal(c.email[0].type, "other", "Email type should match");
Assert.equal(c.email[0].value, "davy.jones@example.com", "Email should match");
Assert.equal(c.email[0].pref, true, "Pref should match");
Assert.equal(c.category[0], "google", "Category should match");
Assert.equal(c.id, "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/2", "UID should match and be scoped to provider");
c = mockDb._store[mockDb._next_guid - 2];
Assert.equal(c.name[0], "noname@example.com", "Full name should match");
Assert.equal(c.email[0].type, "other", "Email type should match");
Assert.equal(c.email[0].value, "noname@example.com", "Email should match");
Assert.equal(c.email[0].pref, true, "Pref should match");
Assert.equal(c.category[0], "google", "Category should match");
Assert.equal(c.id, "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/3", "UID should match and be scoped to provider");
c = mockDb._store[mockDb._next_guid - 1];
Assert.equal(c.name[0], "lycnix", "Full name should match");
Assert.equal(c.email[0].type, "other", "Email type should match");
Assert.equal(c.email[0].value, "lycnix", "Email should match");
Assert.equal(c.email[0].pref, true, "Pref should match");
Assert.equal(c.category[0], "google", "Category should match");
Assert.equal(c.id, "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/7", "UID should match and be scoped to provider");
});

View File

@ -0,0 +1,5 @@
<!DOCTYPE html>
<html>
<head><title>Success code=test-code</title></head>
<body>Le Code.</body>
</html>

View File

@ -0,0 +1,94 @@
<?xml version='1.0' encoding='UTF-8'?>
<feed gd:etag="W/&quot;DUQNRHc8cCt7I2A9XRdSF04.&quot;" xmlns="http://www.w3.org/2005/Atom" xmlns:batch="http://schemas.google.com/gdata/batch" xmlns:gContact="http://schemas.google.com/contact/2008" xmlns:gd="http://schemas.google.com/g/2005" xmlns:openSearch="http://a9.com/-/spec/opensearch/1.1/">
<id>tester@mochi.com</id>
<updated>2014-09-26T13:16:35.978Z</updated>
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
<title>Mochi Tester's Contacts</title>
<link href="http://www.google.com/" rel="alternate" type="text/html"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full" rel="http://schemas.google.com/g/2005#feed" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full" rel="http://schemas.google.com/g/2005#post" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/batch" rel="http://schemas.google.com/g/2005#batch" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full?max-results=25" rel="self" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full?start-index=26&amp;max-results=25" rel="next" type="application/atom+xml"/>
<author>
<name>Mochi Tester</name>
<email>tester@mochi.com</email>
</author>
<generator uri="http://www.google.com/m8/feeds" version="1.0">Contacts</generator>
<openSearch:totalResults>25</openSearch:totalResults>
<openSearch:startIndex>1</openSearch:startIndex>
<openSearch:itemsPerPage>10000000</openSearch:itemsPerPage>
<entry gd:etag="&quot;R3YyejRVLit7I2A9WhJWEkkNQwc.&quot;">
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/0</id>
<updated>2012-08-17T23:50:36.892Z</updated>
<app:edited xmlns:app="http://www.w3.org/2007/app">2012-08-17T23:50:36.892Z</app:edited>
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
<title>John Smith</title>
<link gd:etag="&quot;Ug92D34SfCt7I2BmLHJTRgVzTlgrJXEAU08.&quot;" href="https://www.google.com/m8/feeds/photos/media/tester%40mochi.com/0" rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/0" rel="self" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/0" rel="edit" type="application/atom+xml"/>
<gd:name>
<gd:fullName>John Smith</gd:fullName>
<gd:givenName>John</gd:givenName>
<gd:familyName>Smith</gd:familyName>
</gd:name>
<gd:email address="john.smith@example.com" primary="true" rel="http://schemas.google.com/g/2005#other"/>
<gContact:website href="http://www.google.com/profiles/109576547678240773721" rel="profile"/>
</entry>
<entry gd:etag="&quot;R3YyejRVLit7I2A9WhJWEkkNQwc.&quot;">
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/1</id>
<updated>2012-08-17T23:50:36.892Z</updated>
<app:edited xmlns:app="http://www.w3.org/2007/app">2012-08-17T23:50:36.892Z</app:edited>
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
<title>Jane Smith</title>
<link gd:etag="&quot;WA9BY1xFWit7I2BhLEkieCxLHEYTGCYuNxo.&quot;" href="https://www.google.com/m8/feeds/photos/media/tester%40mochi.com/1" rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/1" rel="self" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/1" rel="edit" type="application/atom+xml"/>
<gd:name>
<gd:fullName>Jane Smith</gd:fullName>
<gd:givenName>Jane</gd:givenName>
<gd:familyName>Smith</gd:familyName>
</gd:name>
<gd:email address="jane.smith@example.com" primary="true" rel="http://schemas.google.com/g/2005#other"/>
<gContact:website href="http://www.google.com/profiles/112886528199784431028" rel="profile"/>
</entry>
<entry gd:etag="&quot;R3YyejRVLit7I2A9WhJWEkkNQwc.&quot;">
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/2</id>
<updated>2012-08-17T23:50:36.892Z</updated>
<app:edited xmlns:app="http://www.w3.org/2007/app">2012-08-17T23:50:36.892Z</app:edited>
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
<title>Davy Randall Jones</title>
<link gd:etag="&quot;KiV2PkYRfCt7I2BuD1AzEBFxD1VcGjwBUyA.&quot;" href="https://www.google.com/m8/feeds/photos/media/tester%40mochi.com/2" rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/2" rel="self" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/2" rel="edit" type="application/atom+xml"/>
<gd:name>
<gd:fullName>Davy Randall Jones</gd:fullName>
<gd:givenName>Davy Randall</gd:givenName>
<gd:familyName>Jones</gd:familyName>
</gd:name>
<gd:email address="davy.jones@example.com" primary="true" rel="http://schemas.google.com/g/2005#other"/>
<gContact:website href="http://www.google.com/profiles/109710625881478599011" rel="profile"/>
</entry>
<entry gd:etag="&quot;Q3w7ezVSLit7I2A9WB5WGUkNRgE.&quot;">
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/3</id>
<updated>2007-08-01T05:45:52.203Z</updated>
<app:edited xmlns:app="http://www.w3.org/2007/app">2007-08-01T05:45:52.203Z</app:edited>
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
<title/>
<link href="https://www.google.com/m8/feeds/photos/media/tester%40mochi.com/3" rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/3" rel="self" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/3" rel="edit" type="application/atom+xml"/>
<gd:email address="noname@example.com" primary="true" rel="http://schemas.google.com/g/2005#other"/>
</entry>
<entry gd:etag="&quot;Q3w7ezVSLit7I2A9WB5WGUkNRgE.&quot;">
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/7</id>
<updated>2007-08-01T05:45:52.203Z</updated>
<app:edited xmlns:app="http://www.w3.org/2007/app">2007-08-01T05:45:52.203Z</app:edited>
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
<title/>
<link href="https://www.google.com/m8/feeds/photos/media/tester%40mochi.com/7" rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/7" rel="self" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/7" rel="edit" type="application/atom+xml"/>
<gd:email address="lycnix" primary="true" rel="http://schemas.google.com/g/2005#other"/>
</entry>
</feed>

View File

@ -0,0 +1,3 @@
{
"access_token": "test-token"
}

View File

@ -0,0 +1,147 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const {classes: Cc, interfaces: Ci, Constructor: CC} = Components;
const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
"nsIBinaryInputStream",
"setInputStream");
function handleRequest(req, res) {
try {
reallyHandleRequest(req, res);
} catch (ex) {
res.setStatusLine("1.0", 200, "AlmostOK");
let msg = "Error handling request: " + ex + "\n" + ex.stack;
log(msg);
res.write(msg);
}
}
function log(msg) {
// dump("GOOGLE-SERVER-MOCK: " + msg + "\n");
}
const kBasePath = "browser/browser/components/loop/test/mochitest/fixtures/";
const kStatusCodes = {
400: "Bad Request",
401: "Unauthorized",
403: "Forbidden",
404: "Not Found",
405: "Method Not Allowed",
500: "Internal Server Error",
501: "Not Implemented",
503: "Service Unavailable"
};
function HTTPError(code = 500, message) {
this.code = code;
this.name = kStatusCodes[code] || "HTTPError";
this.message = message || this.name;
}
HTTPError.prototype = new Error();
HTTPError.prototype.constructor = HTTPError;
function sendError(res, err) {
if (!(err instanceof HTTPError)) {
err = new HTTPError(typeof err == "number" ? err : 500,
err.message || typeof err == "string" ? err : "");
}
res.setStatusLine("1.1", err.code, err.name);
res.write(err.message);
}
function parseQuery(query, params = {}) {
for (let param of query.replace(/^[?&]/, "").split(/(?:&|\?)/)) {
param = param.split("=");
if (!param[0])
continue;
params[unescape(param[0])] = unescape(param[1]);
}
return params;
}
function getRequestBody(req) {
let avail;
let bytes = [];
let body = new BinaryInputStream(req.bodyInputStream);
while ((avail = body.available()) > 0)
Array.prototype.push.apply(bytes, body.readByteArray(avail));
return String.fromCharCode.apply(null, bytes);
}
function getInputStream(path) {
let file = Cc["@mozilla.org/file/directory_service;1"]
.getService(Ci.nsIProperties)
.get("CurWorkD", Ci.nsILocalFile);
for (let part of path.split("/"))
file.append(part);
let fileStream = Cc["@mozilla.org/network/file-input-stream;1"]
.createInstance(Ci.nsIFileInputStream);
fileStream.init(file, 1, 0, false);
return fileStream;
}
function checkAuth(req) {
if (!req.hasHeader("Authorization"))
throw new HTTPError(401, "No Authorization header provided.");
let auth = req.getHeader("Authorization");
if (auth != "Bearer test-token")
throw new HTTPError(401, "Invalid Authorization header content: '" + auth + "'");
}
function reallyHandleRequest(req, res) {
log("method: " + req.method);
let body = getRequestBody(req);
log("body: " + body);
let contentType = req.hasHeader("Content-Type") ? req.getHeader("Content-Type") : null;
log("contentType: " + contentType);
let params = parseQuery(req.queryString);
parseQuery(body, params);
log("params: " + JSON.stringify(params));
// Delegate an authentication request to the correct handler.
if ("action" in params) {
methodHandlers[params.action](req, res, params);
} else {
sendError(res, 501);
}
}
function respondWithFile(res, fileName, mimeType) {
res.setStatusLine("1.1", 200, "OK");
res.setHeader("Content-Type", mimeType);
let inputStream = getInputStream(kBasePath + fileName);
res.bodyOutputStream.writeFrom(inputStream, inputStream.available());
inputStream.close();
}
const methodHandlers = {
auth: function(req, res, params) {
respondWithFile(res, "google_auth.txt", "text/html");
},
token: function(req, res, params) {
respondWithFile(res, "google_token.txt", "application/json");
},
contacts: function(req, res, params) {
try {
checkAuth(req);
} catch (ex) {
sendError(res, ex, ex.code);
return;
}
respondWithFile(res, "google_contacts.txt", "text/xml");
}
};

View File

@ -198,3 +198,51 @@ let mockPushHandler = {
this._notificationCallback(version);
}
};
const mockDb = {
_store: { },
_next_guid: 1,
add: function(details, callback) {
if (!("id" in details)) {
callback(new Error("No 'id' field present"));
return;
}
details._guid = this._next_guid++;
this._store[details._guid] = details;
callback(null, details);
},
remove: function(guid, callback) {
if (!guid in this._store) {
callback(new Error("Could not find _guid '" + guid + "' in database"));
return;
}
delete this._store[guid];
callback(null);
},
getAll: function(callback) {
callback(null, this._store);
},
get: function(guid, callback) {
callback(null, this._store[guid]);
},
getByServiceId: function(serviceId, callback) {
for (let guid in this._store) {
if (serviceId === this._store[guid].id) {
callback(null, this._store[guid]);
return;
}
}
callback(null, null);
},
removeAll: function(callback) {
this._store = {};
this._next_guid = 1;
callback(null);
},
promise: function(method, ...params) {
return new Promise(resolve => {
this[method](...params, (err, res) => err ? reject(err) : resolve(res));
});
}
};

View File

@ -7,4 +7,4 @@ support-files =
manifest.webapp
[browser_manifest_editor.js]
skip-if = os == "linux"
skip-if = true # Bug 989169 - Very intermittent, but App Manager about to be removed

View File

@ -26,8 +26,11 @@ Bug 901519 - [app manager] data store for connections
<script type="application/javascript;version=1.8">
const Cu = Components.utils;
Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
if (!DebuggerServer.initialized) {
DebuggerServer.init(function () { return true; });
DebuggerServer.addBrowserActors();
}
window.onload = function() {
SimpleTest.waitForExplicitFinish();

View File

@ -20,8 +20,11 @@ Bug 901520 - [app manager] data store for device
<script type="application/javascript;version=1.8">
const Cu = Components.utils;
Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
if (!DebuggerServer.initialized) {
DebuggerServer.init(function () { return true; });
DebuggerServer.addBrowserActors();
}
function compare(o1, o2, msg) {
is(JSON.stringify(o1), JSON.stringify(o2), msg);

View File

@ -20,8 +20,11 @@ Bug 912646 - Closing app toolbox causes phone to disconnect
const Cu = Components.utils;
Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
if (!DebuggerServer.initialized) {
DebuggerServer.init(function () { return true; });
DebuggerServer.addBrowserActors();
}
window.onload = function() {
SimpleTest.waitForExplicitFinish();

View File

@ -15,6 +15,7 @@ support-files =
code_blackboxing_three.js
code_blackboxing_two.js
code_breakpoints-break-on-last-line-of-script-on-reload.js
code_breakpoints-other-tabs.js
code_function-search-01.js
code_function-search-02.js
code_function-search-03.js
@ -42,6 +43,8 @@ support-files =
doc_binary_search.html
doc_blackboxing.html
doc_breakpoints-break-on-last-line-of-script-on-reload.html
doc_breakpoints-other-tabs.html
doc_breakpoints-reload.html
doc_closures.html
doc_closure-optimized-out.html
doc_cmd-break.html
@ -133,7 +136,9 @@ skip-if = os == "mac" || e10s # Bug 895426
[browser_dbg_breakpoints-editor.js]
[browser_dbg_breakpoints-highlight.js]
[browser_dbg_breakpoints-new-script.js]
[browser_dbg_breakpoints-other-tabs.js]
[browser_dbg_breakpoints-pane.js]
[browser_dbg_breakpoints-reload.js]
[browser_dbg_chrome-create.js]
[browser_dbg_chrome-debugging.js]
[browser_dbg_clean-exit-window.js]

View File

@ -0,0 +1,35 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Make sure that setting a breakpoint in one tab, doesn't cause another tab at
* the same source to pause at that location.
*/
const TAB_URL = EXAMPLE_URL + "doc_breakpoints-other-tabs.html";
let test = Task.async(function* () {
const [tab1, debuggee1, panel1] = yield initDebugger(TAB_URL);
const [tab2, debuggee2, panel2] = yield initDebugger(TAB_URL);
yield ensureSourceIs(panel1, "code_breakpoints-other-tabs.js", true);
const sources = panel1.panelWin.DebuggerView.Sources;
yield panel1.addBreakpoint({
url: sources.selectedValue,
line: 2
});
const paused = waitForThreadEvents(panel2, "paused");
executeSoon(() => debuggee2.testCase());
const packet = yield paused;
is(packet.why.type, "debuggerStatement",
"Should have stopped at the debugger statement, not the other tab's breakpoint");
is(packet.frame.where.line, 3,
"Should have stopped at line 3 (debugger statement), not line 2 (other tab's breakpoint)");
yield teardown(panel1);
yield resumeDebuggerThenCloseAndFinish(panel2);
});

View File

@ -0,0 +1,35 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Make sure that setting a breakpoint on code that gets run on load, will get
* hit when we reload.
*/
const TAB_URL = EXAMPLE_URL + "doc_breakpoints-reload.html";
let test = Task.async(function* () {
requestLongerTimeout(4);
const [tab, debuggee, panel] = yield initDebugger(TAB_URL);
yield ensureSourceIs(panel, "doc_breakpoints-reload.html", true);
const sources = panel.panelWin.DebuggerView.Sources;
yield panel.addBreakpoint({
url: sources.selectedValue,
line: 10 // "break on me" string
});
const paused = waitForThreadEvents(panel, "paused");
reloadActiveTab(panel);
const packet = yield paused;
is(packet.why.type, "breakpoint",
"Should have hit the breakpoint after the reload");
is(packet.frame.where.line, 10,
"Should have stopped at line 10, where we set the breakpoint");
yield resumeDebuggerThenCloseAndFinish(panel);
});

View File

@ -0,0 +1,4 @@
function testCase() {
var foo = "break on me";
debugger;
}

View File

@ -0,0 +1,8 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<!DOCTYPE html>
<head>
<meta charset="utf-8"/>
<title>Debugger Breakpoints Other Tabs Test Page</title>
</head>
<script src="code_breakpoints-other-tabs.js"></script>

View File

@ -0,0 +1,12 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<!DOCTYPE html>
<head>
<meta charset="utf-8"/>
<title>Debugger Breakpoints Other Tabs Test Page</title>
</head>
<script>
(function () {
window.foo = "break on me";
}());
</script>

View File

@ -36,7 +36,7 @@
* about:telemetry.
*
* You can view telemetry stats for large groups of Firefox users at
* metrics.mozilla.com.
* telemetry.mozilla.org.
*/
const TOOLS_OPENED_PREF = "devtools.telemetry.tools.opened.version";
@ -170,6 +170,11 @@ Telemetry.prototype = {
userHistogram: "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_PER_USER_FLAG",
timerHistogram: "DEVTOOLS_DEVELOPERTOOLBAR_TIME_ACTIVE_SECONDS"
},
webide: {
histogram: "DEVTOOLS_WEBIDE_OPENED_BOOLEAN",
userHistogram: "DEVTOOLS_WEBIDE_OPENED_PER_USER_FLAG",
timerHistogram: "DEVTOOLS_WEBIDE_TIME_ACTIVE_SECONDS"
},
custom: {
histogram: "DEVTOOLS_CUSTOM_OPENED_BOOLEAN",
userHistogram: "DEVTOOLS_CUSTOM_OPENED_PER_USER_FLAG",
@ -194,7 +199,7 @@ Telemetry.prototype = {
this.logOncePerBrowserVersion(charts.userHistogram, true);
}
if (charts.timerHistogram) {
this._timers.set(charts.timerHistogram, new Date());
this.startTimer(charts.timerHistogram);
}
},
@ -205,12 +210,31 @@ Telemetry.prototype = {
return;
}
let startTime = this._timers.get(charts.timerHistogram);
this.stopTimer(charts.timerHistogram);
},
/**
* Record the start time for a timing-based histogram entry.
*
* @param String histogramId
* Histogram in which the data is to be stored.
*/
startTimer: function(histogramId) {
this._timers.set(histogramId, new Date());
},
/**
* Stop the timer and log elasped time for a timing-based histogram entry.
*
* @param String histogramId
* Histogram in which the data is to be stored.
*/
stopTimer: function(histogramId) {
let startTime = this._timers.get(histogramId);
if (startTime) {
let time = (new Date() - startTime) / 1000;
this.log(charts.timerHistogram, time);
this._timers.delete(charts.timerHistogram);
this.log(histogramId, time);
this._timers.delete(histogramId);
}
},
@ -258,11 +282,8 @@ Telemetry.prototype = {
},
destroy: function() {
for (let [histogram, time] of this._timers) {
time = (new Date() - time) / 1000;
this.log(histogram, time);
this._timers.delete(histogram);
for (let histogramId of this._timers.keys()) {
this.stopTimer(histogramId);
}
}
};

View File

@ -88,7 +88,7 @@ function CheckLockState() {
// ADB check
if (AppManager.selectedRuntime instanceof USBRuntime) {
let device = Devices.getByName(AppManager.selectedRuntime.id);
if (device.summonRoot) {
if (device && device.summonRoot) {
device.isRoot().then(isRoot => {
if (isRoot) {
adbCheckResult.textContent = sYes;

View File

@ -21,6 +21,7 @@ const {Devices} = Cu.import("resource://gre/modules/devtools/Devices.jsm");
const {GetAvailableAddons} = require("devtools/webide/addons");
const {GetTemplatesJSON, GetAddonsJSON} = require("devtools/webide/remote-resources");
const utils = require("devtools/webide/utils");
const Telemetry = require("devtools/shared/telemetry");
const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/webide.properties");
@ -47,6 +48,9 @@ window.addEventListener("unload", function onUnload() {
let UI = {
init: function() {
this._telemetry = new Telemetry();
this._telemetry.toolOpened("webide");
AppManager.init();
this.onMessage = this.onMessage.bind(this);
@ -85,6 +89,8 @@ let UI = {
AppManager.off("app-manager-update", this.appManagerUpdate);
AppManager.uninit();
window.removeEventListener("message", this.onMessage);
this.updateConnectionTelemetry();
this._telemetry.toolClosed("webide");
},
canWindowClose: function() {
@ -117,6 +123,7 @@ let UI = {
case "connection":
this.updateRuntimeButton();
this.updateCommands();
this.updateConnectionTelemetry();
break;
case "project":
this._updatePromise = Task.spawn(function() {
@ -225,12 +232,13 @@ let UI = {
},
busyWithProgressUntil: function(promise, operationDescription) {
this.busyUntil(promise, operationDescription);
let busy = this.busyUntil(promise, operationDescription);
let win = document.querySelector("window");
let progress = document.querySelector("#action-busy-determined");
progress.mode = "undetermined";
win.classList.add("busy-determined");
win.classList.remove("busy-undetermined");
return busy;
},
busyUntil: function(promise, operationDescription) {
@ -372,6 +380,7 @@ let UI = {
connectToRuntime: function(runtime) {
let name = runtime.getName();
let promise = AppManager.connectToRuntime(runtime);
promise.then(() => this.initConnectionTelemetry());
return this.busyUntil(promise, "connecting to runtime");
},
@ -396,6 +405,47 @@ let UI = {
this.lastConnectedRuntime);
},
_actionsToLog: new Set(),
/**
* For each new connection, track whether play and debug were ever used. Only
* one value is collected for each button, even if they are used multiple
* times during a connection.
*/
initConnectionTelemetry: function() {
this._actionsToLog.add("play");
this._actionsToLog.add("debug");
},
/**
* Action occurred. Log that it happened, and remove it from the loggable
* set.
*/
onAction: function(action) {
if (!this._actionsToLog.has(action)) {
return;
}
this.logActionState(action, true);
this._actionsToLog.delete(action);
},
/**
* Connection status changed or we are shutting down. Record any loggable
* actions as having not occurred.
*/
updateConnectionTelemetry: function() {
for (let action of this._actionsToLog.values()) {
this.logActionState(action, false);
}
this._actionsToLog.clear();
},
logActionState: function(action, state) {
let histogramId = "DEVTOOLS_WEBIDE_CONNECTION_" +
action.toUpperCase() + "_USED";
this._telemetry.log(histogramId, state);
},
/********** PROJECTS **********/
// Panel & button
@ -837,8 +887,7 @@ let UI = {
splitter.setAttribute("hidden", "true");
document.querySelector("#action-button-debug").removeAttribute("active");
},
}
};
let Cmds = {
quit: function() {
@ -1108,17 +1157,28 @@ let Cmds = {
},
play: function() {
let busy;
switch(AppManager.selectedProject.type) {
case "packaged":
return UI.busyWithProgressUntil(AppManager.installAndRunProject(), "installing and running app");
busy = UI.busyWithProgressUntil(AppManager.installAndRunProject(),
"installing and running app");
break;
case "hosted":
return UI.busyUntil(AppManager.installAndRunProject(), "installing and running app");
busy = UI.busyUntil(AppManager.installAndRunProject(),
"installing and running app");
break;
case "runtimeApp":
return UI.busyUntil(AppManager.launchOrReloadRuntimeApp(), "launching / reloading app");
busy = UI.busyUntil(AppManager.launchOrReloadRuntimeApp(), "launching / reloading app");
break;
case "tab":
return UI.busyUntil(AppManager.reloadTab(), "reloading tab");
busy = UI.busyUntil(AppManager.reloadTab(), "reloading tab");
break;
}
if (!busy) {
return promise.reject();
}
UI.onAction("play");
return busy;
},
stop: function() {
@ -1126,6 +1186,7 @@ let Cmds = {
},
toggleToolbox: function() {
UI.onAction("debug");
if (UI.toolboxIframe) {
UI.destroyToolbox();
return promise.resolve();

View File

@ -26,6 +26,7 @@ const {USBRuntime, WiFiRuntime, SimulatorRuntime,
gLocalRuntime, gRemoteRuntime} = require("devtools/webide/runtimes");
const discovery = require("devtools/toolkit/discovery/discovery");
const {NetUtil} = Cu.import("resource://gre/modules/NetUtil.jsm", {});
const Telemetry = require("devtools/shared/telemetry");
const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/webide.properties");
@ -68,6 +69,8 @@ exports.AppManager = AppManager = {
this.observe = this.observe.bind(this);
Services.prefs.addObserver(WIFI_SCANNING_PREF, this, false);
this._telemetry = new Telemetry();
},
uninit: function() {
@ -372,6 +375,25 @@ exports.AppManager = AppManager = {
}
}, deferred.reject);
// Record connection result in telemetry
let logResult = result => {
this._telemetry.log("DEVTOOLS_WEBIDE_CONNECTION_RESULT", result);
if (runtime.type) {
this._telemetry.log("DEVTOOLS_WEBIDE_" + runtime.type +
"_CONNECTION_RESULT", result);
}
};
deferred.promise.then(() => logResult(true), () => logResult(false));
// If successful, record connection time in telemetry
deferred.promise.then(() => {
const timerId = "DEVTOOLS_WEBIDE_CONNECTION_TIME_SECONDS";
this._telemetry.startTimer(timerId);
this.connection.once(Connection.Events.STATUS_CHANGED, () => {
this._telemetry.stopTimer(timerId);
});
});
return deferred.promise;
},

View File

@ -33,3 +33,4 @@ support-files =
[test_addons.html]
[test_deviceinfo.html]
[test_autoconnect_runtime.html]
[test_telemetry.html]

View File

@ -115,6 +115,14 @@ function waitForUpdate(win, update) {
return deferred.promise;
}
function waitForTime(time) {
let deferred = promise.defer();
setTimeout(() => {
deferred.resolve();
}, time);
return deferred.promise;
}
function documentIsLoaded(doc) {
let deferred = promise.defer();
if (doc.readyState == "complete") {

View File

@ -20,8 +20,11 @@
Task.spawn(function* () {
Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
if (!DebuggerServer.initialized) {
DebuggerServer.init(function () { return true; });
DebuggerServer.addBrowserActors();
}
let win = yield openWebIDE();

View File

@ -18,6 +18,18 @@
window.onload = function() {
SimpleTest.waitForExplicitFinish();
let win;
SimpleTest.registerCleanupFunction(() => {
Task.spawn(function*() {
if (win) {
yield closeWebIDE(win);
}
DebuggerServer.destroy();
yield removeAllProjects();
});
});
Task.spawn(function* () {
function isPlayActive() {
@ -29,10 +41,13 @@
}
Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
if (!DebuggerServer.initialized) {
DebuggerServer.init(function () { return true; });
DebuggerServer.addBrowserActors();
}
let win = yield openWebIDE();
win = yield openWebIDE();
win.AppManager.runtimeList.usb.push({
connect: function(connection) {
@ -119,12 +134,6 @@
yield win.Cmds.disconnectRuntime();
yield closeWebIDE(win);
DebuggerServer.destroy();
yield removeAllProjects();
SimpleTest.finish();
});

View File

@ -0,0 +1,255 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8">
<title></title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
<script type="application/javascript;version=1.8" src="head.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
</head>
<body>
<script type="application/javascript;version=1.8">
const Telemetry = require("devtools/shared/telemetry");
const { USBRuntime, WiFiRuntime, SimulatorRuntime, gRemoteRuntime,
gLocalRuntime } = require("devtools/webide/runtimes");
// Because we need to gather stats for the period of time that a tool has
// been opened we make use of setTimeout() to create tool active times.
const TOOL_DELAY = 200;
function patchTelemetry() {
Telemetry.prototype.telemetryInfo = {};
Telemetry.prototype._oldlog = Telemetry.prototype.log;
Telemetry.prototype.log = function(histogramId, value) {
if (histogramId) {
if (!this.telemetryInfo[histogramId]) {
this.telemetryInfo[histogramId] = [];
}
this.telemetryInfo[histogramId].push(value);
}
}
}
function resetTelemetry() {
Telemetry.prototype.log = Telemetry.prototype._oldlog;
delete Telemetry.prototype._oldlog;
delete Telemetry.prototype.telemetryInfo;
}
function cycleWebIDE() {
return Task.spawn(function*() {
let win = yield openWebIDE();
// Wait a bit, so we're open for a non-zero time
yield waitForTime(TOOL_DELAY);
yield closeWebIDE(win);
});
}
function addFakeRuntimes(win) {
// We use the real runtimes here (and switch out some functionality)
// so we can ensure that logging happens as it would in real use.
let usb = new USBRuntime("fakeUSB");
// Use local pipe instead
usb.connect = function(connection) {
ok(connection, win.AppManager.connection, "connection is valid");
connection.host = null; // force connectPipe
connection.connect();
return promise.resolve();
};
win.AppManager.runtimeList.usb.push(usb);
let wifi = new WiFiRuntime("fakeWiFi");
// Use local pipe instead
wifi.connect = function(connection) {
ok(connection, win.AppManager.connection, "connection is valid");
connection.host = null; // force connectPipe
connection.connect();
return promise.resolve();
};
win.AppManager.runtimeList.wifi.push(wifi);
let sim = new SimulatorRuntime("fakeSimulator");
// Use local pipe instead
sim.connect = function(connection) {
ok(connection, win.AppManager.connection, "connection is valid");
connection.host = null; // force connectPipe
connection.connect();
return promise.resolve();
};
sim.getName = function() {
return this.version;
};
win.AppManager.runtimeList.simulator.push(sim);
let remote = gRemoteRuntime;
// Use local pipe instead
remote.connect = function(connection) {
ok(connection, win.AppManager.connection, "connection is valid");
connection.host = null; // force connectPipe
connection.connect();
return promise.resolve();
};
let local = gLocalRuntime;
win.AppManager.runtimeList.custom = [gRemoteRuntime, gLocalRuntime];
win.AppManager.update("runtimelist");
}
function addTestApp(win) {
return Task.spawn(function*() {
let packagedAppLocation = getTestFilePath("app");
yield win.Cmds.importPackagedApp(packagedAppLocation);
});
}
function startConnection(win, type, index) {
let panelNode = win.document.querySelector("#runtime-panel");
let items = panelNode.querySelectorAll(".runtime-panel-item-" + type);
if (index === undefined) {
is(items.length, 1, "Found one runtime button");
}
let deferred = promise.defer();
win.AppManager.connection.once(
win.Connection.Events.CONNECTED,
() => deferred.resolve());
items[index || 0].click();
return deferred.promise;
}
function waitUntilConnected(win) {
return Task.spawn(function*() {
ok(win.document.querySelector("window").className, "busy", "UI is busy");
yield win.UI._busyPromise;
is(Object.keys(DebuggerServer._connections).length, 1, "Connected");
});
}
function connectToRuntime(win, type, index) {
return Task.spawn(function*() {
yield startConnection(win, type, index);
yield waitUntilConnected(win);
});
}
function checkResults() {
let result = Telemetry.prototype.telemetryInfo;
for (let [histId, value] of Iterator(result)) {
if (histId.endsWith("OPENED_PER_USER_FLAG")) {
ok(value.length === 1 && !!value[0],
"Per user value " + histId + " has a single value of true");
} else if (histId.endsWith("OPENED_BOOLEAN")) {
ok(value.length > 1, histId + " has more than one entry");
let okay = value.every(function(element) {
return !!element;
});
ok(okay, "All " + histId + " entries are true");
} else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
ok(value.length > 1, histId + " has more than one entry");
let okay = value.every(function(element) {
return element > 0;
});
ok(okay, "All " + histId + " entries have time > 0");
} else if (histId === "DEVTOOLS_WEBIDE_CONNECTION_RESULT") {
ok(value.length === 5, histId + " has 5 connection results");
let okay = value.every(function(element) {
return !!element;
});
ok(okay, "All " + histId + " connections succeeded");
} else if (histId.endsWith("CONNECTION_RESULT")) {
ok(value.length === 1 && !!value[0],
histId + " has 1 successful connection");
} else if (histId === "DEVTOOLS_WEBIDE_CONNECTION_TIME_SECONDS") {
ok(value.length === 5, histId + " has 5 connection results");
let okay = value.every(function(element) {
return element > 0;
});
ok(okay, "All " + histId + " connections have time > 0");
} else if (histId.endsWith("USED")) {
ok(value.length === 5, histId + " has 5 connection actions");
let okay = value.every(function(element) {
return !element;
});
ok(okay, "All " + histId + " actions were skipped");
} else {
ok(false, "Unexpected " + histId + " was logged");
}
}
}
window.onload = function() {
SimpleTest.waitForExplicitFinish();
let win;
SimpleTest.registerCleanupFunction(() => {
Task.spawn(function*() {
if (win) {
yield closeWebIDE(win);
}
DebuggerServer.destroy();
yield removeAllProjects();
resetTelemetry();
});
});
Task.spawn(function*() {
Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
if (!DebuggerServer.initialized) {
DebuggerServer.init(function () { return true; });
DebuggerServer.addBrowserActors();
}
patchTelemetry();
// Cycle once, so we can test for multiple opens
yield cycleWebIDE();
win = yield openWebIDE();
// Wait a bit, so we're open for a non-zero time
yield waitForTime(TOOL_DELAY);
addFakeRuntimes(win);
yield addTestApp(win);
// Each one should log a connection result and non-zero connection
// time
yield connectToRuntime(win, "usb");
yield waitForTime(TOOL_DELAY);
yield connectToRuntime(win, "wifi");
yield waitForTime(TOOL_DELAY);
yield connectToRuntime(win, "simulator");
yield waitForTime(TOOL_DELAY);
yield connectToRuntime(win, "custom", 0 /* remote */);
yield waitForTime(TOOL_DELAY);
yield connectToRuntime(win, "custom", 1 /* local */);
yield waitForTime(TOOL_DELAY);
yield closeWebIDE(win);
checkResults();
SimpleTest.finish();
});
}
</script>
</body>
</html>

View File

@ -3968,6 +3968,19 @@ if test -z "$MOZ_GOOGLE_API_KEY"; then
fi
AC_SUBST(MOZ_GOOGLE_API_KEY)
# Allow to specify a Google OAuth API key file that contains the client ID and
# the secret key to be used for various Google OAuth API requests.
MOZ_ARG_WITH_STRING(google-oauth-api-keyfile,
[ --with-google-oauth-api-keyfile=file Use the client id and secret key contained in the given keyfile for Google OAuth API requests],
[MOZ_GOOGLE_OAUTH_API_CLIENTID=`cat $withval | cut -f 1 -d " "`
MOZ_GOOGLE_OAUTH_API_KEY=`cat $withval | cut -f 2 -d " "`])
if test -z "$MOZ_GOOGLE_OAUTH_API_CLIENTID"; then
MOZ_GOOGLE_OAUTH_API_CLIENTID=no-google-oauth-api-clientid
MOZ_GOOGLE_OAUTH_API_KEY=no-google-oauth-api-key
fi
AC_SUBST(MOZ_GOOGLE_OAUTH_API_CLIENTID)
AC_SUBST(MOZ_GOOGLE_OAUTH_API_KEY)
# Allow specifying a Bing API key file that contains the client ID and the
# secret key to be used for the Bing Translation API requests.
MOZ_ARG_WITH_STRING(bing-api-keyfile,

View File

@ -300,20 +300,16 @@ nsDefaultURIFixup::GetFixupURIInfo(const nsACString& aStringURI, uint32_t aFixup
ioService->GetProtocolHandler(scheme.get(), getter_AddRefs(ourHandler));
extHandler = do_GetService(NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX"default");
nsCOMPtr<nsIURI> uri;
if (ourHandler != extHandler || !PossiblyHostPortUrl(uriString)) {
// Just try to create an URL out of it
rv = NS_NewURI(getter_AddRefs(uri), uriString, nullptr);
if (NS_SUCCEEDED(rv)) {
info->mFixedURI = uri;
}
rv = NS_NewURI(getter_AddRefs(info->mFixedURI), uriString, nullptr);
if (!uri && rv != NS_ERROR_MALFORMED_URI) {
if (!info->mFixedURI && rv != NS_ERROR_MALFORMED_URI) {
return rv;
}
}
if (uri && ourHandler == extHandler && sFixupKeywords &&
if (info->mFixedURI && ourHandler == extHandler && sFixupKeywords &&
(aFixupFlags & FIXUP_FLAG_FIX_SCHEME_TYPOS)) {
nsCOMPtr<nsIExternalProtocolService> extProtService =
do_GetService(NS_EXTERNALPROTOCOLSERVICE_CONTRACTID);
@ -328,18 +324,17 @@ nsDefaultURIFixup::GetFixupURIInfo(const nsACString& aStringURI, uint32_t aFixup
// It's more likely the user wants to search, and so we
// chuck this over to their preferred search provider instead:
if (!handlerExists) {
nsresult rv = KeywordToURI(uriString, aPostData, getter_AddRefs(uri));
if (NS_SUCCEEDED(rv) && uri) {
info->mFixupUsedKeyword = true;
}
TryKeywordFixupForURIInfo(uriString, info, aPostData);
}
}
}
if (uri) {
if (info->mFixedURI) {
if (!info->mPreferredURI) {
if (aFixupFlags & FIXUP_FLAGS_MAKE_ALTERNATE_URI)
info->mFixupCreatedAlternateURI = MakeAlternateURI(uri);
info->mPreferredURI = uri;
info->mFixupCreatedAlternateURI = MakeAlternateURI(info->mFixedURI);
info->mPreferredURI = info->mFixedURI;
}
return NS_OK;
}
@ -374,10 +369,11 @@ nsDefaultURIFixup::GetFixupURIInfo(const nsACString& aStringURI, uint32_t aFixup
// Test whether keywords need to be fixed up
if (sFixupKeywords && (aFixupFlags & FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP) &&
!inputHadDuffProtocol) {
KeywordURIFixup(uriString, info, aPostData);
if (info->mPreferredURI)
if (NS_SUCCEEDED(KeywordURIFixup(uriString, info, aPostData)) &&
info->mPreferredURI) {
return NS_OK;
}
}
// Did the caller want us to try an alternative URI?
// If so, attempt to fixup http://foo into http://www.foo.com
@ -415,12 +411,7 @@ nsDefaultURIFixup::GetFixupURIInfo(const nsACString& aStringURI, uint32_t aFixup
// If we still haven't been able to construct a valid URI, try to force a
// keyword match. This catches search strings with '.' or ':' in them.
if (sFixupKeywords && (aFixupFlags & FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP)) {
rv = KeywordToURI(aStringURI, aPostData, getter_AddRefs(info->mPreferredURI));
if (NS_SUCCEEDED(rv) && info->mPreferredURI)
{
info->mFixupUsedKeyword = true;
return NS_OK;
}
rv = TryKeywordFixupForURIInfo(aStringURI, info, aPostData);
}
return rv;
@ -428,9 +419,11 @@ nsDefaultURIFixup::GetFixupURIInfo(const nsACString& aStringURI, uint32_t aFixup
NS_IMETHODIMP nsDefaultURIFixup::KeywordToURI(const nsACString& aKeyword,
nsIInputStream **aPostData,
nsIURI **aURI)
nsIURIFixupInfo **aInfo)
{
*aURI = nullptr;
nsRefPtr<nsDefaultURIFixupInfo> info = new nsDefaultURIFixupInfo(aKeyword);
NS_ADDREF(*aInfo = info);
if (aPostData) {
*aPostData = nullptr;
}
@ -451,10 +444,14 @@ NS_IMETHODIMP nsDefaultURIFixup::KeywordToURI(const nsACString& aKeyword,
ipc::OptionalInputStreamParams postData;
ipc::OptionalURIParams uri;
if (!contentChild->SendKeywordToURI(keyword, &postData, &uri)) {
nsAutoString providerName;
if (!contentChild->SendKeywordToURI(keyword, &providerName, &postData, &uri)) {
return NS_ERROR_FAILURE;
}
CopyUTF8toUTF16(keyword, info->mKeywordAsSent);
info->mKeywordProviderName = providerName;
if (aPostData) {
nsTArray<ipc::FileDescriptor> fds;
nsCOMPtr<nsIInputStream> temp = DeserializeInputStream(postData, fds);
@ -464,7 +461,7 @@ NS_IMETHODIMP nsDefaultURIFixup::KeywordToURI(const nsACString& aKeyword,
}
nsCOMPtr<nsIURI> temp = DeserializeURI(uri);
temp.forget(aURI);
info->mPreferredURI = temp.forget();
return NS_OK;
}
@ -486,7 +483,8 @@ NS_IMETHODIMP nsDefaultURIFixup::KeywordToURI(const nsACString& aKeyword,
responseType.Assign(mozKeywordSearch);
}
defaultEngine->GetSubmission(NS_ConvertUTF8toUTF16(keyword),
NS_ConvertUTF8toUTF16 keywordW(keyword);
defaultEngine->GetSubmission(keywordW,
responseType,
NS_LITERAL_STRING("keyword"),
getter_AddRefs(submission));
@ -504,21 +502,9 @@ NS_IMETHODIMP nsDefaultURIFixup::KeywordToURI(const nsACString& aKeyword,
return NS_ERROR_FAILURE;
}
// This notification is meant for Firefox Health Report so it
// can increment counts from the search engine. The assumption
// here is that this keyword/submission will eventually result
// in a search. Since we only generate a URI here, there is the
// possibility we'll increment the counter without actually
// incurring a search. A robust solution would involve currying
// the search engine's name through various function calls.
nsCOMPtr<nsIObserverService> obsSvc = mozilla::services::GetObserverService();
if (obsSvc) {
// Note that "keyword-search" refers to a search via the url
// bar, not a bookmarks keyword search.
obsSvc->NotifyObservers(defaultEngine, "keyword-search", NS_ConvertUTF8toUTF16(keyword).get());
}
return submission->GetUri(aURI);
defaultEngine->GetName(info->mKeywordProviderName);
info->mKeywordAsSent = keywordW;
return submission->GetUri(getter_AddRefs(info->mPreferredURI));
}
}
}
@ -528,6 +514,22 @@ NS_IMETHODIMP nsDefaultURIFixup::KeywordToURI(const nsACString& aKeyword,
return NS_ERROR_NOT_AVAILABLE;
}
// Helper to deal with passing around uri fixup stuff
nsresult
nsDefaultURIFixup::TryKeywordFixupForURIInfo(const nsACString & aURIString,
nsDefaultURIFixupInfo* aFixupInfo,
nsIInputStream **aPostData)
{
nsCOMPtr<nsIURIFixupInfo> keywordInfo;
nsresult rv = KeywordToURI(aURIString, aPostData, getter_AddRefs(keywordInfo));
if (NS_SUCCEEDED(rv)) {
keywordInfo->GetKeywordProviderName(aFixupInfo->mKeywordProviderName);
keywordInfo->GetKeywordAsSent(aFixupInfo->mKeywordAsSent);
keywordInfo->GetPreferredURI(getter_AddRefs(aFixupInfo->mPreferredURI));
}
return rv;
}
bool nsDefaultURIFixup::MakeAlternateURI(nsIURI *aURI)
{
if (!Preferences::GetRootBranch())
@ -923,7 +925,8 @@ bool nsDefaultURIFixup::PossiblyByteExpandedFileName(const nsAString& aIn)
return false;
}
void nsDefaultURIFixup::KeywordURIFixup(const nsACString & aURIString,
nsresult
nsDefaultURIFixup::KeywordURIFixup(const nsACString & aURIString,
nsDefaultURIFixupInfo* aFixupInfo,
nsIInputStream **aPostData)
{
@ -1023,7 +1026,6 @@ void nsDefaultURIFixup::KeywordURIFixup(const nsACString & aURIString,
looksLikeIpv6 = false;
}
nsresult rv;
nsAutoCString asciiHost;
nsAutoCString host;
@ -1041,7 +1043,7 @@ void nsDefaultURIFixup::KeywordURIFixup(const nsACString & aURIString,
((foundDots + foundDigits == pos - 1) ||
(foundColons == 1 && firstColonLoc > lastDotLoc &&
foundDots + foundDigits + foundColons == pos - 1))) {
return;
return NS_OK;
}
uint32_t posWithNoTrailingSlash = pos;
@ -1054,15 +1056,16 @@ void nsDefaultURIFixup::KeywordURIFixup(const nsACString & aURIString,
((foundDots + foundDigits == posWithNoTrailingSlash) ||
(foundColons == 1 && firstColonLoc > lastDotLoc &&
foundDots + foundDigits + foundColons == posWithNoTrailingSlash))) {
return;
return NS_OK;
}
// If there are only colons and only hexadecimal characters ([a-z][0-9])
// enclosed in [], then don't do a keyword lookup
if (looksLikeIpv6) {
return;
return NS_OK;
}
nsresult rv = NS_OK;
// We do keyword lookups if a space or quote preceded the dot, colon
// or question mark (or if the latter were not found)
// or when the host is the same as asciiHost and there are no
@ -1073,11 +1076,7 @@ void nsDefaultURIFixup::KeywordURIFixup(const nsACString & aURIString,
(isValidAsciiHost && isValidHost && !hasAsciiAlpha &&
host.EqualsIgnoreCase(asciiHost.get()))) {
rv = KeywordToURI(aFixupInfo->mOriginalInput, aPostData,
getter_AddRefs(aFixupInfo->mPreferredURI));
if (NS_SUCCEEDED(rv) && aFixupInfo->mPreferredURI) {
aFixupInfo->mFixupUsedKeyword = true;
}
rv = TryKeywordFixupForURIInfo(aFixupInfo->mOriginalInput, aFixupInfo, aPostData);
}
// ... or if there is no question mark or colon, and there is either no
// dot, or exactly 1 and it is the first or last character of the input:
@ -1086,17 +1085,14 @@ void nsDefaultURIFixup::KeywordURIFixup(const nsACString & aURIString,
firstColonLoc == uint32_t(kNotFound) && firstQMarkLoc == uint32_t(kNotFound)) {
if (isValidAsciiHost && IsDomainWhitelisted(asciiHost, firstDotLoc)) {
return;
return NS_OK;
}
// If we get here, we don't have a valid URI, or we did but the
// host is not whitelisted, so we do a keyword search *anyway*:
rv = KeywordToURI(aFixupInfo->mOriginalInput, aPostData,
getter_AddRefs(aFixupInfo->mPreferredURI));
if (NS_SUCCEEDED(rv) && aFixupInfo->mPreferredURI) {
aFixupInfo->mFixupUsedKeyword = true;
}
rv = TryKeywordFixupForURIInfo(aFixupInfo->mOriginalInput, aFixupInfo, aPostData);
}
return rv;
}
bool nsDefaultURIFixup::IsDomainWhitelisted(const nsAutoCString aAsciiHost,
@ -1134,7 +1130,6 @@ nsresult NS_NewURIFixup(nsIURIFixup **aURIFixup)
NS_IMPL_ISUPPORTS(nsDefaultURIFixupInfo, nsIURIFixupInfo)
nsDefaultURIFixupInfo::nsDefaultURIFixupInfo(const nsACString& aOriginalInput):
mFixupUsedKeyword(false),
mFixupChangedProtocol(false),
mFixupCreatedAlternateURI(false)
{
@ -1178,9 +1173,16 @@ nsDefaultURIFixupInfo::GetFixedURI(nsIURI** aFixedURI)
}
NS_IMETHODIMP
nsDefaultURIFixupInfo::GetFixupUsedKeyword(bool* aOut)
nsDefaultURIFixupInfo::GetKeywordProviderName(nsAString& aOut)
{
*aOut = mFixupUsedKeyword;
aOut = mKeywordProviderName;
return NS_OK;
}
NS_IMETHODIMP
nsDefaultURIFixupInfo::GetKeywordAsSent(nsAString& aOut)
{
aOut = mKeywordAsSent;
return NS_OK;
}

View File

@ -30,7 +30,10 @@ private:
nsresult FixupURIProtocol(const nsACString& aIn,
nsDefaultURIFixupInfo* aFixupInfo,
nsIURI** aURI);
void KeywordURIFixup(const nsACString &aStringURI,
nsresult KeywordURIFixup(const nsACString &aStringURI,
nsDefaultURIFixupInfo* aFixupInfo,
nsIInputStream** aPostData);
nsresult TryKeywordFixupForURIInfo(const nsACString &aStringURI,
nsDefaultURIFixupInfo* aFixupInfo,
nsIInputStream** aPostData);
bool PossiblyByteExpandedFileName(const nsAString& aIn);
@ -58,9 +61,10 @@ private:
nsCOMPtr<nsISupports> mConsumer;
nsCOMPtr<nsIURI> mPreferredURI;
nsCOMPtr<nsIURI> mFixedURI;
bool mFixupUsedKeyword;
bool mFixupChangedProtocol;
bool mFixupCreatedAlternateURI;
nsString mKeywordProviderName;
nsString mKeywordAsSent;
nsAutoCString mOriginalInput;
};
#endif

View File

@ -201,6 +201,10 @@
#include "mozilla/dom/ScriptSettings.h"
#include "mozilla/dom/URLSearchParams.h"
#ifdef MOZ_TOOLKIT_SEARCH
#include "nsIBrowserSearchService.h"
#endif
static NS_DEFINE_CID(kAppShellCID, NS_APPSHELL_CID);
#if defined(DEBUG_bryner) || defined(DEBUG_chb)
@ -4583,6 +4587,7 @@ nsDocShell::LoadURIWithBase(const char16_t * aURI,
aLoadFlags &= ~LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP;
}
nsCOMPtr<nsIURIFixupInfo> fixupInfo;
if (sURIFixup) {
// Call the fixup object. This will clobber the rv from NS_NewURI
// above, but that's fine with us. Note that we need to do this even
@ -4596,7 +4601,6 @@ nsDocShell::LoadURIWithBase(const char16_t * aURI,
fixupFlags |= nsIURIFixup::FIXUP_FLAG_FIX_SCHEME_TYPOS;
}
nsCOMPtr<nsIInputStream> fixupStream;
nsCOMPtr<nsIURIFixupInfo> fixupInfo;
rv = sURIFixup->GetFixupURIInfo(uriString, fixupFlags,
getter_AddRefs(fixupStream),
getter_AddRefs(fixupInfo));
@ -4607,7 +4611,7 @@ nsDocShell::LoadURIWithBase(const char16_t * aURI,
}
if (fixupStream) {
// CreateFixupURI only returns a post data stream if it succeeded
// GetFixupURIInfo only returns a post data stream if it succeeded
// and changed the URI, in which case we should override the
// passed-in post data.
postStream = fixupStream;
@ -4666,6 +4670,13 @@ nsDocShell::LoadURIWithBase(const char16_t * aURI,
loadInfo->SetHeadersStream(aHeaderStream);
loadInfo->SetBaseURI(aBaseURI);
if (fixupInfo) {
nsAutoString searchProvider, keyword;
fixupInfo->GetKeywordProviderName(searchProvider);
fixupInfo->GetKeywordAsSent(keyword);
MaybeNotifyKeywordSearchLoading(searchProvider, keyword);
}
rv = LoadURI(uri, loadInfo, extraFlags, true);
// Save URI string in case it's needed later when
@ -7382,6 +7393,7 @@ nsDocShell::EndPageLoad(nsIWebProgress * aProgress,
//
// First try keyword fixup
//
nsAutoString keywordProviderName, keywordAsSent;
if (aStatus == NS_ERROR_UNKNOWN_HOST && mAllowKeywordFixup) {
bool keywordsEnabled =
Preferences::GetBool("keyword.enabled", false);
@ -7412,11 +7424,12 @@ nsDocShell::EndPageLoad(nsIWebProgress * aProgress,
}
if (keywordsEnabled && (kNotFound == dotLoc)) {
nsCOMPtr<nsIURIFixupInfo> info;
// only send non-qualified hosts to the keyword server
if (!mOriginalUriString.IsEmpty()) {
sURIFixup->KeywordToURI(mOriginalUriString,
getter_AddRefs(newPostData),
getter_AddRefs(newURI));
getter_AddRefs(info));
}
else {
//
@ -7438,13 +7451,19 @@ nsDocShell::EndPageLoad(nsIWebProgress * aProgress,
NS_SUCCEEDED(idnSrv->ConvertACEtoUTF8(host, utf8Host))) {
sURIFixup->KeywordToURI(utf8Host,
getter_AddRefs(newPostData),
getter_AddRefs(newURI));
getter_AddRefs(info));
} else {
sURIFixup->KeywordToURI(host,
getter_AddRefs(newPostData),
getter_AddRefs(newURI));
getter_AddRefs(info));
}
}
info->GetPreferredURI(getter_AddRefs(newURI));
if (newURI) {
info->GetKeywordAsSent(keywordAsSent);
info->GetKeywordProviderName(keywordProviderName);
}
} // end keywordsEnabled
}
@ -7477,6 +7496,8 @@ nsDocShell::EndPageLoad(nsIWebProgress * aProgress,
if (doCreateAlternate) {
newURI = nullptr;
newPostData = nullptr;
keywordProviderName.Truncate();
keywordAsSent.Truncate();
sURIFixup->CreateFixupURI(oldSpec,
nsIURIFixup::FIXUP_FLAGS_MAKE_ALTERNATE_URI,
getter_AddRefs(newPostData),
@ -7497,6 +7518,10 @@ nsDocShell::EndPageLoad(nsIWebProgress * aProgress,
newURI->GetSpec(newSpec);
NS_ConvertUTF8toUTF16 newSpecW(newSpec);
// This notification is meant for Firefox Health Report so it
// can increment counts from the search engine
MaybeNotifyKeywordSearchLoading(keywordProviderName, keywordAsSent);
return LoadURI(newSpecW.get(), // URI string
LOAD_FLAGS_NONE, // Load flags
nullptr, // Referring URI
@ -13508,3 +13533,36 @@ nsDocShell::GetURLSearchParams()
{
return mURLSearchParams;
}
void
nsDocShell::MaybeNotifyKeywordSearchLoading(const nsString &aProvider,
const nsString &aKeyword) {
if (aProvider.IsEmpty()) {
return;
}
if (XRE_GetProcessType() == GeckoProcessType_Content) {
dom::ContentChild* contentChild = dom::ContentChild::GetSingleton();
if (contentChild) {
contentChild->SendNotifyKeywordSearchLoading(aProvider, aKeyword);
}
return;
}
#ifdef MOZ_TOOLKIT_SEARCH
nsCOMPtr<nsIBrowserSearchService> searchSvc = do_GetService("@mozilla.org/browser/search-service;1");
if (searchSvc) {
nsCOMPtr<nsISearchEngine> searchEngine;
searchSvc->GetEngineByName(aProvider, getter_AddRefs(searchEngine));
if (searchEngine) {
nsCOMPtr<nsIObserverService> obsSvc = mozilla::services::GetObserverService();
if (obsSvc) {
// Note that "keyword-search" refers to a search via the url
// bar, not a bookmarks keyword search.
obsSvc->NotifyObservers(searchEngine, "keyword-search", aKeyword.get());
}
}
}
#endif
}

View File

@ -978,6 +978,9 @@ private:
nsIDocShellTreeItem* aOriginalRequestor,
nsIDocShellTreeItem** _retval);
// Notify consumers of a search being loaded through the observer service:
void MaybeNotifyKeywordSearchLoading(const nsString &aProvider, const nsString &aKeyword);
#ifdef DEBUG
// We're counting the number of |nsDocShells| to help find leaks
static unsigned long gNumberOfDocShells;

View File

@ -12,7 +12,7 @@ interface nsIInputStream;
/**
* Interface indicating what we found/corrected when fixing up a URI
*/
[scriptable, uuid(62aac1e0-3da8-4920-bd1b-a54fc2e2eb24)]
[scriptable, uuid(4819f183-b532-4932-ac09-b309cd853be7)]
interface nsIURIFixupInfo : nsISupports
{
/**
@ -36,9 +36,16 @@ interface nsIURIFixupInfo : nsISupports
readonly attribute nsIURI fixedURI;
/**
* Whether the preferred option ended up using a keyword search.
* The name of the keyword search provider used to provide a keyword search;
* empty string if no keyword search was done.
*/
readonly attribute boolean fixupUsedKeyword;
readonly attribute AString keywordProviderName;
/**
* The keyword as used for the search (post trimming etc.)
* empty string if no keyword search was done.
*/
readonly attribute AString keywordAsSent;
/**
* Whether we changed the protocol instead of using one from the input as-is.
@ -63,7 +70,7 @@ interface nsIURIFixupInfo : nsISupports
/**
* Interface implemented by objects capable of fixing up strings into URIs
*/
[scriptable, uuid(49298f2b-3630-4874-aecc-522300a7fead)]
[scriptable, uuid(d2a78abe-e678-4103-9bcc-dd1377460c44)]
interface nsIURIFixup : nsISupports
{
/** No fixup flags. */
@ -146,7 +153,7 @@ interface nsIURIFixup : nsISupports
* @throws NS_ERROR_FAILURE if the resulting URI requires submission of POST
* data and aPostData is null.
*/
nsIURI keywordToURI(in AUTF8String aKeyword,
nsIURIFixupInfo keywordToURI(in AUTF8String aKeyword,
[optional] out nsIInputStream aPostData);
};

View File

@ -95,7 +95,6 @@ skip-if = e10s # Bug ?????? - event handler checks event.target is the content d
[browser_onbeforeunload_navigation.js]
skip-if = e10s
[browser_search_notification.js]
skip-if = e10s
[browser_timelineMarkers-01.js]
[browser_timelineMarkers-02.js]
skip-if = e10s

View File

@ -4,6 +4,27 @@
function test() {
waitForExplicitFinish();
const kSearchEngineID = "test_urifixup_search_engine";
const kSearchEngineURL = "http://localhost/?search={searchTerms}";
Services.search.addEngineWithDetails(kSearchEngineID, "", "", "", "get",
kSearchEngineURL);
let oldDefaultEngine = Services.search.defaultEngine;
Services.search.defaultEngine = Services.search.getEngineByName(kSearchEngineID);
let selectedName = Services.search.defaultEngine.name;
is(selectedName, kSearchEngineID, "Check fake search engine is selected");
registerCleanupFunction(function() {
if (oldDefaultEngine) {
Services.search.defaultEngine = oldDefaultEngine;
}
let engine = Services.search.getEngineByName(kSearchEngineID);
if (engine) {
Services.search.removeEngine(engine);
}
});
let tab = gBrowser.addTab();
gBrowser.selectedTab = tab;

View File

@ -530,7 +530,7 @@ function run_test() {
// Check booleans on input:
let couldDoKeywordLookup = flags & urifixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
do_check_eq(info.fixupUsedKeyword, couldDoKeywordLookup && expectKeywordLookup);
do_check_eq(!!info.keywordProviderName, couldDoKeywordLookup && expectKeywordLookup);
do_check_eq(info.fixupChangedProtocol, expectProtocolChange);
do_check_eq(info.fixupCreatedAlternateURI, makeAlternativeURI && alternativeURI != null);

View File

@ -185,6 +185,10 @@ using namespace mozilla::system;
#include "mozilla/Sandbox.h"
#endif
#ifdef MOZ_TOOLKIT_SEARCH
#include "nsIBrowserSearchService.h"
#endif
static NS_DEFINE_CID(kCClipboardCID, NS_CLIPBOARD_CID);
static const char* sClipboardTextFlavors[] = { kUnicodeMime };
@ -3805,7 +3809,9 @@ ContentParent::RecvSetFakeVolumeState(const nsString& fsName, const int32_t& fsS
}
bool
ContentParent::RecvKeywordToURI(const nsCString& aKeyword, OptionalInputStreamParams* aPostData,
ContentParent::RecvKeywordToURI(const nsCString& aKeyword,
nsString* aProviderName,
OptionalInputStreamParams* aPostData,
OptionalURIParams* aURI)
{
nsCOMPtr<nsIURIFixup> fixup = do_GetService(NS_URIFIXUP_CONTRACTID);
@ -3814,20 +3820,45 @@ ContentParent::RecvKeywordToURI(const nsCString& aKeyword, OptionalInputStreamPa
}
nsCOMPtr<nsIInputStream> postData;
nsCOMPtr<nsIURI> uri;
nsCOMPtr<nsIURIFixupInfo> info;
if (NS_FAILED(fixup->KeywordToURI(aKeyword, getter_AddRefs(postData),
getter_AddRefs(uri)))) {
getter_AddRefs(info)))) {
return true;
}
info->GetKeywordProviderName(*aProviderName);
nsTArray<mozilla::ipc::FileDescriptor> fds;
SerializeInputStream(postData, *aPostData, fds);
MOZ_ASSERT(fds.IsEmpty());
nsCOMPtr<nsIURI> uri;
info->GetPreferredURI(getter_AddRefs(uri));
SerializeURI(uri, *aURI);
return true;
}
bool
ContentParent::RecvNotifyKeywordSearchLoading(const nsString &aProvider,
const nsString &aKeyword) {
#ifdef MOZ_TOOLKIT_SEARCH
nsCOMPtr<nsIBrowserSearchService> searchSvc = do_GetService("@mozilla.org/browser/search-service;1");
if (searchSvc) {
nsCOMPtr<nsISearchEngine> searchEngine;
searchSvc->GetEngineByName(aProvider, getter_AddRefs(searchEngine));
if (searchEngine) {
nsCOMPtr<nsIObserverService> obsSvc = mozilla::services::GetObserverService();
if (obsSvc) {
// Note that "keyword-search" refers to a search via the url
// bar, not a bookmarks keyword search.
obsSvc->NotifyObservers(searchEngine, "keyword-search", aKeyword.get());
}
}
}
#endif
return true;
}
bool
ContentParent::ShouldContinueFromReplyTimeout()
{

View File

@ -631,9 +631,14 @@ private:
virtual bool RecvSetFakeVolumeState(const nsString& fsName, const int32_t& fsState) MOZ_OVERRIDE;
virtual bool RecvKeywordToURI(const nsCString& aKeyword, OptionalInputStreamParams* aPostData,
virtual bool RecvKeywordToURI(const nsCString& aKeyword,
nsString* aProviderName,
OptionalInputStreamParams* aPostData,
OptionalURIParams* aURI) MOZ_OVERRIDE;
virtual bool RecvNotifyKeywordSearchLoading(const nsString &aProvider,
const nsString &aKeyword) MOZ_OVERRIDE;
virtual void ProcessingError(Result what) MOZ_OVERRIDE;
virtual bool RecvAllocateLayerTreeId(uint64_t* aId) MOZ_OVERRIDE;

View File

@ -670,7 +670,9 @@ parent:
async SetFakeVolumeState(nsString fsName, int32_t fsState);
sync KeywordToURI(nsCString keyword)
returns (OptionalInputStreamParams postData, OptionalURIParams uri);
returns (nsString providerName, OptionalInputStreamParams postData, OptionalURIParams uri);
sync NotifyKeywordSearchLoading(nsString providerName, nsString keyword);
// Tell the compositor to allocate a layer tree id for nested remote mozbrowsers.
sync AllocateLayerTreeId()

View File

@ -126,6 +126,9 @@ DEFINES['BIN_SUFFIX'] = '"%s"' % CONFIG['BIN_SUFFIX']
if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('android', 'gtk2', 'gonk', 'qt'):
DEFINES['MOZ_ENABLE_FREETYPE'] = True
if CONFIG['MOZ_TOOLKIT_SEARCH']:
DEFINES['MOZ_TOOLKIT_SEARCH'] = True
for var in ('MOZ_PERMISSIONS', 'MOZ_CHILD_PERMISSIONS'):
if CONFIG[var]:
DEFINES[var] = True

View File

@ -27,7 +27,6 @@ import org.mozilla.gecko.db.BrowserContract.Combined;
import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
import org.mozilla.gecko.db.BrowserContract.SearchHistory;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.db.DBUtils;
import org.mozilla.gecko.db.SuggestedSites;
import org.mozilla.gecko.distribution.Distribution;
import org.mozilla.gecko.favicons.Favicons;
@ -47,6 +46,7 @@ import org.mozilla.gecko.health.SessionInformation;
import org.mozilla.gecko.home.BrowserSearch;
import org.mozilla.gecko.home.HomeBanner;
import org.mozilla.gecko.home.HomePager;
import org.mozilla.gecko.home.HomePager.OnUrlOpenInBackgroundListener;
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
import org.mozilla.gecko.home.HomePanelsManager;
import org.mozilla.gecko.home.SearchEngine;
@ -74,6 +74,7 @@ import org.mozilla.gecko.util.StringUtils;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.util.UIAsyncTask;
import org.mozilla.gecko.widget.ButtonToast;
import org.mozilla.gecko.widget.ButtonToast.ToastListener;
import org.mozilla.gecko.widget.GeckoActionProvider;
import android.app.Activity;
@ -139,6 +140,7 @@ public class BrowserApp extends GeckoApp
BrowserSearch.OnEditSuggestionListener,
HomePager.OnNewTabsListener,
OnUrlOpenListener,
OnUrlOpenInBackgroundListener,
ActionModeCompat.Presenter,
LayoutInflater.Factory {
private static final String LOGTAG = "GeckoBrowserApp";
@ -1771,7 +1773,13 @@ public class BrowserApp extends GeckoApp
/**
* Attempts to switch to an open tab with the given URL.
* <p>
* If the tab exists, this method cancels any in-progress editing as well as
* calling {@link Tabs#selectTab(int)}.
*
* @param url of tab to switch to.
* @param flags to obey: if {@link OnUrlOpenListener.Flags#ALLOW_SWITCH_TO_TAB}
* is not present, return false.
* @return true if we successfully switched to a tab, false otherwise.
*/
private boolean maybeSwitchToTab(String url, EnumSet<OnUrlOpenListener.Flags> flags) {
@ -1792,6 +1800,26 @@ public class BrowserApp extends GeckoApp
return false;
}
return maybeSwitchToTab(tab.getId());
}
/**
* Attempts to switch to an open tab with the given unique tab ID.
* <p>
* If the tab exists, this method cancels any in-progress editing as well as
* calling {@link Tabs#selectTab(int)}.
*
* @param id of tab to switch to.
* @return true if we successfully switched to the tab, false otherwise.
*/
private boolean maybeSwitchToTab(int id) {
final Tabs tabs = Tabs.getInstance();
final Tab tab = tabs.getTab(id);
if (tab == null) {
return false;
}
// Set the target tab to null so it does not get selected (on editing
// mode exit) in lieu of the tab we are about to select.
mTargetTabForEditingMode = null;
@ -3088,6 +3116,53 @@ public class BrowserApp extends GeckoApp
}
}
// HomePager.OnUrlOpenInBackgroundListener
@Override
public void onUrlOpenInBackground(final String url, EnumSet<OnUrlOpenInBackgroundListener.Flags> flags) {
if (url == null) {
throw new IllegalArgumentException("url must not be null");
}
if (flags == null) {
throw new IllegalArgumentException("flags must not be null");
}
final boolean isPrivate = flags.contains(OnUrlOpenInBackgroundListener.Flags.PRIVATE);
int loadFlags = Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_BACKGROUND;
if (isPrivate) {
loadFlags |= Tabs.LOADURL_PRIVATE;
}
final Tab newTab = Tabs.getInstance().loadUrl(url, loadFlags);
// We switch to the desired tab by unique ID, which closes any window
// for a race between opening the tab and closing it, and switching to
// it. We could also switch to the Tab explicitly, but we don't want to
// hold a reference to the Tab itself in the anonymous listener class.
final int newTabId = newTab.getId();
final ToastListener listener = new ButtonToast.ToastListener() {
@Override
public void onButtonClicked() {
maybeSwitchToTab(newTabId);
}
@Override
public void onToastHidden(ButtonToast.ReasonHidden reason) { }
};
final String message = isPrivate ?
getResources().getString(R.string.new_private_tab_opened) :
getResources().getString(R.string.new_tab_opened);
final String buttonMessage = getResources().getString(R.string.switch_button_message);
getButtonToast().show(false,
message,
ButtonToast.LENGTH_SHORT,
buttonMessage,
R.drawable.switch_button_icon,
listener);
}
// BrowserSearch.OnSearchListener
@Override
public void onSearch(SearchEngine engine, String text) {

View File

@ -48,6 +48,7 @@ GARBAGE += \
classes.dex \
gecko.ap_ \
res/values/strings.xml \
res/raw/browsersearch.json \
res/raw/suggestedsites.json \
.aapt.deps \
fennec_ids.txt \
@ -259,11 +260,12 @@ $(ANDROID_GENERATED_RESFILES): $(call mkdir_deps,$(sort $(dir $(ANDROID_GENERATE
# This .deps pattern saves an invocation of the sub-Make: the single
# invocation generates both strings.xml and suggestedsites.json. The
# trailing semi-colon defines an empty recipe: defining no recipe at
# all causes Make to treat the target differently, in a way that
# defeats our dependencies.
# invocation generates strings.xml, browsersearch.json, and
# suggestedsites.json. The trailing semi-colon defines an empty
# recipe: defining no recipe at all causes Make to treat the target
# differently, in a way that defeats our dependencies.
res/values/strings.xml: .locales.deps ;
res/raw/browsersearch.json: .locales.deps ;
res/raw/suggestedsites.json: .locales.deps ;
all_resources = \

View File

@ -101,8 +101,27 @@ public class RemoteTabsExpandableListAdapter extends BaseExpandableListAdapter {
final RemoteClient client = clients.get(groupPosition);
// UI elements whose state depends on isExpanded, roughly from left to
// right: device type icon; client name text color; expanded state
// indicator.
final int deviceTypeResId;
final int textColorResId;
final int deviceExpandedResId;
if (isExpanded && !client.tabs.isEmpty()) {
deviceTypeResId = "desktop".equals(client.deviceType) ? R.drawable.sync_desktop : R.drawable.sync_mobile;
textColorResId = R.color.home_text_color;
deviceExpandedResId = R.drawable.home_group_expanded;
} else {
deviceTypeResId = "desktop".equals(client.deviceType) ? R.drawable.sync_desktop_inactive : R.drawable.sync_mobile_inactive;
textColorResId = R.color.home_text_color_disabled;
deviceExpandedResId = R.drawable.home_group_collapsed;
}
// Now update the UI.
final TextView nameView = (TextView) view.findViewById(R.id.client);
nameView.setText(client.name);
nameView.setTextColor(context.getResources().getColor(textColorResId));
final TextView lastModifiedView = (TextView) view.findViewById(R.id.last_synced);
final long now = System.currentTimeMillis();
@ -113,22 +132,13 @@ public class RemoteTabsExpandableListAdapter extends BaseExpandableListAdapter {
// Therefore, we must handle null.
final ImageView deviceTypeView = (ImageView) view.findViewById(R.id.device_type);
if (deviceTypeView != null) {
if ("desktop".equals(client.deviceType)) {
deviceTypeView.setBackgroundResource(R.drawable.sync_desktop);
} else {
deviceTypeView.setBackgroundResource(R.drawable.sync_mobile);
}
deviceTypeView.setImageResource(deviceTypeResId);
}
final ImageView deviceExpandedView = (ImageView) view.findViewById(R.id.device_expanded);
if (deviceExpandedView != null) {
// If there are no tabs to display, don't show an indicator at all.
if (client.tabs.isEmpty()) {
deviceExpandedView.setBackgroundResource(0);
} else {
final int resourceId = isExpanded ? R.drawable.home_group_expanded : R.drawable.home_group_collapsed;
deviceExpandedView.setBackgroundResource(resourceId);
}
deviceExpandedView.setImageResource(client.tabs.isEmpty() ? 0 : deviceExpandedResId);
}
return view;

View File

@ -5,30 +5,29 @@
package org.mozilla.gecko.home;
import java.util.EnumSet;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.EditBookmarkDialog;
import org.mozilla.gecko.GeckoApp;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoEvent;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.R;
import org.mozilla.gecko.ReaderModeUtils;
import org.mozilla.gecko.Tab;
import org.mozilla.gecko.Tabs;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.db.BrowserContract.SuggestedSites;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.favicons.Favicons;
import org.mozilla.gecko.home.HomeContextMenuInfo.RemoveItemType;
import org.mozilla.gecko.home.HomePager.OnUrlOpenInBackgroundListener;
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
import org.mozilla.gecko.home.TopSitesGridView.TopSitesGridContextMenuInfo;
import org.mozilla.gecko.util.Clipboard;
import org.mozilla.gecko.util.StringUtils;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.util.UIAsyncTask;
import org.mozilla.gecko.widget.ButtonToast;
import android.app.Activity;
import android.content.ContentResolver;
@ -72,6 +71,9 @@ public abstract class HomeFragment extends Fragment {
// On URL open listener
protected OnUrlOpenListener mUrlOpenListener;
// Helper for opening a tab in the background.
private OnUrlOpenInBackgroundListener mUrlOpenInBackgroundListener;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
@ -82,12 +84,20 @@ public abstract class HomeFragment extends Fragment {
throw new ClassCastException(activity.toString()
+ " must implement HomePager.OnUrlOpenListener");
}
try {
mUrlOpenInBackgroundListener = (OnUrlOpenInBackgroundListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement HomePager.OnUrlOpenInBackgroundListener");
}
}
@Override
public void onDetach() {
super.onDetach();
mUrlOpenListener = null;
mUrlOpenInBackgroundListener = null;
}
@Override
@ -205,40 +215,23 @@ public abstract class HomeFragment extends Fragment {
return false;
}
int flags = Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_BACKGROUND;
final boolean isPrivate = (item.getItemId() == R.id.home_open_private_tab);
if (isPrivate) {
flags |= Tabs.LOADURL_PRIVATE;
// Some pinned site items have "user-entered" urls. URLs entered in
// the PinSiteDialog are wrapped in a special URI until we can get a
// valid URL. If the url is a user-entered url, decode the URL
// before loading it.
final String url = StringUtils.decodeUserEnteredUrl(info.isInReadingList()
? ReaderModeUtils.getAboutReaderForUrl(info.url)
: info.url);
final EnumSet<OnUrlOpenInBackgroundListener.Flags> flags = EnumSet.noneOf(OnUrlOpenInBackgroundListener.Flags.class);
if (item.getItemId() == R.id.home_open_private_tab) {
flags.add(OnUrlOpenInBackgroundListener.Flags.PRIVATE);
}
mUrlOpenInBackgroundListener.onUrlOpenInBackground(url, flags);
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.CONTEXT_MENU);
final String url = (info.isInReadingList() ? ReaderModeUtils.getAboutReaderForUrl(info.url) : info.url);
// Some pinned site items have "user-entered" urls. URLs entered in the PinSiteDialog are wrapped in
// a special URI until we can get a valid URL. If the url is a user-entered url, decode the URL before loading it.
final Tab newTab = Tabs.getInstance().loadUrl(StringUtils.decodeUserEnteredUrl(url), flags);
final int newTabId = newTab.getId(); // We don't want to hold a reference to the Tab.
final String message = isPrivate ?
getResources().getString(R.string.new_private_tab_opened) :
getResources().getString(R.string.new_tab_opened);
final String buttonMessage = getResources().getString(R.string.switch_button_message);
final GeckoApp geckoApp = (GeckoApp) context;
geckoApp.getButtonToast().show(false,
message,
ButtonToast.LENGTH_SHORT,
buttonMessage,
R.drawable.switch_button_icon,
new ButtonToast.ToastListener() {
@Override
public void onButtonClicked() {
Tabs.getInstance().selectTab(newTabId);
}
@Override
public void onToastHidden(ButtonToast.ReasonHidden reason) { }
});
return true;
}

View File

@ -81,6 +81,27 @@ public class HomePager extends ViewPager {
public void onUrlOpen(String url, EnumSet<Flags> flags);
}
/**
* Interface for requesting a new tab be opened in the background.
* <p>
* This is the <code>HomeFragment</code> equivalent of opening a new tab by
* long clicking a link and selecting the "Open new [private] tab" context
* menu option.
*/
public interface OnUrlOpenInBackgroundListener {
public enum Flags {
PRIVATE,
}
/**
* Open a new tab with the given URL
*
* @param url to open.
* @param flags to open new tab with.
*/
public void onUrlOpenInBackground(String url, EnumSet<Flags> flags);
}
public interface OnNewTabsListener {
public void onNewTabs(List<String> urls);
}

View File

@ -29,15 +29,11 @@ strings-xml-in := $(srcdir)/../strings.xml.in
GARBAGE += $(strings-xml)
dir-res-raw := ../res/raw
suggestedsites-json := $(dir-res-raw)/suggestedsites.json
GARBAGE += \
$(suggestedsites-json) \
$(NULL)
suggestedsites := $(dir-res-raw)/suggestedsites.json
browsersearch := $(dir-res-raw)/browsersearch.json
libs realchrome:: \
$(strings-xml) \
$(suggestedsites-json) \
$(NULL)
chrome-%:: AB_CD=$*
@ -45,6 +41,7 @@ chrome-%::
@$(MAKE) \
$(dir-res-values)-$(AB_rCD)/strings.xml \
$(dir-res-raw)-$(AB_rCD)/suggestedsites.json \
$(dir-res-raw)-$(AB_rCD)/browsersearch.json \
AB_CD=$*
# setup the path to bookmarks.inc. copied and tweaked version of MERGE_FILE from config/config.mk
@ -94,21 +91,42 @@ $(dir-strings-xml)/strings.xml: $(strings-xml-preqs)
$< \
-o $@)
suggestedsites-srcdir := $(if $(filter en-US,$(AB_CD)),,$(or $(realpath $(L10NBASEDIR)),$(abspath $(L10NBASEDIR)))/$(AB_CD)/mobile/chrome)
# Arg 1: Valid Make identifier, like suggestedsites.
# Arg 2: File name, like suggestedsites.json.
define generated_file_template
# Determine the ../res/raw[-*] path. This can be ../res/raw when no
# locale is explicitly specified.
suggestedsites-json-bypath = $(filter %/suggestedsites.json,$(MAKECMDGOALS))
ifeq (,$(strip $(suggestedsites-json-bypath)))
suggestedsites-json-bypath = $(suggestedsites-json)
$(1)-bypath = $(filter %/$(2),$(MAKECMDGOALS))
ifeq (,$$(strip $$($(1)-bypath)))
$(1)-bypath = $($(1))
endif
suggestedsites-dstdir-raw = $(patsubst %/,%,$(dir $(suggestedsites-json-bypath)))
$(1)-dstdir-raw = $$(patsubst %/,%,$$(dir $$($(1)-bypath)))
GARBAGE += $($(1))
libs realchrome:: $($(1))
endef
# L10NBASEDIR is not defined for en-US.
l10n-srcdir := $(if $(filter en-US,$(AB_CD)),,$(or $(realpath $(L10NBASEDIR)),$(abspath $(L10NBASEDIR)))/$(AB_CD)/mobile/chrome)
$(eval $(call generated_file_template,suggestedsites,suggestedsites.json))
$(suggestedsites-dstdir-raw)/suggestedsites.json: FORCE
$(call py_action,generate_suggestedsites, \
--verbose \
--android-package-name=$(ANDROID_PACKAGE_NAME) \
--resources=$(srcdir)/../resources \
$(if $(filter en-US,$(AB_CD)),,--srcdir=$(suggestedsites-srcdir)) \
$(if $(filter en-US,$(AB_CD)),,--srcdir=$(l10n-srcdir)) \
--srcdir=$(topsrcdir)/mobile/locales/en-US/chrome \
$@)
$(eval $(call generated_file_template,browsersearch,browsersearch.json))
$(browsersearch-dstdir-raw)/browsersearch.json: FORCE
$(call py_action,generate_browsersearch, \
--verbose \
$(if $(filter en-US,$(AB_CD)),,--srcdir=$(l10n-srcdir)) \
--srcdir=$(topsrcdir)/mobile/locales/en-US/chrome \
$@)

View File

@ -134,10 +134,10 @@ public class SendTabDeviceListArrayAdapter extends ArrayAdapter<ParcelableClient
private static int getImage(ParcelableClientRecord record) {
if ("mobile".equals(record.type)) {
return R.drawable.sync_mobile;
return R.drawable.sync_mobile_inactive;
}
return R.drawable.sync_desktop;
return R.drawable.sync_desktop_inactive;
}
public void switchState(State newState) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 549 B

After

Width:  |  Height:  |  Size: 542 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 474 B

After

Width:  |  Height:  |  Size: 552 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 B

After

Width:  |  Height:  |  Size: 381 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 321 B

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 B

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 B

After

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 582 B

After

Width:  |  Height:  |  Size: 469 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 486 B

After

Width:  |  Height:  |  Size: 487 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 898 B

After

Width:  |  Height:  |  Size: 607 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 877 B

After

Width:  |  Height:  |  Size: 667 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 738 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 546 B

View File

@ -18,7 +18,8 @@
android:layout_width="@dimen/favicon_bg"
android:layout_height="@dimen/favicon_bg"
android:layout_marginLeft="10dip"
android:layout_marginRight="10dip" />
android:layout_marginRight="10dip"
android:scaleType="center" />
<LinearLayout
android:layout_width="match_parent"
@ -48,6 +49,7 @@
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_marginLeft="10dip"
android:layout_marginRight="10dip" />
android:layout_marginRight="10dip"
android:scaleType="center" />
</LinearLayout>

View File

@ -18,9 +18,11 @@
<TextView
android:id="@+id/hidden_devices"
style="@style/Widget.Home.ActionItem"
android:background="@drawable/action_bar_button"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:maxLength="1024" />
android:maxLength="1024"
android:textColor="@color/home_text_color_disabled" />
</LinearLayout>

View File

@ -20,7 +20,6 @@
<TextView
android:id="@+id/client_name"
style="@style/ShareOverlayButton.Text"
android:layout_gravity="center"
android:layout_width="0dp"
android:layout_weight="0.5"

View File

@ -106,6 +106,10 @@
<color name="home_button_bar_bg">#FFF5F7F9</color>
<!-- Colour used for share overlay button labels -->
<color name="home_text_color">@color/text_color_primary</color>
<color name="home_text_color_disabled">#AFB1B3</color>
<color name="panel_image_item_background">#D1D9E1</color>
<!-- Swipe to refresh colors for dynamic panel -->

View File

@ -597,6 +597,7 @@
<style name="Widget.RemoteTabsListView" parent="Widget.HomeListView">
<item name="android:childDivider">#E7ECF0</item>
<item name="android:drawSelectorOnTop">true</item>
</style>
<!-- TabsTray Row -->

View File

@ -2854,8 +2854,7 @@ ServerHandler.prototype =
throw e;
}
function writeMore()
{
let writeMore = function () {
gThreadManager.currentThread
.dispatch(writeData, Ci.nsIThread.DISPATCH_NORMAL);
}

View File

@ -43,6 +43,7 @@ PYTHON_UNIT_TESTS += [
'mozbuild/mozbuild/test/frontend/test_sandbox.py',
'mozbuild/mozbuild/test/test_base.py',
'mozbuild/mozbuild/test/test_containers.py',
'mozbuild/mozbuild/test/test_dotproperties.py',
'mozbuild/mozbuild/test/test_expression.py',
'mozbuild/mozbuild/test/test_jarmaker.py',
'mozbuild/mozbuild/test/test_line_endings.py',

View File

@ -0,0 +1,98 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# 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/.
'''
Script to generate the browsersearch.json file for Fennec.
This script follows these steps:
1. Read the region.properties file in all the given source directories (see
srcdir option). Merge all properties into a single dict accounting for the
priority of source directories.
2. Read the default search plugin from the 'browser.search.defaultenginename'.
3. Read the list of search plugins from the 'browser.search.order.INDEX'
properties with values identifying particular search plugins by name.
4. Generate a JSON representation of 2. and 3., and write the result to
browsersearch.json in the locale-specific raw resource directory
e.g. raw/browsersearch.json, raw-pt-rBR/browsersearch.json.
'''
from __future__ import print_function
import argparse
import json
import re
import sys
import os
from mozbuild.dotproperties import (
DotProperties,
)
from mozbuild.util import (
FileAvoidWrite,
)
import mozpack.path as mozpath
def merge_properties(filename, srcdirs):
"""Merges properties from the given file in the given source directories."""
properties = DotProperties()
for srcdir in srcdirs:
path = mozpath.join(srcdir, filename)
try:
properties.update(path)
except IOError:
# Ignore non-existing files
continue
return properties
def main(args):
parser = argparse.ArgumentParser()
parser.add_argument('--verbose', '-v', default=False, action='store_true',
help='be verbose')
parser.add_argument('--silent', '-s', default=False, action='store_true',
help='be silent')
parser.add_argument('--srcdir', metavar='SRCDIR',
action='append', required=True,
help='directories to read inputs from, in order of priority')
parser.add_argument('output', metavar='OUTPUT',
help='output')
opts = parser.parse_args(args)
# Use reversed order so that the first srcdir has higher priority to override keys.
properties = merge_properties('region.properties', reversed(opts.srcdir))
default = properties.get('browser.search.defaultenginename')
engines = properties.get_list('browser.search.order')
if opts.verbose:
print('Read {len} engines: {engines}'.format(len=len(engines), engines=engines))
print("Default engine is '{default}'.".format(default=default))
browsersearch = {}
browsersearch['default'] = default
browsersearch['engines'] = engines
# FileAvoidWrite creates its parent directories.
output = os.path.abspath(opts.output)
fh = FileAvoidWrite(output)
json.dump(browsersearch, fh)
existed, updated = fh.close()
if not opts.silent:
if updated:
print('{output} updated'.format(output=output))
else:
print('{output} already up-to-date'.format(output=output))
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))

View File

@ -29,10 +29,12 @@ from __future__ import print_function
import argparse
import json
import re
import sys
import os
from mozbuild.dotproperties import (
DotProperties,
)
from mozbuild.util import (
FileAvoidWrite,
)
@ -42,59 +44,19 @@ from mozpack.files import (
import mozpack.path as mozpath
def read_properties_file(filename):
"""Reads a properties file into a dict.
Ignores empty, comment lines, and keys not starting with the prefix for
suggested sites ('browser.suggestedsites'). Removes the prefix from all
matching keys i.e. turns 'browser.suggestedsites.foo' into simply 'foo'
"""
prefix = 'browser.suggestedsites.'
properties = {}
for l in open(filename, 'rt').readlines():
line = l.strip()
if not line.startswith(prefix):
continue
(k, v) = re.split('\s*=\s*', line, 1)
properties[k[len(prefix):]] = v
return properties
def merge_properties(filename, srcdirs):
"""Merges properties from the given file in the given source directories."""
properties = {}
properties = DotProperties()
for srcdir in srcdirs:
path = mozpath.join(srcdir, filename)
try:
properties.update(read_properties_file(path))
except IOError, e:
properties.update(path)
except IOError:
# Ignore non-existing files
continue
return properties
def get_site_list_from_properties(properties):
"""Turns {'list.0':'foo', 'list.1':'bar'} into ['foo', 'bar']."""
prefix = 'list.'
indexes = []
for k, v in properties.iteritems():
if not k.startswith(prefix):
continue
indexes.append(int(k[len(prefix):]))
return [properties[prefix + str(index)] for index in sorted(indexes)]
def get_site_from_properties(name, properties):
"""Turns {'foo.title':'title', ...} into {'title':'title', ...}."""
prefix = '{name}.'.format(name=name)
try:
site = dict((k, properties[prefix + k]) for k in ('title', 'url', 'bgcolor'))
except IndexError, e:
raise Exception("Could not find required property for '{name}: {error}'"
.format(name=name, error=str(e)))
return site
def main(args):
parser = argparse.ArgumentParser()
parser.add_argument('--verbose', '-v', default=False, action='store_true',
@ -115,8 +77,8 @@ def main(args):
opts = parser.parse_args(args)
# Use reversed order so that the first srcdir has higher priority to override keys.
all_properties = merge_properties('region.properties', reversed(opts.srcdir))
names = get_site_list_from_properties(all_properties)
properties = merge_properties('region.properties', reversed(opts.srcdir))
names = properties.get_list('browser.suggestedsites.list')
if opts.verbose:
print('Reading {len} suggested sites: {names}'.format(len=len(names), names=names))
@ -128,7 +90,7 @@ def main(args):
# respective image URL.
sites = []
for name in names:
site = get_site_from_properties(name, all_properties)
site = properties.get_dict('browser.suggestedsites.{name}'.format(name=name), required_keys=('title', 'url', 'bgcolor'))
site['imageurl'] = image_url_template.format(name=name)
sites.append(site)

View File

@ -0,0 +1,80 @@
# 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/.
# This file contains utility functions for reading .properties files, like
# region.properties.
from __future__ import unicode_literals
import codecs
import re
import os
import sys
if sys.version_info[0] == 3:
str_type = str
else:
str_type = basestring
class DotProperties:
r'''A thin representation of a key=value .properties file.'''
def __init__(self, file=None):
self._properties = {}
if file:
self.update(file)
def update(self, file):
'''Updates properties from a file name or file-like object.
Ignores empty lines and comment lines.'''
if isinstance(file, str_type):
f = codecs.open(file, 'r', 'utf-8')
else:
f = file
for l in f.readlines():
line = l.strip()
if not line or line.startswith('#'):
continue
(k, v) = re.split('\s*=\s*', line, 1)
self._properties[k] = v
def get(self, key, default=None):
return self._properties.get(key, default)
def get_list(self, prefix):
'''Turns {'list.0':'foo', 'list.1':'bar'} into ['foo', 'bar'].
Returns [] to indicate an empty or missing list.'''
if not prefix.endswith('.'):
prefix = prefix + '.'
indexes = []
for k, v in self._properties.iteritems():
if not k.startswith(prefix):
continue
indexes.append(int(k[len(prefix):]))
return [self._properties[prefix + str(index)] for index in sorted(indexes)]
def get_dict(self, prefix, required_keys=[]):
'''Turns {'foo.title':'title', ...} into {'title':'title', ...}.
If |required_keys| is present, it must be an iterable of required key
names. If a required key is not present, ValueError is thrown.
Returns {} to indicate an empty or missing dict.'''
if not prefix.endswith('.'):
prefix = prefix + '.'
D = dict((k[len(prefix):], v) for k, v in self._properties.iteritems() if k.startswith(prefix))
for required_key in required_keys:
if not required_key in D:
raise ValueError('Required key %s not present' % required_key)
return D

View File

@ -0,0 +1,12 @@
# A region.properties file with invalid unicode byte sequences. The
# sequences were cribbed from Markus Kuhn's "UTF-8 decoder capability
# and stress test", available at
# http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt
# 3.5 Impossible bytes |
# |
# The following two bytes cannot appear in a correct UTF-8 string |
# |
# 3.5.1 fe = "þ" |
# 3.5.2 ff = "ÿ" |
# 3.5.3 fe fe ff ff = "þþÿÿ" |

View File

@ -0,0 +1,11 @@
# A region.properties file with unicode characters.
# Danish.
# #### ~~ Søren Munk Skrøder, sskroeder - 2009-05-30 @ #mozmae
# Korean.
A.title=한메일
# Russian.
list.0 = test
list.1 = Яндекс

View File

@ -0,0 +1,115 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os
import unittest
from StringIO import StringIO
import mozpack.path as mozpath
from mozbuild.dotproperties import (
DotProperties,
)
from mozunit import (
main,
)
test_data_path = mozpath.abspath(mozpath.dirname(__file__))
test_data_path = mozpath.join(test_data_path, 'data')
class TestDotProperties(unittest.TestCase):
def test_get(self):
contents = StringIO('''
key=value
''')
p = DotProperties(contents)
self.assertEqual(p.get('missing'), None)
self.assertEqual(p.get('missing', 'default'), 'default')
self.assertEqual(p.get('key'), 'value')
def test_update(self):
contents = StringIO('''
old=old value
key=value
''')
p = DotProperties(contents)
self.assertEqual(p.get('old'), 'old value')
self.assertEqual(p.get('key'), 'value')
new_contents = StringIO('''
key=new value
''')
p.update(new_contents)
self.assertEqual(p.get('old'), 'old value')
self.assertEqual(p.get('key'), 'new value')
def test_get_list(self):
contents = StringIO('''
list.0=A
list.1=B
list.2=C
order.1=B
order.0=A
order.2=C
''')
p = DotProperties(contents)
self.assertEqual(p.get_list('missing'), [])
self.assertEqual(p.get_list('list'), ['A', 'B', 'C'])
self.assertEqual(p.get_list('order'), ['A', 'B', 'C'])
def test_get_dict(self):
contents = StringIO('''
A.title=title A
B.title=title B
B.url=url B
''')
p = DotProperties(contents)
self.assertEqual(p.get_dict('missing'), {})
self.assertEqual(p.get_dict('A'), {'title': 'title A'})
self.assertEqual(p.get_dict('B'), {'title': 'title B', 'url': 'url B'})
with self.assertRaises(ValueError):
p.get_dict('A', required_keys=['title', 'url'])
with self.assertRaises(ValueError):
p.get_dict('missing', required_keys=['key'])
def test_unicode(self):
contents = StringIO('''
# Danish.
# #### ~~ Søren Munk Skrøder, sskroeder - 2009-05-30 @ #mozmae
# Korean.
A.title=한메일
# Russian.
list.0 = test
list.1 = Яндекс
''')
p = DotProperties(contents)
self.assertEqual(p.get_dict('A'), {'title': '한메일'})
self.assertEqual(p.get_list('list'), ['test', 'Яндекс'])
def test_valid_unicode_from_file(self):
# The contents of valid.properties is identical to the contents of the
# test above. This specifically exercises reading from a file.
p = DotProperties(os.path.join(test_data_path, 'valid.properties'))
self.assertEqual(p.get_dict('A'), {'title': '한메일'})
self.assertEqual(p.get_list('list'), ['test', 'Яндекс'])
def test_bad_unicode_from_file(self):
# The contents of bad.properties is not valid Unicode; see the comments
# in the file itself for details.
with self.assertRaises(UnicodeDecodeError):
DotProperties(os.path.join(test_data_path, 'bad.properties'))
if __name__ == '__main__':
main()

View File

@ -252,6 +252,8 @@ user_pref("browser.newtabpage.directory.ping", "");
// Enable Loop
user_pref("loop.enabled", true);
user_pref("loop.throttled", false);
user_pref("loop.oauth.google.URL", "http://%(server)s/browser/browser/components/loop/test/mochitest/google_service.sjs?action=");
user_pref("loop.oauth.google.getContactsURL", "http://%(server)s/browser/browser/components/loop/test/mochitest/google_service.sjs?action=contacts");
// Ensure UITour won't hit the network
user_pref("browser.uitour.pinnedTabUrl", "http://%(server)s/uitour-dummy/pinnedTab");

View File

@ -153,16 +153,7 @@ this.BookmarkJSONUtils = Object.freeze({
Components.utils.reportError("Unable to report telemetry.");
}
startTime = Date.now();
let hash = generateHash(jsonString);
// Report the time taken to generate the hash.
try {
Services.telemetry
.getHistogramById("PLACES_BACKUPS_HASHING_MS")
.add(Date.now() - startTime);
} catch (ex) {
Components.utils.reportError("Unable to report telemetry.");
}
if (hash === aOptions.failIfHashIs) {
let e = new Error("Hash conflict");

View File

@ -880,11 +880,6 @@ this.PlacesDBUtils = {
query: `SELECT count(*) FROM moz_bookmarks
WHERE parent = :tags_folder` },
{ histogram: "PLACES_FOLDERS_COUNT",
query: `SELECT count(*) FROM moz_bookmarks
WHERE TYPE = :type_folder
AND parent NOT IN (0, :places_root, :tags_folder)` },
{ histogram: "PLACES_KEYWORDS_COUNT",
query: "SELECT count(*) FROM moz_keywords" },
@ -921,14 +916,6 @@ this.PlacesDBUtils = {
}
},
{ histogram: "PLACES_DATABASE_JOURNALSIZE_MB",
callback: function () {
let DBFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
DBFile.append("places.sqlite-wal");
return parseInt(DBFile.fileSize / BYTES_PER_MEBIBYTE);
}
},
{ histogram: "PLACES_DATABASE_PAGESIZE_B",
query: "PRAGMA page_size /* PlacesDBUtils.jsm PAGESIZE_B */" },
@ -946,18 +933,9 @@ this.PlacesDBUtils = {
{ histogram: "PLACES_ANNOS_BOOKMARKS_COUNT",
query: "SELECT count(*) FROM moz_items_annos" },
// LENGTH is not a perfect measure, since it returns the number of bytes
// only for BLOBs, the number of chars for anything else. Though it's
// the best approximation we have.
{ histogram: "PLACES_ANNOS_BOOKMARKS_SIZE_KB",
query: "SELECT SUM(LENGTH(content))/1024 FROM moz_items_annos" },
{ histogram: "PLACES_ANNOS_PAGES_COUNT",
query: "SELECT count(*) FROM moz_annos" },
{ histogram: "PLACES_ANNOS_PAGES_SIZE_KB",
query: "SELECT SUM(LENGTH(content))/1024 FROM moz_annos" },
{ histogram: "PLACES_MAINTENANCE_DAYSFROMLAST",
callback: function () {
try {

View File

@ -15,7 +15,6 @@
#include "nsINavHistoryService.h"
#include "nsPrintfCString.h"
#include "nsNavHistory.h"
#include "mozilla/Telemetry.h"
#include "mozilla/Likely.h"
using namespace mozilla::storage;
@ -457,8 +456,6 @@ namespace places {
NS_ENSURE_SUCCESS(rv, rv);
NS_ASSERTION(numEntries > 0, "unexpected number of arguments");
Telemetry::AutoTimer<Telemetry::PLACES_FRECENCY_CALC_TIME_MS> timer;
int64_t pageId = aArguments->AsInt64(0);
int32_t typed = numEntries > 1 ? aArguments->AsInt32(1) : 0;
int32_t fullVisitCount = numEntries > 2 ? aArguments->AsInt32(2) : 0;

Some files were not shown because too many files have changed in this diff Show More