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

This commit is contained in:
Ryan VanderMeulen 2014-08-30 12:26:22 -04:00
commit 52fab7eaca
818 changed files with 56762 additions and 10760 deletions

View File

@ -1395,6 +1395,9 @@ pref("devtools.scratchpad.showTrailingSpace", false);
pref("devtools.scratchpad.enableCodeFolding", true);
pref("devtools.scratchpad.enableAutocompletion", true);
// Enable the Storage Inspector
pref("devtools.storage.enabled", false);
// Enable the Style Editor.
pref("devtools.styleeditor.enabled", true);
pref("devtools.styleeditor.source-maps-enabled", false);

View File

@ -6,9 +6,9 @@
var gSafeBrowsing = {
setReportPhishingMenu: function() {
// A phishing page will have a specific about:blocked content documentURI
var isPhishingPage = content.document.documentURI.startsWith("about:blocked?e=phishingBlocked");
var uri = getBrowser().currentURI;
var isPhishingPage = uri && uri.spec.startsWith("about:blocked?e=phishingBlocked");
// Show/hide the appropriate menu item.
document.getElementById("menu_HelpPopup_reportPhishingtoolmenu")
@ -24,7 +24,6 @@ var gSafeBrowsing = {
if (!broadcaster)
return;
var uri = getBrowser().currentURI;
if (uri && (uri.schemeIs("http") || uri.schemeIs("https")))
broadcaster.removeAttribute("disabled");
else

View File

@ -0,0 +1,463 @@
/* 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/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/Log.jsm");
this.EXPORTED_SYMBOLS = ["CardDavImporter"];
let log = Log.repository.getLogger("Loop.Importer.CardDAV");
log.level = Log.Level.Debug;
log.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter()));
const DEPTH_RESOURCE_ONLY = "0";
const DEPTH_RESOURCE_AND_CHILDREN = "1";
const DEPTH_RESOURCE_AND_ALL_DESCENDENTS = "infinity";
this.CardDavImporter = function() {
};
/**
* CardDAV Address Book importer for Loop.
*
* The model for address book importers is to have a single public method,
* "startImport." When the import is done (or upon a fatal error), the
* caller's callback method is called.
*
* The current model for this importer is based on the subset of CardDAV
* implemented by Google. In theory, it should work with other CardDAV
* sources, but it has only been tested against Google at the moment.
*
* At the moment, this importer assumes that no local changes will be made
* to data retreived from a remote source: when performing a re-import,
* any records that have been previously imported will be completely
* removed and replaced with the data received from the CardDAV server.
* Witout this behavior, it would be impossible for users to take any
* actions to remove fields that are no longer valid.
*/
this.CardDavImporter.prototype = {
/**
* Begin import of an address book from a CardDAV server.
*
* @param {Object} options Information needed to perform the address
* book import. The following fields are currently
* defined:
* - "host": CardDAV server base address
* (e.g., "google.com")
* - "auth": Authentication mechanism to use.
* Currently, only "basic" is implemented.
* - "user": Username to use for basic auth
* - "password": Password to use for basic auth
* @param {Function} callback Callback function that will be invoked once the
* import operation is complete. The first argument
* passed to the callback will be an 'Error' object
* or 'null'. If the import operation was
* successful, then the second parameter will be a
* count of the number of contacts that were
* successfully imported.
* @param {Object} db Database to add imported contacts into.
* Nominally, this is the LoopContacts API. In
* practice, anything with the same interface
* should work here.
*/
startImport: function(options, callback, db) {
let auth;
if (!("auth" in options)) {
callback(new Error("No authentication specified"));
return;
}
if (options.auth === "basic") {
if (!("user" in options) || !("password" in options)) {
callback(new Error("Missing user or password for basic authentication"));
return;
}
auth = { method: "basic",
user: options.user,
password: options.password };
} else {
callback(new Error("Unknown authentication method"));
return;
}
if (!("host" in options)){
callback(new Error("Missing host for CardDav import"));
return;
}
let host = options.host;
Task.spawn(function* () {
log.info("Starting CardDAV import from " + host);
let baseURL = "https://" + host;
let startURL = baseURL + "/.well-known/carddav";
let abookURL;
// Get list of contact URLs
let body = "<d:propfind xmlns:d='DAV:'><d:prop><d:getetag />" +
"</d:prop></d:propfind>";
let abook = yield this._davPromise("PROPFIND", startURL, auth,
DEPTH_RESOURCE_AND_CHILDREN, body);
// Build multiget REPORT body from URLs in PROPFIND result
let contactElements = abook.responseXML.
getElementsByTagNameNS("DAV:", "href");
body = "<c:addressbook-multiget xmlns:d='DAV:' " +
"xmlns:c='urn:ietf:params:xml:ns:carddav'>" +
"<d:prop><d:getetag /> <c:address-data /></d:prop>\n";
for (let element of contactElements) {
let href = element.textContent;
if (href.substr(-1) == "/") {
abookURL = baseURL + href;
} else {
body += "<d:href>" + href + "</d:href>\n";
}
}
body += "</c:addressbook-multiget>";
// Retreive contact URL contents
let allEntries = yield this._davPromise("REPORT", abookURL, auth,
DEPTH_RESOURCE_AND_CHILDREN,
body);
// Parse multiget entites and add to DB
let addressData = allEntries.responseXML.getElementsByTagNameNS(
"urn:ietf:params:xml:ns:carddav", "address-data");
log.info("Retreived " + addressData.length + " contacts from " +
host + "; importing into database");
let importCount = 0;
for (let i = 0; i < addressData.length; i++) {
let vcard = addressData.item(i).textContent;
let contact = this._convertVcard(vcard);
contact.id += "@" + host;
contact.category = ["carddav@" + host];
let existing = yield this._dbPromise(db, "getByServiceId", contact.id);
if (existing) {
yield this._dbPromise(db, "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 (!("tel" in contact) && !("email" in contact)) {
continue;
}
yield this._dbPromise(db, "add", contact);
importCount++;
}
return importCount;
}.bind(this)).then(
(result) => {
log.info("Import complete: " + result + " contacts imported.");
callback(null, result);
},
(error) => {
log.error("Aborting import: " + error.fileName + ":" +
error.lineNumber + ": " + error.message);
callback(error);
}).then(null,
(error) => {
log.error("Error in callback: " + error.fileName +
":" + error.lineNumber + ": " + error.message);
callback(error);
}).then(null,
(error) => {
log.error("Error calling failure callback, giving up: " +
error.fileName + ":" + error.lineNumber + ": " +
error.message);
});
},
/**
* Wrap a LoopContacts-style operation in a promise. The operation is run
* immediately, and a corresponding Promise is returned. Error callbacks
* cause the promise to be rejected, and success cause it to be resolved.
*
* @param {Object} db Object the operation is to be performed on
* @param {String} method Name of operation being wrapped
* @param {Object} param Parameter to be passed to the operation
*
* @return {Object} Promise corresponding to the result of the operation.
*/
_dbPromise: function(db, method, param) {
return new Promise((resolve, reject) => {
db[method](param, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
},
/**
* Converts a contact in VCard format (see RFC 6350) to the format used
* by the LoopContacts class.
*
* @param {String} vcard The contact to convert, in vcard format
* @return {Object} a LoopContacts-style contact object containing
* the relevant fields from the vcard.
*/
_convertVcard: function(vcard) {
let contact = {};
let nickname;
vcard.split(/[\r\n]+(?! )/).forEach(
function (contentline) {
contentline = contentline.replace(/[\r\n]+ /g, "");
let match = /^(.*?[^\\]):(.*)$/.exec(contentline);
if (match) {
let nameparam = match[1];
let value = match[2];
// Poor-man's unescaping
value = value.replace(/\\:/g, ":");
value = value.replace(/\\,/g, ",");
value = value.replace(/\\n/gi, "\n");
value = value.replace(/\\\\/g, "\\");
let param = nameparam.split(/;/);
let name = param[0];
let pref = false;
let type = [];
for (let i = 1; i < param.length; i++) {
if (/^PREF/.exec(param[i]) || /^TYPE=PREF/.exec(param[i])) {
pref = true;
}
let typeMatch = /^TYPE=(.*)/.exec(param[i]);
if (typeMatch) {
type.push(typeMatch[1].toLowerCase());
}
}
if (!type.length) {
type.push("other");
}
if (name === "FN") {
value = value.replace(/\\;/g, ";");
contact.name = [value];
}
if (name === "N") {
// Because we don't have lookbehinds, matching unescaped
// semicolons is a pain. Luckily, we know that \r and \n
// cannot appear in the strings, so we use them to swap
// unescaped semicolons for \n.
value = value.replace(/\\;/g, "\r");
value = value.replace(/;/g, "\n");
value = value.replace(/\r/g, ";");
let family, given, additional, prefix, suffix;
let values = value.split(/\n/);
if (values.length >= 5) {
[family, given, additional, prefix, suffix] = values;
if (prefix.length) {
contact.honorificPrefix = [prefix];
}
if (given.length) {
contact.givenName = [given];
}
if (additional.length) {
contact.additionalName = [additional];
}
if (family.length) {
contact.familyName = [family];
}
if (suffix.length) {
contact.honorificSuffix = [suffix];
}
}
}
if (name === "EMAIL") {
value = value.replace(/\\;/g, ";");
if (!("email" in contact)) {
contact.email = [];
}
contact.email.push({
pref: pref,
type: type,
value: value
});
}
if (name === "NICKNAME") {
value = value.replace(/\\;/g, ";");
// We don't store nickname in contact because it's not
// a supported field. We're saving it off here in case we
// need to use it if the fullname is blank.
nickname = value;
};
if (name === "ADR") {
value = value.replace(/\\;/g, "\r");
value = value.replace(/;/g, "\n");
value = value.replace(/\r/g, ";");
let pobox, extra, street, locality, region, code, country;
let values = value.split(/\n/);
if (values.length >= 7) {
[pobox, extra, street, locality, region, code, country] = values;
if (!("adr" in contact)) {
contact.adr = [];
}
contact.adr.push({
pref: pref,
type: type,
streetAddress: (street || pobox) + (extra ? (" " + extra) : ""),
locality: locality,
region: region,
postalCode: code,
countryName: country
});
}
}
if (name === "TEL") {
value = value.replace(/\\;/g, ";");
if (!("tel" in contact)) {
contact.tel = [];
}
contact.tel.push({
pref: pref,
type: type,
value: value
});
}
if (name === "ORG") {
value = value.replace(/\\;/g, "\r");
value = value.replace(/;/g, "\n");
value = value.replace(/\r/g, ";");
if (!("org" in contact)) {
contact.org = [];
}
contact.org.push(value.replace(/\n.*/, ""));
}
if (name === "TITLE") {
value = value.replace(/\\;/g, ";");
if (!("jobTitle" in contact)) {
contact.jobTitle = [];
}
contact.jobTitle.push(value);
}
if (name === "BDAY") {
value = value.replace(/\\;/g, ";");
contact.bday = Date.parse(value);
}
if (name === "UID") {
contact.id = value;
}
if (name === "NOTE") {
value = value.replace(/\\;/g, ";");
if (!("note" in contact)) {
contact.note = [];
}
contact.note.push(value);
}
}
}
);
// 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 {
if (nickname) {
contact.name = [nickname];
} 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 if ("email" in contact) {
contact.name = [contact.email[0].value];
} else if ("tel" in contact) {
contact.name = [contact.tel[0].value];
}
}
}
return contact;
},
/**
* Issues a CardDAV request (see RFC 6352) and returns a Promise to represent
* the success or failure state of the request.
*
* @param {String} method WebDAV method to use (e.g., "PROPFIND")
* @param {String} url HTTP URL to use for the request
* @param {Object} auth Object with authentication-related configuration.
* See documentation for startImport for details.
* @param {Number} depth Value to use for the WebDAV (HTTP) "Depth" header
* @param {String} body Body to include in the WebDAV (HTTP) request
*
* @return {Object} Promise representing the request operation outcome.
* If resolved, the resolution value is the XMLHttpRequest
* that was used to perform the request.
*/
_davPromise: function(method, url, auth, depth, body) {
return new Promise((resolve, reject) => {
let req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
createInstance(Ci.nsIXMLHttpRequest);
let user = "";
let password = "";
if (auth.method == "basic") {
user = auth.user;
password = auth.password;
}
req.open(method, url, true, user, password);
req.setRequestHeader("Depth", depth);
req.setRequestHeader("Content-Type", "application/xml; charset=utf-8");
req.onload = function() {
if (req.status < 400) {
resolve(req);
} else {
reject(new Error(req.status + " " + req.statusText));
}
};
req.onerror = function(error) {
reject(error);
}
req.send(body);
});
}
};

View File

@ -10,6 +10,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "console",
"resource://gre/modules/devtools/Console.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LoopStorage",
"resource:///modules/loop/LoopStorage.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "CardDavImporter",
"resource:///modules/loop/CardDavImporter.jsm");
XPCOMUtils.defineLazyGetter(this, "eventEmitter", function() {
const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {});
return new EventEmitter();
@ -318,6 +320,13 @@ LoopStorage.on("upgrade", function(e, db) {
* violated. You'll notice this as well in the documentation for each method.
*/
let LoopContactsInternal = Object.freeze({
/**
* Map of contact importer names to instances
*/
_importServices: {
"carddav": new CardDavImporter()
},
/**
* Add a contact to the data store.
*
@ -757,8 +766,15 @@ let LoopContactsInternal = Object.freeze({
* be the result of the operation, if successfull.
*/
startImport: function(options, callback) {
//TODO in bug 972000.
callback(new Error("Not implemented yet!"));
if (!("service" in options)) {
callback(new Error("No import service specified in options"));
return;
}
if (!(options.service in this._importServices)) {
callback(new Error("Unknown import service specified: " + options.service));
return;
}
this._importServices[options.service].startImport(options, callback, this);
},
/**

View File

@ -543,7 +543,7 @@ loop.shared.views = (function(_, OT, l10n) {
return (
FeedbackLayout({title: l10n.get("feedback_thank_you_heading")},
React.DOM.p({className: "info thank-you"},
l10n.get("feedback_window_will_close_in", {
l10n.get("feedback_window_will_close_in2", {
countdown: this.state.countdown,
num: this.state.countdown
}))

View File

@ -543,7 +543,7 @@ loop.shared.views = (function(_, OT, l10n) {
return (
<FeedbackLayout title={l10n.get("feedback_thank_you_heading")}>
<p className="info thank-you">{
l10n.get("feedback_window_will_close_in", {
l10n.get("feedback_window_will_close_in2", {
countdown: this.state.countdown,
num: this.state.countdown
})}</p>

View File

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

View File

@ -4,6 +4,7 @@ support-files =
loop_fxa.sjs
../../../../base/content/test/general/browser_fxa_oauth.html
[browser_CardDavImporter.js]
[browser_fxa_login.js]
skip-if = !debug
[browser_loop_fxa_server.js]

View File

@ -0,0 +1,364 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
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",
"password": "p455w0rd"
}
// "pid" for "provider ID"
let vcards = [
"VERSION:3.0\n" +
"N:Smith;John;;;\n" +
"FN:John Smith\n" +
"EMAIL;TYPE=work:john.smith@example.com\n" +
"REV:2011-07-12T14:43:20Z\n" +
"UID:pid1\n" +
"END:VCARD\n",
"VERSION:3.0\n" +
"N:Smith;Jane;;;\n" +
"FN:Jane Smith\n" +
"EMAIL:jane.smith@example.com\n" +
"REV:2011-07-12T14:43:20Z\n" +
"UID:pid2\n" +
"END:VCARD\n",
"VERSION:3.0\n" +
"N:García Fernández;Miguel Angel;José Antonio;Mr.;Jr.\n" +
"FN:Mr. Miguel Angel José Antonio\n García Fernández, Jr.\n" +
"EMAIL:mike@example.org\n" +
"EMAIL;PREF=1;TYPE=work:miguel.angel@example.net\n" +
"EMAIL;TYPE=home;UNKNOWNPARAMETER=frotz:majacf@example.com\n" +
"TEL:+3455555555\n" +
"TEL;PREF=1;TYPE=work:+3455556666\n" +
"TEL;TYPE=home;UNKNOWNPARAMETER=frotz:+3455557777\n" +
"ADR:;Suite 123;Calle Aduana\\, 29;MADRID;;28070;SPAIN\n" +
"ADR;TYPE=work:P.O. BOX 555;;;Washington;DC;20024-00555;USA\n" +
"ORG:Acme España SL\n" +
"TITLE:President\n" +
"BDAY:1965-05-05\n" +
"NOTE:Likes tulips\n" +
"REV:2011-07-12T14:43:20Z\n" +
"UID:pid3\n" +
"END:VCARD\n",
"VERSION:3.0\n" +
"N:Jones;Bob;;;\n" +
"EMAIL:bob.jones@example.com\n" +
"REV:2011-07-12T14:43:20Z\n" +
"UID:pid4\n" +
"END:VCARD\n",
"VERSION:3.0\n" +
"N:Jones;Davy;Randall;;\n" +
"EMAIL:davy.jones@example.com\n" +
"REV:2011-07-12T14:43:20Z\n" +
"UID:pid5\n" +
"END:VCARD\n",
"VERSION:3.0\n" +
"EMAIL:trip@example.com\n" +
"NICKNAME:Trip\n" +
"REV:2011-07-12T14:43:20Z\n" +
"UID:pid6\n" +
"END:VCARD\n",
"VERSION:3.0\n" +
"EMAIL:acme@example.com\n" +
"ORG:Acme, Inc.\n" +
"REV:2011-07-12T14:43:20Z\n" +
"UID:pid7\n" +
"END:VCARD\n",
"VERSION:3.0\n" +
"EMAIL:anyone@example.com\n" +
"REV:2011-07-12T14:43:20Z\n" +
"UID:pid8\n" +
"END:VCARD\n",
];
const monkeyPatchImporter = function(importer) {
// Set up the response bodies
let listPropfind =
'<?xml version="1.0" encoding="UTF-8"?>\n' +
'<d:multistatus xmlns:card="urn:ietf:params:xml:ns:carddav"\n' +
' xmlns:d="DAV:">\n' +
' <d:response>\n' +
' <d:href>/carddav/abook/</d:href>\n' +
' <d:propstat>\n' +
' <d:status>HTTP/1.1 200 OK</d:status>\n' +
' </d:propstat>\n' +
' <d:propstat>\n' +
' <d:status>HTTP/1.1 404 Not Found</d:status>\n' +
' <d:prop>\n' +
' <d:getetag/>\n' +
' </d:prop>\n' +
' </d:propstat>\n' +
' </d:response>\n';
let listReportMultiget =
'<?xml version="1.0" encoding="UTF-8"?>\n' +
'<d:multistatus xmlns:card="urn:ietf:params:xml:ns:carddav"\n' +
' xmlns:d="DAV:">\n';
vcards.forEach(vcard => {
let uid = /\nUID:(.*?)\n/.exec(vcard);
listPropfind +=
' <d:response>\n' +
' <d:href>/carddav/abook/' + uid + '</d:href>\n' +
' <d:propstat>\n' +
' <d:status>HTTP/1.1 200 OK</d:status>\n' +
' <d:prop>\n' +
' <d:getetag>"2011-07-12T07:43:20.855-07:00"</d:getetag>\n' +
' </d:prop>\n' +
' </d:propstat>\n' +
' </d:response>\n';
listReportMultiget +=
' <d:response>\n' +
' <d:href>/carddav/abook/' + uid + '</d:href>\n' +
' <d:propstat>\n' +
' <d:status>HTTP/1.1 200 OK</d:status>\n' +
' <d:prop>\n' +
' <d:getetag>"2011-07-12T07:43:20.855-07:00"</d:getetag>\n' +
' <card:address-data>' + vcard + '</card:address-data>\n' +
' </d:prop>\n' +
' </d:propstat>\n' +
' </d:response>\n';
});
listPropfind += "</d:multistatus>\n";
listReportMultiget += "</d:multistatus>\n";
importer._davPromise = function(method, url, auth, depth, body) {
return new Promise((resolve, reject) => {
if (auth.method != "basic" ||
auth.user != kAuth.user ||
auth.password != kAuth.password) {
reject(new Error("401 Auth Failure"));
return;
}
let request = method + " " + url + " " + depth;
let xmlParser = new DOMParser();
let responseXML;
switch (request) {
case "PROPFIND https://example.com/.well-known/carddav 1":
responseXML = xmlParser.parseFromString(listPropfind, "text/xml");
break;
case "REPORT https://example.com/carddav/abook/ 1":
responseXML = xmlParser.parseFromString(listReportMultiget, "text/xml");
break;
default:
reject(new Error("404 Not Found"));
return;
}
resolve({"responseXML": responseXML});
});
}.bind(importer);
return importer;
}
add_task(function* test_CardDavImport() {
let importer = monkeyPatchImporter(new CardDavImporter());
yield new Promise ((resolve, reject) => {
info("Initiating import");
importer.startImport({
"host": "example.com",
"auth": kAuth.method,
"user": kAuth.user,
"password": kAuth.password
}, (err, result) => { err ? reject(err) : resolve(result); }, mockDb);
});
info("Import succeeded");
Assert.equal(vcards.length, Object.keys(mockDb._store).length,
"Should import all VCards into database");
// Basic checks
let c = mockDb._store[1];
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, "work", "Email type should match");
Assert.equal(c.email[0].value, "john.smith@example.com", "Email should match");
Assert.equal(c.email[0].pref, false, "Pref should match");
Assert.equal(c.id, "pid1@example.com", "UID should match and be scoped to provider");
c = mockDb._store[2];
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, false, "Pref should match");
Assert.equal(c.id, "pid2@example.com", "UID should match and be scoped to provider");
// Check every field
c = mockDb._store[3];
Assert.equal(c.name[0], "Mr. Miguel Angel José Antonio García Fernández, Jr.", "Full name should match");
Assert.equal(c.givenName[0], "Miguel Angel", "Given name should match");
Assert.equal(c.additionalName[0], "José Antonio", "Other name should match");
Assert.equal(c.familyName[0], "García Fernández", "Family name should match");
Assert.equal(c.email.length, 3, "Email count should match");
Assert.equal(c.email[0].type, "other", "Email type should match");
Assert.equal(c.email[0].value, "mike@example.org", "Email should match");
Assert.equal(c.email[0].pref, false, "Pref should match");
Assert.equal(c.email[1].type, "work", "Email type should match");
Assert.equal(c.email[1].value, "miguel.angel@example.net", "Email should match");
Assert.equal(c.email[1].pref, true, "Pref should match");
Assert.equal(c.email[2].type, "home", "Email type should match");
Assert.equal(c.email[2].value, "majacf@example.com", "Email should match");
Assert.equal(c.email[2].pref, false, "Pref should match");
Assert.equal(c.tel.length, 3, "Phone number count should match");
Assert.equal(c.tel[0].type, "other", "Phone type should match");
Assert.equal(c.tel[0].value, "+3455555555", "Phone number should match");
Assert.equal(c.tel[0].pref, false, "Pref should match");
Assert.equal(c.tel[1].type, "work", "Phone type should match");
Assert.equal(c.tel[1].value, "+3455556666", "Phone number should match");
Assert.equal(c.tel[1].pref, true, "Pref should match");
Assert.equal(c.tel[2].type, "home", "Phone type should match");
Assert.equal(c.tel[2].value, "+3455557777", "Phone number should match");
Assert.equal(c.tel[2].pref, false, "Pref should match");
Assert.equal(c.adr.length, 2, "Address count should match");
Assert.equal(c.adr[0].pref, false, "Pref should match");
Assert.equal(c.adr[0].type, "other", "Type should match");
Assert.equal(c.adr[0].streetAddress, "Calle Aduana, 29 Suite 123", "Street address should match");
Assert.equal(c.adr[0].locality, "MADRID", "Locality should match");
Assert.equal(c.adr[0].postalCode, "28070", "Post code should match");
Assert.equal(c.adr[0].countryName, "SPAIN", "Country should match");
Assert.equal(c.adr[1].pref, false, "Pref should match");
Assert.equal(c.adr[1].type, "work", "Type should match");
Assert.equal(c.adr[1].streetAddress, "P.O. BOX 555", "Street address should match");
Assert.equal(c.adr[1].locality, "Washington", "Locality should match");
Assert.equal(c.adr[1].region, "DC", "Region should match");
Assert.equal(c.adr[1].postalCode, "20024-00555", "Post code should match");
Assert.equal(c.adr[1].countryName, "USA", "Country should match");
Assert.equal(c.org[0], "Acme España SL", "Org should match");
Assert.equal(c.jobTitle[0], "President", "Title should match");
Assert.equal(c.note[0], "Likes tulips", "Note should match");
let bday = new Date(c.bday);
Assert.equal(bday.getUTCFullYear(), 1965, "Birthday year should match");
Assert.equal(bday.getUTCMonth(), 4, "Birthday month should match");
Assert.equal(bday.getUTCDate(), 5, "Birthday day should match");
Assert.equal(c.id, "pid3@example.com", "UID should match and be scoped to provider");
// Check name synthesis
c = mockDb._store[4];
Assert.equal(c.name[0], "Jones, Bob", "Full name should be synthesized correctly");
c = mockDb._store[5];
Assert.equal(c.name[0], "Jones, Davy Randall", "Full name should be synthesized correctly");
c = mockDb._store[6];
Assert.equal(c.name[0], "Trip", "Full name should be synthesized correctly");
c = mockDb._store[7];
Assert.equal(c.name[0], "Acme, Inc.", "Full name should be synthesized correctly");
c = mockDb._store[8];
Assert.equal(c.name[0], "anyone@example.com", "Full name should be synthesized correctly");
// Check that a re-import doesn't cause contact duplication.
yield new Promise ((resolve, reject) => {
info("Initiating import");
importer.startImport({
"host": "example.com",
"auth": kAuth.method,
"user": kAuth.user,
"password": kAuth.password
}, (err, result) => { err ? reject(err) : resolve(result); }, mockDb);
});
Assert.equal(vcards.length, Object.keys(mockDb._store).length,
"Second import shouldn't increase DB size");
// Check that errors are propagated back to caller
let error = yield new Promise ((resolve, reject) => {
info("Initiating import");
importer.startImport({
"host": "example.com",
"auth": kAuth.method,
"user": kAuth.user,
"password": "invalidpassword"
}, (err, result) => { err ? resolve(err) : reject(new Error("Should have failed")); }, mockDb);
});
Assert.equal(error.message, "401 Auth Failure", "Auth error should propagate");
let error = yield new Promise ((resolve, reject) => {
info("Initiating import");
importer.startImport({
"host": "example.invalid",
"auth": kAuth.method,
"user": kAuth.user,
"password": kAuth.password
}, (err, result) => { err ? resolve(err) : reject(new Error("Should have failed")); }, mockDb);
});
Assert.equal(error.message, "404 Not Found", "Not found error should propagate");
let tmp = mockDb.getByServiceId;
mockDb.getByServiceId = function(serviceId, callback) {
callback(new Error("getByServiceId failed"));
};
let error = yield new Promise ((resolve, reject) => {
info("Initiating import");
importer.startImport({
"host": "example.com",
"auth": kAuth.method,
"user": kAuth.user,
"password": kAuth.password
}, (err, result) => { err ? resolve(err) : reject(new Error("Should have failed")); }, mockDb);
});
Assert.equal(error.message, "getByServiceId failed", "Database error should propagate");
mockDb.getByServiceId = tmp;
let error = yield new Promise ((resolve, reject) => {
info("Initiating import");
importer.startImport({
"host": "example.com",
}, (err, result) => { err ? resolve(err) : reject(new Error("Should have failed")); }, mockDb);
});
Assert.equal(error.message, "No authentication specified", "Missing parameters should generate error");
})

View File

@ -11,7 +11,7 @@ add_task(loadLoopPanel);
add_task(function* test_mozLoop_pluralStrings() {
Assert.ok(gMozLoopAPI, "mozLoop should exist");
var strings = JSON.parse(gMozLoopAPI.getStrings("feedback_window_will_close_in"));
var strings = JSON.parse(gMozLoopAPI.getStrings("feedback_window_will_close_in2"));
Assert.equal(gMozLoopAPI.getPluralForm(0, strings.textContent),
"This window will close in {{countdown}} seconds");
Assert.equal(gMozLoopAPI.getPluralForm(1, strings.textContent),

View File

@ -29,7 +29,6 @@ const MAX_ORDINAL = 99;
this.DevTools = function DevTools() {
this._tools = new Map(); // Map<toolId, tool>
this._themes = new Map(); // Map<themeId, theme>
this._eventParsers = new Map(); // Map<parserID, [handlers]>
this._toolboxes = new Map(); // Map<target, toolbox>
// destroy() is an observer's handler so we need to preserve context.
@ -42,7 +41,7 @@ this.DevTools = function DevTools() {
Services.obs.addObserver(this._teardown, "devtools-unloaded", false);
Services.obs.addObserver(this.destroy, "quit-application", false);
}
};
DevTools.prototype = {
/**
@ -66,10 +65,6 @@ DevTools.prototype = {
}
},
get eventParsers() {
return this._eventParsers;
},
/**
* Register a new developer tool.
*
@ -145,85 +140,6 @@ DevTools.prototype = {
}
},
/**
* Register a new event parser to be used in the processing of event info.
*
* @param {Object} parserObj
* Each parser must contain the following properties:
* - parser, which must take the following form:
* {
* id {String}: "jQuery events", // Unique id.
* getListeners: function(node) { }, // Function that takes a node and
* // returns an array of eventInfo
* // objects (see below).
*
* hasListeners: function(node) { }, // Optional function that takes a
* // node and returns a boolean
* // indicating whether a node has
* // listeners attached.
*
* normalizeHandler: function(fnDO) { }, // Optional function that takes a
* // Debugger.Object instance and
* // climbs the scope chain to get
* // the function that should be
* // displayed in the event bubble
* // see the following url for
* // details:
* // https://developer.mozilla.org/
* // docs/Tools/Debugger-API/
* // Debugger.Object
* }
*
* An eventInfo object should take the following form:
* {
* type {String}: "click",
* handler {Function}: event handler,
* tags {String}: "jQuery,Live", // These tags will be displayed as
* // attributes in the events popup.
* hide: { // Hide or show fields:
* debugger: false, // Debugger icon
* type: false, // Event type e.g. click
* filename: false, // Filename
* capturing: false, // Capturing
* dom0: false // DOM 0
* },
*
* override: { // The following can be overridden:
* type: "click",
* origin: "http://www.mozilla.com",
* searchString: 'onclick="doSomething()"',
* DOM0: true,
* capturing: true
* }
* }
*/
registerEventParser: function(parserObj) {
let parserId = parserObj.id;
if (!parserId) {
throw new Error("Cannot register new event parser with id " + parserId);
}
if (this._eventParsers.has(parserId)) {
throw new Error("Duplicate event parser id " + parserId);
}
this._eventParsers.set(parserId, {
getListeners: parserObj.getListeners,
hasListeners: parserObj.hasListeners,
normalizeHandler: parserObj.normalizeHandler
});
},
/**
* Removes parser that matches a given parserId.
*
* @param {String} parserId
* id of the event parser to unregister.
*/
unregisterEventParser: function(parserId) {
this._eventParsers.delete(parserId);
},
/**
* Sorting function used for sorting tools based on their ordinals.
*/
@ -555,10 +471,6 @@ DevTools.prototype = {
this.unregisterTool(key, true);
}
for (let [id] of this._eventParsers) {
this.unregisterEventParser(id, true);
}
// Cleaning down the toolboxes: i.e.
// for (let [target, toolbox] of this._toolboxes) toolbox.destroy();
// Is taken care of by the gDevToolsBrowser.forgetBrowserWindow

View File

@ -26,6 +26,7 @@ browser.jar:
content/browser/devtools/frame-script-utils.js (shared/frame-script-utils.js)
content/browser/devtools/styleeditor.xul (styleeditor/styleeditor.xul)
content/browser/devtools/styleeditor.css (styleeditor/styleeditor.css)
content/browser/devtools/storage.xul (storage/storage.xul)
content/browser/devtools/computedview.xhtml (styleinspector/computedview.xhtml)
content/browser/devtools/cssruleview.xhtml (styleinspector/cssruleview.xhtml)
content/browser/devtools/ruleview.css (styleinspector/ruleview.css)

View File

@ -21,8 +21,6 @@ loader.lazyGetter(this, "osString", () => Cc["@mozilla.org/xre/app-info;1"].getS
let events = require("sdk/system/events");
require("devtools/toolkit/event-parsers");
// Panels
loader.lazyGetter(this, "OptionsPanel", () => require("devtools/framework/toolbox-options").OptionsPanel);
loader.lazyGetter(this, "InspectorPanel", () => require("devtools/inspector/inspector-panel").InspectorPanel);
@ -35,6 +33,7 @@ loader.lazyGetter(this, "WebAudioEditorPanel", () => require("devtools/webaudioe
loader.lazyGetter(this, "ProfilerPanel", () => require("devtools/profiler/panel").ProfilerPanel);
loader.lazyGetter(this, "NetMonitorPanel", () => require("devtools/netmonitor/panel").NetMonitorPanel);
loader.lazyGetter(this, "ScratchpadPanel", () => require("devtools/scratchpad/scratchpad-panel").ScratchpadPanel);
loader.lazyGetter(this, "StoragePanel", () => require("devtools/storage/panel").StoragePanel);
// Strings
const toolboxProps = "chrome://browser/locale/devtools/toolbox.properties";
@ -44,11 +43,12 @@ const styleEditorProps = "chrome://browser/locale/devtools/styleeditor.propertie
const shaderEditorProps = "chrome://browser/locale/devtools/shadereditor.properties";
const canvasDebuggerProps = "chrome://browser/locale/devtools/canvasdebugger.properties";
const webAudioEditorProps = "chrome://browser/locale/devtools/webaudioeditor.properties";
const webConsoleProps = "chrome://browser/locale/devtools/webconsole.properties";
const profilerProps = "chrome://browser/locale/devtools/profiler.properties";
const netMonitorProps = "chrome://browser/locale/devtools/netmonitor.properties";
const scratchpadProps = "chrome://browser/locale/devtools/scratchpad.properties";
const storageProps = "chrome://browser/locale/devtools/storage.properties";
loader.lazyGetter(this, "toolboxStrings", () => Services.strings.createBundle(toolboxProps));
loader.lazyGetter(this, "webConsoleStrings", () => Services.strings.createBundle(webConsoleProps));
loader.lazyGetter(this, "debuggerStrings", () => Services.strings.createBundle(debuggerProps));
@ -60,6 +60,7 @@ loader.lazyGetter(this, "inspectorStrings", () => Services.strings.createBundle(
loader.lazyGetter(this, "profilerStrings",() => Services.strings.createBundle(profilerProps));
loader.lazyGetter(this, "netMonitorStrings", () => Services.strings.createBundle(netMonitorProps));
loader.lazyGetter(this, "scratchpadStrings", () => Services.strings.createBundle(scratchpadProps));
loader.lazyGetter(this, "storageStrings", () => Services.strings.createBundle(storageProps));
let Tools = {};
exports.Tools = Tools;
@ -318,9 +319,34 @@ Tools.netMonitor = {
}
};
Tools.storage = {
id: "storage",
key: l10n("open.commandkey", storageStrings),
ordinal: 9,
accesskey: l10n("open.accesskey", storageStrings),
modifiers: "shift",
visibilityswitch: "devtools.storage.enabled",
icon: "chrome://browser/skin/devtools/tool-storage.svg",
invertIconForLightTheme: true,
url: "chrome://browser/content/devtools/storage.xul",
label: l10n("storage.label", storageStrings),
tooltip: l10n("storage.tooltip", storageStrings),
inMenu: true,
isTargetSupported: function(target) {
return target.isLocalTab ||
(target.client.traits.storageInspector && !target.isAddon);
},
build: function(iframeWindow, toolbox) {
let panel = new StoragePanel(iframeWindow, toolbox);
return panel.open();
}
};
Tools.scratchpad = {
id: "scratchpad",
ordinal: 9,
ordinal: 10,
visibilityswitch: "devtools.scratchpad.enabled",
icon: "chrome://browser/skin/devtools/tool-scratchpad.svg",
invertIconForLightTheme: true,
@ -352,6 +378,7 @@ let defaultTools = [
Tools.webAudioEditor,
Tools.jsprofiler,
Tools.netMonitor,
Tools.storage,
Tools.scratchpad
];

View File

@ -23,6 +23,7 @@ DIRS += [
'shadereditor',
'shared',
'sourceeditor',
'storage',
'styleeditor',
'styleinspector',
'tilt',

View File

@ -16,7 +16,7 @@ function test() {
"-H 'User-Agent: " + aDebuggee.navigator.userAgent + "'",
"-H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'",
"-H 'Accept-Language: " + aDebuggee.navigator.language + "'",
"-H 'Accept-Encoding: gzip, deflate'",
"--compressed",
"-H 'X-Custom-Header-1: Custom value'",
"-H 'X-Custom-Header-2: 8.8.8.8'",
"-H 'X-Custom-Header-3: Mon, 3 Mar 2014 11:11:11 GMT'",
@ -31,7 +31,7 @@ function test() {
'-H "User-Agent: ' + aDebuggee.navigator.userAgent + '"',
'-H "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"',
'-H "Accept-Language: ' + aDebuggee.navigator.language + '"',
'-H "Accept-Encoding: gzip, deflate"',
"--compressed",
'-H "X-Custom-Header-1: Custom value"',
'-H "X-Custom-Header-2: 8.8.8.8"',
'-H "X-Custom-Header-3: Mon, 3 Mar 2014 11:11:11 GMT"',

View File

@ -57,6 +57,13 @@ var Resource = Class({
*/
get hasChildren() { return this.children && this.children.size > 0; },
/**
* Is this Resource the root (top level for the store)?
*/
get isRoot() {
return !this.parent
},
/**
* Sorted array of children for display
*/

View File

@ -84,6 +84,9 @@ var ResourceContainer = Class({
evt.stopPropagation();
}, true);
if (!this.resource.isRoot) {
this.expanded = false;
}
this.update();
},

View File

@ -35,8 +35,14 @@ let test = asyncTest(function*() {
function selectFileFirstLoad(projecteditor, resource) {
ok (resource && resource.path, "A valid resource has been passed in for selection " + (resource && resource.path));
projecteditor.projectTree.selectResource(resource);
let container = projecteditor.projectTree.getViewContainer(resource);
if (resource.isRoot) {
ok (container.expanded, "The root directory is expanded by default.");
return;
}
if (resource.isDir) {
ok (!container.expanded, "A directory is not expanded by default.");
return;
}

View File

@ -120,6 +120,10 @@ this.Curl = {
}
for (let i = 0; i < headers.length; i++) {
let header = headers[i];
if (header.name === "Accept-Encoding"){
command.push("--compressed");
continue;
}
if (ignoredHeaders.has(header.name)) {
continue;
}

View File

@ -38,7 +38,7 @@ const MAX_VISIBLE_STRING_SIZE = 100;
* entry in the table. Default: name.
* - emptyText: text to display when no entries in the table to display.
* - highlightUpdated: true to highlight the changed/added row.
* - removableColumns: Whether columns are removeable. If set to true,
* - removableColumns: Whether columns are removeable. If set to false,
* the context menu in the headers will not appear.
* - firstColumn: key of the first column that should appear.
*/
@ -55,7 +55,7 @@ function TableWidget(node, options={}) {
this.uniqueId = uniqueId || "name";
this.firstColumn = firstColumn || "";
this.highlightUpdated = highlightUpdated || false;
this.removableColumns = removableColumns || false;
this.removableColumns = removableColumns !== false;
this.tbody = this.document.createElementNS(XUL_NS, "hbox");
this.tbody.className = "table-widget-body theme-body";
@ -276,7 +276,7 @@ TableWidget.prototype = {
item = item[this.uniqueId];
}
return item == this.selectedRow[this.uniqueId];
return this.selectedRow && item == this.selectedRow[this.uniqueId];
},
/**
@ -687,6 +687,8 @@ Column.prototype = {
/**
* Event handler for the command event coming from the header context menu.
* Toggles the column if it was requested by the user.
* When called explicitly without parameters, it toggles the corresponding
* column.
*
* @param {string} event
* The name of the event. i.e. EVENTS.HEADER_CONTEXT_MENU
@ -696,6 +698,11 @@ Column.prototype = {
* true if the column is visible
*/
toggleColumn: function(event, id, checked) {
if (arguments.length == 0) {
// Act like a toggling method when called with no params
id = this.id;
checked = this.wrapper.hasAttribute("hidden");
}
if (id != this.id) {
return;
}
@ -960,6 +967,8 @@ Cell.prototype = {
*/
flash: function() {
this.label.classList.remove("flash-out");
// Cause a reflow so that the animation retriggers on adding back the class
let a = this.label.parentNode.offsetWidth;
this.label.classList.add("flash-out");
},

View File

@ -65,7 +65,7 @@ TreeWidget.prototype = {
this._selectedLabel = this._selectedItem = null;
return;
}
if (!typeof id == "array") {
if (!Array.isArray(id)) {
return;
}
this._selectedLabel = this.root.setSelectedItem(id);

View File

@ -0,0 +1,12 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
EXTRA_JS_MODULES.devtools.storage += [
'panel.js',
'ui.js'
]

View File

@ -0,0 +1,83 @@
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const {Cc, Ci, Cu, Cr} = require("chrome");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
let EventEmitter = require("devtools/toolkit/event-emitter");
loader.lazyGetter(this, "StorageFront",
() => require("devtools/server/actors/storage").StorageFront);
loader.lazyGetter(this, "StorageUI",
() => require("devtools/storage/ui").StorageUI);
this.StoragePanel = function StoragePanel(panelWin, toolbox) {
EventEmitter.decorate(this);
this._toolbox = toolbox;
this._target = toolbox.target;
this._panelWin = panelWin;
this.destroy = this.destroy.bind(this);
}
exports.StoragePanel = StoragePanel;
StoragePanel.prototype = {
get target() this._toolbox.target,
get panelWindow() this._panelWin,
/**
* open is effectively an asynchronous constructor
*/
open: function() {
let targetPromise;
// We always interact with the target as if it were remote
if (!this.target.isRemote) {
targetPromise = this.target.makeRemote();
} else {
targetPromise = Promise.resolve(this.target);
}
return targetPromise.then(() => {
this.target.on("close", this.destroy);
this._front = new StorageFront(this.target.client, this.target.form);
this.UI = new StorageUI(this._front, this._target, this._panelWin);
this.isReady = true;
this.emit("ready");
return this;
}, console.error);
},
/**
* Destroy the style editor.
*/
destroy: function() {
if (!this._destroyed) {
this.UI.destroy();
this._destroyed = true;
this._target.off("close", this.destroy);
this._target = null;
this._toolbox = null;
this._panelDoc = null;
}
return Promise.resolve(null);
},
}
XPCOMUtils.defineLazyGetter(StoragePanel.prototype, "strings",
function () {
return Services.strings.createBundle(
"chrome://browser/locale/devtools/storage.properties");
});

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?>
<?xml-stylesheet href="chrome://browser/content/devtools/widgets.css" type="text/css"?>
<?xml-stylesheet href="chrome://browser/skin/devtools/common.css" type="text/css"?>
<?xml-stylesheet href="chrome://browser/skin/devtools/widgets.css" type="text/css"?>
<?xml-stylesheet href="chrome://browser/skin/devtools/storage.css" type="text/css"?>
<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script type="application/javascript;version=1.8"
src="chrome://browser/content/devtools/theme-switching.js"/>
<script type="text/javascript" src="chrome://global/content/globalOverlay.js"/>
<commandset id="editMenuCommands"/>
<box flex="1" class="devtools-responsive-container theme-body">
<vbox id="storage-tree"/>
<splitter class="devtools-side-splitter"/>
<vbox id="storage-table" class="theme-sidebar" flex="1"/>
<splitter class="devtools-side-splitter"/>
<vbox id="storage-sidebar" class="devtools-sidebar-tabs" hidden="true">
<vbox flex="1"/>
</vbox>
</box>
</window>

View File

@ -0,0 +1,15 @@
[DEFAULT]
skip-if = e10s # Bug 1049888 - storage actors do not work in e10s for now
subsuite = devtools
support-files =
storage-complex-values.html
storage-listings.html
storage-secured-iframe.html
storage-unsecured-iframe.html
storage-updates.html
head.js
[browser_storage_basic.js]
[browser_storage_dynamic_updates.js]
[browser_storage_sidebar.js]
[browser_storage_values.js]

View File

@ -0,0 +1,114 @@
/* 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/. */
// Basic test to assert that the storage tree and table corresponding to each
// item in the storage tree is correctly displayed
// Entries that should be present in the tree for this test
// Format for each entry in the array :
// [
// ["path", "to", "tree", "item"], - The path to the tree item to click formed
// by id of each item
// ["key_value1", "key_value2", ...] - The value of the first (unique) column
// for each row in the table corresponding
// to the tree item selected.
// ]
// These entries are formed by the cookies, local storage, session storage and
// indexedDB entries created in storage-listings.html,
// storage-secured-iframe.html and storage-unsecured-iframe.html
const storeItems = [
[["cookies", "test1.example.org"],
["c1", "cs2", "c3", "uc1"]],
[["cookies", "sectest1.example.org"],
["uc1", "cs2", "sc1"]],
[["localStorage", "http://test1.example.org"],
["ls1", "ls2"]],
[["localStorage", "http://sectest1.example.org"],
["iframe-u-ls1"]],
[["localStorage", "https://sectest1.example.org"],
["iframe-s-ls1"]],
[["sessionStorage", "http://test1.example.org"],
["ss1"]],
[["sessionStorage", "http://sectest1.example.org"],
["iframe-u-ss1", "iframe-u-ss2"]],
[["sessionStorage", "https://sectest1.example.org"],
["iframe-s-ss1"]],
[["indexedDB", "http://test1.example.org"],
["idb1", "idb2"]],
[["indexedDB", "http://test1.example.org", "idb1"],
["obj1", "obj2"]],
[["indexedDB", "http://test1.example.org", "idb2"],
["obj3"]],
[["indexedDB", "http://test1.example.org", "idb1", "obj1"],
[1, 2, 3]],
[["indexedDB", "http://test1.example.org", "idb1", "obj2"],
[1]],
[["indexedDB", "http://test1.example.org", "idb2", "obj3"],
[]],
[["indexedDB", "http://sectest1.example.org"],
[]],
[["indexedDB", "https://sectest1.example.org"],
["idb-s1", "idb-s2"]],
[["indexedDB", "https://sectest1.example.org", "idb-s1"],
["obj-s1"]],
[["indexedDB", "https://sectest1.example.org", "idb-s2"],
["obj-s2"]],
[["indexedDB", "https://sectest1.example.org", "idb-s1", "obj-s1"],
[6, 7]],
[["indexedDB", "https://sectest1.example.org", "idb-s2", "obj-s2"],
[16]],
];
/**
* Test that the desired number of tree items are present
*/
function testTree() {
let doc = gPanelWindow.document;
for (let item of storeItems) {
ok(doc.querySelector("[data-id='" + JSON.stringify(item[0]) + "']"),
"Tree item " + item[0] + " should be present in the storage tree");
}
}
/**
* Test that correct table entries are shown for each of the tree item
*/
let testTables = Task.async(function*() {
let doc = gPanelWindow.document;
// Expand all nodes so that the synthesized click event actually works
gUI.tree.expandAll();
// First tree item is already selected so no clicking and waiting for update
for (let id of storeItems[0][1]) {
ok(doc.querySelector(".table-widget-cell[data-id='" + id + "']"),
"Table item " + id + " should be present");
}
// Click rest of the tree items and wait for the table to be updated
for (let item of storeItems.slice(1)) {
selectTreeItem(item[0]);
yield gUI.once("store-objects-updated");
// Check whether correct number of items are present in the table
is(doc.querySelectorAll(
".table-widget-wrapper:first-of-type .table-widget-cell"
).length, item[1].length, "Number of items in table is correct");
// Check if all the desired items are present in the table
for (let id of item[1]) {
ok(doc.querySelector(".table-widget-cell[data-id='" + id + "']"),
"Table item " + id + " should be present");
}
}
});
let startTest = Task.async(function*() {
testTree();
yield testTables();
finishTests();
});
function test() {
openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html").then(startTest);
}

View File

@ -0,0 +1,211 @@
/* 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/. */
let testUpdates = Task.async(function*() {
let $ = id => gPanelWindow.document.querySelector(id);
let $$ = sel => gPanelWindow.document.querySelectorAll(sel);
gUI.tree.expandAll();
ok(gUI.sidebar.hidden, "Sidebar is initially hidden");
selectTableItem("c1");
yield gUI.once("sidebar-updated");
// test that value is something initially
let initialValue = [[
{name: "c1", value: "1.2.3.4.5.6.7"},
{name: "c1.path", value: "/browser"}
],[
{name: "c1", value: "Array"},
{name: "c1.0", value: "1"},
{name: "c1.6", value: "7"}
]];
// test that value is something initially
let finalValue = [[
{name: "c1", value: '{"foo": 4,"bar":6}'},
{name: "c1.path", value: "/browser"}
],[
{name: "c1", value: "Object"},
{name: "c1.foo", value: "4"},
{name: "c1.bar", value: "6"}
]];
// Check that sidebar shows correct initial value
yield findVariableViewProperties(initialValue[0], false);
yield findVariableViewProperties(initialValue[1], true);
// Check if table shows correct initial value
ok($("#value [data-id='c1'].table-widget-cell"), "cell is present");
is($("#value [data-id='c1'].table-widget-cell").value, "1.2.3.4.5.6.7",
"correct initial value in table");
gWindow.addCookie("c1", '{"foo": 4,"bar":6}', "/browser");
yield gUI.once("sidebar-updated");
yield findVariableViewProperties(finalValue[0], false);
yield findVariableViewProperties(finalValue[1], true);
ok($("#value [data-id='c1'].table-widget-cell"), "cell is present after update");
is($("#value [data-id='c1'].table-widget-cell").value, '{"foo": 4,"bar":6}',
"correct final value in table");
// Add a new entry
is($$("#value .table-widget-cell").length, 2,
"Correct number of rows before update 0");
gWindow.addCookie("c3", "booyeah");
// Wait once for update and another time for value fetching
yield gUI.once("store-objects-updated");
yield gUI.once("store-objects-updated");
is($$("#value .table-widget-cell").length, 3,
"Correct number of rows after update 1");
// Add another
gWindow.addCookie("c4", "booyeah");
// Wait once for update and another time for value fetching
yield gUI.once("store-objects-updated");
yield gUI.once("store-objects-updated");
is($$("#value .table-widget-cell").length, 4,
"Correct number of rows after update 2");
// Removing cookies
gWindow.removeCookie("c1", "/browser");
yield gUI.once("sidebar-updated");
is($$("#value .table-widget-cell").length, 3,
"Correct number of rows after delete update 3");
ok(!$("#c1"), "Correct row got deleted");
ok(!gUI.sidebar.hidden, "Sidebar still visible for next row");
// Check if next element's value is visible in sidebar
yield findVariableViewProperties([{name: "c2", value: "foobar"}]);
// Keep deleting till no rows
gWindow.removeCookie("c3");
yield gUI.once("store-objects-updated");
is($$("#value .table-widget-cell").length, 2,
"Correct number of rows after delete update 4");
// Check if next element's value is visible in sidebar
yield findVariableViewProperties([{name: "c2", value: "foobar"}]);
gWindow.removeCookie("c2", "/browser");
yield gUI.once("sidebar-updated");
yield findVariableViewProperties([{name: "c4", value: "booyeah"}]);
is($$("#value .table-widget-cell").length, 1,
"Correct number of rows after delete update 5");
gWindow.removeCookie("c4");
yield gUI.once("store-objects-updated");
is($$("#value .table-widget-cell").length, 0,
"Correct number of rows after delete update 6");
ok(gUI.sidebar.hidden, "Sidebar is hidden when no rows");
// Testing in local storage
selectTreeItem(["localStorage", "http://test1.example.org"]);
yield gUI.once("store-objects-updated");
is($$("#value .table-widget-cell").length, 7,
"Correct number of rows after delete update 7");
ok($(".table-widget-cell[data-id='ls4']"), "ls4 exists before deleting");
gWindow.localStorage.removeItem("ls4");
yield gUI.once("store-objects-updated");
is($$("#value .table-widget-cell").length, 6,
"Correct number of rows after delete update 8");
ok(!$(".table-widget-cell[data-id='ls4']"),
"ls4 does not exists after deleting");
gWindow.localStorage.setItem("ls4", "again");
yield gUI.once("store-objects-updated");
yield gUI.once("store-objects-updated");
is($$("#value .table-widget-cell").length, 7,
"Correct number of rows after delete update 9");
ok($(".table-widget-cell[data-id='ls4']"),
"ls4 came back after adding it again");
// Updating a row
gWindow.localStorage.setItem("ls2", "ls2-changed");
yield gUI.once("store-objects-updated");
yield gUI.once("store-objects-updated");
is($("#value [data-id='ls2']").value, "ls2-changed",
"Value got updated for local storage");
// Testing in session storage
selectTreeItem(["sessionStorage", "http://test1.example.org"]);
yield gUI.once("store-objects-updated");
is($$("#value .table-widget-cell").length, 3,
"Correct number of rows for session storage");
gWindow.sessionStorage.setItem("ss4", "new-item");
yield gUI.once("store-objects-updated");
yield gUI.once("store-objects-updated");
is($$("#value .table-widget-cell").length, 4,
"Correct number of rows after session storage update");
// deleting item
gWindow.sessionStorage.removeItem("ss3");
yield gUI.once("store-objects-updated");
gWindow.sessionStorage.removeItem("ss1");
yield gUI.once("store-objects-updated");
is($$("#value .table-widget-cell").length, 2,
"Correct number of rows after removing items from session storage");
selectTableItem("ss2");
yield gUI.once("sidebar-updated");
ok(!gUI.sidebar.hidden, "sidebar is visible");
// Checking for correct value in sidebar before update
yield findVariableViewProperties([{name: "ss2", value: "foobar"}]);
gWindow.sessionStorage.setItem("ss2", "changed=ss2");
yield gUI.once("sidebar-updated");
is($("#value [data-id='ss2']").value, "changed=ss2",
"Value got updated for session storage in the table");
yield findVariableViewProperties([{name: "ss2", value: "changed=ss2"}]);
});
let startTest = Task.async(function*() {
yield testUpdates();
finishTests();
});
function test() {
openTabAndSetupStorage(MAIN_DOMAIN + "storage-updates.html").then(startTest);
}

View File

@ -0,0 +1,67 @@
/* 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/. */
// Test to verify that the sidebar opens, closes and updates
// This test is not testing the values in the sidebar, being tested in _values
// Format: [
// <id of the table item to click> or <id array for tree item to select> or
// null to press Escape,
// <do we wait for the async "sidebar-updated" event>,
// <is the sidebar open>
// ]
const testCases = [
[["cookies", "sectest1.example.org"], 0, 0],
["cs2", 1, 1],
[null, 0, 0],
["cs2", 1, 1],
["uc1", 1, 1],
["uc1", 0, 1],
[["localStorage", "http://sectest1.example.org"], 0, 0],
["iframe-u-ls1", 1, 1],
["iframe-u-ls1", 0, 1],
[null, 0, 0],
[["sessionStorage", "http://test1.example.org"], 0, 0],
["ss1", 1, 1],
[null, 0, 0],
[["indexedDB", "http://test1.example.org"], 0, 0],
["idb2", 1, 1],
[["indexedDB", "http://test1.example.org", "idb2", "obj3"], 0, 0],
[["indexedDB", "https://sectest1.example.org", "idb-s2"], 0, 0],
["obj-s2", 1, 1],
[null, 0, 0],
[null, 0, 0],
["obj-s2", 1, 1],
[null, 0, 0],
];
let testSidebar = Task.async(function*() {
let doc = gPanelWindow.document;
for (let item of testCases) {
info("clicking for item " + item);
if (Array.isArray(item[0])) {
selectTreeItem(item[0]);
yield gUI.once("store-objects-updated");
}
else if (item[0]) {
selectTableItem(item[0]);
}
else {
EventUtils.sendKey("ESCAPE", gPanelWindow);
}
if (item[1]) {
yield gUI.once("sidebar-updated");
}
is(!item[2], gUI.sidebar.hidden, "Correct visibility state of sidebar");
}
});
let startTest = Task.async(function*() {
yield testSidebar();
finishTests();
});
function test() {
openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html").then(startTest);
}

View File

@ -0,0 +1,143 @@
/* 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/. */
// Test to verify that the values shown in sidebar are correct
// Format: [
// <id of the table item to click> or <id array for tree item to select> or
// null do click nothing,
// null to skip checking value in variables view or a key value pair object
// which will be asserted to exist in the storage sidebar,
// true if the check is to be made in the parsed value section
// ]
const testCases = [
["cs2", [
{name: "cs2", value: "sessionCookie"},
{name: "cs2.path", value: "/"},
{name: "cs2.isDomain", value: "true"},
{name: "cs2.isHttpOnly", value: "false"},
{name: "cs2.host", value: ".example.org"},
{name: "cs2.expires", value: "Session"},
{name: "cs2.isSecure", value: "false"},
]],
["c1", [
{name: "c1", value: JSON.stringify(["foo", "Bar", {foo: "Bar"}])},
{name: "c1.path", value: "/browser"},
{name: "c1.isDomain", value: "false"},
{name: "c1.isHttpOnly", value: "false"},
{name: "c1.host", value: "test1.example.org"},
{name: "c1.expires", value: new Date(2000000000000).toLocaleString()},
{name: "c1.isSecure", value: "false"},
]],
[/*"c1"*/, [
{name: "c1", value: "Array"},
{name: "c1.0", value: "foo"},
{name: "c1.1", value: "Bar"},
{name: "c1.2", value: "Object"},
{name: "c1.2.foo", value: "Bar"},
], true],
[["localStorage", "http://test1.example.org"]],
["ls2", [
{name: "ls2", value: "foobar-2"}
]],
["ls1", [
{name: "ls1", value: JSON.stringify({
es6: "for", the: "win", baz: [0, 2, 3, {
deep: "down",
nobody: "cares"
}]})}
]],
[/*ls1*/, [
{name: "ls1", value: "Object"},
{name: "ls1.es6", value: "for"},
{name: "ls1.the", value: "win"},
{name: "ls1.baz", value: "Array"},
{name: "ls1.baz.0", value: "0"},
{name: "ls1.baz.1", value: "2"},
{name: "ls1.baz.2", value: "3"},
{name: "ls1.baz.3", value: "Object"},
{name: "ls1.baz.3.deep", value: "down"},
{name: "ls1.baz.3.nobody", value: "cares"},
], true],
["ls3", [
{name: "ls3", "value": "http://foobar.com/baz.php"}
]],
[/*ls3*/, [
{name: "ls3", "value": "http://foobar.com/baz.php", dontMatch: true}
], true],
[["sessionStorage", "http://test1.example.org"]],
["ss1", [
{name: "ss1", value: "This#is#an#array"}
]],
[/*ss1*/, [
{name: "ss1", value: "Array"},
{name: "ss1.0", value: "This"},
{name: "ss1.1", value: "is"},
{name: "ss1.2", value: "an"},
{name: "ss1.3", value: "array"},
], true],
["ss2", [
{name: "ss2", value: "Array"},
{name: "ss2.0", value: "This"},
{name: "ss2.1", value: "is"},
{name: "ss2.2", value: "another"},
{name: "ss2.3", value: "array"},
], true],
["ss3", [
{name: "ss3", value: "Object"},
{name: "ss3.this", value: "is"},
{name: "ss3.an", value: "object"},
{name: "ss3.foo", value: "bar"},
], true],
[["indexedDB", "http://test1.example.org", "idb1", "obj1"]],
[1, [
{name: 1, value: JSON.stringify({id: 1, name: "foo", email: "foo@bar.com"})}
]],
[/*1*/, [
{name: "1.id", value: "1"},
{name: "1.name", value: "foo"},
{name: "1.email", value: "foo@bar.com"},
], true],
[["indexedDB", "http://test1.example.org", "idb1", "obj2"]],
[1, [
{name: 1, value: JSON.stringify({
id2: 1, name: "foo", email: "foo@bar.com", extra: "baz"
})}
]],
[/*1*/, [
{name: "1.id2", value: "1"},
{name: "1.name", value: "foo"},
{name: "1.email", value: "foo@bar.com"},
{name: "1.extra", value: "baz"},
], true]
];
let testValues = Task.async(function*() {
gUI.tree.expandAll();
let doc = gPanelWindow.document;
for (let item of testCases) {
info("clicking for item " + item);
if (Array.isArray(item[0])) {
selectTreeItem(item[0]);
yield gUI.once("store-objects-updated");
continue;
}
else if (item[0]) {
selectTableItem(item[0]);
}
if (item[0] && item[1]) {
yield gUI.once("sidebar-updated");
}
yield findVariableViewProperties(item[1], item[2]);
}
});
let startTest = Task.async(function*() {
yield testValues();
finishTests();
});
function test() {
openTabAndSetupStorage(MAIN_DOMAIN + "storage-complex-values.html").then(startTest);
}

View File

@ -0,0 +1,499 @@
/* 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";
let tempScope = {};
Cu.import("resource://gre/modules/devtools/Loader.jsm", tempScope);
Cu.import("resource://gre/modules/devtools/Console.jsm", tempScope);
const console = tempScope.console;
const devtools = tempScope.devtools;
tempScope = null;
const require = devtools.require;
const TargetFactory = devtools.TargetFactory;
const SPLIT_CONSOLE_PREF = "devtools.toolbox.splitconsoleEnabled";
const STORAGE_PREF = "devtools.storage.enabled";
const PATH = "browser/browser/devtools/storage/test/";
const MAIN_DOMAIN = "http://test1.example.org/" + PATH;
const ALT_DOMAIN = "http://sectest1.example.org/" + PATH;
const ALT_DOMAIN_SECURED = "https://sectest1.example.org:443/" + PATH;
let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
waitForExplicitFinish();
let gToolbox, gPanelWindow, gWindow, gUI;
Services.prefs.setBoolPref(STORAGE_PREF, true);
gDevTools.testing = true;
registerCleanupFunction(() => {
gToolbox = gPanelWindow = gWindow = gUI = null;
Services.prefs.clearUserPref(STORAGE_PREF);
Services.prefs.clearUserPref(SPLIT_CONSOLE_PREF);
gDevTools.testing = false;
while (gBrowser.tabs.length > 1) {
gBrowser.removeCurrentTab();
}
});
/**
* Add a new test tab in the browser and load the given url.
*
* @param {String} url The url to be loaded in the new tab
*
* @return a promise that resolves to the content window when the url is loaded
*/
function addTab(url) {
info("Adding a new tab with URL: '" + url + "'");
let def = promise.defer();
// Bug 921935 should bring waitForFocus() support to e10s, which would
// probably cover the case of the test losing focus when the page is loading.
// For now, we just make sure the window is focused.
window.focus();
let tab = window.gBrowser.selectedTab = window.gBrowser.addTab(url);
let linkedBrowser = tab.linkedBrowser;
linkedBrowser.addEventListener("load", function onload(event) {
if (event.originalTarget.location.href != url) {
return;
}
linkedBrowser.removeEventListener("load", onload, true);
info("URL '" + url + "' loading complete");
def.resolve(tab.linkedBrowser.contentWindow);
}, true);
return def.promise;
}
/**
* Opens the given url in a new tab, then sets up the page by waiting for
* all cookies, indexedDB items etc. to be created; Then opens the storage
* inspector and waits for the storage tree and table to be populated
*
* @param url {String} The url to be opened in the new tab
*
* @return {Promise} A promise that resolves after storage inspector is ready
*/
let openTabAndSetupStorage = Task.async(function*(url) {
/**
* This method iterates over iframes in a window and setups the indexed db
* required for this test.
*/
let setupIDBInFrames = (w, i, c) => {
if (w[i] && w[i].idbGenerator) {
w[i].setupIDB = w[i].idbGenerator(() => setupIDBInFrames(w, i + 1, c));
w[i].setupIDB.next();
}
else if (w[i] && w[i + 1]) {
setupIDBInFrames(w, i + 1, c);
}
else {
c();
}
};
let content = yield addTab(url);
let def = promise.defer();
// Setup the indexed db in main window.
gWindow = content.wrappedJSObject;
if (gWindow.idbGenerator) {
gWindow.setupIDB = gWindow.idbGenerator(() => {
setupIDBInFrames(gWindow, 0, () => {
def.resolve();
});
});
gWindow.setupIDB.next();
yield def.promise;
}
// open storage inspector
return yield openStoragePanel();
});
/**
* Open the toolbox, with the storage tool visible.
*
* @param cb {Function} Optional callback, if you don't want to use the returned
* promise
*
* @return {Promise} a promise that resolves when the storage inspector is ready
*/
let openStoragePanel = Task.async(function*(cb) {
info("Opening the storage inspector");
let target = TargetFactory.forTab(gBrowser.selectedTab);
let storage, toolbox;
// Checking if the toolbox and the storage are already loaded
// The storage-updated event should only be waited for if the storage
// isn't loaded yet
toolbox = gDevTools.getToolbox(target);
if (toolbox) {
storage = toolbox.getPanel("storage");
if (storage) {
gPanelWindow = storage.panelWindow;
gUI = storage.UI;
gToolbox = toolbox;
info("Toolbox and storage already open");
if (cb) {
return cb(storage, toolbox);
} else {
return {
toolbox: toolbox,
storage: storage
};
}
}
}
info("Opening the toolbox");
toolbox = yield gDevTools.showToolbox(target, "storage");
storage = toolbox.getPanel("storage");
gPanelWindow = storage.panelWindow;
gUI = storage.UI;
gToolbox = toolbox;
info("Waiting for the stores to update");
yield gUI.once("store-objects-updated");
yield waitForToolboxFrameFocus(toolbox);
if (cb) {
return cb(storage, toolbox);
} else {
return {
toolbox: toolbox,
storage: storage
};
}
});
/**
* Wait for the toolbox frame to receive focus after it loads
*
* @param toolbox {Toolbox}
*
* @return a promise that resolves when focus has been received
*/
function waitForToolboxFrameFocus(toolbox) {
info("Making sure that the toolbox's frame is focused");
let def = promise.defer();
let win = toolbox.frame.contentWindow;
waitForFocus(def.resolve, win);
return def.promise;
}
/**
* Forces GC, CC and Shrinking GC to get rid of disconnected docshells and
* windows.
*/
function forceCollections() {
Cu.forceGC();
Cu.forceCC();
Cu.forceShrinkingGC();
}
/**
* Cleans up and finishes the test
*/
function finishTests() {
// Cleanup so that indexed db created from this test do not interfere next ones
/**
* This method iterates over iframes in a window and clears the indexed db
* created by this test.
*/
let clearIDB = (w, i, c) => {
if (w[i] && w[i].clear) {
w[i].clearIterator = w[i].clear(() => clearIDB(w, i + 1, c));
w[i].clearIterator.next();
}
else if (w[i] && w[i + 1]) {
clearIDB(w, i + 1, c);
}
else {
c();
}
};
gWindow.clearIterator = gWindow.clear(() => {
clearIDB(gWindow, 0, () => {
// Forcing GC/CC to get rid of docshells and windows created by this test.
forceCollections();
finish();
});
});
gWindow.clearIterator.next();
}
// Sends a click event on the passed DOM node in an async manner
function click(node) {
node.scrollIntoView()
executeSoon(() => EventUtils.synthesizeMouseAtCenter(node, {}, gPanelWindow));
}
/**
* Recursively expand the variables view up to a given property.
*
* @param aOptions
* Options for view expansion:
* - rootVariable: start from the given scope/variable/property.
* - expandTo: string made up of property names you want to expand.
* For example: "body.firstChild.nextSibling" given |rootVariable:
* document|.
* @return object
* A promise that is resolved only when the last property in |expandTo|
* is found, and rejected otherwise. Resolution reason is always the
* last property - |nextSibling| in the example above. Rejection is
* always the last property that was found.
*/
function variablesViewExpandTo(aOptions) {
let root = aOptions.rootVariable;
let expandTo = aOptions.expandTo.split(".");
let lastDeferred = promise.defer();
function getNext(aProp) {
let name = expandTo.shift();
let newProp = aProp.get(name);
if (expandTo.length > 0) {
ok(newProp, "found property " + name);
if (newProp && newProp.expand) {
newProp.expand();
getNext(newProp);
}
else {
lastDeferred.reject(aProp);
}
}
else {
if (newProp) {
lastDeferred.resolve(newProp);
}
else {
lastDeferred.reject(aProp);
}
}
}
function fetchError(aProp) {
lastDeferred.reject(aProp);
}
if (root && root.expand) {
root.expand();
getNext(root);
}
else {
lastDeferred.resolve(root)
}
return lastDeferred.promise;
}
/**
* Find variables or properties in a VariablesView instance.
*
* @param array aRules
* The array of rules you want to match. Each rule is an object with:
* - name (string|regexp): property name to match.
* - value (string|regexp): property value to match.
* - dontMatch (boolean): make sure the rule doesn't match any property.
* @param boolean aParsed
* true if we want to test the rules in the parse value section of the
* storage sidebar
* @return object
* A promise object that is resolved when all the rules complete
* matching. The resolved callback is given an array of all the rules
* you wanted to check. Each rule has a new property: |matchedProp|
* which holds a reference to the Property object instance from the
* VariablesView. If the rule did not match, then |matchedProp| is
* undefined.
*/
function findVariableViewProperties(aRules, aParsed) {
// Initialize the search.
function init() {
// If aParsed is true, we are checking rules in the parsed value section of
// the storage sidebar. That scope uses a blank variable as a placeholder
// Thus, adding a blank parent to each name
if (aParsed) {
aRules = aRules.map(({name, value, dontMatch}) => {
return {name: "." + name, value, dontMatch}
});
}
// Separate out the rules that require expanding properties throughout the
// view.
let expandRules = [];
let rules = aRules.filter((aRule) => {
if (typeof aRule.name == "string" && aRule.name.indexOf(".") > -1) {
expandRules.push(aRule);
return false;
}
return true;
});
// Search through the view those rules that do not require any properties to
// be expanded. Build the array of matchers, outstanding promises to be
// resolved.
let outstanding = [];
finder(rules, gUI.view, outstanding);
// Process the rules that need to expand properties.
let lastStep = processExpandRules.bind(null, expandRules);
// Return the results - a promise resolved to hold the updated aRules array.
let returnResults = onAllRulesMatched.bind(null, aRules);
return promise.all(outstanding).then(lastStep).then(returnResults);
}
function onMatch(aProp, aRule, aMatched) {
if (aMatched && !aRule.matchedProp) {
aRule.matchedProp = aProp;
}
}
function finder(aRules, aView, aPromises) {
for (let scope of aView) {
for (let [id, prop] of scope) {
for (let rule of aRules) {
let matcher = matchVariablesViewProperty(prop, rule);
aPromises.push(matcher.then(onMatch.bind(null, prop, rule)));
}
}
}
}
function processExpandRules(aRules) {
let rule = aRules.shift();
if (!rule) {
return promise.resolve(null);
}
let deferred = promise.defer();
let expandOptions = {
rootVariable: gUI.view.getScopeAtIndex(aParsed ? 1: 0),
expandTo: rule.name
};
variablesViewExpandTo(expandOptions).then(function onSuccess(aProp) {
let name = rule.name;
let lastName = name.split(".").pop();
rule.name = lastName;
let matched = matchVariablesViewProperty(aProp, rule);
return matched.then(onMatch.bind(null, aProp, rule)).then(function() {
rule.name = name;
});
}, function onFailure() {
return promise.resolve(null);
}).then(processExpandRules.bind(null, aRules)).then(function() {
deferred.resolve(null);
});
return deferred.promise;
}
function onAllRulesMatched(aRules) {
for (let rule of aRules) {
let matched = rule.matchedProp;
if (matched && !rule.dontMatch) {
ok(true, "rule " + rule.name + " matched for property " + matched.name);
}
else if (matched && rule.dontMatch) {
ok(false, "rule " + rule.name + " should not match property " +
matched.name);
}
else {
ok(rule.dontMatch, "rule " + rule.name + " did not match any property");
}
}
return aRules;
}
return init();
}
/**
* Check if a given Property object from the variables view matches the given
* rule.
*
* @param object aProp
* The variable's view Property instance.
* @param object aRule
* Rules for matching the property. See findVariableViewProperties() for
* details.
* @return object
* A promise that is resolved when all the checks complete. Resolution
* result is a boolean that tells your promise callback the match
* result: true or false.
*/
function matchVariablesViewProperty(aProp, aRule) {
function resolve(aResult) {
return promise.resolve(aResult);
}
if (!aProp) {
return resolve(false);
}
if (aRule.name) {
let match = aRule.name instanceof RegExp ?
aRule.name.test(aProp.name) :
aProp.name == aRule.name;
if (!match) {
return resolve(false);
}
}
if ("value" in aRule) {
let displayValue = aProp.displayValue;
if (aProp.displayValueClassName == "token-string") {
displayValue = displayValue.substring(1, displayValue.length - 1);
}
let match = aRule.value instanceof RegExp ?
aRule.value.test(displayValue) :
displayValue == aRule.value;
if (!match) {
info("rule " + aRule.name + " did not match value, expected '" +
aRule.value + "', found '" + displayValue + "'");
return resolve(false);
}
}
return resolve(true);
}
/**
* Click selects a row in the table.
*
* @param {[String]} ids
* The array id of the item in the tree
*/
function selectTreeItem(ids) {
// Expand tree as some/all items could be collapsed leading to click on an
// incorrect tree item
gUI.tree.expandAll();
click(gPanelWindow.document.querySelector("[data-id='" + JSON.stringify(ids) +
"'] > .tree-widget-item"));
}
/**
* Click selects a row in the table.
*
* @param {String} id
* The id of the row in the table widget
*/
function selectTableItem(id) {
click(gPanelWindow.document.querySelector(".table-widget-cell[data-id='" +
id + "']"));
}

View File

@ -0,0 +1,8 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
BROWSER_CHROME_MANIFESTS += ['browser.ini']

View File

@ -0,0 +1,99 @@
<!DOCTYPE HTML>
<html>
<!--
Bug 970517 - Storage inspector front end - tests
-->
<head>
<meta charset="utf-8">
<title>Storage inspector test for correct values in the sidebar</title>
</head>
<body>
<script type="application/javascript;version=1.7">
let partialHostname = location.hostname.match(/^[^.]+(\..*)$/)[1];
let cookieExpiresTime = 2000000000000;
// Setting up some cookies to eat.
document.cookie = "c1=" + JSON.stringify([
"foo", "Bar", {
foo: "Bar"
}]) + "; expires=" + new Date(cookieExpiresTime).toGMTString() +
"; path=/browser";
document.cookie = "cs2=sessionCookie; path=/; domain=" + partialHostname;
// ... and some local storage items ..
var es6 = "for";
localStorage.setItem("ls1", JSON.stringify({
es6, the: "win", baz: [0, 2, 3, {
deep: "down",
nobody: "cares"
}]}));
localStorage.setItem("ls2", "foobar-2");
localStorage.setItem("ls3", "http://foobar.com/baz.php");
// ... and finally some session storage items too
sessionStorage.setItem("ss1", "This#is#an#array");
sessionStorage.setItem("ss2", "This~is~another~array");
sessionStorage.setItem("ss3", "this#is~an#object~foo#bar");
console.log("added cookies and stuff from main page");
function success(event) {
setupIDB.next(event);
}
window.idbGenerator = function*(callback) {
let request = indexedDB.open("idb1", 1);
request.onupgradeneeded = success;
request.onerror = function(e) {
throw new Error("error opening db connection");
};
let event = yield undefined;
let db = event.target.result;
let store1 = db.createObjectStore("obj1", { keyPath: "id" });
store1.createIndex("name", "name", { unique: false });
store1.createIndex("email", "email", { unique: true });
let store2 = db.createObjectStore("obj2", { keyPath: "id2" });
store1.add({id: 1, name: "foo", email: "foo@bar.com"}).onsuccess = success;
yield undefined;
store1.add({id: 2, name: "foo2", email: "foo2@bar.com"}).onsuccess = success;
yield undefined;
store1.add({id: 3, name: "foo2", email: "foo3@bar.com"}).onsuccess = success;
yield undefined;
store2.add({id2: 1, name: "foo", email: "foo@bar.com", extra: "baz"}).onsuccess = success;
yield undefined;
store1.transaction.oncomplete = success;
yield undefined;
db.close();
request = indexedDB.open("idb2", 1);
request.onupgradeneeded = success;
event = yield undefined;
let db2 = event.target.result;
let store3 = db2.createObjectStore("obj3", { keyPath: "id3" });
store3.createIndex("name2", "name2", { unique: true });
store3.transaction.oncomplete = success;
yield undefined;
db2.close();
console.log("added cookies and stuff from main page");
callback();
}
function successClear(event) {
clearIterator.next(event);
}
window.clear = function*(callback) {
document.cookie = "c1=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/browser";
document.cookie = "cs2=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
localStorage.clear();
sessionStorage.clear();
indexedDB.deleteDatabase("idb1").onsuccess = successClear;
yield undefined;
indexedDB.deleteDatabase("idb2").onsuccess = successClear;
yield undefined;
console.log("removed cookies and stuff from main page");
callback();
}
</script>
</body>
</html>

View File

@ -0,0 +1,94 @@
<!DOCTYPE HTML>
<html>
<!--
Bug 970517 - Storage inspector front end - tests
-->
<head>
<meta charset="utf-8">
<title>Storage inspector test for listing hosts and storages</title>
</head>
<body>
<iframe src="http://sectest1.example.org/browser/browser/devtools/storage/test/storage-unsecured-iframe.html"></iframe>
<iframe src="https://sectest1.example.org:443/browser/browser/devtools/storage/test/storage-secured-iframe.html"></iframe>
<script type="application/javascript;version=1.7">
let partialHostname = location.hostname.match(/^[^.]+(\..*)$/)[1];
let cookieExpiresTime1 = 2000000000000;
let cookieExpiresTime2 = 2000000001000;
// Setting up some cookies to eat.
document.cookie = "c1=foobar; expires=" +
new Date(cookieExpiresTime1).toGMTString() + "; path=/browser";
document.cookie = "cs2=sessionCookie; path=/; domain=" + partialHostname;
document.cookie = "c3=foobar-2; secure=true; expires=" +
new Date(cookieExpiresTime2).toGMTString() + "; path=/";
// ... and some local storage items ..
localStorage.setItem("ls1", "foobar");
localStorage.setItem("ls2", "foobar-2");
// ... and finally some session storage items too
sessionStorage.setItem("ss1", "foobar-3");
console.log("added cookies and stuff from main page");
function success(event) {
setupIDB.next(event);
}
window.idbGenerator = function*(callback) {
let request = indexedDB.open("idb1", 1);
request.onupgradeneeded = success;
request.onerror = function(e) {
throw new Error("error opening db connection");
};
let event = yield undefined;
let db = event.target.result;
let store1 = db.createObjectStore("obj1", { keyPath: "id" });
store1.createIndex("name", "name", { unique: false });
store1.createIndex("email", "email", { unique: true });
let store2 = db.createObjectStore("obj2", { keyPath: "id2" });
store1.add({id: 1, name: "foo", email: "foo@bar.com"}).onsuccess = success;
yield undefined;
store1.add({id: 2, name: "foo2", email: "foo2@bar.com"}).onsuccess = success;
yield undefined;
store1.add({id: 3, name: "foo2", email: "foo3@bar.com"}).onsuccess = success;
yield undefined;
store2.add({id2: 1, name: "foo", email: "foo@bar.com", extra: "baz"}).onsuccess = success;
yield undefined;
store1.transaction.oncomplete = success;
yield undefined;
db.close();
request = indexedDB.open("idb2", 1);
request.onupgradeneeded = success;
event = yield undefined;
let db2 = event.target.result;
let store3 = db2.createObjectStore("obj3", { keyPath: "id3" });
store3.createIndex("name2", "name2", { unique: true });
store3.transaction.oncomplete = success;
yield undefined;
db2.close();
console.log("added cookies and stuff from main page");
callback();
}
function successClear(event) {
clearIterator.next(event);
}
window.clear = function*(callback) {
document.cookie = "c1=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/browser";
document.cookie = "c3=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; secure=true";
document.cookie = "cs2=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=" + partialHostname;
localStorage.clear();
sessionStorage.clear();
indexedDB.deleteDatabase("idb1").onsuccess = successClear;
yield undefined;
indexedDB.deleteDatabase("idb2").onsuccess = successClear;
yield undefined;
console.log("removed cookies and stuff from main page");
callback();
}
</script>
</body>
</html>

View File

@ -0,0 +1,71 @@
<!DOCTYPE HTML>
<html>
<!--
Iframe for testing multiple host detetion in storage actor
-->
<head>
<meta charset="utf-8">
</head>
<body>
<script type="application/javascript;version=1.7">
document.cookie = "sc1=foobar;";
localStorage.setItem("iframe-s-ls1", "foobar");
sessionStorage.setItem("iframe-s-ss1", "foobar-2");
function success(event) {
setupIDB.next(event);
}
window.idbGenerator = function*(callback) {
let request = indexedDB.open("idb-s1", 1);
request.onupgradeneeded = success;
request.onerror = function(e) {
throw new Error("error opening db connection");
};
let event = yield undefined;
let db = event.target.result;
let store1 = db.createObjectStore("obj-s1", { keyPath: "id" });
store1.add({id: 6, name: "foo", email: "foo@bar.com"}).onsuccess = success;
yield undefined;
store1.add({id: 7, name: "foo2", email: "foo2@bar.com"}).onsuccess = success;
yield undefined;
store1.transaction.oncomplete = success;
yield undefined;
db.close();
request = indexedDB.open("idb-s2", 1);
request.onupgradeneeded = success;
event = yield undefined;
let db2 = event.target.result;
let store3 = db2.createObjectStore("obj-s2", { keyPath: "id3", autoIncrement: true });
store3.createIndex("name2", "name2", { unique: true });
store3.add({id3: 16, name2: "foo", email: "foo@bar.com"}).onsuccess = success;
yield undefined;
store3.transaction.oncomplete = success;
yield undefined;
db2.close();
console.log("added cookies and stuff from secured iframe");
callback();
}
function successClear(event) {
clearIterator.next(event);
}
window.clear = function*(callback) {
document.cookie = "sc1=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
localStorage.clear();
sessionStorage.clear();
indexedDB.deleteDatabase("idb-s1").onsuccess = successClear;
yield undefined;
indexedDB.deleteDatabase("idb-s2").onsuccess = successClear;
yield undefined;
console.log("removed cookies and stuff from secured iframe");
callback();
}
</script>
</body>
</html>

View File

@ -0,0 +1,27 @@
<!DOCTYPE HTML>
<html>
<!--
Iframe for testing multiple host detetion in storage actor
-->
<head>
<meta charset="utf-8">
</head>
<body>
<script>
document.cookie = "uc1=foobar; domain=.example.org; path=/; secure=true";
localStorage.setItem("iframe-u-ls1", "foobar");
sessionStorage.setItem("iframe-u-ss1", "foobar1");
sessionStorage.setItem("iframe-u-ss2", "foobar2");
console.log("added cookies and stuff from unsecured iframe");
window.clear = function*(callback) {
document.cookie = "uc1=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.example.org; secure=true";
localStorage.clear();
sessionStorage.clear();
console.log("removed cookies and stuff from unsecured iframe");
callback();
}
</script>
</body>
</html>

View File

@ -0,0 +1,63 @@
<!DOCTYPE HTML>
<html>
<!--
Bug 965872 - Storage inspector actor with cookies, local storage and session storage.
-->
<head>
<meta charset="utf-8">
<title>Storage inspector blank html for tests</title>
</head>
<body>
<script>
window.addCookie = function(name, value, path, domain, expires, secure) {
var cookieString = name + "=" + value + ";";
if (path) {
cookieString += "path=" + path + ";";
}
if (domain) {
cookieString += "domain=" + domain + ";";
}
if (expires) {
cookieString += "expires=" + expires + ";";
}
if (secure) {
cookieString += "secure=true;";
}
document.cookie = cookieString;
};
window.removeCookie = function(name, path) {
document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=" + path;
}
window.clear = function*(callback) {
var cookies = document.cookie;
for (var cookie of cookies.split(";")) {
removeCookie(cookie.split("=")[0]);
removeCookie(cookie.split("=")[0], "/browser");
}
localStorage.clear();
sessionStorage.clear();
callback();
}
window.onload = function() {
addCookie("c1", "1.2.3.4.5.6.7", "/browser");
addCookie("c2", "foobar", "/browser");
localStorage.setItem("ls1", "testing");
localStorage.setItem("ls2", "testing");
localStorage.setItem("ls3", "testing");
localStorage.setItem("ls4", "testing");
localStorage.setItem("ls5", "testing");
localStorage.setItem("ls6", "testing");
localStorage.setItem("ls7", "testing");
sessionStorage.setItem("ss1", "foobar");
sessionStorage.setItem("ss2", "foobar");
sessionStorage.setItem("ss3", "foobar");
}
</script>
</body>
</html>

View File

@ -0,0 +1,565 @@
/* vim:set ts=2 sw=2 sts=2 et: */
/* 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 {Cu} = require("chrome");
const STORAGE_STRINGS = "chrome://browser/locale/devtools/storage.properties";
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyGetter(this, "TreeWidget",
() => require("devtools/shared/widgets/TreeWidget").TreeWidget);
XPCOMUtils.defineLazyGetter(this, "TableWidget",
() => require("devtools/shared/widgets/TableWidget").TableWidget);
XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
"resource://gre/modules/devtools/event-emitter.js");
XPCOMUtils.defineLazyModuleGetter(this, "ViewHelpers",
"resource:///modules/devtools/ViewHelpers.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "VariablesView",
"resource:///modules/devtools/VariablesView.jsm");
/**
* Localization convenience methods.
*/
let L10N = new ViewHelpers.L10N(STORAGE_STRINGS);
const GENERIC_VARIABLES_VIEW_SETTINGS = {
lazyEmpty: true,
lazyEmptyDelay: 10, // ms
searchEnabled: true,
searchPlaceholder: L10N.getStr("storage.search.placeholder"),
preventDescriptorModifiers: true
};
// Columns which are hidden by default in the storage table
const HIDDEN_COLUMNS = [
"creationTime",
"isDomain",
"isSecure"
];
/**
* StorageUI is controls and builds the UI of the Storage Inspector.
*
* @param {Front} front
* Front for the storage actor
* @param {Target} target
* Interface for the page we're debugging
* @param {Window} panelWin
* Window of the toolbox panel to populate UI in.
*/
this.StorageUI = function StorageUI(front, target, panelWin) {
EventEmitter.decorate(this);
this._target = target;
this._window = panelWin;
this._panelDoc = panelWin.document;
this.front = front;
let treeNode = this._panelDoc.getElementById("storage-tree");
this.tree = new TreeWidget(treeNode, {defaultType: "dir"});
this.onHostSelect = this.onHostSelect.bind(this);
this.tree.on("select", this.onHostSelect);
let tableNode = this._panelDoc.getElementById("storage-table");
this.table = new TableWidget(tableNode, {
emptyText: L10N.getStr("table.emptyText"),
highlightUpdated: true,
});
this.displayObjectSidebar = this.displayObjectSidebar.bind(this);
this.table.on(TableWidget.EVENTS.ROW_SELECTED, this.displayObjectSidebar)
this.sidebar = this._panelDoc.getElementById("storage-sidebar");
this.sidebar.setAttribute("width", "300");
this.view = new VariablesView(this.sidebar.firstChild,
GENERIC_VARIABLES_VIEW_SETTINGS);
this.front.listStores().then(storageTypes => {
this.populateStorageTree(storageTypes);
});
this.onUpdate = this.onUpdate.bind(this);
this.front.on("stores-update", this.onUpdate);
this.handleKeypress = this.handleKeypress.bind(this);
this._panelDoc.addEventListener("keypress", this.handleKeypress);
}
exports.StorageUI = StorageUI;
StorageUI.prototype = {
storageTypes: null,
shouldResetColumns: true,
destroy: function() {
this.front.off("stores-update", this.onUpdate);
this._panelDoc.removeEventListener("keypress", this.handleKeypress)
},
/**
* Empties and hides the object viewer sidebar
*/
hideSidebar: function() {
this.view.empty();
this.sidebar.hidden = true;
this.table.clearSelection();
},
/**
* Removes the given item from the storage table. Reselects the next item in
* the table and repopulates the sidebar with that item's data if the item
* being removed was selected.
*/
removeItemFromTable: function(name) {
if (this.table.isSelected(name)) {
if (this.table.selectedIndex == 0) {
this.table.selectNextRow()
}
else {
this.table.selectPreviousRow();
}
this.table.remove(name);
this.displayObjectSidebar();
}
else {
this.table.remove(name);
}
},
/**
* Event handler for "stores-update" event coming from the storage actor.
*
* @param {object} argument0
* An object containing the details of the added, changed and deleted
* storage objects.
* Each of these 3 objects are of the following format:
* {
* <store_type1>: {
* <host1>: [<store_names1>, <store_name2>...],
* <host2>: [<store_names34>...], ...
* },
* <store_type2>: {
* <host1>: [<store_names1>, <store_name2>...],
* <host2>: [<store_names34>...], ...
* }, ...
* }
* Where store_type1 and store_type2 is one of cookies, indexedDB,
* sessionStorage and localStorage; host1, host2 are the host in which
* this change happened; and [<store_namesX] is an array of the names
* of the changed store objects. This array is empty for deleted object
* if the host was completely removed.
*/
onUpdate: function({ changed, added, deleted }) {
if (deleted) {
for (let type in deleted) {
for (let host in deleted[type]) {
if (!deleted[type][host].length) {
// This means that the whole host is deleted, thus the item should
// be removed from the storage tree
if (this.tree.isSelected([type, host])) {
this.table.clear();
this.hideSidebar();
this.tree.selectPreviousItem();
}
this.tree.remove([type, host]);
}
else if (this.tree.isSelected([type, host])) {
for (let name of deleted[type][host]) {
try {
// trying to parse names in case its for indexedDB
let names = JSON.parse(name);
if (!names[2]) {
if (this.tree.isSelected([type, host, names[0], names[1]])) {
this.tree.selectPreviousItem();
this.tree.remove([type, host, names[0], names[1]]);
this.table.clear();
this.hideSidebar();
}
}
else if (this.tree.isSelected([type, host, names[0], names[1]])) {
this.removeItemFromTable(names[2]);
}
}
catch (ex) {
this.removeItemFromTable(name);
}
}
}
}
}
}
if (added) {
for (let type in added) {
for (let host in added[type]) {
this.tree.add([type, {id: host, type: "url"}]);
for (let name of added[type][host]) {
try {
name = JSON.parse(name);
if (name.length == 3) {
name.splice(2, 1);
}
this.tree.add([type, host, ...name]);
if (!this.tree.selectedItem) {
this.tree.selectedItem = [type, host, name[0], name[1]];
this.fetchStorageObjects(type, host, [JSON.stringify(name)], 1);
}
} catch(ex) {}
}
if (this.tree.isSelected([type, host])) {
this.fetchStorageObjects(type, host, added[type][host], 1);
}
}
}
}
if (changed) {
let [type, host, db, objectStore] = this.tree.selectedItem;
if (changed[type] && changed[type][host]) {
if (changed[type][host].length) {
try {
let toUpdate = [];
for (let name of changed[type][host]) {
let names = JSON.parse(name);
if (names[0] == db && names[1] == objectStore && names[2]) {
toUpdate.push(name);
}
}
this.fetchStorageObjects(type, host, toUpdate, 2);
}
catch (ex) {
this.fetchStorageObjects(type, host, changed[type][host], 2);
}
}
}
}
if (added || deleted || changed) {
this.emit("store-objects-updated");
}
},
/**
* Fetches the storage objects from the storage actor and populates the
* storage table with the returned data.
*
* @param {string} type
* The type of storage. Ex. "cookies"
* @param {string} host
* Hostname
* @param {array} names
* Names of particular store objects. Empty if all are requested
* @param {number} reason
* 2 for update, 1 for new row in an existing table and 0 when
* populating a table for the first time for the given host/type
*/
fetchStorageObjects: function(type, host, names, reason) {
this.storageTypes[type].getStoreObjects(host, names).then(({data}) => {
if (!data.length) {
this.emit("store-objects-updated");
return;
}
if (this.shouldResetColumns) {
this.resetColumns(data[0], type);
}
this.populateTable(data, reason);
this.emit("store-objects-updated");
});
},
/**
* Populates the storage tree which displays the list of storages present for
* the page.
*
* @param {object} storageTypes
* List of storages and their corresponding hosts returned by the
* StorageFront.listStores call.
*/
populateStorageTree: function(storageTypes) {
this.storageTypes = {};
for (let type in storageTypes) {
let typeLabel = L10N.getStr("tree.labels." + type);
this.tree.add([{id: type, label: typeLabel, type: "store"}]);
if (storageTypes[type].hosts) {
this.storageTypes[type] = storageTypes[type];
for (let host in storageTypes[type].hosts) {
this.tree.add([type, {id: host, type: "url"}]);
for (let name of storageTypes[type].hosts[host]) {
try {
let names = JSON.parse(name);
this.tree.add([type, host, ...names]);
if (!this.tree.selectedItem) {
this.tree.selectedItem = [type, host, names[0], names[1]];
this.fetchStorageObjects(type, host, [name], 0);
}
} catch(ex) {}
}
if (!this.tree.selectedItem) {
this.tree.selectedItem = [type, host];
this.fetchStorageObjects(type, host, null, 0);
}
}
}
}
},
/**
* Populates the selected entry from teh table in the sidebar for a more
* detailed view.
*/
displayObjectSidebar: function() {
let item = this.table.selectedRow;
if (!item) {
// Make sure that sidebar is hidden and return
this.sidebar.hidden = true;
return;
}
this.sidebar.hidden = false;
this.view.empty();
let mainScope = this.view.addScope(L10N.getStr("storage.data.label"));
mainScope.expanded = true;
if (item.name && item.valueActor) {
let itemVar = mainScope.addItem(item.name + "", {}, true);
item.valueActor.string().then(value => {
// The main area where the value will be displayed
itemVar.setGrip(value);
// May be the item value is a json or a key value pair itself
this.parseItemValue(item.name, value);
// By default the item name and value are shown. If this is the only
// information available, then nothing else is to be displayed.
let itemProps = Object.keys(item);
if (itemProps.length == 3) {
this.emit("sidebar-updated");
return;
}
// Display any other information other than the item name and value
// which may be available.
let rawObject = Object.create(null);
let otherProps =
itemProps.filter(e => e != "name" && e != "value" && e != "valueActor");
for (let prop of otherProps) {
rawObject[prop] = item[prop];
}
itemVar.populate(rawObject, {sorted: true});
itemVar.twisty = true;
itemVar.expanded = true;
this.emit("sidebar-updated");
});
return;
}
// Case when displaying IndexedDB db/object store properties.
for (let key in item) {
mainScope.addItem(key, {}, true).setGrip(item[key]);
this.parseItemValue(key, item[key]);
}
this.emit("sidebar-updated");
},
/**
* Tries to parse a string value into either a json or a key-value separated
* object and populates the sidebar with the parsed value. The value can also
* be a key separated array.
*
* @param {string} name
* The key corresponding to the `value` string in the object
* @param {string} value
* The string to be parsed into an object
*/
parseItemValue: function(name, value) {
let json = null
try {
json = JSON.parse(value);
}
catch (ex) {
json = null;
}
if (!json && value) {
json = this._extractKeyValPairs(value);
}
// return if json is null, or same as value, or just a string.
if (!json || json == value || typeof json == "string") {
return;
}
// One special case is a url which gets separated as key value pair on :
if ((json.length == 2 || Object.keys(json).length == 1) &&
((json[0] || Object.keys(json)[0]) + "").match(/^(http|file|ftp)/)) {
return;
}
let jsonObject = Object.create(null);
jsonObject[name] = json;
let valueScope = this.view.getScopeAtIndex(1) ||
this.view.addScope(L10N.getStr("storage.parsedValue.label"));
valueScope.expanded = true;
let jsonVar = valueScope.addItem("", Object.create(null), true);
jsonVar.expanded = true;
jsonVar.twisty = true;
jsonVar.populate(jsonObject, {expanded: true});
},
/**
* Tries to parse a string into an object on the basis of key-value pairs,
* separated by various separators. If failed, tries to parse for single
* separator separated values to form an array.
*
* @param {string} value
* The string to be parsed into an object or array
*/
_extractKeyValPairs: function(value) {
let makeObject = (keySep, pairSep) => {
let object = {};
for (let pair of value.split(pairSep)) {
let [key, val] = pair.split(keySep);
object[key] = val;
}
return object;
};
// Possible separators.
const separators = ["=", ":", "~", "#", "&", "\\*", ",", "\\."];
// Testing for object
for (let i = 0; i < separators.length; i++) {
let kv = separators[i];
for (let j = 0; j < separators.length; j++) {
if (i == j) {
continue;
}
let p = separators[j];
let regex = new RegExp("^([^" + kv + p + "]*" + kv + "+[^" + kv + p +
"]*" + p + "*)+$", "g");
if (value.match(regex) && value.contains(kv) &&
(value.contains(p) || value.split(kv).length == 2)) {
return makeObject(kv, p);
}
}
}
// Testing for array
for (let i = 0; i < separators.length; i++) {
let p = separators[i];
let regex = new RegExp("^[^" + p + "]+(" + p + "+[^" + p + "]*)+$", "g");
if (value.match(regex)) {
return value.split(p.replace(/\\*/g, ""));
}
}
return null;
},
/**
* Select handler for the storage tree. Fetches details of the selected item
* from the storage details and populates the storage tree.
*
* @param {string} event
* The name of the event fired
* @param {array} item
* An array of ids which represent the location of the selected item in
* the storage tree
*/
onHostSelect: function(event, item) {
this.table.clear();
this.hideSidebar();
let [type, host] = item;
let names = null;
if (!host) {
return;
}
if (item.length > 2) {
names = [JSON.stringify(item.slice(2))];
}
this.shouldResetColumns = true;
this.fetchStorageObjects(type, host, names, 0);
},
/**
* Resets the column headers in the storage table with the pased object `data`
*
* @param {object} data
* The object from which key and values will be used for naming the
* headers of the columns
* @param {string} type
* The type of storage corresponding to the after-reset columns in the
* table.
*/
resetColumns: function(data, type) {
let columns = {};
let uniqueKey = null;
for (let key in data) {
if (!uniqueKey) {
this.table.uniqueId = uniqueKey = key;
}
columns[key] = L10N.getStr("table.headers." + type + "." + key);
}
this.table.setColumns(columns, null, HIDDEN_COLUMNS);
this.shouldResetColumns = false;
this.hideSidebar();
},
/**
* Populates or updates the rows in the storage table.
*
* @param {array[object]} data
* Array of objects to be populated in the storage table
* @param {number} reason
* The reason of this populateTable call. 2 for update, 1 for new row
* in an existing table and 0 when populating a table for the first
* time for the given host/type
*/
populateTable: function(data, reason) {
for (let item of data) {
if (item.value) {
item.valueActor = item.value;
item.value = item.value.initial || "";
}
if (item.expires != null) {
item.expires = item.expires
? new Date(item.expires).toLocaleString()
: L10N.getStr("label.expires.session");
}
if (item.creationTime != null) {
item.creationTime = new Date(item.creationTime).toLocaleString();
}
if (item.lastAccessed != null) {
item.lastAccessed = new Date(item.lastAccessed).toLocaleString();
}
if (reason < 2) {
this.table.push(item, reason == 0);
}
else {
this.table.update(item);
if (item == this.table.selectedRow && !this.sidebar.hidden) {
this.displayObjectSidebar();
}
}
}
},
/**
* Handles keypress event on the body table to close the sidebar when open
*
* @param {DOMEvent} event
* The event passed by the keypress event.
*/
handleKeypress: function(event) {
if (event.keyCode == event.DOM_VK_ESCAPE && !this.sidebar.hidden) {
// Stop Propagation to prevent opening up of split console
this.hideSidebar();
event.stopPropagation();
event.preventDefault();
}
}
}

View File

@ -0,0 +1,100 @@
# 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/.
# LOCALIZATION NOTE These strings are used inside the Storage Editor tool.
# LOCALIZATION NOTE The correct localization of this file might be to keep it
# in English, or another language commonly spoken among web developers.
# You want to make that choice consistent across the developer tools.
# A good criteria is the language in which you'd find the best documentation
# on web development on the web.
# LOCALIZATION NOTE (chromeWindowTitle): This is the title of the Storage
# 'chrome' window. That is, the main window with all the cookies, etc.
# The argument is either the content document's title or its href if no title
# is available.
chromeWindowTitle=Storage [%S]
# LOCALIZATION NOTE (open.commandkey): This the key to use in
# conjunction with shift to open the storage editor
open.commandkey=VK_F9
# LOCALIZATION NOTE (open.accesskey): The access key used to open the storage
# editor.
open.accesskey=d
# LOCALIZATION NOTE (storage.label):
# This string is displayed in the title of the tab when the storage editor is
# displayed inside the developer tools window and in the Developer Tools Menu.
storage.label=Storage
# LOCALIZATION NOTE (storage.tooltip):
# This string is displayed in the tooltip of the tab when the storage editor is
# displayed inside the developer tools window.
storage.tooltip=Storage Inspector (Cookies, Local Storage ...)
# LOCALIZATION NOTE (tree.emptyText):
# This string is displayed when the Storage Tree is empty. This can happen when
# there are no websites on the current page (about:blank)
tree.emptyText=No hosts on the page
# LOCALIZATION NOTE (table.emptyText):
# This string is displayed when there are no rows in the Storage Table for the
# selected host.
table.emptyText=No data present for selected host
# LOCALIZATION NOTE (tree.labels.*):
# These strings are the labels for Storage type groups present in the Storage
# Tree, like cookies, local storage etc.
tree.labels.cookies=Cookies
tree.labels.localStorage=Local Storage
tree.labels.sessionStorage=Session Storage
tree.labels.indexedDB=Indexed DB
# LOCALIZATION NOTE (table.headers.*.*):
# These strings are the header names of the columns in the Storage Table for
# each type of storage available through the Storage Tree to the side.
table.headers.cookies.name=Name
table.headers.cookies.path=Path
table.headers.cookies.host=Domain
table.headers.cookies.expires=Expires on
table.headers.cookies.value=Value
table.headers.cookies.lastAccessed:Last accessed on
table.headers.cookies.creationTime:Created on
table.headers.cookies.isHttpOnly:isHttpOnly
table.headers.cookies.isSecure:isSecure
table.headers.cookies.isDomain:isDomain
table.headers.localStorage.name=Key
table.headers.localStorage.value=Value
table.headers.sessionStorage.name=Key
table.headers.sessionStorage.value=Value
table.headers.indexedDB.name=Key
table.headers.indexedDB.db=Database Name
table.headers.indexedDB.objectStore=Object Store Name
table.headers.indexedDB.value=Value
table.headers.indexedDB.origin=Origin
table.headers.indexedDB.version=Version
table.headers.indexedDB.objectStores=Object Stores
table.headers.indexedDB.keyPath=Key
table.headers.indexedDB.autoIncrement=Auto Increment
table.headers.indexedDB.indexes=Indexes
# LOCALIZATION NOTE (label.expires.session):
# This string is displayed in the expires column when the cookie is Session
# Cookie
label.expires.session=Session
# LOCALIZATION NOTE (storage.search.placeholder):
# This is the placeholder text in the sidebar search box
storage.search.placeholder=Filter values
# LOCALIZATION NOTE (storage.data.label):
# This is the heading displayed over the item value in the sidebar
storage.data.label=Data
# LOCALIZATION NOTE (storage.parsedValue.label):
# This is the heading displayed over the item parsed value in the sidebar
storage.parsedValue.label=Parsed Value

View File

@ -55,11 +55,11 @@ feedback_category_other=Other:
feedback_custom_category_text_placeholder=What went wrong?
feedback_submit_button=Submit
feedback_back_button=Back
## LOCALIZATION NOTE (feedback_window_will_close_in):
## LOCALIZATION NOTE (feedback_window_will_close_in2):
## Semicolon-separated list of plural forms. See:
## http://developer.mozilla.org/en/docs/Localization_and_Plurals
## In this item, don't translate the part between {{..}}
feedback_window_will_close_in=This window will close in {{countdown}} second;This window will close in {{countdown}} seconds
feedback_window_will_close_in2=This window will close in {{countdown}} second;This window will close in {{countdown}} seconds
share_email_subject2=Invitation to chat
## LOCALIZATION NOTE (share_email_body2): In this item, don't translate the

View File

@ -43,6 +43,7 @@
locale/browser/devtools/tilt.properties (%chrome/browser/devtools/tilt.properties)
locale/browser/devtools/scratchpad.properties (%chrome/browser/devtools/scratchpad.properties)
locale/browser/devtools/scratchpad.dtd (%chrome/browser/devtools/scratchpad.dtd)
locale/browser/devtools/storage.properties (%chrome/browser/devtools/storage.properties)
locale/browser/devtools/styleeditor.properties (%chrome/browser/devtools/styleeditor.properties)
locale/browser/devtools/styleeditor.dtd (%chrome/browser/devtools/styleeditor.dtd)
locale/browser/devtools/styleinspector.dtd (%chrome/browser/devtools/styleinspector.dtd)

View File

@ -203,6 +203,7 @@ browser.jar:
skin/classic/browser/devtools/filetype-dir-close.svg (../shared/devtools/images/filetypes/dir-close.svg)
skin/classic/browser/devtools/filetype-dir-open.svg (../shared/devtools/images/filetypes/dir-open.svg)
skin/classic/browser/devtools/filetype-globe.svg (../shared/devtools/images/filetypes/globe.svg)
skin/classic/browser/devtools/filetype-store.svg (../shared/devtools/images/filetypes/store.svg)
skin/classic/browser/devtools/commandline-icon.png (../shared/devtools/images/commandline-icon.png)
skin/classic/browser/devtools/commandline-icon@2x.png (../shared/devtools/images/commandline-icon@2x.png)
skin/classic/browser/devtools/command-paintflashing.png (../shared/devtools/images/command-paintflashing.png)
@ -248,6 +249,7 @@ browser.jar:
* skin/classic/browser/devtools/shadereditor.css (devtools/shadereditor.css)
* skin/classic/browser/devtools/splitview.css (../shared/devtools/splitview.css)
skin/classic/browser/devtools/styleeditor.css (../shared/devtools/styleeditor.css)
skin/classic/browser/devtools/storage.css (../shared/devtools/storage.css)
* skin/classic/browser/devtools/webaudioeditor.css (devtools/webaudioeditor.css)
skin/classic/browser/devtools/magnifying-glass.png (../shared/devtools/images/magnifying-glass.png)
skin/classic/browser/devtools/magnifying-glass@2x.png (../shared/devtools/images/magnifying-glass@2x.png)
@ -310,6 +312,7 @@ browser.jar:
skin/classic/browser/devtools/tool-inspector.svg (../shared/devtools/images/tool-inspector.svg)
skin/classic/browser/devtools/tool-inspector.svg (../shared/devtools/images/tool-inspector.svg)
skin/classic/browser/devtools/tool-styleeditor.svg (../shared/devtools/images/tool-styleeditor.svg)
skin/classic/browser/devtools/tool-storage.svg (../shared/devtools/images/tool-storage.svg)
skin/classic/browser/devtools/tool-profiler.svg (../shared/devtools/images/tool-profiler.svg)
skin/classic/browser/devtools/tool-network.svg (../shared/devtools/images/tool-network.svg)
skin/classic/browser/devtools/tool-scratchpad.svg (../shared/devtools/images/tool-scratchpad.svg)

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -145,6 +145,10 @@ browser.jar:
skin/classic/browser/loop/toolbar@2x.png (loop/toolbar@2x.png)
skin/classic/browser/loop/toolbar-inverted.png (loop/toolbar-inverted.png)
skin/classic/browser/loop/toolbar-inverted@2x.png (loop/toolbar-inverted@2x.png)
skin/classic/browser/yosemite/loop/menuPanel.png (loop/menuPanel-yosemite.png)
skin/classic/browser/yosemite/loop/menuPanel@2x.png (loop/menuPanel-yosemite@2x.png)
skin/classic/browser/yosemite/loop/toolbar.png (loop/toolbar-yosemite.png)
skin/classic/browser/yosemite/loop/toolbar@2x.png (loop/toolbar-yosemite@2x.png)
skin/classic/browser/customizableui/background-noise-toolbar.png (customizableui/background-noise-toolbar.png)
skin/classic/browser/customizableui/customize-titleBar-toggle.png (customizableui/customize-titleBar-toggle.png)
skin/classic/browser/customizableui/customize-titleBar-toggle@2x.png (customizableui/customize-titleBar-toggle@2x.png)
@ -326,6 +330,7 @@ browser.jar:
skin/classic/browser/devtools/filetype-dir-close.svg (../shared/devtools/images/filetypes/dir-close.svg)
skin/classic/browser/devtools/filetype-dir-open.svg (../shared/devtools/images/filetypes/dir-open.svg)
skin/classic/browser/devtools/filetype-globe.svg (../shared/devtools/images/filetypes/globe.svg)
skin/classic/browser/devtools/filetype-store.svg (../shared/devtools/images/filetypes/store.svg)
skin/classic/browser/devtools/commandline-icon.png (../shared/devtools/images/commandline-icon.png)
skin/classic/browser/devtools/commandline-icon@2x.png (../shared/devtools/images/commandline-icon@2x.png)
skin/classic/browser/devtools/command-paintflashing.png (../shared/devtools/images/command-paintflashing.png)
@ -372,6 +377,7 @@ browser.jar:
* skin/classic/browser/devtools/shadereditor.css (devtools/shadereditor.css)
* skin/classic/browser/devtools/splitview.css (../shared/devtools/splitview.css)
skin/classic/browser/devtools/styleeditor.css (../shared/devtools/styleeditor.css)
skin/classic/browser/devtools/storage.css (../shared/devtools/storage.css)
* skin/classic/browser/devtools/webaudioeditor.css (devtools/webaudioeditor.css)
skin/classic/browser/devtools/magnifying-glass.png (../shared/devtools/images/magnifying-glass.png)
skin/classic/browser/devtools/magnifying-glass@2x.png (../shared/devtools/images/magnifying-glass@2x.png)
@ -434,6 +440,7 @@ browser.jar:
skin/classic/browser/devtools/tool-inspector.svg (../shared/devtools/images/tool-inspector.svg)
skin/classic/browser/devtools/tool-inspector.svg (../shared/devtools/images/tool-inspector.svg)
skin/classic/browser/devtools/tool-styleeditor.svg (../shared/devtools/images/tool-styleeditor.svg)
skin/classic/browser/devtools/tool-storage.svg (../shared/devtools/images/tool-storage.svg)
skin/classic/browser/devtools/tool-profiler.svg (../shared/devtools/images/tool-profiler.svg)
skin/classic/browser/devtools/tool-network.svg (../shared/devtools/images/tool-network.svg)
skin/classic/browser/devtools/tool-scratchpad.svg (../shared/devtools/images/tool-scratchpad.svg)
@ -503,6 +510,22 @@ browser.jar:
skin/classic/browser/tabbrowser/alltabs-box-bkgnd-icon@2x.png (tabbrowser/alltabs-box-bkgnd-icon-lion@2x.png)
skin/classic/browser/lion/tabview/tabview.png (tabview/tabview-lion.png)
skin/classic/browser/lion/places/toolbar.png (places/toolbar-lion.png)
skin/classic/browser/yosemite/Toolbar.png (Toolbar-yosemite.png)
skin/classic/browser/yosemite/Toolbar@2x.png (Toolbar-yosemite@2x.png)
skin/classic/browser/yosemite/menuPanel.png (menuPanel-yosemite.png)
skin/classic/browser/yosemite/menuPanel@2x.png (menuPanel-yosemite@2x.png)
skin/classic/browser/yosemite/menuPanel-customize.png (menuPanel-customize-yosemite.png)
skin/classic/browser/yosemite/menuPanel-customize@2x.png (menuPanel-customize-yosemite@2x.png)
skin/classic/browser/yosemite/menuPanel-exit.png (menuPanel-exit-yosemite.png)
skin/classic/browser/yosemite/menuPanel-exit@2x.png (menuPanel-exit-yosemite@2x.png)
skin/classic/browser/yosemite/menuPanel-help.png (menuPanel-help-yosemite.png)
skin/classic/browser/yosemite/menuPanel-help@2x.png (menuPanel-help-yosemite@2x.png)
skin/classic/browser/yosemite/menuPanel-small.png (menuPanel-small-yosemite.png)
skin/classic/browser/yosemite/menuPanel-small@2x.png (menuPanel-small-yosemite@2x.png)
skin/classic/browser/yosemite/reload-stop-go.png (reload-stop-go-yosemite.png)
skin/classic/browser/yosemite/reload-stop-go@2x.png (reload-stop-go-yosemite@2x.png)
skin/classic/browser/yosemite/sync-horizontalbar.png (sync-horizontalbar-yosemite.png)
skin/classic/browser/yosemite/sync-horizontalbar@2x.png (sync-horizontalbar-yosemite@2x.png)
skin/classic/browser/notification-pluginNormal.png (../shared/plugins/notification-pluginNormal.png)
skin/classic/browser/notification-pluginAlert.png (../shared/plugins/notification-pluginAlert.png)
skin/classic/browser/notification-pluginBlocked.png (../shared/plugins/notification-pluginBlocked.png)
@ -522,3 +545,23 @@ browser.jar:
% override chrome://browser/skin/tabbrowser/alltabs-box-bkgnd-icon.png chrome://browser/skin/lion/tabbrowser/alltabs-box-bkgnd-icon.png os=Darwin osversion>=10.7
% override chrome://browser/skin/tabview/tabview.png chrome://browser/skin/lion/tabview/tabview.png os=Darwin osversion>=10.7
% override chrome://browser/skin/places/toolbar.png chrome://browser/skin/lion/places/toolbar.png os=Darwin osversion>=10.7
% override chrome://browser/skin/Toolbar.png chrome://browser/skin/yosemite/Toolbar.png os=Darwin osversion>=10.10
% override chrome://browser/skin/Toolbar@2x.png chrome://browser/skin/yosemite/Toolbar@2x.png os=Darwin osversion>=10.10
% override chrome://browser/skin/menuPanel.png chrome://browser/skin/yosemite/menuPanel.png os=Darwin osversion>=10.10
% override chrome://browser/skin/menuPanel@2x.png chrome://browser/skin/yosemite/menuPanel@2x.png os=Darwin osversion>=10.10
% override chrome://browser/skin/loop/menuPanel.png chrome://browser/skin/yosemite/loop/menuPanel.png os=Darwin osversion>=10.10
% override chrome://browser/skin/loop/menuPanel@2x.png chrome://browser/skin/yosemite/loop/menuPanel@2x.png os=Darwin osversion>=10.10
% override chrome://browser/skin/loop/toolbar.png chrome://browser/skin/yosemite/loop/toolbar.png os=Darwin osversion>=10.10
% override chrome://browser/skin/loop/toolbar@2x.png chrome://browser/skin/yosemite/loop/toolbar@2x.png os=Darwin osversion>=10.10
% override chrome://browser/skin/menuPanel-customize.png chrome://browser/skin/yosemite/menuPanel-customize.png os=Darwin osversion>=10.10
% override chrome://browser/skin/menuPanel-customize@2x.png chrome://browser/skin/yosemite/menuPanel-customize@2x.png os=Darwin osversion>=10.10
% override chrome://browser/skin/menuPanel-exit.png chrome://browser/skin/yosemite/menuPanel-exit.png os=Darwin osversion>=10.10
% override chrome://browser/skin/menuPanel-exit@2x.png chrome://browser/skin/yosemite/menuPanel-exit@2x.png os=Darwin osversion>=10.10
% override chrome://browser/skin/menuPanel-help.png chrome://browser/skin/yosemite/menuPanel-help.png os=Darwin osversion>=10.10
% override chrome://browser/skin/menuPanel-help@2x.png chrome://browser/skin/yosemite/menuPanel-help@2x.png os=Darwin osversion>=10.10
% override chrome://browser/skin/menuPanel-small.png chrome://browser/skin/yosemite/menuPanel-small.png os=Darwin osversion>=10.10
% override chrome://browser/skin/menuPanel-small@2x.png chrome://browser/skin/yosemite/menuPanel-small@2x.png os=Darwin osversion>=10.10
% override chrome://browser/skin/reload-stop-go.png chrome://browser/skin/yosemite/reload-stop-go.png os=Darwin osversion>=10.10
% override chrome://browser/skin/reload-stop-go@2x.png chrome://browser/skin/yosemite/reload-stop-go@2x.png os=Darwin osversion>=10.10
% override chrome://browser/skin/sync-horizontalbar.png chrome://browser/skin/yosemite/sync-horizontalbar.png os=Darwin osversion>=10.10
% override chrome://browser/skin/sync-horizontalbar@2x.png chrome://browser/skin/yosemite/sync-horizontalbar@2x.png os=Darwin osversion>=10.10

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 939 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 923 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 B

View File

@ -0,0 +1,7 @@
<svg width="16" xmlns="http://www.w3.org/2000/svg" height="16" viewBox="0 0 16 16" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 16 16">
<g>
<path d="m1.3,12.5v-2.4c0,0 0,2.5 6.7,2.5 6.7,0 6.7-2.5 6.7-2.5v2.4c0,0 0,2.7-6.8,2.7-6.6,0-6.6-2.7-6.6-2.7z"/>
<path d="m14.7,3.4c0-1.4-3-2.5-6.7-2.5s-6.7,1.1-6.7,2.5c0,.2 0,.3 .1,.5-.1-.3-.1-.4-.1-.4v1.5c0,0 0,2.7 6.7,2.7 6.7,0 6.8-2.7 6.8-2.7v-1.6c0,.1 0,.2-.1,.5-0-.2-0-.3-0-.5z"/>
<path d="m1.3,8.7v-2.4c0,0 0,2.5 6.7,2.5 6.7,0 6.7-2.5 6.7-2.5v2.4c0,0 0,2.7-6.8,2.7-6.6-0-6.6-2.7-6.6-2.7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 574 B

View File

@ -0,0 +1,7 @@
<svg width="16" xmlns="http://www.w3.org/2000/svg" height="16" viewBox="0 0 16 16" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 16 16">
<g fill="#edf0f1">
<path d="m1.3,12.5v-2.4c0,0 0,2.5 6.7,2.5 6.7,0 6.7-2.5 6.7-2.5v2.4c0,0 0,2.7-6.8,2.7-6.6,0-6.6-2.7-6.6-2.7z"/>
<path d="m14.7,3.4c0-1.4-3-2.5-6.7-2.5s-6.7,1.1-6.7,2.5c0,.2 0,.3 .1,.5-.1-.3-.1-.4-.1-.4v1.5c0,0 0,2.7 6.7,2.7 6.7,0 6.8-2.7 6.8-2.7v-1.6c0,.1 0,.2-.1,.5-0-.2-0-.3-0-.5z"/>
<path d="m1.3,8.7v-2.4c0,0 0,2.5 6.7,2.5 6.7,0 6.7-2.5 6.7-2.5v2.4c0,0 0,2.7-6.8,2.7-6.6-0-6.6-2.7-6.6-2.7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 589 B

View File

@ -0,0 +1,48 @@
/* 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/. */
/* Storage Host Tree */
#storage-tree {
min-width: 220px;
max-width: 500px;
overflow: auto;
}
.theme-dark #storage-tree {
background: #343c45; /* Toolbars */
}
#storage-tree .tree-widget-item[type="store"]:after {
background-image: url(chrome://browser/skin/devtools/filetype-store.svg);
background-size: 18px 18px;
background-position: -1px 0;
}
/* Columns with date should have a min width so that date is visible */
#expires, #lastAccessed, #creationTime {
min-width: 150px;
}
/* Variables View Sidebar */
#storage-sidebar {
max-width: 500px;
min-width: 250px;
}
/* Responsive sidebar */
@media (max-width: 700px) {
#storage-tree {
max-width: 100%;
}
#storage-table #path {
display: none;
}
#storage-table .table-widget-cell {
min-width: 100px;
}
}

View File

@ -1430,6 +1430,7 @@
padding: 10px 20px;
font-size: medium;
background: transparent;
pointer-events: none;
}
/* Tree Item */

View File

@ -239,6 +239,7 @@ browser.jar:
skin/classic/browser/devtools/filetype-dir-close.svg (../shared/devtools/images/filetypes/dir-close.svg)
skin/classic/browser/devtools/filetype-dir-open.svg (../shared/devtools/images/filetypes/dir-open.svg)
skin/classic/browser/devtools/filetype-globe.svg (../shared/devtools/images/filetypes/globe.svg)
skin/classic/browser/devtools/filetype-store.svg (../shared/devtools/images/filetypes/store.svg)
skin/classic/browser/devtools/commandline-icon.png (../shared/devtools/images/commandline-icon.png)
skin/classic/browser/devtools/commandline-icon@2x.png (../shared/devtools/images/commandline-icon@2x.png)
skin/classic/browser/devtools/alerticon-warning.png (../shared/devtools/images/alerticon-warning.png)
@ -283,6 +284,7 @@ browser.jar:
* skin/classic/browser/devtools/profiler.css (devtools/profiler.css)
* skin/classic/browser/devtools/scratchpad.css (devtools/scratchpad.css)
* skin/classic/browser/devtools/shadereditor.css (devtools/shadereditor.css)
skin/classic/browser/devtools/storage.css (../shared/devtools/storage.css)
* skin/classic/browser/devtools/splitview.css (../shared/devtools/splitview.css)
skin/classic/browser/devtools/styleeditor.css (../shared/devtools/styleeditor.css)
* skin/classic/browser/devtools/webaudioeditor.css (devtools/webaudioeditor.css)
@ -346,6 +348,7 @@ browser.jar:
skin/classic/browser/devtools/tool-debugger-paused.svg (../shared/devtools/images/tool-debugger-paused.svg)
skin/classic/browser/devtools/tool-inspector.svg (../shared/devtools/images/tool-inspector.svg)
skin/classic/browser/devtools/tool-styleeditor.svg (../shared/devtools/images/tool-styleeditor.svg)
skin/classic/browser/devtools/tool-storage.svg (../shared/devtools/images/tool-storage.svg)
skin/classic/browser/devtools/tool-profiler.svg (../shared/devtools/images/tool-profiler.svg)
skin/classic/browser/devtools/tool-network.svg (../shared/devtools/images/tool-network.svg)
skin/classic/browser/devtools/tool-scratchpad.svg (../shared/devtools/images/tool-scratchpad.svg)
@ -654,6 +657,7 @@ browser.jar:
skin/classic/aero/browser/devtools/filetype-dir-close.svg (../shared/devtools/images/filetypes/dir-close.svg)
skin/classic/aero/browser/devtools/filetype-dir-open.svg (../shared/devtools/images/filetypes/dir-open.svg)
skin/classic/aero/browser/devtools/filetype-globe.svg (../shared/devtools/images/filetypes/globe.svg)
skin/classic/aero/browser/devtools/filetype-store.svg (../shared/devtools/images/filetypes/store.svg)
skin/classic/aero/browser/devtools/commandline-icon.png (../shared/devtools/images/commandline-icon.png)
skin/classic/aero/browser/devtools/commandline-icon@2x.png (../shared/devtools/images/commandline-icon@2x.png)
skin/classic/aero/browser/devtools/command-paintflashing.png (../shared/devtools/images/command-paintflashing.png)
@ -700,6 +704,7 @@ browser.jar:
* skin/classic/aero/browser/devtools/shadereditor.css (devtools/shadereditor.css)
* skin/classic/aero/browser/devtools/splitview.css (../shared/devtools/splitview.css)
skin/classic/aero/browser/devtools/styleeditor.css (../shared/devtools/styleeditor.css)
skin/classic/aero/browser/devtools/storage.css (../shared/devtools/storage.css)
* skin/classic/aero/browser/devtools/webaudioeditor.css (devtools/webaudioeditor.css)
skin/classic/aero/browser/devtools/magnifying-glass.png (../shared/devtools/images/magnifying-glass.png)
skin/classic/aero/browser/devtools/magnifying-glass@2x.png (../shared/devtools/images/magnifying-glass@2x.png)
@ -761,6 +766,7 @@ browser.jar:
skin/classic/aero/browser/devtools/tool-debugger-paused.svg (../shared/devtools/images/tool-debugger-paused.svg)
skin/classic/aero/browser/devtools/tool-inspector.svg (../shared/devtools/images/tool-inspector.svg)
skin/classic/aero/browser/devtools/tool-styleeditor.svg (../shared/devtools/images/tool-styleeditor.svg)
skin/classic/aero/browser/devtools/tool-storage.svg (../shared/devtools/images/tool-storage.svg)
skin/classic/aero/browser/devtools/tool-profiler.svg (../shared/devtools/images/tool-profiler.svg)
skin/classic/aero/browser/devtools/tool-network.svg (../shared/devtools/images/tool-network.svg)
skin/classic/aero/browser/devtools/tool-scratchpad.svg (../shared/devtools/images/tool-scratchpad.svg)

View File

@ -129,10 +129,14 @@
<activity-alias android:name=".App"
android:label="@MOZ_APP_DISPLAYNAME@"
android:targetActivity="org.mozilla.gecko.BrowserApp">
<intent-filter>
<!-- android:priority ranges between -1000 and 1000. We never want
another activity to usurp the MAIN action, so we ratchet our
priority up. -->
<intent-filter android:priority="999">
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER"/>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data android:name="com.sec.minimode.icon.portrait.normal"
@ -232,8 +236,7 @@
<activity android:name="org.mozilla.gecko.StartPane"
android:theme="@style/GeckoStartPane"
android:excludeFromRecents="true"
android:noHistory="true" />
android:excludeFromRecents="true"/>
<activity android:name="org.mozilla.gecko.webapp.Dispatcher"
android:noHistory="true" >

View File

@ -94,6 +94,14 @@ public class AppConstants {
// add additional quotes we end up with ""x.y"", which is a syntax error.
public static final String MOZILLA_VERSION = @MOZILLA_VERSION@;
public static final String MOZ_STUMBLER_API_KEY =
#ifdef MOZ_ANDROID_MLS_STUMBLER
"@MOZ_STUMBLER_API_KEY@";
#else
null;
#endif
public static final boolean MOZ_STUMBLER_BUILD_TIME_ENABLED = (MOZ_STUMBLER_API_KEY != null);
public static final String MOZ_CHILD_PROCESS_NAME = "@MOZ_CHILD_PROCESS_NAME@";
public static final String MOZ_UPDATE_CHANNEL = "@MOZ_UPDATE_CHANNEL@";
public static final String OMNIJAR_NAME = "@OMNIJAR_NAME@";

View File

@ -669,6 +669,10 @@ public class BrowserApp extends GeckoApp
super.onResume();
EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener)this,
"Prompt:ShowTop");
if (AppConstants.MOZ_STUMBLER_BUILD_TIME_ENABLED) {
// Starts or pings the stumbler lib, see also usage in handleMessage(): Gecko:DelayedStartup.
GeckoPreferences.broadcastStumblerPref(this);
}
}
@Override
@ -1418,6 +1422,11 @@ public class BrowserApp extends GeckoApp
}
});
if (AppConstants.MOZ_STUMBLER_BUILD_TIME_ENABLED) {
// Start (this acts as ping if started already) the stumbler lib; if the stumbler has queued data it will upload it.
// Stumbler operates on its own thread, and startup impact is further minimized by delaying work (such as upload) a few seconds.
GeckoPreferences.broadcastStumblerPref(this);
}
super.handleMessage(event, message);
} else if (event.equals("Gecko:Ready")) {
// Handle this message in GeckoApp, but also enable the Settings

View File

@ -108,7 +108,8 @@ public class GeckoEvent {
TELEMETRY_UI_SESSION_STOP(43),
TELEMETRY_UI_EVENT(44),
GAMEPAD_ADDREMOVE(45),
GAMEPAD_DATA(46);
GAMEPAD_DATA(46),
LONG_PRESS(47);
public final int value;
@ -420,6 +421,16 @@ public class GeckoEvent {
return event;
}
/**
* Creates a GeckoEvent that contains the data from the LongPressEvent, to be
* dispatched in CSS pixels relative to gecko's scroll position.
*/
public static GeckoEvent createLongPressEvent(MotionEvent m) {
GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.LONG_PRESS);
event.initMotionEvent(m, false);
return event;
}
private void initMotionEvent(MotionEvent m, boolean keepInViewCoordinates) {
mAction = m.getActionMasked();
mTime = (System.currentTimeMillis() - SystemClock.elapsedRealtime()) + m.getEventTime();

View File

@ -2,15 +2,13 @@
* 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/. */
package org.mozilla.gecko.sync.setup.activities;
package org.mozilla.gecko;
import org.mozilla.gecko.BrowserLocaleManager;
import org.mozilla.gecko.LocaleManager;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.os.StrictMode;
import android.support.v4.app.FragmentActivity;
@ -25,19 +23,14 @@ import android.support.v4.app.FragmentActivity;
* or <code>LocaleAwareActivity</code>.
*/
public class LocaleAware {
@TargetApi(Build.VERSION_CODES.GINGERBREAD)
public static void initializeLocale(Context context) {
final LocaleManager localeManager = BrowserLocaleManager.getInstance();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) {
final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
StrictMode.allowThreadDiskWrites();
try {
localeManager.getAndApplyPersistedLocale(context);
} else {
final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
StrictMode.allowThreadDiskWrites();
try {
localeManager.getAndApplyPersistedLocale(context);
} finally {
StrictMode.setThreadPolicy(savedPolicy);
}
} finally {
StrictMode.setThreadPolicy(savedPolicy);
}
}

View File

@ -0,0 +1,149 @@
/* 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/. */
package org.mozilla.gecko;
import java.util.ArrayList;
import java.util.List;
import org.mozilla.gecko.TabsAccessor.RemoteClient;
import org.mozilla.gecko.TabsAccessor.RemoteTab;
import android.content.Context;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseExpandableListAdapter;
import android.widget.TextView;
/**
* An adapter that populates group and child views with remote client and tab
* data maintained in a monolithic static array.
* <p>
* The group and child view resources are parameters to allow future
* specialization to home fragment styles.
*/
public class RemoteTabsExpandableListAdapter extends BaseExpandableListAdapter {
protected final ArrayList<RemoteClient> clients;
protected int groupLayoutId;
protected int childLayoutId;
/**
* Construct a new adapter.
* <p>
* It's fine to create with clients to be null, and then to use
* {@link RemoteTabsExpandableListAdapter#replaceClients(List)} to
* update this list of clients.
*
* @param groupLayoutId
* @param childLayoutId
* @param clients
* initial list of clients; can be null.
*/
public RemoteTabsExpandableListAdapter(int groupLayoutId, int childLayoutId, List<RemoteClient> clients) {
this.groupLayoutId = groupLayoutId;
this.childLayoutId = childLayoutId;
this.clients = new ArrayList<TabsAccessor.RemoteClient>();
if (clients != null) {
this.clients.addAll(clients);
}
}
public void replaceClients(List<RemoteClient> clients) {
this.clients.clear();
if (clients != null) {
this.clients.addAll(clients);
this.notifyDataSetChanged();
} else {
this.notifyDataSetInvalidated();
}
}
@Override
public boolean hasStableIds() {
return false; // Client GUIDs are stable, but tab hashes are not.
}
@Override
public long getGroupId(int groupPosition) {
return clients.get(groupPosition).guid.hashCode();
}
@Override
public int getGroupCount() {
return clients.size();
}
@Override
public Object getGroup(int groupPosition) {
return clients.get(groupPosition);
}
@Override
public int getChildrenCount(int groupPosition) {
return clients.get(groupPosition).tabs.size();
}
@Override
public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
final Context context = parent.getContext();
final View view;
if (convertView != null) {
view = convertView;
} else {
final LayoutInflater inflater = LayoutInflater.from(context);
view = inflater.inflate(groupLayoutId, parent, false);
}
final RemoteClient client = clients.get(groupPosition);
final TextView nameView = (TextView) view.findViewById(R.id.client);
nameView.setText(client.name);
final TextView lastModifiedView = (TextView) view.findViewById(R.id.last_synced);
final long now = System.currentTimeMillis();
lastModifiedView.setText(TabsAccessor.getLastSyncedString(context, now, client.lastModified));
return view;
}
@Override
public boolean isChildSelectable(int groupPosition, int childPosition) {
return true;
}
@Override
public Object getChild(int groupPosition, int childPosition) {
return clients.get(groupPosition).tabs.get(childPosition);
}
@Override
public long getChildId(int groupPosition, int childPosition) {
return clients.get(groupPosition).tabs.get(childPosition).hashCode();
}
@Override
public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
final Context context = parent.getContext();
final View view;
if (convertView != null) {
view = convertView;
} else {
final LayoutInflater inflater = LayoutInflater.from(context);
view = inflater.inflate(childLayoutId, parent, false);
}
final RemoteClient client = clients.get(groupPosition);
final RemoteTab tab = client.tabs.get(childPosition);
final TextView titleView = (TextView) view.findViewById(R.id.title);
titleView.setText(TextUtils.isEmpty(tab.title) ? tab.url : tab.title);
final TextView urlView = (TextView) view.findViewById(R.id.url);
urlView.setText(tab.url);
return view;
}
}

View File

@ -4,127 +4,241 @@
package org.mozilla.gecko;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
import org.json.JSONArray;
import org.json.JSONException;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.util.UIAsyncTask;
import org.json.JSONArray;
import org.json.JSONException;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Log;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
public final class TabsAccessor {
private static final String LOGTAG = "GeckoTabsAccessor";
private static final String[] CLIENTS_AVAILABILITY_PROJECTION = new String[] {
BrowserContract.Clients.GUID
};
private static final String[] TABS_PROJECTION_COLUMNS = new String[] {
public static final String[] TABS_PROJECTION_COLUMNS = new String[] {
BrowserContract.Tabs.TITLE,
BrowserContract.Tabs.URL,
BrowserContract.Clients.GUID,
BrowserContract.Clients.NAME,
BrowserContract.Clients.LAST_MODIFIED,
BrowserContract.Clients.DEVICE_TYPE,
};
// Projection column numbers
public static enum TABS_COLUMN {
TITLE,
URL,
GUID,
NAME,
LAST_MODIFIED,
};
private static final String LOCAL_TABS_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS NULL";
private static final String REMOTE_TABS_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS NOT NULL";
private static final String CLIENTS_SELECTION = BrowserContract.Clients.GUID + " IS NOT NULL";
private static final String TABS_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS NOT NULL";
private static final String REMOTE_TABS_SORT_ORDER =
// Most recently synced clients first.
BrowserContract.Clients.LAST_MODIFIED + " DESC, " +
// If two clients somehow had the same last modified time, this will
// group them (arbitrarily).
BrowserContract.Clients.GUID + " DESC, " +
// Within a single client, most recently used tabs first.
BrowserContract.Tabs.LAST_USED + " DESC";
private static final String LOCAL_CLIENT_SELECTION = BrowserContract.Clients.GUID + " IS NULL";
private static final String LOCAL_TABS_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS NULL";
private static final Pattern FILTERED_URL_PATTERN = Pattern.compile("^(about|chrome|wyciwyg|file):");
/**
* A thin representation of a remote client.
* <p>
* We use the hash of the client's GUID as the ID in
* {@link RemoteTabsExpandableListAdapter#getGroupId(int)}.
*/
public static class RemoteClient {
public final String guid;
public final String name;
public final long lastModified;
public final String deviceType;
public final ArrayList<RemoteTab> tabs;
public RemoteClient(String guid, String name, long lastModified, String deviceType) {
this.guid = guid;
this.name = name;
this.lastModified = lastModified;
this.deviceType = deviceType;
this.tabs = new ArrayList<RemoteTab>();
}
}
/**
* A thin representation of a remote tab.
* <p>
* We use the hash of the tab as the ID in
* {@link RemoteTabsExpandableListAdapter#getClientId(int)}, and therefore we
* must implement equality as well. These are generated functions.
*/
public static class RemoteTab {
public String title;
public String url;
public String guid;
public String name;
/**
* This is the last time the remote client uploaded a tabs record; that
* is, it is not per tab, but per remote client.
*/
public long lastModified;
public final String title;
public final String url;
public RemoteTab(String title, String url) {
this.title = title;
this.url = url;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((title == null) ? 0 : title.hashCode());
result = prime * result + ((url == null) ? 0 : url.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
RemoteTab other = (RemoteTab) obj;
if (title == null) {
if (other.title != null) {
return false;
}
} else if (!title.equals(other.title)) {
return false;
}
if (url == null) {
if (other.url != null) {
return false;
}
} else if (!url.equals(other.url)) {
return false;
}
return true;
}
}
/**
* Extract client and tab records from a cursor.
* <p>
* The position of the cursor is moved to before the first record before
* reading. The cursor is advanced until there are no more records to be
* read. The position of the cursor is restored before returning.
*
* @param cursor
* to extract records from. The records should already be grouped
* by client GUID.
* @return list of clients, each containing list of tabs.
*/
public static List<RemoteClient> getClientsFromCursor(final Cursor cursor) {
final ArrayList<RemoteClient> clients = new ArrayList<TabsAccessor.RemoteClient>();
final int originalPosition = cursor.getPosition();
try {
if (!cursor.moveToFirst()) {
return clients;
}
final int tabTitleIndex = cursor.getColumnIndex(BrowserContract.Tabs.TITLE);
final int tabUrlIndex = cursor.getColumnIndex(BrowserContract.Tabs.URL);
final int clientGuidIndex = cursor.getColumnIndex(BrowserContract.Clients.GUID);
final int clientNameIndex = cursor.getColumnIndex(BrowserContract.Clients.NAME);
final int clientLastModifiedIndex = cursor.getColumnIndex(BrowserContract.Clients.LAST_MODIFIED);
final int clientDeviceTypeIndex = cursor.getColumnIndex(BrowserContract.Clients.DEVICE_TYPE);
// A walking partition, chunking by client GUID. We assume the
// cursor records are already grouped by client GUID; see the query
// sort order.
RemoteClient lastClient = null;
while (!cursor.isAfterLast()) {
final String clientGuid = cursor.getString(clientGuidIndex);
if (lastClient == null || !TextUtils.equals(lastClient.guid, clientGuid)) {
final String clientName = cursor.getString(clientNameIndex);
final long lastModified = cursor.getLong(clientLastModifiedIndex);
final String deviceType = cursor.getString(clientDeviceTypeIndex);
lastClient = new RemoteClient(clientGuid, clientName, lastModified, deviceType);
clients.add(lastClient);
}
final String tabTitle = cursor.getString(tabTitleIndex);
final String tabUrl = cursor.getString(tabUrlIndex);
lastClient.tabs.add(new RemoteTab(tabTitle, tabUrl));
cursor.moveToNext();
}
} finally {
cursor.moveToPosition(originalPosition);
}
return clients;
}
public static Cursor getRemoteTabsCursor(Context context) {
return getRemoteTabsCursor(context, -1);
}
public static Cursor getRemoteTabsCursor(Context context, int limit) {
Uri uri = BrowserContract.Tabs.CONTENT_URI;
if (limit > 0) {
uri = uri.buildUpon()
.appendQueryParameter(BrowserContract.PARAM_LIMIT, String.valueOf(limit))
.build();
}
final Cursor cursor = context.getContentResolver().query(uri,
TABS_PROJECTION_COLUMNS,
REMOTE_TABS_SELECTION,
null,
REMOTE_TABS_SORT_ORDER);
return cursor;
}
public interface OnQueryTabsCompleteListener {
public void onQueryTabsComplete(List<RemoteTab> tabs);
public void onQueryTabsComplete(List<RemoteClient> clients);
}
// This method returns all tabs from all remote clients,
// ordered by most recent client first, most recent tab first
// This method returns all tabs from all remote clients,
// ordered by most recent client first, most recent tab first
public static void getTabs(final Context context, final OnQueryTabsCompleteListener listener) {
getTabs(context, 0, listener);
}
// This method returns limited number of tabs from all remote clients,
// ordered by most recent client first, most recent tab first
// This method returns limited number of tabs from all remote clients,
// ordered by most recent client first, most recent tab first
public static void getTabs(final Context context, final int limit, final OnQueryTabsCompleteListener listener) {
// If there is no listener, no point in doing work.
if (listener == null)
return;
(new UIAsyncTask.WithoutParams<List<RemoteTab>>(ThreadUtils.getBackgroundHandler()) {
(new UIAsyncTask.WithoutParams<List<RemoteClient>>(ThreadUtils.getBackgroundHandler()) {
@Override
protected List<RemoteTab> doInBackground() {
Uri uri = BrowserContract.Tabs.CONTENT_URI;
if (limit > 0) {
uri = uri.buildUpon()
.appendQueryParameter(BrowserContract.PARAM_LIMIT, String.valueOf(limit))
.build();
}
Cursor cursor = context.getContentResolver().query(uri,
TABS_PROJECTION_COLUMNS,
TABS_SELECTION,
null,
null);
protected List<RemoteClient> doInBackground() {
final Cursor cursor = getRemoteTabsCursor(context, limit);
if (cursor == null)
return null;
RemoteTab tab;
final ArrayList<RemoteTab> tabs = new ArrayList<RemoteTab> ();
try {
while (cursor.moveToNext()) {
tab = new RemoteTab();
tab.title = cursor.getString(TABS_COLUMN.TITLE.ordinal());
tab.url = cursor.getString(TABS_COLUMN.URL.ordinal());
tab.guid = cursor.getString(TABS_COLUMN.GUID.ordinal());
tab.name = cursor.getString(TABS_COLUMN.NAME.ordinal());
tab.lastModified = cursor.getLong(TABS_COLUMN.LAST_MODIFIED.ordinal());
tabs.add(tab);
}
try {
return Collections.unmodifiableList(getClientsFromCursor(cursor));
} finally {
cursor.close();
}
return Collections.unmodifiableList(tabs);
}
}
@Override
protected void onPostExecute(List<RemoteTab> tabs) {
listener.onQueryTabsComplete(tabs);
protected void onPostExecute(List<RemoteClient> clients) {
listener.onQueryTabsComplete(clients);
}
}).execute();
}
@ -210,4 +324,16 @@ public final class TabsAccessor {
private static boolean isFilteredURL(String url) {
return FILTERED_URL_PATTERN.matcher(url).lookingAt();
}
/**
* Return a relative "Last synced" time span for the given tab record.
*
* @param now local time.
* @param time to format string for.
* @return string describing time span
*/
public static String getLastSyncedString(Context context, long now, long time) {
final CharSequence relativeTimeSpanString = DateUtils.getRelativeTimeSpanString(time, now, DateUtils.MINUTE_IN_MILLIS);
return context.getResources().getString(R.string.remote_tabs_last_synced, relativeTimeSpanString);
}
}

View File

@ -5,67 +5,115 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
sync_thirdparty_java_files = [
'ch/boye/httpclientandroidlib/androidextra/Base64.java',
'ch/boye/httpclientandroidlib/androidextra/HttpClientAndroidLog.java',
'ch/boye/httpclientandroidlib/annotation/GuardedBy.java',
'ch/boye/httpclientandroidlib/annotation/Immutable.java',
'ch/boye/httpclientandroidlib/annotation/NotThreadSafe.java',
'ch/boye/httpclientandroidlib/annotation/package-info.java',
'ch/boye/httpclientandroidlib/annotation/ThreadSafe.java',
'ch/boye/httpclientandroidlib/auth/AUTH.java',
'ch/boye/httpclientandroidlib/auth/AuthenticationException.java',
'ch/boye/httpclientandroidlib/auth/AuthOption.java',
'ch/boye/httpclientandroidlib/auth/AuthProtocolState.java',
'ch/boye/httpclientandroidlib/auth/AuthScheme.java',
'ch/boye/httpclientandroidlib/auth/AuthSchemeFactory.java',
'ch/boye/httpclientandroidlib/auth/AuthSchemeProvider.java',
'ch/boye/httpclientandroidlib/auth/AuthSchemeRegistry.java',
'ch/boye/httpclientandroidlib/auth/AuthScope.java',
'ch/boye/httpclientandroidlib/auth/AuthState.java',
'ch/boye/httpclientandroidlib/auth/BasicUserPrincipal.java',
'ch/boye/httpclientandroidlib/auth/ChallengeState.java',
'ch/boye/httpclientandroidlib/auth/ContextAwareAuthScheme.java',
'ch/boye/httpclientandroidlib/auth/Credentials.java',
'ch/boye/httpclientandroidlib/auth/InvalidCredentialsException.java',
'ch/boye/httpclientandroidlib/auth/MalformedChallengeException.java',
'ch/boye/httpclientandroidlib/auth/NTCredentials.java',
'ch/boye/httpclientandroidlib/auth/NTUserPrincipal.java',
'ch/boye/httpclientandroidlib/auth/package-info.java',
'ch/boye/httpclientandroidlib/auth/params/AuthParamBean.java',
'ch/boye/httpclientandroidlib/auth/params/AuthParams.java',
'ch/boye/httpclientandroidlib/auth/params/AuthPNames.java',
'ch/boye/httpclientandroidlib/auth/params/package-info.java',
'ch/boye/httpclientandroidlib/auth/UsernamePasswordCredentials.java',
'ch/boye/httpclientandroidlib/client/AuthCache.java',
'ch/boye/httpclientandroidlib/client/AuthenticationHandler.java',
'ch/boye/httpclientandroidlib/client/AuthenticationStrategy.java',
'ch/boye/httpclientandroidlib/client/BackoffManager.java',
'ch/boye/httpclientandroidlib/client/cache/CacheResponseStatus.java',
'ch/boye/httpclientandroidlib/client/cache/HeaderConstants.java',
'ch/boye/httpclientandroidlib/client/cache/HttpCacheContext.java',
'ch/boye/httpclientandroidlib/client/cache/HttpCacheEntry.java',
'ch/boye/httpclientandroidlib/client/cache/HttpCacheEntrySerializationException.java',
'ch/boye/httpclientandroidlib/client/cache/HttpCacheEntrySerializer.java',
'ch/boye/httpclientandroidlib/client/cache/HttpCacheInvalidator.java',
'ch/boye/httpclientandroidlib/client/cache/HttpCacheStorage.java',
'ch/boye/httpclientandroidlib/client/cache/HttpCacheUpdateCallback.java',
'ch/boye/httpclientandroidlib/client/cache/HttpCacheUpdateException.java',
'ch/boye/httpclientandroidlib/client/cache/InputLimit.java',
'ch/boye/httpclientandroidlib/client/cache/Resource.java',
'ch/boye/httpclientandroidlib/client/cache/ResourceFactory.java',
'ch/boye/httpclientandroidlib/client/CircularRedirectException.java',
'ch/boye/httpclientandroidlib/client/ClientProtocolException.java',
'ch/boye/httpclientandroidlib/client/config/AuthSchemes.java',
'ch/boye/httpclientandroidlib/client/config/CookieSpecs.java',
'ch/boye/httpclientandroidlib/client/config/package-info.java',
'ch/boye/httpclientandroidlib/client/config/RequestConfig.java',
'ch/boye/httpclientandroidlib/client/ConnectionBackoffStrategy.java',
'ch/boye/httpclientandroidlib/client/CookieStore.java',
'ch/boye/httpclientandroidlib/client/CredentialsProvider.java',
'ch/boye/httpclientandroidlib/client/entity/DecompressingEntity.java',
'ch/boye/httpclientandroidlib/client/entity/DeflateDecompressingEntity.java',
'ch/boye/httpclientandroidlib/client/entity/DeflateInputStream.java',
'ch/boye/httpclientandroidlib/client/entity/EntityBuilder.java',
'ch/boye/httpclientandroidlib/client/entity/GzipCompressingEntity.java',
'ch/boye/httpclientandroidlib/client/entity/GzipDecompressingEntity.java',
'ch/boye/httpclientandroidlib/client/entity/LazyDecompressingInputStream.java',
'ch/boye/httpclientandroidlib/client/entity/package-info.java',
'ch/boye/httpclientandroidlib/client/entity/UrlEncodedFormEntity.java',
'ch/boye/httpclientandroidlib/client/HttpClient.java',
'ch/boye/httpclientandroidlib/client/HttpRequestRetryHandler.java',
'ch/boye/httpclientandroidlib/client/HttpResponseException.java',
'ch/boye/httpclientandroidlib/client/methods/AbortableHttpRequest.java',
'ch/boye/httpclientandroidlib/client/methods/AbstractExecutionAwareRequest.java',
'ch/boye/httpclientandroidlib/client/methods/CloseableHttpResponse.java',
'ch/boye/httpclientandroidlib/client/methods/Configurable.java',
'ch/boye/httpclientandroidlib/client/methods/HttpDelete.java',
'ch/boye/httpclientandroidlib/client/methods/HttpEntityEnclosingRequestBase.java',
'ch/boye/httpclientandroidlib/client/methods/HttpExecutionAware.java',
'ch/boye/httpclientandroidlib/client/methods/HttpGet.java',
'ch/boye/httpclientandroidlib/client/methods/HttpHead.java',
'ch/boye/httpclientandroidlib/client/methods/HttpOptions.java',
'ch/boye/httpclientandroidlib/client/methods/HttpPatch.java',
'ch/boye/httpclientandroidlib/client/methods/HttpPost.java',
'ch/boye/httpclientandroidlib/client/methods/HttpPut.java',
'ch/boye/httpclientandroidlib/client/methods/HttpRequestBase.java',
'ch/boye/httpclientandroidlib/client/methods/HttpRequestWrapper.java',
'ch/boye/httpclientandroidlib/client/methods/HttpTrace.java',
'ch/boye/httpclientandroidlib/client/methods/HttpUriRequest.java',
'ch/boye/httpclientandroidlib/client/methods/package-info.java',
'ch/boye/httpclientandroidlib/client/methods/RequestBuilder.java',
'ch/boye/httpclientandroidlib/client/NonRepeatableRequestException.java',
'ch/boye/httpclientandroidlib/client/package-info.java',
'ch/boye/httpclientandroidlib/client/params/AllClientPNames.java',
'ch/boye/httpclientandroidlib/client/params/AuthPolicy.java',
'ch/boye/httpclientandroidlib/client/params/ClientParamBean.java',
'ch/boye/httpclientandroidlib/client/params/ClientPNames.java',
'ch/boye/httpclientandroidlib/client/params/CookiePolicy.java',
'ch/boye/httpclientandroidlib/client/params/HttpClientParamConfig.java',
'ch/boye/httpclientandroidlib/client/params/HttpClientParams.java',
'ch/boye/httpclientandroidlib/client/params/package-info.java',
'ch/boye/httpclientandroidlib/client/protocol/ClientContext.java',
'ch/boye/httpclientandroidlib/client/protocol/ClientContextConfigurer.java',
'ch/boye/httpclientandroidlib/client/protocol/HttpClientContext.java',
'ch/boye/httpclientandroidlib/client/protocol/package-info.java',
'ch/boye/httpclientandroidlib/client/protocol/RequestAcceptEncoding.java',
'ch/boye/httpclientandroidlib/client/protocol/RequestAddCookies.java',
'ch/boye/httpclientandroidlib/client/protocol/RequestAuthCache.java',
'ch/boye/httpclientandroidlib/client/protocol/RequestAuthenticationBase.java',
'ch/boye/httpclientandroidlib/client/protocol/RequestClientConnControl.java',
'ch/boye/httpclientandroidlib/client/protocol/RequestDefaultHeaders.java',
'ch/boye/httpclientandroidlib/client/protocol/RequestExpectContinue.java',
'ch/boye/httpclientandroidlib/client/protocol/RequestProxyAuthentication.java',
'ch/boye/httpclientandroidlib/client/protocol/RequestTargetAuthentication.java',
'ch/boye/httpclientandroidlib/client/protocol/ResponseAuthCache.java',
@ -76,14 +124,30 @@ sync_thirdparty_java_files = [
'ch/boye/httpclientandroidlib/client/RedirectStrategy.java',
'ch/boye/httpclientandroidlib/client/RequestDirector.java',
'ch/boye/httpclientandroidlib/client/ResponseHandler.java',
'ch/boye/httpclientandroidlib/client/ServiceUnavailableRetryStrategy.java',
'ch/boye/httpclientandroidlib/client/UserTokenHandler.java',
'ch/boye/httpclientandroidlib/client/utils/CloneUtils.java',
'ch/boye/httpclientandroidlib/client/utils/DateUtils.java',
'ch/boye/httpclientandroidlib/client/utils/HttpClientUtils.java',
'ch/boye/httpclientandroidlib/client/utils/Idn.java',
'ch/boye/httpclientandroidlib/client/utils/JdkIdn.java',
'ch/boye/httpclientandroidlib/client/utils/package-info.java',
'ch/boye/httpclientandroidlib/client/utils/Punycode.java',
'ch/boye/httpclientandroidlib/client/utils/Rfc3492Idn.java',
'ch/boye/httpclientandroidlib/client/utils/URIBuilder.java',
'ch/boye/httpclientandroidlib/client/utils/URIUtils.java',
'ch/boye/httpclientandroidlib/client/utils/URLEncodedUtils.java',
'ch/boye/httpclientandroidlib/concurrent/BasicFuture.java',
'ch/boye/httpclientandroidlib/concurrent/Cancellable.java',
'ch/boye/httpclientandroidlib/concurrent/FutureCallback.java',
'ch/boye/httpclientandroidlib/concurrent/package-info.java',
'ch/boye/httpclientandroidlib/config/ConnectionConfig.java',
'ch/boye/httpclientandroidlib/config/Lookup.java',
'ch/boye/httpclientandroidlib/config/MessageConstraints.java',
'ch/boye/httpclientandroidlib/config/package-info.java',
'ch/boye/httpclientandroidlib/config/Registry.java',
'ch/boye/httpclientandroidlib/config/RegistryBuilder.java',
'ch/boye/httpclientandroidlib/config/SocketConfig.java',
'ch/boye/httpclientandroidlib/conn/BasicEofSensorWatcher.java',
'ch/boye/httpclientandroidlib/conn/BasicManagedEntity.java',
'ch/boye/httpclientandroidlib/conn/ClientConnectionManager.java',
@ -93,14 +157,21 @@ sync_thirdparty_java_files = [
'ch/boye/httpclientandroidlib/conn/ConnectionKeepAliveStrategy.java',
'ch/boye/httpclientandroidlib/conn/ConnectionPoolTimeoutException.java',
'ch/boye/httpclientandroidlib/conn/ConnectionReleaseTrigger.java',
'ch/boye/httpclientandroidlib/conn/ConnectionRequest.java',
'ch/boye/httpclientandroidlib/conn/ConnectTimeoutException.java',
'ch/boye/httpclientandroidlib/conn/DnsResolver.java',
'ch/boye/httpclientandroidlib/conn/EofSensorInputStream.java',
'ch/boye/httpclientandroidlib/conn/EofSensorWatcher.java',
'ch/boye/httpclientandroidlib/conn/HttpClientConnectionManager.java',
'ch/boye/httpclientandroidlib/conn/HttpConnectionFactory.java',
'ch/boye/httpclientandroidlib/conn/HttpHostConnectException.java',
'ch/boye/httpclientandroidlib/conn/HttpInetSocketAddress.java',
'ch/boye/httpclientandroidlib/conn/HttpRoutedConnection.java',
'ch/boye/httpclientandroidlib/conn/ManagedClientConnection.java',
'ch/boye/httpclientandroidlib/conn/ManagedHttpClientConnection.java',
'ch/boye/httpclientandroidlib/conn/MultihomePlainSocketFactory.java',
'ch/boye/httpclientandroidlib/conn/OperatedClientConnection.java',
'ch/boye/httpclientandroidlib/conn/package-info.java',
'ch/boye/httpclientandroidlib/conn/params/ConnConnectionParamBean.java',
'ch/boye/httpclientandroidlib/conn/params/ConnConnectionPNames.java',
'ch/boye/httpclientandroidlib/conn/params/ConnManagerParamBean.java',
@ -111,36 +182,58 @@ sync_thirdparty_java_files = [
'ch/boye/httpclientandroidlib/conn/params/ConnRouteParamBean.java',
'ch/boye/httpclientandroidlib/conn/params/ConnRouteParams.java',
'ch/boye/httpclientandroidlib/conn/params/ConnRoutePNames.java',
'ch/boye/httpclientandroidlib/conn/params/package-info.java',
'ch/boye/httpclientandroidlib/conn/routing/BasicRouteDirector.java',
'ch/boye/httpclientandroidlib/conn/routing/HttpRoute.java',
'ch/boye/httpclientandroidlib/conn/routing/HttpRouteDirector.java',
'ch/boye/httpclientandroidlib/conn/routing/HttpRoutePlanner.java',
'ch/boye/httpclientandroidlib/conn/routing/package-info.java',
'ch/boye/httpclientandroidlib/conn/routing/RouteInfo.java',
'ch/boye/httpclientandroidlib/conn/routing/RouteTracker.java',
'ch/boye/httpclientandroidlib/conn/scheme/HostNameResolver.java',
'ch/boye/httpclientandroidlib/conn/scheme/LayeredSchemeSocketFactory.java',
'ch/boye/httpclientandroidlib/conn/scheme/LayeredSchemeSocketFactoryAdaptor.java',
'ch/boye/httpclientandroidlib/conn/scheme/LayeredSocketFactory.java',
'ch/boye/httpclientandroidlib/conn/scheme/LayeredSocketFactoryAdaptor.java',
'ch/boye/httpclientandroidlib/conn/scheme/package-info.java',
'ch/boye/httpclientandroidlib/conn/scheme/PlainSocketFactory.java',
'ch/boye/httpclientandroidlib/conn/scheme/Scheme.java',
'ch/boye/httpclientandroidlib/conn/scheme/SchemeLayeredSocketFactory.java',
'ch/boye/httpclientandroidlib/conn/scheme/SchemeLayeredSocketFactoryAdaptor.java',
'ch/boye/httpclientandroidlib/conn/scheme/SchemeLayeredSocketFactoryAdaptor2.java',
'ch/boye/httpclientandroidlib/conn/scheme/SchemeRegistry.java',
'ch/boye/httpclientandroidlib/conn/scheme/SchemeSocketFactory.java',
'ch/boye/httpclientandroidlib/conn/scheme/SchemeSocketFactoryAdaptor.java',
'ch/boye/httpclientandroidlib/conn/scheme/SocketFactory.java',
'ch/boye/httpclientandroidlib/conn/scheme/SocketFactoryAdaptor.java',
'ch/boye/httpclientandroidlib/conn/SchemePortResolver.java',
'ch/boye/httpclientandroidlib/conn/socket/ConnectionSocketFactory.java',
'ch/boye/httpclientandroidlib/conn/socket/LayeredConnectionSocketFactory.java',
'ch/boye/httpclientandroidlib/conn/socket/package-info.java',
'ch/boye/httpclientandroidlib/conn/socket/PlainConnectionSocketFactory.java',
'ch/boye/httpclientandroidlib/conn/ssl/AbstractVerifier.java',
'ch/boye/httpclientandroidlib/conn/ssl/AllowAllHostnameVerifier.java',
'ch/boye/httpclientandroidlib/conn/ssl/BrowserCompatHostnameVerifier.java',
'ch/boye/httpclientandroidlib/conn/ssl/DistinguishedNameParser.java',
'ch/boye/httpclientandroidlib/conn/ssl/package-info.java',
'ch/boye/httpclientandroidlib/conn/ssl/PrivateKeyDetails.java',
'ch/boye/httpclientandroidlib/conn/ssl/PrivateKeyStrategy.java',
'ch/boye/httpclientandroidlib/conn/ssl/SSLConnectionSocketFactory.java',
'ch/boye/httpclientandroidlib/conn/ssl/SSLContextBuilder.java',
'ch/boye/httpclientandroidlib/conn/ssl/SSLContexts.java',
'ch/boye/httpclientandroidlib/conn/ssl/SSLInitializationException.java',
'ch/boye/httpclientandroidlib/conn/ssl/SSLSocketFactory.java',
'ch/boye/httpclientandroidlib/conn/ssl/StrictHostnameVerifier.java',
'ch/boye/httpclientandroidlib/conn/ssl/TrustManagerDecorator.java',
'ch/boye/httpclientandroidlib/conn/ssl/TokenParser.java',
'ch/boye/httpclientandroidlib/conn/ssl/TrustSelfSignedStrategy.java',
'ch/boye/httpclientandroidlib/conn/ssl/TrustStrategy.java',
'ch/boye/httpclientandroidlib/conn/ssl/X509HostnameVerifier.java',
'ch/boye/httpclientandroidlib/conn/UnsupportedSchemeException.java',
'ch/boye/httpclientandroidlib/conn/util/InetAddressUtils.java',
'ch/boye/httpclientandroidlib/conn/util/package-info.java',
'ch/boye/httpclientandroidlib/ConnectionClosedException.java',
'ch/boye/httpclientandroidlib/ConnectionReuseStrategy.java',
'ch/boye/httpclientandroidlib/Consts.java',
'ch/boye/httpclientandroidlib/ContentTooLongException.java',
'ch/boye/httpclientandroidlib/cookie/ClientCookie.java',
'ch/boye/httpclientandroidlib/cookie/Cookie.java',
'ch/boye/httpclientandroidlib/cookie/CookieAttributeHandler.java',
@ -150,10 +243,13 @@ sync_thirdparty_java_files = [
'ch/boye/httpclientandroidlib/cookie/CookieRestrictionViolationException.java',
'ch/boye/httpclientandroidlib/cookie/CookieSpec.java',
'ch/boye/httpclientandroidlib/cookie/CookieSpecFactory.java',
'ch/boye/httpclientandroidlib/cookie/CookieSpecProvider.java',
'ch/boye/httpclientandroidlib/cookie/CookieSpecRegistry.java',
'ch/boye/httpclientandroidlib/cookie/MalformedCookieException.java',
'ch/boye/httpclientandroidlib/cookie/package-info.java',
'ch/boye/httpclientandroidlib/cookie/params/CookieSpecParamBean.java',
'ch/boye/httpclientandroidlib/cookie/params/CookieSpecPNames.java',
'ch/boye/httpclientandroidlib/cookie/params/package-info.java',
'ch/boye/httpclientandroidlib/cookie/SetCookie.java',
'ch/boye/httpclientandroidlib/cookie/SetCookie2.java',
'ch/boye/httpclientandroidlib/cookie/SM.java',
@ -163,10 +259,32 @@ sync_thirdparty_java_files = [
'ch/boye/httpclientandroidlib/entity/ByteArrayEntity.java',
'ch/boye/httpclientandroidlib/entity/ContentLengthStrategy.java',
'ch/boye/httpclientandroidlib/entity/ContentProducer.java',
'ch/boye/httpclientandroidlib/entity/ContentType.java',
'ch/boye/httpclientandroidlib/entity/EntityTemplate.java',
'ch/boye/httpclientandroidlib/entity/FileEntity.java',
'ch/boye/httpclientandroidlib/entity/HttpEntityWrapper.java',
'ch/boye/httpclientandroidlib/entity/InputStreamEntity.java',
'ch/boye/httpclientandroidlib/entity/mime/AbstractMultipartForm.java',
'ch/boye/httpclientandroidlib/entity/mime/content/AbstractContentBody.java',
'ch/boye/httpclientandroidlib/entity/mime/content/ByteArrayBody.java',
'ch/boye/httpclientandroidlib/entity/mime/content/ContentBody.java',
'ch/boye/httpclientandroidlib/entity/mime/content/ContentDescriptor.java',
'ch/boye/httpclientandroidlib/entity/mime/content/FileBody.java',
'ch/boye/httpclientandroidlib/entity/mime/content/InputStreamBody.java',
'ch/boye/httpclientandroidlib/entity/mime/content/package-info.java',
'ch/boye/httpclientandroidlib/entity/mime/content/StringBody.java',
'ch/boye/httpclientandroidlib/entity/mime/FormBodyPart.java',
'ch/boye/httpclientandroidlib/entity/mime/Header.java',
'ch/boye/httpclientandroidlib/entity/mime/HttpBrowserCompatibleMultipart.java',
'ch/boye/httpclientandroidlib/entity/mime/HttpMultipartMode.java',
'ch/boye/httpclientandroidlib/entity/mime/HttpRFC6532Multipart.java',
'ch/boye/httpclientandroidlib/entity/mime/HttpStrictMultipart.java',
'ch/boye/httpclientandroidlib/entity/mime/MIME.java',
'ch/boye/httpclientandroidlib/entity/mime/MinimalField.java',
'ch/boye/httpclientandroidlib/entity/mime/MultipartEntityBuilder.java',
'ch/boye/httpclientandroidlib/entity/mime/MultipartFormEntity.java',
'ch/boye/httpclientandroidlib/entity/mime/package-info.java',
'ch/boye/httpclientandroidlib/entity/package-info.java',
'ch/boye/httpclientandroidlib/entity/SerializableEntity.java',
'ch/boye/httpclientandroidlib/entity/StringEntity.java',
'ch/boye/httpclientandroidlib/FormattedHeader.java',
@ -176,6 +294,7 @@ sync_thirdparty_java_files = [
'ch/boye/httpclientandroidlib/HeaderIterator.java',
'ch/boye/httpclientandroidlib/HttpClientConnection.java',
'ch/boye/httpclientandroidlib/HttpConnection.java',
'ch/boye/httpclientandroidlib/HttpConnectionFactory.java',
'ch/boye/httpclientandroidlib/HttpConnectionMetrics.java',
'ch/boye/httpclientandroidlib/HttpEntity.java',
'ch/boye/httpclientandroidlib/HttpEntityEnclosingRequest.java',
@ -200,22 +319,80 @@ sync_thirdparty_java_files = [
'ch/boye/httpclientandroidlib/impl/auth/BasicSchemeFactory.java',
'ch/boye/httpclientandroidlib/impl/auth/DigestScheme.java',
'ch/boye/httpclientandroidlib/impl/auth/DigestSchemeFactory.java',
'ch/boye/httpclientandroidlib/impl/auth/HttpAuthenticator.java',
'ch/boye/httpclientandroidlib/impl/auth/HttpEntityDigester.java',
'ch/boye/httpclientandroidlib/impl/auth/NTLMEngine.java',
'ch/boye/httpclientandroidlib/impl/auth/NTLMEngineException.java',
'ch/boye/httpclientandroidlib/impl/auth/NTLMEngineImpl.java',
'ch/boye/httpclientandroidlib/impl/auth/NTLMScheme.java',
'ch/boye/httpclientandroidlib/impl/auth/NTLMSchemeFactory.java',
'ch/boye/httpclientandroidlib/impl/auth/package-info.java',
'ch/boye/httpclientandroidlib/impl/auth/RFC2617Scheme.java',
'ch/boye/httpclientandroidlib/impl/auth/SpnegoTokenGenerator.java',
'ch/boye/httpclientandroidlib/impl/auth/UnsupportedDigestAlgorithmException.java',
'ch/boye/httpclientandroidlib/impl/BHttpConnectionBase.java',
'ch/boye/httpclientandroidlib/impl/client/AbstractAuthenticationHandler.java',
'ch/boye/httpclientandroidlib/impl/client/AbstractHttpClient.java',
'ch/boye/httpclientandroidlib/impl/client/AIMDBackoffManager.java',
'ch/boye/httpclientandroidlib/impl/client/AuthenticationStrategyAdaptor.java',
'ch/boye/httpclientandroidlib/impl/client/AuthenticationStrategyImpl.java',
'ch/boye/httpclientandroidlib/impl/client/AutoRetryHttpClient.java',
'ch/boye/httpclientandroidlib/impl/client/BasicAuthCache.java',
'ch/boye/httpclientandroidlib/impl/client/BasicCookieStore.java',
'ch/boye/httpclientandroidlib/impl/client/BasicCredentialsProvider.java',
'ch/boye/httpclientandroidlib/impl/client/BasicResponseHandler.java',
'ch/boye/httpclientandroidlib/impl/client/cache/AsynchronousValidationRequest.java',
'ch/boye/httpclientandroidlib/impl/client/cache/AsynchronousValidator.java',
'ch/boye/httpclientandroidlib/impl/client/cache/BasicHttpCache.java',
'ch/boye/httpclientandroidlib/impl/client/cache/BasicHttpCacheStorage.java',
'ch/boye/httpclientandroidlib/impl/client/cache/BasicIdGenerator.java',
'ch/boye/httpclientandroidlib/impl/client/cache/CacheableRequestPolicy.java',
'ch/boye/httpclientandroidlib/impl/client/cache/CacheConfig.java',
'ch/boye/httpclientandroidlib/impl/client/cache/CachedHttpResponseGenerator.java',
'ch/boye/httpclientandroidlib/impl/client/cache/CachedResponseSuitabilityChecker.java',
'ch/boye/httpclientandroidlib/impl/client/cache/CacheEntity.java',
'ch/boye/httpclientandroidlib/impl/client/cache/CacheEntryUpdater.java',
'ch/boye/httpclientandroidlib/impl/client/cache/CacheInvalidator.java',
'ch/boye/httpclientandroidlib/impl/client/cache/CacheKeyGenerator.java',
'ch/boye/httpclientandroidlib/impl/client/cache/CacheMap.java',
'ch/boye/httpclientandroidlib/impl/client/cache/CacheValidityPolicy.java',
'ch/boye/httpclientandroidlib/impl/client/cache/CachingExec.java',
'ch/boye/httpclientandroidlib/impl/client/cache/CachingHttpClientBuilder.java',
'ch/boye/httpclientandroidlib/impl/client/cache/CachingHttpClients.java',
'ch/boye/httpclientandroidlib/impl/client/cache/CombinedEntity.java',
'ch/boye/httpclientandroidlib/impl/client/cache/ConditionalRequestBuilder.java',
'ch/boye/httpclientandroidlib/impl/client/cache/DefaultFailureCache.java',
'ch/boye/httpclientandroidlib/impl/client/cache/DefaultHttpCacheEntrySerializer.java',
'ch/boye/httpclientandroidlib/impl/client/cache/ExponentialBackOffSchedulingStrategy.java',
'ch/boye/httpclientandroidlib/impl/client/cache/FailureCache.java',
'ch/boye/httpclientandroidlib/impl/client/cache/FailureCacheValue.java',
'ch/boye/httpclientandroidlib/impl/client/cache/FileResource.java',
'ch/boye/httpclientandroidlib/impl/client/cache/FileResourceFactory.java',
'ch/boye/httpclientandroidlib/impl/client/cache/HeapResource.java',
'ch/boye/httpclientandroidlib/impl/client/cache/HeapResourceFactory.java',
'ch/boye/httpclientandroidlib/impl/client/cache/HttpCache.java',
'ch/boye/httpclientandroidlib/impl/client/cache/ImmediateSchedulingStrategy.java',
'ch/boye/httpclientandroidlib/impl/client/cache/IOUtils.java',
'ch/boye/httpclientandroidlib/impl/client/cache/ManagedHttpCacheStorage.java',
'ch/boye/httpclientandroidlib/impl/client/cache/OptionsHttp11Response.java',
'ch/boye/httpclientandroidlib/impl/client/cache/Proxies.java',
'ch/boye/httpclientandroidlib/impl/client/cache/RequestProtocolCompliance.java',
'ch/boye/httpclientandroidlib/impl/client/cache/RequestProtocolError.java',
'ch/boye/httpclientandroidlib/impl/client/cache/ResourceReference.java',
'ch/boye/httpclientandroidlib/impl/client/cache/ResponseCachingPolicy.java',
'ch/boye/httpclientandroidlib/impl/client/cache/ResponseProtocolCompliance.java',
'ch/boye/httpclientandroidlib/impl/client/cache/ResponseProxyHandler.java',
'ch/boye/httpclientandroidlib/impl/client/cache/SchedulingStrategy.java',
'ch/boye/httpclientandroidlib/impl/client/cache/SizeLimitedResponseReader.java',
'ch/boye/httpclientandroidlib/impl/client/cache/Variant.java',
'ch/boye/httpclientandroidlib/impl/client/cache/WarningValue.java',
'ch/boye/httpclientandroidlib/impl/client/ClientParamsStack.java',
'ch/boye/httpclientandroidlib/impl/client/Clock.java',
'ch/boye/httpclientandroidlib/impl/client/CloseableHttpClient.java',
'ch/boye/httpclientandroidlib/impl/client/CloseableHttpResponseProxy.java',
'ch/boye/httpclientandroidlib/impl/client/ContentEncodingHttpClient.java',
'ch/boye/httpclientandroidlib/impl/client/DecompressingHttpClient.java',
'ch/boye/httpclientandroidlib/impl/client/DefaultBackoffStrategy.java',
'ch/boye/httpclientandroidlib/impl/client/DefaultConnectionKeepAliveStrategy.java',
'ch/boye/httpclientandroidlib/impl/client/DefaultHttpClient.java',
'ch/boye/httpclientandroidlib/impl/client/DefaultHttpRequestRetryHandler.java',
@ -224,41 +401,86 @@ sync_thirdparty_java_files = [
'ch/boye/httpclientandroidlib/impl/client/DefaultRedirectStrategy.java',
'ch/boye/httpclientandroidlib/impl/client/DefaultRedirectStrategyAdaptor.java',
'ch/boye/httpclientandroidlib/impl/client/DefaultRequestDirector.java',
'ch/boye/httpclientandroidlib/impl/client/DefaultServiceUnavailableRetryStrategy.java',
'ch/boye/httpclientandroidlib/impl/client/DefaultTargetAuthenticationHandler.java',
'ch/boye/httpclientandroidlib/impl/client/DefaultUserTokenHandler.java',
'ch/boye/httpclientandroidlib/impl/client/EntityEnclosingRequestWrapper.java',
'ch/boye/httpclientandroidlib/impl/client/FutureRequestExecutionMetrics.java',
'ch/boye/httpclientandroidlib/impl/client/FutureRequestExecutionService.java',
'ch/boye/httpclientandroidlib/impl/client/HttpAuthenticator.java',
'ch/boye/httpclientandroidlib/impl/client/HttpClientBuilder.java',
'ch/boye/httpclientandroidlib/impl/client/HttpClients.java',
'ch/boye/httpclientandroidlib/impl/client/HttpRequestFutureTask.java',
'ch/boye/httpclientandroidlib/impl/client/HttpRequestTaskCallable.java',
'ch/boye/httpclientandroidlib/impl/client/InternalHttpClient.java',
'ch/boye/httpclientandroidlib/impl/client/LaxRedirectStrategy.java',
'ch/boye/httpclientandroidlib/impl/client/MinimalHttpClient.java',
'ch/boye/httpclientandroidlib/impl/client/NoopUserTokenHandler.java',
'ch/boye/httpclientandroidlib/impl/client/NullBackoffStrategy.java',
'ch/boye/httpclientandroidlib/impl/client/package-info.java',
'ch/boye/httpclientandroidlib/impl/client/ProxyAuthenticationStrategy.java',
'ch/boye/httpclientandroidlib/impl/client/ProxyClient.java',
'ch/boye/httpclientandroidlib/impl/client/RedirectLocations.java',
'ch/boye/httpclientandroidlib/impl/client/RequestWrapper.java',
'ch/boye/httpclientandroidlib/impl/client/RoutedRequest.java',
'ch/boye/httpclientandroidlib/impl/client/StandardHttpRequestRetryHandler.java',
'ch/boye/httpclientandroidlib/impl/client/SystemClock.java',
'ch/boye/httpclientandroidlib/impl/client/SystemDefaultCredentialsProvider.java',
'ch/boye/httpclientandroidlib/impl/client/SystemDefaultHttpClient.java',
'ch/boye/httpclientandroidlib/impl/client/TargetAuthenticationStrategy.java',
'ch/boye/httpclientandroidlib/impl/client/TunnelRefusedException.java',
'ch/boye/httpclientandroidlib/impl/conn/AbstractClientConnAdapter.java',
'ch/boye/httpclientandroidlib/impl/conn/AbstractPooledConnAdapter.java',
'ch/boye/httpclientandroidlib/impl/conn/AbstractPoolEntry.java',
'ch/boye/httpclientandroidlib/impl/conn/BasicClientConnectionManager.java',
'ch/boye/httpclientandroidlib/impl/conn/BasicHttpClientConnectionManager.java',
'ch/boye/httpclientandroidlib/impl/conn/ConnectionShutdownException.java',
'ch/boye/httpclientandroidlib/impl/conn/CPool.java',
'ch/boye/httpclientandroidlib/impl/conn/CPoolEntry.java',
'ch/boye/httpclientandroidlib/impl/conn/CPoolProxy.java',
'ch/boye/httpclientandroidlib/impl/conn/DefaultClientConnection.java',
'ch/boye/httpclientandroidlib/impl/conn/DefaultClientConnectionOperator.java',
'ch/boye/httpclientandroidlib/impl/conn/DefaultHttpResponseParser.java',
'ch/boye/httpclientandroidlib/impl/conn/DefaultHttpResponseParserFactory.java',
'ch/boye/httpclientandroidlib/impl/conn/DefaultHttpRoutePlanner.java',
'ch/boye/httpclientandroidlib/impl/conn/DefaultManagedHttpClientConnection.java',
'ch/boye/httpclientandroidlib/impl/conn/DefaultProxyRoutePlanner.java',
'ch/boye/httpclientandroidlib/impl/conn/DefaultResponseParser.java',
'ch/boye/httpclientandroidlib/impl/conn/HttpInetSocketAddress.java',
'ch/boye/httpclientandroidlib/impl/conn/DefaultRoutePlanner.java',
'ch/boye/httpclientandroidlib/impl/conn/DefaultSchemePortResolver.java',
'ch/boye/httpclientandroidlib/impl/conn/HttpClientConnectionOperator.java',
'ch/boye/httpclientandroidlib/impl/conn/HttpConnPool.java',
'ch/boye/httpclientandroidlib/impl/conn/HttpPoolEntry.java',
'ch/boye/httpclientandroidlib/impl/conn/IdleConnectionHandler.java',
'ch/boye/httpclientandroidlib/impl/conn/InMemoryDnsResolver.java',
'ch/boye/httpclientandroidlib/impl/conn/LoggingInputStream.java',
'ch/boye/httpclientandroidlib/impl/conn/LoggingManagedHttpClientConnection.java',
'ch/boye/httpclientandroidlib/impl/conn/LoggingOutputStream.java',
'ch/boye/httpclientandroidlib/impl/conn/LoggingSessionInputBuffer.java',
'ch/boye/httpclientandroidlib/impl/conn/LoggingSessionOutputBuffer.java',
'ch/boye/httpclientandroidlib/impl/conn/ManagedClientConnectionImpl.java',
'ch/boye/httpclientandroidlib/impl/conn/ManagedHttpClientConnectionFactory.java',
'ch/boye/httpclientandroidlib/impl/conn/package-info.java',
'ch/boye/httpclientandroidlib/impl/conn/PoolingClientConnectionManager.java',
'ch/boye/httpclientandroidlib/impl/conn/PoolingHttpClientConnectionManager.java',
'ch/boye/httpclientandroidlib/impl/conn/ProxySelectorRoutePlanner.java',
'ch/boye/httpclientandroidlib/impl/conn/SchemeRegistryFactory.java',
'ch/boye/httpclientandroidlib/impl/conn/SingleClientConnManager.java',
'ch/boye/httpclientandroidlib/impl/conn/SystemDefaultDnsResolver.java',
'ch/boye/httpclientandroidlib/impl/conn/SystemDefaultRoutePlanner.java',
'ch/boye/httpclientandroidlib/impl/conn/tsccm/AbstractConnPool.java',
'ch/boye/httpclientandroidlib/impl/conn/tsccm/BasicPooledConnAdapter.java',
'ch/boye/httpclientandroidlib/impl/conn/tsccm/BasicPoolEntry.java',
'ch/boye/httpclientandroidlib/impl/conn/tsccm/BasicPoolEntryRef.java',
'ch/boye/httpclientandroidlib/impl/conn/tsccm/ConnPoolByRoute.java',
'ch/boye/httpclientandroidlib/impl/conn/tsccm/package-info.java',
'ch/boye/httpclientandroidlib/impl/conn/tsccm/PoolEntryRequest.java',
'ch/boye/httpclientandroidlib/impl/conn/tsccm/RefQueueHandler.java',
'ch/boye/httpclientandroidlib/impl/conn/tsccm/RefQueueWorker.java',
'ch/boye/httpclientandroidlib/impl/conn/tsccm/RouteSpecificPool.java',
'ch/boye/httpclientandroidlib/impl/conn/tsccm/ThreadSafeClientConnManager.java',
'ch/boye/httpclientandroidlib/impl/conn/tsccm/WaitingThread.java',
'ch/boye/httpclientandroidlib/impl/conn/tsccm/WaitingThreadAborter.java',
'ch/boye/httpclientandroidlib/impl/conn/Wire.java',
'ch/boye/httpclientandroidlib/impl/ConnSupport.java',
'ch/boye/httpclientandroidlib/impl/cookie/AbstractCookieAttributeHandler.java',
'ch/boye/httpclientandroidlib/impl/cookie/AbstractCookieSpec.java',
'ch/boye/httpclientandroidlib/impl/cookie/BasicClientCookie.java',
@ -273,6 +495,7 @@ sync_thirdparty_java_files = [
'ch/boye/httpclientandroidlib/impl/cookie/BestMatchSpecFactory.java',
'ch/boye/httpclientandroidlib/impl/cookie/BrowserCompatSpec.java',
'ch/boye/httpclientandroidlib/impl/cookie/BrowserCompatSpecFactory.java',
'ch/boye/httpclientandroidlib/impl/cookie/BrowserCompatVersionAttributeHandler.java',
'ch/boye/httpclientandroidlib/impl/cookie/CookieSpecBase.java',
'ch/boye/httpclientandroidlib/impl/cookie/DateParseException.java',
'ch/boye/httpclientandroidlib/impl/cookie/DateUtils.java',
@ -282,6 +505,7 @@ sync_thirdparty_java_files = [
'ch/boye/httpclientandroidlib/impl/cookie/NetscapeDraftHeaderParser.java',
'ch/boye/httpclientandroidlib/impl/cookie/NetscapeDraftSpec.java',
'ch/boye/httpclientandroidlib/impl/cookie/NetscapeDraftSpecFactory.java',
'ch/boye/httpclientandroidlib/impl/cookie/package-info.java',
'ch/boye/httpclientandroidlib/impl/cookie/PublicSuffixFilter.java',
'ch/boye/httpclientandroidlib/impl/cookie/PublicSuffixListParser.java',
'ch/boye/httpclientandroidlib/impl/cookie/RFC2109DomainHandler.java',
@ -295,16 +519,37 @@ sync_thirdparty_java_files = [
'ch/boye/httpclientandroidlib/impl/cookie/RFC2965Spec.java',
'ch/boye/httpclientandroidlib/impl/cookie/RFC2965SpecFactory.java',
'ch/boye/httpclientandroidlib/impl/cookie/RFC2965VersionAttributeHandler.java',
'ch/boye/httpclientandroidlib/impl/DefaultBHttpClientConnection.java',
'ch/boye/httpclientandroidlib/impl/DefaultBHttpClientConnectionFactory.java',
'ch/boye/httpclientandroidlib/impl/DefaultBHttpServerConnection.java',
'ch/boye/httpclientandroidlib/impl/DefaultBHttpServerConnectionFactory.java',
'ch/boye/httpclientandroidlib/impl/DefaultConnectionReuseStrategy.java',
'ch/boye/httpclientandroidlib/impl/DefaultHttpClientConnection.java',
'ch/boye/httpclientandroidlib/impl/DefaultHttpRequestFactory.java',
'ch/boye/httpclientandroidlib/impl/DefaultHttpResponseFactory.java',
'ch/boye/httpclientandroidlib/impl/DefaultHttpServerConnection.java',
'ch/boye/httpclientandroidlib/impl/EnglishReasonPhraseCatalog.java',
'ch/boye/httpclientandroidlib/impl/entity/DisallowIdentityContentLengthStrategy.java',
'ch/boye/httpclientandroidlib/impl/entity/EntityDeserializer.java',
'ch/boye/httpclientandroidlib/impl/entity/EntitySerializer.java',
'ch/boye/httpclientandroidlib/impl/entity/LaxContentLengthStrategy.java',
'ch/boye/httpclientandroidlib/impl/entity/package-info.java',
'ch/boye/httpclientandroidlib/impl/entity/StrictContentLengthStrategy.java',
'ch/boye/httpclientandroidlib/impl/execchain/BackoffStrategyExec.java',
'ch/boye/httpclientandroidlib/impl/execchain/ClientExecChain.java',
'ch/boye/httpclientandroidlib/impl/execchain/ConnectionHolder.java',
'ch/boye/httpclientandroidlib/impl/execchain/HttpResponseProxy.java',
'ch/boye/httpclientandroidlib/impl/execchain/MainClientExec.java',
'ch/boye/httpclientandroidlib/impl/execchain/MinimalClientExec.java',
'ch/boye/httpclientandroidlib/impl/execchain/package-info.java',
'ch/boye/httpclientandroidlib/impl/execchain/ProtocolExec.java',
'ch/boye/httpclientandroidlib/impl/execchain/RedirectExec.java',
'ch/boye/httpclientandroidlib/impl/execchain/RequestAbortedException.java',
'ch/boye/httpclientandroidlib/impl/execchain/RequestEntityProxy.java',
'ch/boye/httpclientandroidlib/impl/execchain/ResponseEntityProxy.java',
'ch/boye/httpclientandroidlib/impl/execchain/RetryExec.java',
'ch/boye/httpclientandroidlib/impl/execchain/ServiceUnavailableRetryExec.java',
'ch/boye/httpclientandroidlib/impl/execchain/TunnelRefusedException.java',
'ch/boye/httpclientandroidlib/impl/HttpConnectionMetricsImpl.java',
'ch/boye/httpclientandroidlib/impl/io/AbstractMessageParser.java',
'ch/boye/httpclientandroidlib/impl/io/AbstractMessageWriter.java',
@ -314,6 +559,14 @@ sync_thirdparty_java_files = [
'ch/boye/httpclientandroidlib/impl/io/ChunkedOutputStream.java',
'ch/boye/httpclientandroidlib/impl/io/ContentLengthInputStream.java',
'ch/boye/httpclientandroidlib/impl/io/ContentLengthOutputStream.java',
'ch/boye/httpclientandroidlib/impl/io/DefaultHttpRequestParser.java',
'ch/boye/httpclientandroidlib/impl/io/DefaultHttpRequestParserFactory.java',
'ch/boye/httpclientandroidlib/impl/io/DefaultHttpRequestWriter.java',
'ch/boye/httpclientandroidlib/impl/io/DefaultHttpRequestWriterFactory.java',
'ch/boye/httpclientandroidlib/impl/io/DefaultHttpResponseParser.java',
'ch/boye/httpclientandroidlib/impl/io/DefaultHttpResponseParserFactory.java',
'ch/boye/httpclientandroidlib/impl/io/DefaultHttpResponseWriter.java',
'ch/boye/httpclientandroidlib/impl/io/DefaultHttpResponseWriterFactory.java',
'ch/boye/httpclientandroidlib/impl/io/HttpRequestParser.java',
'ch/boye/httpclientandroidlib/impl/io/HttpRequestWriter.java',
'ch/boye/httpclientandroidlib/impl/io/HttpResponseParser.java',
@ -321,16 +574,27 @@ sync_thirdparty_java_files = [
'ch/boye/httpclientandroidlib/impl/io/HttpTransportMetricsImpl.java',
'ch/boye/httpclientandroidlib/impl/io/IdentityInputStream.java',
'ch/boye/httpclientandroidlib/impl/io/IdentityOutputStream.java',
'ch/boye/httpclientandroidlib/impl/io/package-info.java',
'ch/boye/httpclientandroidlib/impl/io/SessionInputBufferImpl.java',
'ch/boye/httpclientandroidlib/impl/io/SessionOutputBufferImpl.java',
'ch/boye/httpclientandroidlib/impl/io/SocketInputBuffer.java',
'ch/boye/httpclientandroidlib/impl/io/SocketOutputBuffer.java',
'ch/boye/httpclientandroidlib/impl/NoConnectionReuseStrategy.java',
'ch/boye/httpclientandroidlib/impl/package-info.java',
'ch/boye/httpclientandroidlib/impl/pool/BasicConnFactory.java',
'ch/boye/httpclientandroidlib/impl/pool/BasicConnPool.java',
'ch/boye/httpclientandroidlib/impl/pool/BasicPoolEntry.java',
'ch/boye/httpclientandroidlib/impl/pool/package-info.java',
'ch/boye/httpclientandroidlib/impl/SocketHttpClientConnection.java',
'ch/boye/httpclientandroidlib/impl/SocketHttpServerConnection.java',
'ch/boye/httpclientandroidlib/io/BufferInfo.java',
'ch/boye/httpclientandroidlib/io/EofSensor.java',
'ch/boye/httpclientandroidlib/io/HttpMessageParser.java',
'ch/boye/httpclientandroidlib/io/HttpMessageParserFactory.java',
'ch/boye/httpclientandroidlib/io/HttpMessageWriter.java',
'ch/boye/httpclientandroidlib/io/HttpMessageWriterFactory.java',
'ch/boye/httpclientandroidlib/io/HttpTransportMetrics.java',
'ch/boye/httpclientandroidlib/io/package-info.java',
'ch/boye/httpclientandroidlib/io/SessionInputBuffer.java',
'ch/boye/httpclientandroidlib/io/SessionOutputBuffer.java',
'ch/boye/httpclientandroidlib/MalformedChunkCodingException.java',
@ -357,10 +621,13 @@ sync_thirdparty_java_files = [
'ch/boye/httpclientandroidlib/message/HeaderValueParser.java',
'ch/boye/httpclientandroidlib/message/LineFormatter.java',
'ch/boye/httpclientandroidlib/message/LineParser.java',
'ch/boye/httpclientandroidlib/message/package-info.java',
'ch/boye/httpclientandroidlib/message/ParserCursor.java',
'ch/boye/httpclientandroidlib/MessageConstraintException.java',
'ch/boye/httpclientandroidlib/MethodNotSupportedException.java',
'ch/boye/httpclientandroidlib/NameValuePair.java',
'ch/boye/httpclientandroidlib/NoHttpResponseException.java',
'ch/boye/httpclientandroidlib/package-info.java',
'ch/boye/httpclientandroidlib/params/AbstractHttpParams.java',
'ch/boye/httpclientandroidlib/params/BasicHttpParams.java',
'ch/boye/httpclientandroidlib/params/CoreConnectionPNames.java',
@ -369,28 +636,46 @@ sync_thirdparty_java_files = [
'ch/boye/httpclientandroidlib/params/HttpAbstractParamBean.java',
'ch/boye/httpclientandroidlib/params/HttpConnectionParamBean.java',
'ch/boye/httpclientandroidlib/params/HttpConnectionParams.java',
'ch/boye/httpclientandroidlib/params/HttpParamConfig.java',
'ch/boye/httpclientandroidlib/params/HttpParams.java',
'ch/boye/httpclientandroidlib/params/HttpParamsNames.java',
'ch/boye/httpclientandroidlib/params/HttpProtocolParamBean.java',
'ch/boye/httpclientandroidlib/params/HttpProtocolParams.java',
'ch/boye/httpclientandroidlib/params/package-info.java',
'ch/boye/httpclientandroidlib/params/SyncBasicHttpParams.java',
'ch/boye/httpclientandroidlib/ParseException.java',
'ch/boye/httpclientandroidlib/pool/AbstractConnPool.java',
'ch/boye/httpclientandroidlib/pool/ConnFactory.java',
'ch/boye/httpclientandroidlib/pool/ConnPool.java',
'ch/boye/httpclientandroidlib/pool/ConnPoolControl.java',
'ch/boye/httpclientandroidlib/pool/package-info.java',
'ch/boye/httpclientandroidlib/pool/PoolEntry.java',
'ch/boye/httpclientandroidlib/pool/PoolEntryCallback.java',
'ch/boye/httpclientandroidlib/pool/PoolEntryFuture.java',
'ch/boye/httpclientandroidlib/pool/PoolStats.java',
'ch/boye/httpclientandroidlib/pool/RouteSpecificPool.java',
'ch/boye/httpclientandroidlib/protocol/BasicHttpContext.java',
'ch/boye/httpclientandroidlib/protocol/BasicHttpProcessor.java',
'ch/boye/httpclientandroidlib/protocol/ChainBuilder.java',
'ch/boye/httpclientandroidlib/protocol/DefaultedHttpContext.java',
'ch/boye/httpclientandroidlib/protocol/ExecutionContext.java',
'ch/boye/httpclientandroidlib/protocol/HTTP.java',
'ch/boye/httpclientandroidlib/protocol/HttpContext.java',
'ch/boye/httpclientandroidlib/protocol/HttpCoreContext.java',
'ch/boye/httpclientandroidlib/protocol/HttpDateGenerator.java',
'ch/boye/httpclientandroidlib/protocol/HttpExpectationVerifier.java',
'ch/boye/httpclientandroidlib/protocol/HttpProcessor.java',
'ch/boye/httpclientandroidlib/protocol/HttpProcessorBuilder.java',
'ch/boye/httpclientandroidlib/protocol/HttpRequestExecutor.java',
'ch/boye/httpclientandroidlib/protocol/HttpRequestHandler.java',
'ch/boye/httpclientandroidlib/protocol/HttpRequestHandlerMapper.java',
'ch/boye/httpclientandroidlib/protocol/HttpRequestHandlerRegistry.java',
'ch/boye/httpclientandroidlib/protocol/HttpRequestHandlerResolver.java',
'ch/boye/httpclientandroidlib/protocol/HttpRequestInterceptorList.java',
'ch/boye/httpclientandroidlib/protocol/HttpResponseInterceptorList.java',
'ch/boye/httpclientandroidlib/protocol/HttpService.java',
'ch/boye/httpclientandroidlib/protocol/ImmutableHttpProcessor.java',
'ch/boye/httpclientandroidlib/protocol/package-info.java',
'ch/boye/httpclientandroidlib/protocol/RequestConnControl.java',
'ch/boye/httpclientandroidlib/protocol/RequestContent.java',
'ch/boye/httpclientandroidlib/protocol/RequestDate.java',
@ -402,6 +687,7 @@ sync_thirdparty_java_files = [
'ch/boye/httpclientandroidlib/protocol/ResponseDate.java',
'ch/boye/httpclientandroidlib/protocol/ResponseServer.java',
'ch/boye/httpclientandroidlib/protocol/SyncBasicHttpContext.java',
'ch/boye/httpclientandroidlib/protocol/UriHttpRequestHandlerMapper.java',
'ch/boye/httpclientandroidlib/protocol/UriPatternMatcher.java',
'ch/boye/httpclientandroidlib/ProtocolException.java',
'ch/boye/httpclientandroidlib/ProtocolVersion.java',
@ -411,12 +697,18 @@ sync_thirdparty_java_files = [
'ch/boye/httpclientandroidlib/TokenIterator.java',
'ch/boye/httpclientandroidlib/TruncatedChunkException.java',
'ch/boye/httpclientandroidlib/UnsupportedHttpVersionException.java',
'ch/boye/httpclientandroidlib/util/Args.java',
'ch/boye/httpclientandroidlib/util/Asserts.java',
'ch/boye/httpclientandroidlib/util/ByteArrayBuffer.java',
'ch/boye/httpclientandroidlib/util/CharArrayBuffer.java',
'ch/boye/httpclientandroidlib/util/CharsetUtils.java',
'ch/boye/httpclientandroidlib/util/EncodingUtils.java',
'ch/boye/httpclientandroidlib/util/EntityUtils.java',
'ch/boye/httpclientandroidlib/util/ExceptionUtils.java',
'ch/boye/httpclientandroidlib/util/LangUtils.java',
'ch/boye/httpclientandroidlib/util/NetUtils.java',
'ch/boye/httpclientandroidlib/util/package-info.java',
'ch/boye/httpclientandroidlib/util/TextUtils.java',
'ch/boye/httpclientandroidlib/util/VersionInfo.java',
'org/json/simple/ItemList.java',
'org/json/simple/JSONArray.java',
@ -771,7 +1063,6 @@ sync_java_files = [
'sync/setup/activities/AccountActivity.java',
'sync/setup/activities/ActivityUtils.java',
'sync/setup/activities/ClientRecordArrayAdapter.java',
'sync/setup/activities/LocaleAware.java',
'sync/setup/activities/RedirectToSetupActivity.java',
'sync/setup/activities/SendTabActivity.java',
'sync/setup/activities/SendTabData.java',

View File

@ -24,7 +24,6 @@ import android.database.SQLException;
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Build;
import android.util.SparseArray;
/**
@ -234,22 +233,14 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
return parent.getAbsolutePath() + File.separator + name;
}
public static boolean CAN_USE_ABSOLUTE_DB_PATH = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO);
public HealthReportSQLiteOpenHelper(Context context, File profileDirectory, String name) {
this(context, profileDirectory, name, CURRENT_VERSION);
}
// For testing DBs of different versions.
public HealthReportSQLiteOpenHelper(Context context, File profileDirectory, String name, int version) {
super(
(CAN_USE_ABSOLUTE_DB_PATH ? context : new AbsolutePathContext(context, profileDirectory)),
(CAN_USE_ABSOLUTE_DB_PATH ? getAbsolutePath(profileDirectory, name) : name),
null,
version);
if (CAN_USE_ABSOLUTE_DB_PATH) {
Logger.pii(LOG_TAG, "Opening: " + getAbsolutePath(profileDirectory, name));
}
super(context, getAbsolutePath(profileDirectory, name), null, version);
Logger.pii(LOG_TAG, "Opening: " + getAbsolutePath(profileDirectory, name));
}
@Override

View File

@ -9,7 +9,7 @@ import org.mozilla.gecko.background.fxa.FxAccountAgeLockoutHelper;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
import org.mozilla.gecko.sync.setup.activities.LocaleAware.LocaleAwareActivity;
import org.mozilla.gecko.LocaleAware.LocaleAwareActivity;
import android.accounts.Account;
import android.app.Activity;

View File

@ -12,7 +12,7 @@ import org.mozilla.gecko.background.fxa.FxAccountAgeLockoutHelper;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
import org.mozilla.gecko.sync.setup.activities.LocaleAware;
import org.mozilla.gecko.LocaleAware;
import android.accounts.AccountAuthenticatorActivity;
import android.content.Intent;

View File

@ -4,18 +4,33 @@
package org.mozilla.gecko.fxa.activities;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.R;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.sync.setup.activities.LocaleAware.LocaleAwareFragmentActivity;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.LocaleAware.LocaleAwareFragmentActivity;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.ActionBar;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.util.TypedValue;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.Toast;
/**
* Activity which displays account status.
@ -93,14 +108,81 @@ public class FxAccountStatusActivity extends LocaleAwareFragmentActivity {
return new AndroidFxAccount(this, account);
}
/**
* Helper function to maybe remove the given Android account.
*/
@SuppressLint("InlinedApi")
public void maybeDeleteAndroidAccount(final Account account) {
if (account == null) {
Logger.warn(LOG_TAG, "Trying to delete null account; ignoring request.");
return;
}
final AccountManagerCallback<Boolean> callback = new AccountManagerCallback<Boolean>() {
@Override
public void run(AccountManagerFuture<Boolean> future) {
Logger.info(LOG_TAG, "Account " + Utils.obfuscateEmail(account.name) + " removed.");
final Activity activity = FxAccountStatusActivity.this;
final String text = activity.getResources().getString(R.string.fxaccount_remove_account_toast, account.name);
Toast.makeText(activity, text, Toast.LENGTH_LONG).show();
finish();
}
};
/*
* Get the best dialog icon from the theme on v11+.
* See http://stackoverflow.com/questions/14910536/android-dialog-theme-makes-icon-too-light/14910945#14910945.
*/
final int icon;
if (AppConstants.Versions.feature11Plus) {
final TypedValue typedValue = new TypedValue();
getTheme().resolveAttribute(android.R.attr.alertDialogIcon, typedValue, true);
icon = typedValue.resourceId;
} else {
icon = android.R.drawable.ic_dialog_alert;
}
final AlertDialog dialog = new AlertDialog.Builder(this)
.setTitle(R.string.fxaccount_remove_account_dialog_title)
.setIcon(icon)
.setMessage(R.string.fxaccount_remove_account_dialog_message)
.setPositiveButton(android.R.string.ok, new Dialog.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
AccountManager.get(FxAccountStatusActivity.this).removeAccount(account, callback, null);
}
})
.setNegativeButton(android.R.string.cancel, new Dialog.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
}
})
.create();
dialog.show();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int itemId = item.getItemId();
switch (itemId) {
case android.R.id.home:
if (itemId == android.R.id.home) {
finish();
return true;
} else if (itemId == R.id.remove_account) {
maybeDeleteAndroidAccount(FirefoxAccounts.getFirefoxAccount(this));
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
final MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.fxaccount_status_menu, menu);
return super.onCreateOptionsMenu(menu);
};
}

View File

@ -21,6 +21,7 @@ import org.mozilla.gecko.fxa.tasks.FxAccountCodeResender;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
import org.mozilla.gecko.sync.SyncConfiguration;
import org.mozilla.gecko.util.HardwareUtils;
import android.accounts.Account;
import android.content.ContentResolver;
@ -84,6 +85,7 @@ public class FxAccountStatusFragment
protected EditTextPreference deviceNamePreference;
protected Preference syncServerPreference;
protected Preference morePreference;
protected volatile AndroidFxAccount fxAccount;
// The contract is: when fxAccount is non-null, then clientsDataDelegate is
@ -111,6 +113,13 @@ public class FxAccountStatusFragment
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// We need to do this before we can query the hardware menu button state.
// We're guaranteed to have an activity at this point (onAttach is called
// before onCreate). It's okay to call this multiple times (with different
// contexts).
HardwareUtils.init(getActivity());
addPreferences();
}
@ -155,6 +164,12 @@ public class FxAccountStatusFragment
deviceNamePreference.setOnPreferenceChangeListener(this);
syncServerPreference = ensureFindPreference("sync_server");
morePreference = ensureFindPreference("more");
morePreference.setOnPreferenceClickListener(this);
if (HardwareUtils.hasMenuButton()) {
syncCategory.removePreference(morePreference);
}
}
/**
@ -209,6 +224,11 @@ public class FxAccountStatusFragment
return true;
}
if (preference == morePreference) {
getActivity().openOptionsMenu();
return true;
}
return false;
}

View File

@ -1344,7 +1344,8 @@ class JavaPanZoomController
@Override
public void onLongPress(MotionEvent motionEvent) {
sendPointToGecko("Gesture:LongPress", motionEvent);
GeckoEvent e = GeckoEvent.createLongPressEvent(motionEvent);
GeckoAppShell.sendEventToGecko(e);
}
@Override

View File

@ -206,6 +206,15 @@
the two uses differently. -->
<!ENTITY fxaccount_status_linktos 'Terms of Service'>
<!ENTITY fxaccount_status_linkprivacy 'Privacy Notice'>
<!ENTITY fxaccount_status_more 'More&ellipsis;'>
<!ENTITY fxaccount_remove_account_dialog_title 'Remove Firefox Account?'>
<!ENTITY fxaccount_remove_account_dialog_message '&brandShortName; will stop syncing with your account, but wont delete any of your browsing data on this device.'>
<!-- Localization note: format string below will be replaced
with the Firefox Account's email address. -->
<!ENTITY fxaccount_remove_account_toast 'Firefox Account &formatS; removed.'>
<!ENTITY fxaccount_remove_account_menu_item 'Remove Account'>
<!-- Localization note: this is the name shown by the Android system
itself for a Firefox Account. Don't localize this. -->
@ -235,6 +244,6 @@
<!ENTITY fxaccount_remote_error_COULD_NOT_CONNECT 'Cannot connect to network'>
<!ENTITY fxaccount_sync_sign_in_error_notification_title2 '&syncBrand.shortName.label; is not connected'>
<!-- Note to translators: the format string below will be replaced
<!-- Localization note: the format string below will be replaced
with the Firefox Account's email address. -->
<!ENTITY fxaccount_sync_sign_in_error_notification_text2 'Tap to sign in as &formatS;'>

View File

@ -314,6 +314,7 @@ gbjar.sources += [
'JavaAddonManager.java',
'LightweightTheme.java',
'LightweightThemeDrawable.java',
'LocaleAware.java',
'LocaleManager.java',
'MediaCastingBar.java',
'MemoryMonitor.java',
@ -368,6 +369,7 @@ gbjar.sources += [
'prompts/PromptService.java',
'prompts/TabInput.java',
'ReaderModeUtils.java',
'RemoteTabsExpandableListAdapter.java',
'Restarter.java',
'ScrollAnimator.java',
'ServiceNotificationClient.java',
@ -781,6 +783,7 @@ if CONFIG['MOZ_CRASHREPORTER']:
if CONFIG['MOZ_ANDROID_MLS_STUMBLER']:
main.included_projects += ['../FennecStumbler']
main.referenced_projects += ['../FennecStumbler']
DEFINES['MOZ_STUMBLER_API_KEY'] = CONFIG['MOZ_MOZILLA_API_KEY']
if CONFIG['MOZ_ANDROID_SEARCH_ACTIVITY']:
searchres = add_android_eclipse_library_project('FennecResourcesSearch')

View File

@ -115,6 +115,7 @@ OnSharedPreferenceChangeListener
private static final String PREFS_DEVTOOLS_REMOTE_ENABLED = "devtools.debugger.remote-enabled";
private static final String PREFS_DISPLAY_REFLOW_ON_ZOOM = "browser.zoom.reflowOnZoom";
private static final String PREFS_SYNC = NON_PREF_PREFIX + "sync";
private static final String PREFS_STUMBLER_ENABLED = AppConstants.ANDROID_PACKAGE_NAME + ".STUMBLER_PREF";
// This isn't a Gecko pref, even if it looks like one.
private static final String PREFS_BROWSER_LOCALE = "locale";
@ -849,6 +850,34 @@ OnSharedPreferenceChangeListener
broadcastAction(context, intent);
}
/**
* Broadcast the provided value as the value of the
* <code>PREFS_STUMBLER_ENABLED</code> pref.
*/
public static void broadcastStumblerPref(final Context context, final boolean value) {
Intent intent = new Intent(PREFS_STUMBLER_ENABLED)
.putExtra("pref", PREFS_GEO_REPORTING)
.putExtra("branch", GeckoSharedPrefs.APP_PREFS_NAME)
.putExtra("enabled", value)
.putExtra("moz_mozilla_api_key", AppConstants.MOZ_STUMBLER_API_KEY);
if (GeckoAppShell.getGeckoInterface() != null) {
intent.putExtra("user_agent", GeckoAppShell.getGeckoInterface().getDefaultUAString());
}
if (!AppConstants.MOZILLA_OFFICIAL) {
intent.putExtra("is_debug", true);
}
broadcastAction(context, intent);
}
/**
* Broadcast the current value of the
* <code>PREFS_STUMBLER_ENABLED</code> pref.
*/
public static void broadcastStumblerPref(final Context context) {
final boolean value = getBooleanPref(context, PREFS_GEO_REPORTING, true);
broadcastStumblerPref(context, value);
}
/**
* Return the value of the named preference in the default preferences file.
*
@ -1009,6 +1038,7 @@ OnSharedPreferenceChangeListener
// repeated background upload attempts.
broadcastHealthReportUploadPref(this, ((Boolean) newValue).booleanValue());
} else if (PREFS_GEO_REPORTING.equals(prefName)) {
broadcastStumblerPref(this, ((Boolean) newValue).booleanValue());
// Translate boolean value to int for geo reporting pref.
newValue = ((Boolean) newValue) ? 1 : 0;
} else if (handlers.containsKey(prefName)) {

View File

@ -12,7 +12,7 @@
android:paddingLeft="4dp"
android:paddingRight="4dp">
<TextView android:id="@+id/tab"
<TextView android:id="@+id/title"
style="@style/TabRowTextAppearance"
android:layout_width="match_parent"
android:layout_height="wrap_content"

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="@+id/remove_account"
android:title="@string/fxaccount_remove_account_menu_item" />
</menu>

View File

@ -78,12 +78,18 @@
android:persistent="false"
android:title="@string/fxaccount_status_device_name" />
<Preference
<Preference
android:editable="false"
android:key="sync_server"
android:persistent="false"
android:title="@string/fxaccount_status_sync_server" />
<Preference
android:editable="false"
android:key="more"
android:persistent="false"
android:title="@string/fxaccount_status_more" />
</PreferenceCategory>
<PreferenceCategory
android:key="legal_category"

View File

@ -27,7 +27,7 @@ import org.mozilla.gecko.sync.repositories.NullCursorException;
import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor;
import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
import org.mozilla.gecko.sync.setup.SyncAccounts;
import org.mozilla.gecko.sync.setup.activities.LocaleAware.LocaleAwareActivity;
import org.mozilla.gecko.LocaleAware.LocaleAwareActivity;
import org.mozilla.gecko.sync.syncadapter.SyncAdapter;
import android.accounts.Account;

View File

@ -5,42 +5,32 @@
package org.mozilla.gecko.tabs;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import org.mozilla.gecko.R;
import org.mozilla.gecko.RemoteTabsExpandableListAdapter;
import org.mozilla.gecko.Tabs;
import org.mozilla.gecko.TabsAccessor;
import org.mozilla.gecko.TabsAccessor.RemoteClient;
import org.mozilla.gecko.TabsAccessor.RemoteTab;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import android.content.Context;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ExpandableListView;
import android.widget.SimpleExpandableListAdapter;
/**
* The actual list of synced tabs. This serves as the only child view of {@link RemoteTabsContainerPanel}
* so it can be refreshed using a swipe-to-refresh gesture.
*/
class RemoteTabsList extends ExpandableListView
implements ExpandableListView.OnGroupClickListener,
ExpandableListView.OnChildClickListener,
TabsAccessor.OnQueryTabsCompleteListener {
private static final String[] CLIENT_KEY = new String[] { "name", "last_synced" };
private static final String[] TAB_KEY = new String[] { "title", "url" };
private static final int[] CLIENT_RESOURCE = new int[] { R.id.client, R.id.last_synced };
private static final int[] TAB_RESOURCE = new int[] { R.id.tab, R.id.url };
private final Context context;
public class RemoteTabsList extends ExpandableListView
implements ExpandableListView.OnGroupClickListener,
ExpandableListView.OnChildClickListener,
TabsAccessor.OnQueryTabsCompleteListener {
private TabsPanel tabsPanel;
private ArrayList <HashMap <String, String>> clients;
private ArrayList <ArrayList <HashMap <String, String>>> tabsList;
// A list of the clients that are currently expanded.
private List<String> expandedClientList;
@ -49,10 +39,10 @@ class RemoteTabsList extends ExpandableListView
public RemoteTabsList(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
setOnGroupClickListener(this);
setOnChildClickListener(this);
setAdapter(new RemoteTabsExpandableListAdapter(R.layout.remote_tabs_group, R.layout.remote_tabs_child, null));
}
public void setTabsPanel(TabsPanel panel) {
@ -65,7 +55,8 @@ class RemoteTabsList extends ExpandableListView
@Override
public boolean onGroupClick(ExpandableListView parent, View view, int groupPosition, long id) {
final String clientGuid = clients.get(groupPosition).get("guid");
final RemoteClient client = (RemoteClient) parent.getExpandableListAdapter().getGroup(groupPosition);
final String clientGuid = client.guid;
if (isGroupExpanded(groupPosition)) {
collapseGroup(groupPosition);
@ -81,7 +72,7 @@ class RemoteTabsList extends ExpandableListView
@Override
public boolean onChildClick(ExpandableListView parent, View view, int groupPosition, int childPosition, long id) {
HashMap <String, String> tab = tabsList.get(groupPosition).get(childPosition);
final RemoteTab tab = (RemoteTab) parent.getExpandableListAdapter().getChild(groupPosition, childPosition);
if (tab == null) {
autoHidePanel();
return true;
@ -89,64 +80,23 @@ class RemoteTabsList extends ExpandableListView
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM, "remote");
Tabs.getInstance().loadUrl(tab.get("url"), Tabs.LOADURL_NEW_TAB);
Tabs.getInstance().loadUrl(tab.url, Tabs.LOADURL_NEW_TAB);
autoHidePanel();
clientScrollPosition = clients.get(groupPosition).get("guid");
final RemoteClient client = (RemoteClient) parent.getExpandableListAdapter().getGroup(groupPosition);
clientScrollPosition = client.guid;
return true;
}
@Override
public void onQueryTabsComplete(List<TabsAccessor.RemoteTab> remoteTabsList) {
ArrayList<TabsAccessor.RemoteTab> remoteTabs = new ArrayList<TabsAccessor.RemoteTab> (remoteTabsList);
if (remoteTabs == null || remoteTabs.size() == 0)
return;
clients = new ArrayList <HashMap <String, String>>();
tabsList = new ArrayList <ArrayList <HashMap <String, String>>>();
String oldGuid = null;
ArrayList <HashMap <String, String>> tabsForClient = null;
HashMap <String, String> client;
HashMap <String, String> tab;
final long now = System.currentTimeMillis();
for (TabsAccessor.RemoteTab remoteTab : remoteTabs) {
final String clientGuid = remoteTab.guid;
if (oldGuid == null || !TextUtils.equals(oldGuid, clientGuid)) {
client = new HashMap <String, String>();
client.put("name", remoteTab.name);
client.put("last_synced", getLastSyncedString(now, remoteTab.lastModified));
client.put("guid", clientGuid);
clients.add(client);
tabsForClient = new ArrayList <HashMap <String, String>>();
tabsList.add(tabsForClient);
oldGuid = new String(clientGuid);
}
tab = new HashMap<String, String>();
tab.put("title", TextUtils.isEmpty(remoteTab.title) ? remoteTab.url : remoteTab.title);
tab.put("url", remoteTab.url);
tabsForClient.add(tab);
}
setAdapter(new SimpleExpandableListAdapter(context,
clients,
R.layout.remote_tabs_group,
CLIENT_KEY,
CLIENT_RESOURCE,
tabsList,
R.layout.remote_tabs_child,
TAB_KEY,
TAB_RESOURCE));
public void onQueryTabsComplete(List<RemoteClient> clients) {
((RemoteTabsExpandableListAdapter) getExpandableListAdapter()).replaceClients(clients);
// Either set the initial UI state, or restore it.
List<String> newExpandedClientList = new ArrayList<String>();
for (int i = 0; i < clients.size(); i++) {
final String clientGuid = clients.get(i).get("guid");
final String clientGuid = clients.get(i).guid;
if (expandedClientList == null) {
// On initial entry we expand all clients by default.
@ -167,16 +117,4 @@ class RemoteTabsList extends ExpandableListView
}
expandedClientList = newExpandedClientList;
}
/**
* Return a relative "Last synced" time span for the given tab record.
*
* @param now local time.
* @param time to format string for.
* @return string describing time span
*/
protected String getLastSyncedString(long now, long time) {
CharSequence relativeTimeSpanString = DateUtils.getRelativeTimeSpanString(time, now, DateUtils.MINUTE_IN_MILLIS);
return getResources().getString(R.string.remote_tabs_last_synced, relativeTimeSpanString);
}
}

View File

@ -702,7 +702,7 @@ var SelectionHandler = {
attachCaret: function sh_attachCaret(aElement) {
// Ensure it isn't disabled, isn't handled by Android native dialog, and is editable text element
if (aElement.disabled || InputWidgetHelper.hasInputWidget(aElement) || !this.isElementEditableText(aElement)) {
return;
return false;
}
this._initTargetInfo(aElement, this.TYPE_CURSOR);
@ -722,6 +722,8 @@ var SelectionHandler = {
handles: [this.HANDLE_TYPE_MIDDLE]
});
this._updateMenu();
return true;
},
// Target initialization for both TYPE_CURSOR and TYPE_SELECTION

View File

@ -2044,16 +2044,17 @@ var NativeWindow = {
}
}
},
contextmenus: {
items: {}, // a list of context menu items that we may show
DEFAULT_HTML5_ORDER: -1, // Sort order for HTML5 context menu items
init: function() {
Services.obs.addObserver(this, "Gesture:LongPress", false);
BrowserApp.deck.addEventListener("contextmenu", this.show.bind(this), false);
},
uninit: function() {
Services.obs.removeObserver(this, "Gesture:LongPress");
BrowserApp.deck.removeEventListener("contextmenu", this.show.bind(this), false);
},
add: function() {
@ -2295,7 +2296,7 @@ var NativeWindow = {
},
// Returns true if there are any context menu items to show
shouldShow: function() {
_shouldShow: function() {
for (let context in this.menus) {
let menu = this.menus[context];
if (menu.length > 0) {
@ -2378,36 +2379,51 @@ var NativeWindow = {
* any html5 context menus we are about to show, and fire some local notifications
* for chrome consumers to do lazy menuitem construction
*/
_sendToContent: function(x, y) {
let target = this._findTarget(x, y);
if (!target)
show: function(event) {
// Android Long-press / contextmenu event provides clientX/Y data. This is not provided
// by mochitest: test_browserElement_inproc_ContextmenuEvents.html.
if (!event.clientX || !event.clientY) {
return;
}
this._target = target;
// Find the target of the long-press / contextmenu event.
this._target = this._findTarget(event.clientX, event.clientY);
if (!this._target) {
return;
}
Services.obs.notifyObservers(null, "before-build-contextmenu", "");
this._buildMenu(x, y);
// Try to build a list of contextmenu items. If successful, actually show the
// native context menu by passing the list to Java.
this._buildMenu(event.clientX, event.clientY);
if (this._shouldShow()) {
BrowserEventHandler._cancelTapHighlight();
// only send the contextmenu event to content if we are planning to show a context menu (i.e. not on every long tap)
if (this.shouldShow()) {
let event = target.ownerDocument.createEvent("MouseEvent");
event.initMouseEvent("contextmenu", true, true, target.defaultView,
0, x, y, x, y, false, false, false, false,
0, null);
target.ownerDocument.defaultView.addEventListener("contextmenu", this, false);
target.dispatchEvent(event);
} else {
this.menus = null;
Services.obs.notifyObservers({target: target, x: x, y: y}, "context-menu-not-shown", "");
// Consume / preventDefault the event, and show the contextmenu.
event.preventDefault();
this._innerShow(this._target, event.clientX, event.clientY);
this._target = null;
if (SelectionHandler.canSelect(target)) {
if (!SelectionHandler.startSelection(target, {
mode: SelectionHandler.SELECT_AT_POINT,
x: x,
y: y
})) {
SelectionHandler.attachCaret(target);
}
return;
}
// If no context-menu for long-press event, it may be meant to trigger text-selection.
this.menus = null;
Services.obs.notifyObservers(
{target: this._target, x: event.clientX, y: event.clientY}, "context-menu-not-shown", "");
if (SelectionHandler.canSelect(this._target)) {
// If textSelection WORD is successful,
// consume / preventDefault the context menu event.
if (SelectionHandler.startSelection(this._target,
{ mode: SelectionHandler.SELECT_AT_POINT, x: event.clientX, y: event.clientY })) {
event.preventDefault();
return;
}
// If textSelection caret-attachment is successful,
// consume / preventDefault the context menu event.
if (SelectionHandler.attachCaret(this._target)) {
event.preventDefault();
return;
}
}
},
@ -2477,17 +2493,6 @@ var NativeWindow = {
}
},
// Actually shows the native context menu by passing a list of context menu items to
// show to the Java.
_show: function(aEvent) {
let popupNode = this._target;
this._target = null;
if (aEvent.defaultPrevented || !popupNode) {
return;
}
this._innerShow(popupNode, aEvent.clientX, aEvent.clientY);
},
// Walks the DOM tree to find a title from a node
_findTitle: function(node) {
let title = "";
@ -2645,20 +2650,6 @@ var NativeWindow = {
}
},
// Called when the contextmenu is done propagating to content. If the event wasn't cancelled, will show a contextmenu.
handleEvent: function(aEvent) {
BrowserEventHandler._cancelTapHighlight();
aEvent.target.ownerDocument.defaultView.removeEventListener("contextmenu", this, false);
this._show(aEvent);
},
// Called when a long press is observed in the native Java frontend. Will start the process of generating/showing a contextmenu.
observe: function(aSubject, aTopic, aData) {
let data = JSON.parse(aData);
// content gets first crack at cancelling context menus
this._sendToContent(data.x, data.y);
},
// XXX - These are stolen from Util.js, we should remove them if we bring it back
makeURLAbsolute: function makeURLAbsolute(base, url) {
// Note: makeURI() will throw if url is not a valid URI
@ -5512,6 +5503,12 @@ var FormAssistant = {
// autocomplete suggestions
_currentInputElement: null,
// The value of the currently focused input
_currentInputValue: null,
// Whether we're in the middle of an autocomplete
_doingAutocomplete: false,
_isBlocklisted: false,
// Keep track of whether or not an invalid form has been submitted
@ -5526,6 +5523,7 @@ var FormAssistant = {
// We need to use a capturing listener for focus events
BrowserApp.deck.addEventListener("focus", this, true);
BrowserApp.deck.addEventListener("blur", this, true);
BrowserApp.deck.addEventListener("click", this, true);
BrowserApp.deck.addEventListener("input", this, false);
BrowserApp.deck.addEventListener("pageshow", this, false);
@ -5541,6 +5539,7 @@ var FormAssistant = {
Services.obs.removeObserver(this, "PanZoom:StateChange");
BrowserApp.deck.removeEventListener("focus", this);
BrowserApp.deck.removeEventListener("blur", this);
BrowserApp.deck.removeEventListener("click", this);
BrowserApp.deck.removeEventListener("input", this);
BrowserApp.deck.removeEventListener("pageshow", this);
@ -5572,6 +5571,8 @@ var FormAssistant = {
let editableElement = this._currentInputElement.QueryInterface(Ci.nsIDOMNSEditableElement);
this._doingAutocomplete = true;
// If we have an active composition string, commit it before sending
// the autocomplete event with the text that will replace it.
try {
@ -5581,10 +5582,14 @@ var FormAssistant = {
} catch (e) {}
editableElement.setUserInput(aData);
this._currentInputValue = aData;
let event = this._currentInputElement.ownerDocument.createEvent("Events");
event.initEvent("DOMAutoComplete", true, true);
this._currentInputElement.dispatchEvent(event);
this._doingAutocomplete = false;
break;
case "FormAssist:Blocklisted":
@ -5615,15 +5620,21 @@ var FormAssistant = {
handleEvent: function(aEvent) {
switch (aEvent.type) {
case "focus":
case "focus": {
let currentElement = aEvent.target;
// Only show a validation message on focus.
this._showValidationMessage(currentElement);
break;
}
case "click":
currentElement = aEvent.target;
case "blur": {
this._currentInputValue = null;
break;
}
case "click": {
let currentElement = aEvent.target;
// Prioritize a form validation message over autocomplete suggestions
// when the element is first focused (a form validation message will
@ -5639,9 +5650,21 @@ var FormAssistant = {
this._showAutoCompleteSuggestions(currentElement, checkResultsClick);
break;
}
case "input":
currentElement = aEvent.target;
case "input": {
let currentElement = aEvent.target;
// If this element isn't focused, we're already in middle of an
// autocomplete, or its value hasn't changed, don't show the
// autocomplete popup.
if (currentElement !== BrowserApp.getFocusedInput(BrowserApp.selectedBrowser) ||
this._doingAutocomplete ||
currentElement.value === this._currentInputValue) {
break;
}
this._currentInputValue = currentElement.value;
// Since we can only show one popup at a time, prioritze autocomplete
// suggestions over a form validation message
@ -5658,9 +5681,10 @@ var FormAssistant = {
this._showAutoCompleteSuggestions(currentElement, checkResultsInput);
break;
}
// Reset invalid submit state on each pageshow
case "pageshow":
case "pageshow": {
if (!this._invalidSubmit)
return;
@ -5671,6 +5695,8 @@ var FormAssistant = {
if (target == selectedDocument || target.ownerDocument == selectedDocument)
this._invalidSubmit = false;
}
break;
}
}
},

View File

@ -23,8 +23,4 @@ public class Constants {
public static final int SUGGESTION_MAX = 5;
public static final String ABOUT_BLANK = "about:blank";
// The default search engine for new users. This should match one of
// the SearchEngineFactory.Engine enum values.
public static final String DEFAULT_SEARCH_ENGINE = "YAHOO";
}

View File

@ -21,6 +21,10 @@ import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.search.providers.SearchEngine;
import org.mozilla.search.providers.SearchEngineManager;
import java.util.List;
/**
* This activity allows users to modify the settings for the search activity.
@ -40,6 +44,8 @@ public class SearchPreferenceActivity extends PreferenceActivity
public static final String PREF_CLEAR_HISTORY_KEY = "search.not_a_preference.clear_history";
public static final String PREF_SEARCH_ENGINE_KEY = "search.engines.default";
private SearchEngineManager searchEngineManager;
@Override
@SuppressWarnings("deprecation")
protected void onCreate(Bundle savedInstanceState) {
@ -47,6 +53,8 @@ public class SearchPreferenceActivity extends PreferenceActivity
getPreferenceManager().setSharedPreferencesName(GeckoSharedPrefs.APP_PREFS_NAME);
searchEngineManager = new SearchEngineManager(this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
if (getActionBar() != null) {
getActionBar().setDisplayHomeAsUpEnabled(true);
@ -54,6 +62,12 @@ public class SearchPreferenceActivity extends PreferenceActivity
}
}
@Override
protected void onDestroy() {
super.onDestroy();
searchEngineManager.destroy();
}
@Override
protected void onResume() {
super.onResume();
@ -96,12 +110,39 @@ public class SearchPreferenceActivity extends PreferenceActivity
}
});
// Set summary for search engine picker.
final ListPreference searchEnginePref = (ListPreference) findPreference(PREF_SEARCH_ENGINE_KEY);
if (searchEnginePref.getValue() == null) {
searchEnginePref.setValue(Constants.DEFAULT_SEARCH_ENGINE);
}
searchEnginePref.setSummary(searchEnginePref.getEntry());
setUpSearchEnginePref();
}
@SuppressWarnings("deprecation")
private void setUpSearchEnginePref() {
final AsyncTask<Void, Void, List<SearchEngine>> task = new AsyncTask<Void, Void, List<SearchEngine>>() {
@Override
protected List<SearchEngine> doInBackground(Void... params) {
return searchEngineManager.getAllEngines();
}
@Override
protected void onPostExecute(List<SearchEngine> engines) {
final CharSequence[] entries = new CharSequence[engines.size()];
final CharSequence[] entryValues = new CharSequence[engines.size()];
for (int i = 0; i < engines.size(); i++) {
final SearchEngine engine = engines.get(i);
entries[i] = engine.getName();
entryValues[i] = engine.getIdentifier();
}
final ListPreference searchEnginePref = (ListPreference) findPreference(PREF_SEARCH_ENGINE_KEY);
searchEnginePref.setEntries(entries);
searchEnginePref.setEntryValues(entryValues);
if (searchEnginePref.getValue() == null) {
searchEnginePref.setValue(getResources().getString(R.string.default_engine_identifier));
}
searchEnginePref.setSummary(searchEnginePref.getEntry());
}
};
task.execute();
}
private void clearHistory() {

View File

@ -5,6 +5,7 @@
package org.mozilla.search.providers;
import android.net.Uri;
import android.util.Log;
import android.util.Xml;
import org.xmlpull.v1.XmlPullParser;
@ -19,6 +20,7 @@ import java.util.Locale;
* the search activity.
*/
public class SearchEngine {
private static final String LOG_TAG = "SearchEngine";
private static final String URLTYPE_SUGGEST_JSON = "application/x-suggestions+json";
private static final String URLTYPE_SEARCH_HTML = "text/html";
@ -166,6 +168,10 @@ public class SearchEngine {
return String.format(STYLE_INJECTION_SCRIPT, css);
}
public String getIdentifier() {
return identifier;
}
public String getName() {
return shortName;
}
@ -184,6 +190,10 @@ public class SearchEngine {
* @param query The user's query. This method will escape and encode the query.
*/
public String resultsUriForQuery(String query) {
if (resultsUri == null) {
Log.e(LOG_TAG, "No results URL for search engine: " + identifier);
return "";
}
final String template = Uri.decode(resultsUri.toString());
return paramSubstitution(template, Uri.encode(query));
}
@ -194,6 +204,10 @@ public class SearchEngine {
* @param query The user's query. This method will escape and encode the query.
*/
public String getSuggestionTemplate(String query) {
if (suggestUri == null) {
Log.e(LOG_TAG, "No suggestions template for search engine: " + identifier);
return "";
}
final String template = Uri.decode(suggestUri.toString());
return paramSubstitution(template, Uri.encode(query));
}

View File

@ -13,12 +13,16 @@ import android.util.Log;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.util.GeckoJarReader;
import org.mozilla.search.Constants;
import org.mozilla.search.R;
import org.mozilla.search.SearchPreferenceActivity;
import org.xmlpull.v1.XmlPullParserException;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
public class SearchEngineManager implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String LOG_TAG = "SearchEngineManager";
@ -81,9 +85,10 @@ public class SearchEngineManager implements SharedPreferences.OnSharedPreference
final AsyncTask<Void, Void, SearchEngine> task = new AsyncTask<Void, Void, SearchEngine>() {
@Override
protected SearchEngine doInBackground(Void... params) {
final String identifier = GeckoSharedPrefs.forApp(context)
.getString(SearchPreferenceActivity.PREF_SEARCH_ENGINE_KEY, Constants.DEFAULT_SEARCH_ENGINE)
.toLowerCase();
String identifier = GeckoSharedPrefs.forApp(context).getString(SearchPreferenceActivity.PREF_SEARCH_ENGINE_KEY, null);
if (TextUtils.isEmpty(identifier)) {
identifier = context.getResources().getString(R.string.default_engine_identifier);
}
return createEngine(identifier);
}
@ -99,6 +104,55 @@ public class SearchEngineManager implements SharedPreferences.OnSharedPreference
task.execute();
}
/**
* Creates a list of SearchEngine instances from all available open search plugins.
* This method does disk I/O, call it from a background thread.
*
* @return List of SearchEngine instances
*/
public List<SearchEngine> getAllEngines() {
// First try to read the engine list from the jar.
final String url = getSearchPluginsJarUrl("list.txt");
InputStream in = GeckoJarReader.getStream(url);
// Fallback for standalone search activity.
if (in == null) {
try {
in = context.getResources().getAssets().open("engines/list.txt");
} catch (IOException e) {
throw new IllegalStateException("Error reading list.txt");
}
}
final List<SearchEngine> list = new ArrayList<SearchEngine>();
InputStreamReader isr = null;
try {
isr = new InputStreamReader(in);
BufferedReader br = new BufferedReader(isr);
String identifier;
while ((identifier = br.readLine()) != null) {
list.add(createEngine(identifier));
}
} catch (IOException e) {
throw new IllegalStateException("Error creating all search engines from list.txt");
} finally {
if (isr != null) {
try {
isr.close();
} catch (IOException e) {
// Ignore.
}
}
try {
in.close();
} catch (IOException e) {
// Ignore.
}
}
return list;
}
/**
* Creates a SearchEngine instance from an open search plugin.
* This method does disk I/O, call it from a background thread.
@ -142,7 +196,7 @@ public class SearchEngineManager implements SharedPreferences.OnSharedPreference
*/
private InputStream getEngineFromAssets(String identifier) {
try {
return context.getResources().getAssets().open(identifier + ".xml");
return context.getResources().getAssets().open("engines/" + identifier + ".xml");
} catch (IOException e) {
Log.e(LOG_TAG, "Exception getting search engine from assets", e);
return null;
@ -157,12 +211,21 @@ public class SearchEngineManager implements SharedPreferences.OnSharedPreference
* @return InputStream for open search plugin XML
*/
private InputStream getEngineFromJar(String identifier) {
final String url = getSearchPluginsJarUrl(identifier + ".xml");
return GeckoJarReader.getStream(url);
}
/**
* Gets the jar URL for a file in the searchplugins directory
*
* @param fileName
* @return
*/
private String getSearchPluginsJarUrl(String fileName) {
// TODO: Get the real value for this
final String locale = "en-US";
final String path = "!/chrome/" + locale + "/locale/" + locale + "/browser/searchplugins/" + identifier + ".xml";
final String url = "jar:jar:file://" + context.getPackageResourcePath() + "!/" + AppConstants.OMNIJAR_NAME + path;
return GeckoJarReader.getStream(url);
final String path = "!/chrome/" + locale + "/locale/" + locale + "/browser/searchplugins/" + fileName;
return "jar:jar:file://" + context.getPackageResourcePath() + "!/" + AppConstants.OMNIJAR_NAME + path;
}
}

View File

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="pref_searchProvider_entries">
<item>@string/pref_searchProvider_bing</item>
<item>@string/pref_searchProvider_google</item>
<item>@string/pref_searchProvider_yahoo</item>
</string-array>
<string-array name="pref_searchProvider_values">
<item>BING</item>
<item>GOOGLE</item>
<item>YAHOO</item>
</string-array>
</resources>

View File

@ -10,7 +10,5 @@
<ListPreference
android:key="search.engines.default"
android:title="@string/pref_searchProvider_title"
android:dialogTitle="@string/pref_searchProvider_title"
android:entries="@array/pref_searchProvider_entries"
android:entryValues="@array/pref_searchProvider_values" />
android:dialogTitle="@string/pref_searchProvider_title" />
</PreferenceScreen>

View File

@ -18,9 +18,8 @@
<!ENTITY pref_searchProvider_title 'Search engine'>
<!ENTITY pref_searchProvider_bing 'Bing'>
<!ENTITY pref_searchProvider_google 'Google'>
<!ENTITY pref_searchProvider_yahoo 'Yahoo'>
<!ENTITY search_widget_button_label 'Search'>
<!-- Localization note (default_engine_identifier): Search engine identifier for the default
engine. This should be one of the identifiers listed in /searchplugins/list.txt -->
<!ENTITY default_engine_identifier 'yahoo'>

View File

@ -12,9 +12,7 @@
<string name="pref_searchProvider_title">&pref_searchProvider_title;</string>
<string name="pref_searchProvider_bing">&pref_searchProvider_bing;</string>
<string name="pref_searchProvider_google">&pref_searchProvider_google;</string>
<string name="pref_searchProvider_yahoo">&pref_searchProvider_yahoo;</string>
<string name="search_widget_name">&search_app_name;</string>
<string name="search_widget_button_label">&search_widget_button_label;</string>
<string name="default_engine_identifier">&default_engine_identifier;</string>

View File

@ -190,6 +190,7 @@
<string name="fxaccount_status_legal">&fxaccount_status_legal;</string>
<string name="fxaccount_status_linktos">&fxaccount_status_linktos;</string>
<string name="fxaccount_status_linkprivacy">&fxaccount_status_linkprivacy;</string>
<string name="fxaccount_status_more">&fxaccount_status_more;</string>
<string name="fxaccount_label">&fxaccount_account_type_label;</string>
@ -208,3 +209,9 @@
<string name="fxaccount_sync_sign_in_error_notification_title">&fxaccount_sync_sign_in_error_notification_title2;</string>
<string name="fxaccount_sync_sign_in_error_notification_text">&fxaccount_sync_sign_in_error_notification_text2;</string>
<!-- Remove Account -->
<string name="fxaccount_remove_account_dialog_title">&fxaccount_remove_account_dialog_title;</string>
<string name="fxaccount_remove_account_dialog_message">&fxaccount_remove_account_dialog_message;</string>
<string name="fxaccount_remove_account_toast">&fxaccount_remove_account_toast;</string>
<string name="fxaccount_remove_account_menu_item">&fxaccount_remove_account_menu_item;</string>

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