This commit is contained in:
Ryan VanderMeulen 2014-10-02 13:14:06 -04:00
Родитель b8425df99c 4c98b09137
Коммит b20021a33c
157 изменённых файлов: 2850 добавлений и 815 удалений

Просмотреть файл

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

Просмотреть файл

@ -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>

Просмотреть файл

@ -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

Двоичные данные
mobile/android/base/resources/drawable-hdpi/sync_desktop.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 325 B

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 357 B

Двоичные данные
mobile/android/base/resources/drawable-hdpi/sync_mobile.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 309 B

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 310 B

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 326 B

После

Ширина:  |  Высота:  |  Размер: 381 B

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 321 B

После

Ширина:  |  Высота:  |  Размер: 406 B

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 206 B

После

Ширина:  |  Высота:  |  Размер: 218 B

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 256 B

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 213 B

После

Ширина:  |  Высота:  |  Размер: 260 B

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 250 B

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 582 B

После

Ширина:  |  Высота:  |  Размер: 469 B

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 486 B

После

Ширина:  |  Высота:  |  Размер: 487 B

Двоичные данные
mobile/android/base/resources/drawable-xhdpi/sync_desktop.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 383 B

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 419 B

Двоичные данные
mobile/android/base/resources/drawable-xhdpi/sync_mobile.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 431 B

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 445 B

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 898 B

После

Ширина:  |  Высота:  |  Размер: 607 B

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 877 B

После

Ширина:  |  Высота:  |  Размер: 667 B

Двоичные данные
mobile/android/base/resources/drawable-xxhdpi/sync_desktop.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 624 B

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 738 B

Двоичные данные
mobile/android/base/resources/drawable-xxhdpi/sync_mobile.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 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"

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше