Merge m-c to inbound. a=merge
|
@ -15,7 +15,7 @@
|
|||
<project name="platform_build" path="build" remote="b2g" revision="3a2947df41a480de1457a6dcdbf46ad0af70d8e0">
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="191d805f4911628d37a8a90a1e23a6013995138f"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="d711d1e469eeeecf25a02b2407a542a598918b2c"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5883a99b6528ced9dafaed8d3ca2405fb285537e"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="191d805f4911628d37a8a90a1e23a6013995138f"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="d711d1e469eeeecf25a02b2407a542a598918b2c"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5883a99b6528ced9dafaed8d3ca2405fb285537e"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
|
||||
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</project>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="191d805f4911628d37a8a90a1e23a6013995138f"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="d711d1e469eeeecf25a02b2407a542a598918b2c"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5883a99b6528ced9dafaed8d3ca2405fb285537e"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
|
||||
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="4f9042d3a705307849a6f63961eaaaa2e1d85d77"/>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<project name="platform_build" path="build" remote="b2g" revision="3a2947df41a480de1457a6dcdbf46ad0af70d8e0">
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="191d805f4911628d37a8a90a1e23a6013995138f"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="d711d1e469eeeecf25a02b2407a542a598918b2c"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5883a99b6528ced9dafaed8d3ca2405fb285537e"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="191d805f4911628d37a8a90a1e23a6013995138f"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="d711d1e469eeeecf25a02b2407a542a598918b2c"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5883a99b6528ced9dafaed8d3ca2405fb285537e"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
|
||||
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<project name="platform_build" path="build" remote="b2g" revision="3a2947df41a480de1457a6dcdbf46ad0af70d8e0">
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="191d805f4911628d37a8a90a1e23a6013995138f"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="d711d1e469eeeecf25a02b2407a542a598918b2c"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5883a99b6528ced9dafaed8d3ca2405fb285537e"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</project>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="191d805f4911628d37a8a90a1e23a6013995138f"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="d711d1e469eeeecf25a02b2407a542a598918b2c"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5883a99b6528ced9dafaed8d3ca2405fb285537e"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
|
||||
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="4f9042d3a705307849a6f63961eaaaa2e1d85d77"/>
|
||||
|
|
|
@ -4,6 +4,6 @@
|
|||
"remote": "",
|
||||
"branch": ""
|
||||
},
|
||||
"revision": "52f7b7099a47ab3904a70d9a295ab0ed927ad59e",
|
||||
"revision": "3e43be9b8c24802b40fdfbcf17895c4355e6d238",
|
||||
"repo_path": "/integration/gaia-central"
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="191d805f4911628d37a8a90a1e23a6013995138f"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="d711d1e469eeeecf25a02b2407a542a598918b2c"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5883a99b6528ced9dafaed8d3ca2405fb285537e"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="191d805f4911628d37a8a90a1e23a6013995138f"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="d711d1e469eeeecf25a02b2407a542a598918b2c"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5883a99b6528ced9dafaed8d3ca2405fb285537e"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</project>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="191d805f4911628d37a8a90a1e23a6013995138f"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="d711d1e469eeeecf25a02b2407a542a598918b2c"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5883a99b6528ced9dafaed8d3ca2405fb285537e"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
|
||||
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="4f9042d3a705307849a6f63961eaaaa2e1d85d77"/>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="191d805f4911628d37a8a90a1e23a6013995138f"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="d711d1e469eeeecf25a02b2407a542a598918b2c"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5883a99b6528ced9dafaed8d3ca2405fb285537e"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
|
||||
|
|
|
@ -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/");
|
||||
|
|
|
@ -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 };
|
||||
|
||||
|
|
|
@ -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
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
|
@ -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),
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -14,6 +14,7 @@ BROWSER_CHROME_MANIFESTS += [
|
|||
|
||||
EXTRA_JS_MODULES.loop += [
|
||||
'CardDavImporter.jsm',
|
||||
'GoogleImporter.jsm',
|
||||
'LoopContacts.jsm',
|
||||
'LoopStorage.jsm',
|
||||
'MozLoopAPI.jsm',
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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");
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Success code=test-code</title></head>
|
||||
<body>Le Code.</body>
|
||||
</html>
|
|
@ -0,0 +1,94 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<feed gd:etag="W/"DUQNRHc8cCt7I2A9XRdSF04."" 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&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=""R3YyejRVLit7I2A9WhJWEkkNQwc."">
|
||||
<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=""Ug92D34SfCt7I2BmLHJTRgVzTlgrJXEAU08."" 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=""R3YyejRVLit7I2A9WhJWEkkNQwc."">
|
||||
<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=""WA9BY1xFWit7I2BhLEkieCxLHEYTGCYuNxo."" 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=""R3YyejRVLit7I2A9WhJWEkkNQwc."">
|
||||
<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=""KiV2PkYRfCt7I2BuD1AzEBFxD1VcGjwBUyA."" 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=""Q3w7ezVSLit7I2A9WB5WGUkNRgE."">
|
||||
<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=""Q3w7ezVSLit7I2A9WB5WGUkNRgE."">
|
||||
<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>
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"access_token": "test-token"
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
};
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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);
|
||||
});
|
|
@ -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);
|
||||
});
|
|
@ -0,0 +1,4 @@
|
|||
function testCase() {
|
||||
var foo = "break on me";
|
||||
debugger;
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
||||
|
|
|
@ -33,3 +33,4 @@ support-files =
|
|||
[test_addons.html]
|
||||
[test_deviceinfo.html]
|
||||
[test_autoconnect_runtime.html]
|
||||
[test_telemetry.html]
|
||||
|
|
|
@ -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") {
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
});
|
||||
|
|
|
@ -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>
|
13
configure.in
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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()
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -9,6 +9,7 @@ var NotificationTest = (function () {
|
|||
SimpleTest.waitForExplicitFinish();
|
||||
// turn on testing pref (used by notification.cpp, and mock the alerts
|
||||
SpecialPowers.setBoolPref("notification.prompt.testing", true);
|
||||
SpecialPowers.setAllAppsLaunchable(true);
|
||||
}
|
||||
|
||||
function teardown_testing_env() {
|
||||
|
|
|
@ -719,12 +719,14 @@ public:
|
|||
}
|
||||
|
||||
private:
|
||||
static void StackWalkCallback(void* aPc, void* aSp, void* aClosure)
|
||||
static void StackWalkCallback(uint32_t aFrameNumber, void* aPc, void* aSp,
|
||||
void* aClosure)
|
||||
{
|
||||
StackTrace* st = (StackTrace*) aClosure;
|
||||
MOZ_ASSERT(st->mLength < MaxFrames);
|
||||
st->mPcs[st->mLength] = aPc;
|
||||
st->mLength++;
|
||||
MOZ_ASSERT(st->mLength == aFrameNumber);
|
||||
}
|
||||
|
||||
static int Cmp(const void* aA, const void* aB)
|
||||
|
@ -755,7 +757,7 @@ StackTrace::Print(const Writer& aWriter, CodeAddressService* aLocService) const
|
|||
static const size_t buflen = 1024;
|
||||
char buf[buflen];
|
||||
for (uint32_t i = 0; i < mLength; i++) {
|
||||
aLocService->GetLocation(Pc(i), buf, buflen);
|
||||
aLocService->GetLocation(i + 1, Pc(i), buf, buflen);
|
||||
aWriter.Write(" %s\n", buf);
|
||||
}
|
||||
}
|
||||
|
@ -1574,7 +1576,8 @@ Options::BadArg(const char* aArg)
|
|||
|
||||
#ifdef XP_MACOSX
|
||||
static void
|
||||
NopStackWalkCallback(void* aPc, void* aSp, void* aClosure)
|
||||
NopStackWalkCallback(uint32_t aFrameNumber, void* aPc, void* aSp,
|
||||
void* aClosure)
|
||||
{
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 = \
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 \
|
||||
$@)
|
||||
|
|
|
@ -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) {
|
||||
|
|
До Ширина: | Высота: | Размер: 549 B После Ширина: | Высота: | Размер: 542 B |
До Ширина: | Высота: | Размер: 474 B После Ширина: | Высота: | Размер: 552 B |
После Ширина: | Высота: | Размер: 325 B |
После Ширина: | Высота: | Размер: 357 B |
После Ширина: | Высота: | Размер: 309 B |
После Ширина: | Высота: | Размер: 310 B |
До Ширина: | Высота: | Размер: 326 B После Ширина: | Высота: | Размер: 381 B |
До Ширина: | Высота: | Размер: 321 B После Ширина: | Высота: | Размер: 406 B |
Двоичные данные
mobile/android/base/resources/drawable-mdpi/sync_desktop.png
До Ширина: | Высота: | Размер: 206 B После Ширина: | Высота: | Размер: 218 B |
После Ширина: | Высота: | Размер: 256 B |
Двоичные данные
mobile/android/base/resources/drawable-mdpi/sync_mobile.png
До Ширина: | Высота: | Размер: 213 B После Ширина: | Высота: | Размер: 260 B |
После Ширина: | Высота: | Размер: 250 B |
До Ширина: | Высота: | Размер: 582 B После Ширина: | Высота: | Размер: 469 B |
До Ширина: | Высота: | Размер: 486 B После Ширина: | Высота: | Размер: 487 B |
После Ширина: | Высота: | Размер: 383 B |
После Ширина: | Высота: | Размер: 419 B |
После Ширина: | Высота: | Размер: 431 B |
После Ширина: | Высота: | Размер: 445 B |
До Ширина: | Высота: | Размер: 898 B После Ширина: | Высота: | Размер: 607 B |
До Ширина: | Высота: | Размер: 877 B После Ширина: | Высота: | Размер: 667 B |
После Ширина: | Высота: | Размер: 624 B |
После Ширина: | Высота: | Размер: 738 B |
После Ширина: | Высота: | Размер: 602 B |
После Ширина: | Высота: | Размер: 546 B |
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|