summaryrefslogtreecommitdiff
path: root/data
diff options
context:
space:
mode:
Diffstat (limited to 'data')
-rw-r--r--data/patches/0001-Bug-1013064-part-1-prompt-for-master-password-unlock.patch60
-rw-r--r--data/patches/0002-Bug-1013064-part-2-Store-sensitive-FxA-credentials-i.patch551
-rw-r--r--data/patches/0003-Bug-1013064-part-3-only-migrate-data-into-the-loginm.patch194
-rw-r--r--data/patches/0004-Bug-1013064-part-4b-browserid_identity-and-sync-chang.patch286
-rw-r--r--data/patches/0005-Bug-1013064-part-5-stop-disabling-the-password-engin.patch519
-rw-r--r--data/patches/disable/0001-upgrade-mobile-fxa-to-v32.patch992
-rw-r--r--data/patches/disable/0002-update-mobile-sqlite-to-v35.patch246
-rw-r--r--data/patches/disable/0003-upgrade-mobile-sync-to-v32.patch992
-rw-r--r--data/patches/gnuzilla-bug-1010972.patch11
-rw-r--r--data/patches/gnuzilla-bug-1025689.patch19
-rw-r--r--data/patches/gnuzilla-bug-1030899-1.patch60
-rw-r--r--data/patches/gnuzilla-bug-1030899-2.patch59
-rw-r--r--data/patches/gnuzilla-bug-1032460.patch16
-rw-r--r--data/patches/gnuzilla-bug-1034167-1.patch93
-rw-r--r--data/patches/gnuzilla-bug-1034167-2.patch1205
-rw-r--r--data/patches/gnuzilla-bug-1036286-1.patch188
-rw-r--r--data/patches/gnuzilla-bug-1036286-2.patch105
-rw-r--r--data/patches/gnuzilla-bug-1050780.patch53
-rw-r--r--data/patches/gnuzilla-bug-1055166.patch33
-rw-r--r--data/patches/gnuzilla-bug-1058778.patch10
-rw-r--r--data/patches/gnuzilla-bug-1089931.patch40
-rw-r--r--data/patches/gnuzilla-bug-1091987.patch40
-rw-r--r--data/patches/gnuzilla-bug-1095298.patch30
-rw-r--r--data/patches/reduceGeckoPriority.patch18
-rw-r--r--data/patches/reorder-mobile_android_base_android-services_mozbuild.patch34
-rw-r--r--data/patches/revert-gnuzilla-bug-973138.patch25
-rw-r--r--data/patches/torbrowser-bug-12811-2.patch33
-rw-r--r--data/patches/torbrowser-bug-12811.patch19
-rw-r--r--data/patches/torbrowser-bug-9837.patch22
29 files changed, 5953 insertions, 0 deletions
diff --git a/data/patches/0001-Bug-1013064-part-1-prompt-for-master-password-unlock.patch b/data/patches/0001-Bug-1013064-part-1-prompt-for-master-password-unlock.patch
new file mode 100644
index 0000000..1880604
--- /dev/null
+++ b/data/patches/0001-Bug-1013064-part-1-prompt-for-master-password-unlock.patch
@@ -0,0 +1,60 @@
+From 18f9d132020afee6004e63c9006245e4d3b04e18 Mon Sep 17 00:00:00 2001
+From: Mark Hammond <mhammond@skippinet.com.au>
+Date: Wed, 11 Jun 2014 17:34:24 +1000
+Subject: Bug 1013064 (part 1) - prompt for master-password unlock when
+ interacting with about:accounts. r=ttaubert
+
+---
+ browser/base/content/aboutaccounts/aboutaccounts.js | 9 +++++++++
+ 1 file changed, 9 insertions(+)
+
+diff --git a/browser/base/content/aboutaccounts/aboutaccounts.js b/browser/base/content/aboutaccounts/aboutaccounts.js
+index 5cceae15..af31be9 100644
+--- a/browser/base/content/aboutaccounts/aboutaccounts.js
++++ b/browser/base/content/aboutaccounts/aboutaccounts.js
+@@ -7,16 +7,19 @@
+ const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+ Cu.import("resource://gre/modules/Services.jsm");
+ Cu.import("resource://gre/modules/FxAccounts.jsm");
+
+ let fxAccountsCommon = {};
+ Cu.import("resource://gre/modules/FxAccountsCommon.js", fxAccountsCommon);
+
++// for master-password utilities
++Cu.import("resource://services-sync/util.js");
++
+ const PREF_LAST_FXA_USER = "identity.fxaccounts.lastSignedInUserHash";
+ const PREF_SYNC_SHOW_CUSTOMIZATION = "services.sync.ui.showCustomizationDialog";
+
+ const OBSERVER_TOPICS = [
+ fxAccountsCommon.ONVERIFIED_NOTIFICATION,
+ fxAccountsCommon.ONLOGOUT_NOTIFICATION,
+ ];
+
+@@ -99,16 +102,22 @@ let wrapper = {
+ .wrappedJSObject;
+
+ // Don't show about:accounts with FxA disabled.
+ if (!weave.fxAccountsEnabled) {
+ document.body.remove();
+ return;
+ }
+
++ // If a master-password is enabled, we want to encourage the user to
++ // unlock it. Things still work if not, but the user will probably need
++ // to re-auth next startup (in which case we will get here again and
++ // re-prompt)
++ Utils.ensureMPUnlocked();
++
+ let iframe = document.getElementById("remote");
+ this.iframe = iframe;
+ iframe.addEventListener("load", this);
+
+ try {
+ iframe.src = url || fxAccounts.getAccountsSignUpURI();
+ } catch (e) {
+ error("Couldn't init Firefox Account wrapper: " + e.message);
+--
+1.8.3.msysgit.0
+
diff --git a/data/patches/0002-Bug-1013064-part-2-Store-sensitive-FxA-credentials-i.patch b/data/patches/0002-Bug-1013064-part-2-Store-sensitive-FxA-credentials-i.patch
new file mode 100644
index 0000000..bbd6f8d
--- /dev/null
+++ b/data/patches/0002-Bug-1013064-part-2-Store-sensitive-FxA-credentials-i.patch
@@ -0,0 +1,551 @@
+From 4a92f9ee1ba35989f82a24bba18806f8616a5be8 Mon Sep 17 00:00:00 2001
+From: Mark Hammond <mhammond@skippinet.com.au>
+Date: Sat, 14 Jun 2014 14:33:20 +1000
+Subject: Bug 1013064 (part 2) - Store sensitive FxA credentials in the login
+ manager. r=ckarlof,dveditz
+
+---
+ services/fxaccounts/FxAccounts.jsm | 170 ++++++++++++++++++
+ services/fxaccounts/FxAccountsCommon.js | 13 ++
+ services/fxaccounts/moz.build | 5 +-
+ .../tests/xpcshell/test_loginmgr_storage.js | 196 +++++++++++++++++++++
+ services/fxaccounts/tests/xpcshell/xpcshell.ini | 2 +
+ services/sync/modules/util.js | 12 +-
+ 6 files changed, 396 insertions(+), 2 deletions(-)
+ create mode 100644 services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js
+
+diff --git a/services/fxaccounts/FxAccounts.jsm b/services/fxaccounts/FxAccounts.jsm
+index cc521b8..e9328dd 100644
+--- a/services/fxaccounts/FxAccounts.jsm
++++ b/services/fxaccounts/FxAccounts.jsm
+@@ -298,17 +298,22 @@ function FxAccountsInternal() {
+ // able to abort all work on the first sign-in process. The currentTimer and
+ // currentAccountState are used for this purpose.
+ // (XXX - should the timer be directly on the currentAccountState?)
+ this.currentTimer = null;
+ this.currentAccountState = new AccountState(this);
+
+ // We don't reference |profileDir| in the top-level module scope
+ // as we may be imported before we know where it is.
++ // We only want the fancy new LoginManagerStorage on desktop.
++#if defined(MOZ_B2G)
+ this.signedInUserStorage = new JSONStorage({
++#else
++ this.signedInUserStorage = new LoginManagerStorage({
++#endif
+ filename: DEFAULT_STORAGE_FILENAME,
+ baseDir: OS.Constants.Path.profileDir,
+ });
+ }
+
+ /**
+ * The internal API's prototype.
+ */
+@@ -896,16 +901,181 @@ JSONStorage.prototype = {
+ .then(CommonUtils.writeJSON.bind(null, contents, this.path));
+ },
+
+ get: function() {
+ return CommonUtils.readJSON(this.path);
+ }
+ };
+
++/**
++ * LoginManagerStorage constructor that creates instances that may set/get
++ * from a combination of a clear-text JSON file and stored securely in
++ * the nsILoginManager.
++ *
++ * @param options {
++ * filename: of the plain-text file to write to
++ * baseDir: directory where the file resides
++ * }
++ * @return instance
++ */
++
++function LoginManagerStorage(options) {
++ // we reuse the JSONStorage for writing the plain-text stuff.
++ this.jsonStorage = new JSONStorage(options);
++}
++
++LoginManagerStorage.prototype = {
++ // The fields in the credentials JSON object that are stored in plain-text
++ // in the profile directory. All other fields are stored in the login manager,
++ // and thus are only available when the master-password is unlocked.
++
++ // a hook point for testing.
++ get _isLoggedIn() {
++ return Services.logins.isLoggedIn;
++ },
++
++ // Clear any data from the login manager. Returns true if the login manager
++ // was unlocked (even if no existing logins existed) or false if it was
++ // locked (meaning we don't even know if it existed or not.)
++ _clearLoginMgrData: Task.async(function* () {
++ try { // Services.logins might be third-party and broken...
++ yield Services.logins.initializationPromise;
++ if (!this._isLoggedIn) {
++ return false;
++ }
++ let logins = Services.logins.findLogins({}, FXA_PWDMGR_HOST, null, FXA_PWDMGR_REALM);
++ for (let login of logins) {
++ Services.logins.removeLogin(login);
++ }
++ return true;
++ } catch (ex) {
++ log.error("Failed to clear login data: ${}", ex);
++ return false;
++ }
++ }),
++
++ set: Task.async(function* (contents) {
++ if (!contents) {
++ // User is signing out - write the null to the json file.
++ yield this.jsonStorage.set(contents);
++
++ // And nuke it from the login manager.
++ let cleared = yield this._clearLoginMgrData();
++ if (!cleared) {
++ // just log a message - we verify that the email address matches when
++ // we reload it, so having a stale entry doesn't really hurt.
++ log.info("not removing credentials from login manager - not logged in");
++ }
++ return;
++ }
++
++ // We are saving actual data.
++ // Split the data into 2 chunks - one to go to the plain-text, and the
++ // other to write to the login manager.
++ let toWriteJSON = {version: contents.version};
++ let accountDataJSON = toWriteJSON.accountData = {};
++ let toWriteLoginMgr = {version: contents.version};
++ let accountDataLoginMgr = toWriteLoginMgr.accountData = {};
++ for (let [name, value] of Iterator(contents.accountData)) {
++ if (FXA_PWDMGR_PLAINTEXT_FIELDS.indexOf(name) >= 0) {
++ accountDataJSON[name] = value;
++ } else {
++ accountDataLoginMgr[name] = value;
++ }
++ }
++ yield this.jsonStorage.set(toWriteJSON);
++
++ try { // Services.logins might be third-party and broken...
++ // and the stuff into the login manager.
++ yield Services.logins.initializationPromise;
++ // If MP is locked we silently fail - the user may need to re-auth
++ // next startup.
++ if (!this._isLoggedIn) {
++ log.info("not saving credentials to login manager - not logged in");
++ return;
++ }
++ // write the rest of the data to the login manager.
++ let loginInfo = new Components.Constructor(
++ "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init");
++ let login = new loginInfo(FXA_PWDMGR_HOST,
++ null, // aFormSubmitURL,
++ FXA_PWDMGR_REALM, // aHttpRealm,
++ contents.accountData.email, // aUsername
++ JSON.stringify(toWriteLoginMgr), // aPassword
++ "", // aUsernameField
++ "");// aPasswordField
++
++ let existingLogins = Services.logins.findLogins({}, FXA_PWDMGR_HOST, null,
++ FXA_PWDMGR_REALM);
++ if (existingLogins.length) {
++ Services.logins.modifyLogin(existingLogins[0], login);
++ } else {
++ Services.logins.addLogin(login);
++ }
++ } catch (ex) {
++ log.error("Failed to save data to the login manager: ${}", ex);
++ }
++ }),
++
++ get: Task.async(function* () {
++ // we need to suck some data from the .json file in the profile dir and
++ // some other from the login manager.
++ let data = yield this.jsonStorage.get();
++ if (!data) {
++ // no user logged in, nuke the storage data incase we couldn't remove
++ // it previously and then we are done.
++ yield this._clearLoginMgrData();
++ return null;
++ }
++
++ // if we have encryption keys it must have been saved before we
++ // used the login manager, so re-save it.
++ if (data.accountData.kA || data.accountData.kB || data.keyFetchToken) {
++ log.info("account data needs migration to the login manager.");
++ yield this.set(data);
++ }
++
++ try { // Services.logins might be third-party and broken...
++ // read the data from the login manager and merge it for return.
++ yield Services.logins.initializationPromise;
++
++ if (!this._isLoggedIn) {
++ log.info("returning partial account data as the login manager is locked.");
++ return data;
++ }
++
++ let logins = Services.logins.findLogins({}, FXA_PWDMGR_HOST, null, FXA_PWDMGR_REALM);
++ if (logins.length == 0) {
++ // This could happen if the MP was locked when we wrote the data.
++ log.info("Can't find the rest of the credentials in the login manager");
++ return data;
++ }
++ let login = logins[0];
++ if (login.username == data.accountData.email) {
++ let lmData = JSON.parse(login.password);
++ if (lmData.version == data.version) {
++ // Merge the login manager data
++ copyObjectProperties(lmData.accountData, data.accountData);
++ } else {
++ log.info("version field in the login manager doesn't match - ignoring it");
++ yield this._clearLoginMgrData();
++ }
++ } else {
++ log.info("username in the login manager doesn't match - ignoring it");
++ yield this._clearLoginMgrData();
++ }
++ } catch (ex) {
++ log.error("Failed to get data from the login manager: ${}", ex);
++ }
++ return data;
++ }),
++
++}
++
+ // A getter for the instance to export
+ XPCOMUtils.defineLazyGetter(this, "fxAccounts", function() {
+ let a = new FxAccounts();
+
+ // XXX Bug 947061 - We need a strategy for resuming email verification after
+ // browser restart
+ a.loadAndPoll();
+
+diff --git a/services/fxaccounts/FxAccountsCommon.js b/services/fxaccounts/FxAccountsCommon.js
+index 2bef093..476e528 100644
+--- a/services/fxaccounts/FxAccountsCommon.js
++++ b/services/fxaccounts/FxAccountsCommon.js
+@@ -170,10 +170,23 @@ SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_AUTH_NONCE] = ERROR_INVALID_AUTH
+ SERVER_ERRNO_TO_ERROR[ERRNO_ENDPOINT_NO_LONGER_SUPPORTED] = ERROR_ENDPOINT_NO_LONGER_SUPPORTED;
+ SERVER_ERRNO_TO_ERROR[ERRNO_INCORRECT_LOGIN_METHOD] = ERROR_INCORRECT_LOGIN_METHOD;
+ SERVER_ERRNO_TO_ERROR[ERRNO_INCORRECT_KEY_RETRIEVAL_METHOD] = ERROR_INCORRECT_KEY_RETRIEVAL_METHOD;
+ SERVER_ERRNO_TO_ERROR[ERRNO_INCORRECT_API_VERSION] = ERROR_INCORRECT_API_VERSION;
+ SERVER_ERRNO_TO_ERROR[ERRNO_INCORRECT_EMAIL_CASE] = ERROR_INCORRECT_EMAIL_CASE;
+ SERVER_ERRNO_TO_ERROR[ERRNO_SERVICE_TEMP_UNAVAILABLE] = ERROR_SERVICE_TEMP_UNAVAILABLE;
+ SERVER_ERRNO_TO_ERROR[ERRNO_UNKNOWN_ERROR] = ERROR_UNKNOWN;
+
++// FxAccounts has the ability to "split" the credentials between a plain-text
++// JSON file in the profile dir and in the login manager.
++// These constants relate to that.
++
++// The fields we save in the plaintext JSON.
++// See bug 1013064 comments 23-25 for why the sessionToken is "safe"
++this.FXA_PWDMGR_PLAINTEXT_FIELDS = ["email", "verified", "authAt",
++ "sessionToken", "uid"];
++// The pseudo-host we use in the login manager
++this.FXA_PWDMGR_HOST = "chrome://FirefoxAccounts";
++// The realm we use in the login manager.
++this.FXA_PWDMGR_REALM = "Firefox Accounts credentials";
++
+ // Allow this file to be imported via Components.utils.import().
+ this.EXPORTED_SYMBOLS = Object.keys(this);
+diff --git a/services/fxaccounts/moz.build b/services/fxaccounts/moz.build
+index f959714..1bb8c09 100644
+--- a/services/fxaccounts/moz.build
++++ b/services/fxaccounts/moz.build
+@@ -5,16 +5,19 @@
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+ PARALLEL_DIRS += ['interfaces']
+
+ TEST_DIRS += ['tests']
+
+ EXTRA_JS_MODULES += [
+ 'Credentials.jsm',
+- 'FxAccounts.jsm',
+ 'FxAccountsClient.jsm',
+ 'FxAccountsCommon.js'
+ ]
+
++EXTRA_PP_JS_MODULES += [
++ 'FxAccounts.jsm',
++]
++
+ # For now, we will only be using the FxA manager in B2G.
+ if CONFIG['MOZ_B2G']:
+ EXTRA_JS_MODULES += ['FxAccountsManager.jsm']
+diff --git a/services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js b/services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js
+new file mode 100644
+index 0000000..297b256
+--- /dev/null
++++ b/services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js
+@@ -0,0 +1,196 @@
++/* Any copyright is dedicated to the Public Domain.
++ * http://creativecommons.org/publicdomain/zero/1.0/ */
++
++"use strict";
++
++// Tests for FxAccounts, storage and the master password.
++
++// Stop us hitting the real auth server.
++Services.prefs.setCharPref("identity.fxaccounts.auth.uri", "http://localhost");
++
++Cu.import("resource://gre/modules/Services.jsm");
++Cu.import("resource://gre/modules/FxAccounts.jsm");
++Cu.import("resource://gre/modules/FxAccountsClient.jsm");
++Cu.import("resource://gre/modules/FxAccountsCommon.js");
++Cu.import("resource://gre/modules/osfile.jsm");
++Cu.import("resource://services-common/utils.js");
++Cu.import("resource://gre/modules/FxAccountsCommon.js");
++
++initTestLogging("Trace");
++// See verbose logging from FxAccounts.jsm
++Services.prefs.setCharPref("identity.fxaccounts.loglevel", "DEBUG");
++
++function run_test() {
++ run_next_test();
++}
++
++function getLoginMgrData() {
++ let logins = Services.logins.findLogins({}, FXA_PWDMGR_HOST, null, FXA_PWDMGR_REALM);
++ if (logins.length == 0) {
++ return null;
++ }
++ Assert.equal(logins.length, 1, "only 1 login available");
++ return logins[0];
++}
++
++add_task(function test_simple() {
++ let fxa = new FxAccounts({});
++
++ let creds = {
++ email: "test@example.com",
++ sessionToken: "sessionToken",
++ kA: "the kA value",
++ kB: "the kB value",
++ verified: true
++ };
++ yield fxa.setSignedInUser(creds);
++
++ // This should have stored stuff in both the .json file in the profile
++ // dir, and the login dir.
++ let path = OS.Path.join(OS.Constants.Path.profileDir, "signedInUser.json");
++ let data = yield CommonUtils.readJSON(path);
++
++ Assert.strictEqual(data.accountData.email, creds.email, "correct email in the clear text");
++ Assert.strictEqual(data.accountData.sessionToken, creds.sessionToken, "correct sessionToken in the clear text");
++ Assert.strictEqual(data.accountData.verified, creds.verified, "correct verified flag");
++
++ Assert.ok(!("kA" in data.accountData), "kA not stored in clear text");
++ Assert.ok(!("kB" in data.accountData), "kB not stored in clear text");
++
++ let login = getLoginMgrData();
++ Assert.strictEqual(login.username, creds.email, "email matches");
++ let loginData = JSON.parse(login.password);
++ Assert.strictEqual(loginData.version, data.version, "same version flag in both places");
++ Assert.strictEqual(loginData.accountData.kA, creds.kA, "correct kA in the login mgr");
++ Assert.strictEqual(loginData.accountData.kB, creds.kB, "correct kB in the login mgr");
++
++ Assert.ok(!("email" in loginData), "email not stored in the login mgr json");
++ Assert.ok(!("sessionToken" in loginData), "sessionToken not stored in the login mgr json");
++ Assert.ok(!("verified" in loginData), "verified not stored in the login mgr json");
++
++ yield fxa.signOut(/* localOnly = */ true);
++ Assert.strictEqual(getLoginMgrData(), null, "login mgr data deleted on logout");
++});
++
++add_task(function test_MPLocked() {
++ let fxa = new FxAccounts({});
++
++ let creds = {
++ email: "test@example.com",
++ sessionToken: "sessionToken",
++ kA: "the kA value",
++ kB: "the kB value",
++ verified: true
++ };
++
++ // tell the storage that the MP is locked.
++ fxa.internal.signedInUserStorage.__defineGetter__("_isLoggedIn", function() false);
++ yield fxa.setSignedInUser(creds);
++
++ // This should have stored stuff in the .json, and the login manager stuff
++ // will not exist.
++ let path = OS.Path.join(OS.Constants.Path.profileDir, "signedInUser.json");
++ let data = yield CommonUtils.readJSON(path);
++
++ Assert.strictEqual(data.accountData.email, creds.email, "correct email in the clear text");
++ Assert.strictEqual(data.accountData.sessionToken, creds.sessionToken, "correct sessionToken in the clear text");
++ Assert.strictEqual(data.accountData.verified, creds.verified, "correct verified flag");
++
++ Assert.ok(!("kA" in data.accountData), "kA not stored in clear text");
++ Assert.ok(!("kB" in data.accountData), "kB not stored in clear text");
++
++ Assert.strictEqual(getLoginMgrData(), null, "login mgr data doesn't exist");
++ yield fxa.signOut(/* localOnly = */ true)
++});
++
++add_task(function test_consistentWithMPEdgeCases() {
++ let fxa = new FxAccounts({});
++
++ let creds1 = {
++ email: "test@example.com",
++ sessionToken: "sessionToken",
++ kA: "the kA value",
++ kB: "the kB value",
++ verified: true
++ };
++
++ let creds2 = {
++ email: "test2@example.com",
++ sessionToken: "sessionToken2",
++ kA: "the kA value2",
++ kB: "the kB value2",
++ verified: false,
++ };
++
++ // Log a user in while MP is unlocked.
++ yield fxa.setSignedInUser(creds1);
++
++ // tell the storage that the MP is locked - this will prevent logout from
++ // being able to clear the data.
++ fxa.internal.signedInUserStorage.__defineGetter__("_isLoggedIn", function() false);
++
++ // now set the second credentials.
++ yield fxa.setSignedInUser(creds2);
++
++ // We should still have creds1 data in the login manager.
++ let login = getLoginMgrData();
++ Assert.strictEqual(login.username, creds1.email);
++ // and that we do have the first kA in the login manager.
++ Assert.strictEqual(JSON.parse(login.password).accountData.kA, creds1.kA,
++ "stale data still in login mgr");
++
++ // Make a new FxA instance (otherwise the values in memory will be used.)
++ // Because we haven't overridden _isLoggedIn for this new instance it will
++ // treat the MP as unlocked.
++ let fxa = new FxAccounts({});
++
++ let accountData = yield fxa.getSignedInUser();
++ Assert.strictEqual(accountData.email, creds2.email);
++ // we should have no kA at all.
++ Assert.strictEqual(accountData.kA, undefined, "stale kA wasn't used");
++ yield fxa.signOut(/* localOnly = */ true)
++});
++
++add_task(function test_migration() {
++ // manually write out the full creds data to the JSON - this will look like
++ // old data that needs migration.
++ let creds = {
++ email: "test@example.com",
++ sessionToken: "sessionToken",
++ kA: "the kA value",
++ kB: "the kB value",
++ verified: true
++ };
++ let toWrite = {
++ version: 1,
++ accountData: creds,
++ };
++
++ let path = OS.Path.join(OS.Constants.Path.profileDir, "signedInUser.json");
++ let data = yield CommonUtils.writeJSON(toWrite, path);
++
++ // Create an FxA object - and tell it to load the data.
++ let fxa = new FxAccounts({});
++ data = yield fxa.getSignedInUser();
++
++ Assert.deepEqual(data, creds, "we should have everything available");
++
++ // now sniff the data on disk - it should have been magically migrated.
++ data = yield CommonUtils.readJSON(path);
++
++ Assert.strictEqual(data.accountData.email, creds.email, "correct email in the clear text");
++ Assert.strictEqual(data.accountData.sessionToken, creds.sessionToken, "correct sessionToken in the clear text");
++ Assert.strictEqual(data.accountData.verified, creds.verified, "correct verified flag");
++
++ Assert.ok(!("kA" in data.accountData), "kA not stored in clear text");
++ Assert.ok(!("kB" in data.accountData), "kB not stored in clear text");
++
++ // and it should magically be in the login manager.
++ let login = getLoginMgrData();
++ Assert.strictEqual(login.username, creds.email);
++ // and that we do have the first kA in the login manager.
++ Assert.strictEqual(JSON.parse(login.password).accountData.kA, creds.kA,
++ "kA was migrated");
++
++ yield fxa.signOut(/* localOnly = */ true)
++});
+diff --git a/services/fxaccounts/tests/xpcshell/xpcshell.ini b/services/fxaccounts/tests/xpcshell/xpcshell.ini
+index 9d9bffe..4448d76 100644
+--- a/services/fxaccounts/tests/xpcshell/xpcshell.ini
++++ b/services/fxaccounts/tests/xpcshell/xpcshell.ini
+@@ -1,10 +1,12 @@
+ [DEFAULT]
+ head = head.js ../../../common/tests/unit/head_helpers.js ../../../common/tests/unit/head_http.js
+ tail =
+
+ [test_accounts.js]
+ [test_client.js]
+ [test_credentials.js]
++[test_loginmgr_storage.js]
++skip-if = appname == 'b2g' # login manager storage only used on desktop.
+ [test_manager.js]
+ run-if = appname == 'b2g'
+ reason = FxAccountsManager is only available for B2G for now
+diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js
+index 194e0b2..4691428 100644
+--- a/services/sync/modules/util.js
++++ b/services/sync/modules/util.js
+@@ -14,16 +14,23 @@ Cu.import("resource://services-common/async.js", this);
+ Cu.import("resource://services-crypto/utils.js");
+ Cu.import("resource://services-sync/constants.js");
+ Cu.import("resource://gre/modules/Preferences.jsm");
+ Cu.import("resource://gre/modules/Services.jsm", this);
+ Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+ Cu.import("resource://gre/modules/osfile.jsm", this);
+ Cu.import("resource://gre/modules/Task.jsm", this);
+
++// FxAccountsCommon.js doesn't use a "namespace", so create one here.
++XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function() {
++ let FxAccountsCommon = {};
++ Cu.import("resource://gre/modules/FxAccountsCommon.js", FxAccountsCommon);
++ return FxAccountsCommon;
++});
++
+ /*
+ * Utility functions
+ */
+
+ this.Utils = {
+ // Alias in functions from CommonUtils. These previously were defined here.
+ // In the ideal world, references to these would be removed.
+ nextTick: CommonUtils.nextTick,
+@@ -589,18 +596,21 @@ this.Utils = {
+ * reset when we drop sync credentials, etc.
+ */
+ getSyncCredentialsHosts: function() {
+ // This is somewhat expensive and the result static, so we cache the result.
+ if (this._syncCredentialsHosts) {
+ return this._syncCredentialsHosts;
+ }
+ let result = new Set();
+- // the legacy sync host.
++ // the legacy sync host
+ result.add(PWDMGR_HOST);
++ // the FxA host
++ result.add(FxAccountsCommon.FXA_PWDMGR_HOST);
++ //
+ // The FxA hosts - these almost certainly all have the same hostname, but
+ // better safe than sorry...
+ for (let prefName of ["identity.fxaccounts.remote.force_auth.uri",
+ "identity.fxaccounts.remote.signup.uri",
+ "identity.fxaccounts.remote.signin.uri",
+ "identity.fxaccounts.settings.uri"]) {
+ let prefVal;
+ try {
+--
+1.8.3.msysgit.0
+
diff --git a/data/patches/0003-Bug-1013064-part-3-only-migrate-data-into-the-loginm.patch b/data/patches/0003-Bug-1013064-part-3-only-migrate-data-into-the-loginm.patch
new file mode 100644
index 0000000..063a031
--- /dev/null
+++ b/data/patches/0003-Bug-1013064-part-3-only-migrate-data-into-the-loginm.patch
@@ -0,0 +1,194 @@
+From 86c67f9b9081ce905442c86b38575b3422c8dce3 Mon Sep 17 00:00:00 2001
+From: Mark Hammond <mhammond@skippinet.com.au>
+Date: Wed, 18 Jun 2014 15:07:41 +1000
+Subject: Bug 1013064 (part 3) - only migrate data into the loginmgr when it is
+ unlocked. r=ckarlof
+
+---
+ services/fxaccounts/FxAccounts.jsm | 25 ++++-
+ .../tests/xpcshell/test_loginmgr_storage.js | 113 +++++++++++++++++++++
+ 2 files changed, 137 insertions(+), 1 deletion(-)
+
+diff --git a/services/fxaccounts/FxAccounts.jsm b/services/fxaccounts/FxAccounts.jsm
+index e9328dd..14d71fc 100644
+--- a/services/fxaccounts/FxAccounts.jsm
++++ b/services/fxaccounts/FxAccounts.jsm
+@@ -1025,17 +1025,40 @@ LoginManagerStorage.prototype = {
+ // it previously and then we are done.
+ yield this._clearLoginMgrData();
+ return null;
+ }
+
+ // if we have encryption keys it must have been saved before we
+ // used the login manager, so re-save it.
+ if (data.accountData.kA || data.accountData.kB || data.keyFetchToken) {
+- log.info("account data needs migration to the login manager.");
++ // We need to migrate, but the MP might be locked (eg, on the first run
++ // with this enabled, we will get here very soon after startup, so will
++ // certainly be locked.) This means we can't actually store the data in
++ // the login manager (and thus might lose it if we migrated now)
++ // So if the MP is locked, we *don't* migrate, but still just return
++ // the subset of data we now store in the JSON.
++ // This will cause sync to notice the lack of keys, force an unlock then
++ // re-fetch the account data to see if the keys are there. At *that*
++ // point we will end up back here, but because the MP is now unlocked
++ // we can actually perform the migration.
++ if (!this._isLoggedIn) {
++ // return the "safe" subset but leave the storage alone.
++ log.info("account data needs migration to the login manager but the MP is locked.");
++ let result = {
++ version: data.version,
++ accountData: {},
++ };
++ for (let fieldName of FXA_PWDMGR_PLAINTEXT_FIELDS) {
++ result.accountData[fieldName] = data.accountData[fieldName];
++ }
++ return result;
++ }
++ // actually migrate - just calling .set() will split everything up.
++ log.info("account data is being migrated to the login manager.");
+ yield this.set(data);
+ }
+
+ try { // Services.logins might be third-party and broken...
+ // read the data from the login manager and merge it for return.
+ yield Services.logins.initializationPromise;
+
+ if (!this._isLoggedIn) {
+diff --git a/services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js b/services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js
+index 297b256..a9cf5f4 100644
+--- a/services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js
++++ b/services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js
+@@ -98,16 +98,129 @@ add_task(function test_MPLocked() {
+
+ Assert.ok(!("kA" in data.accountData), "kA not stored in clear text");
+ Assert.ok(!("kB" in data.accountData), "kB not stored in clear text");
+
+ Assert.strictEqual(getLoginMgrData(), null, "login mgr data doesn't exist");
+ yield fxa.signOut(/* localOnly = */ true)
+ });
+
++add_task(function test_migrationMPUnlocked() {
++ // first manually save a signedInUser.json to simulate a first-run with
++ // pre-migrated data.
++ let fxa = new FxAccounts({});
++
++ let creds = {
++ email: "test@example.com",
++ sessionToken: "sessionToken",
++ kA: "the kA value",
++ kB: "the kB value",
++ verified: true
++ };
++ let toWrite = {
++ version: fxa.version,
++ accountData: creds,
++ }
++
++ let path = OS.Path.join(OS.Constants.Path.profileDir, "signedInUser.json");
++ yield CommonUtils.writeJSON(toWrite, path);
++
++ // now load it - it should migrate.
++ let data = yield fxa.getSignedInUser();
++ Assert.deepEqual(data, creds, "we got all the data back");
++
++ // and verify it was actually migrated - re-read signedInUser back.
++ let data = yield CommonUtils.readJSON(path);
++
++ Assert.strictEqual(data.accountData.email, creds.email, "correct email in the clear text");
++ Assert.strictEqual(data.accountData.sessionToken, creds.sessionToken, "correct sessionToken in the clear text");
++ Assert.strictEqual(data.accountData.verified, creds.verified, "correct verified flag");
++
++ Assert.ok(!("kA" in data.accountData), "kA not stored in clear text");
++ Assert.ok(!("kB" in data.accountData), "kB not stored in clear text");
++
++ let login = getLoginMgrData();
++ Assert.strictEqual(login.username, creds.email, "email matches");
++ let loginData = JSON.parse(login.password);
++ Assert.strictEqual(loginData.version, data.version, "same version flag in both places");
++ Assert.strictEqual(loginData.accountData.kA, creds.kA, "correct kA in the login mgr");
++ Assert.strictEqual(loginData.accountData.kB, creds.kB, "correct kB in the login mgr");
++
++ Assert.ok(!("email" in loginData), "email not stored in the login mgr json");
++ Assert.ok(!("sessionToken" in loginData), "sessionToken not stored in the login mgr json");
++ Assert.ok(!("verified" in loginData), "verified not stored in the login mgr json");
++
++ yield fxa.signOut(/* localOnly = */ true);
++ Assert.strictEqual(getLoginMgrData(), null, "login mgr data deleted on logout");
++});
++
++add_task(function test_migrationMPLocked() {
++ // first manually save a signedInUser.json to simulate a first-run with
++ // pre-migrated data.
++ let fxa = new FxAccounts({});
++
++ let creds = {
++ email: "test@example.com",
++ sessionToken: "sessionToken",
++ kA: "the kA value",
++ kB: "the kB value",
++ verified: true
++ };
++ let toWrite = {
++ version: fxa.version,
++ accountData: creds,
++ }
++
++ let path = OS.Path.join(OS.Constants.Path.profileDir, "signedInUser.json");
++ yield CommonUtils.writeJSON(toWrite, path);
++
++ // pretend the MP is locked.
++ fxa.internal.signedInUserStorage.__defineGetter__("_isLoggedIn", function() false);
++
++ // now load it - it should *not* migrate, but should only give the JSON-safe
++ // data back.
++ let data = yield fxa.getSignedInUser();
++ Assert.ok(!data.kA);
++ Assert.ok(!data.kB);
++
++ // and verify the data on disk wan't migrated.
++ data = yield CommonUtils.readJSON(path);
++ Assert.deepEqual(data, toWrite);
++
++ // Now "unlock" and re-ask for the signedInUser - it should migrate.
++ fxa.internal.signedInUserStorage.__defineGetter__("_isLoggedIn", function() true);
++ data = yield fxa.getSignedInUser();
++ // this time we should have got all the data, not just the JSON-safe fields.
++ Assert.strictEqual(data.kA, creds.kA);
++ Assert.strictEqual(data.kB, creds.kB);
++
++ // And verify the data in the JSON was migrated
++ data = yield CommonUtils.readJSON(path);
++ Assert.strictEqual(data.accountData.email, creds.email, "correct email in the clear text");
++ Assert.strictEqual(data.accountData.sessionToken, creds.sessionToken, "correct sessionToken in the clear text");
++ Assert.strictEqual(data.accountData.verified, creds.verified, "correct verified flag");
++
++ Assert.ok(!("kA" in data.accountData), "kA not stored in clear text");
++ Assert.ok(!("kB" in data.accountData), "kB not stored in clear text");
++
++ let login = getLoginMgrData();
++ Assert.strictEqual(login.username, creds.email, "email matches");
++ let loginData = JSON.parse(login.password);
++ Assert.strictEqual(loginData.version, data.version, "same version flag in both places");
++ Assert.strictEqual(loginData.accountData.kA, creds.kA, "correct kA in the login mgr");
++ Assert.strictEqual(loginData.accountData.kB, creds.kB, "correct kB in the login mgr");
++
++ Assert.ok(!("email" in loginData), "email not stored in the login mgr json");
++ Assert.ok(!("sessionToken" in loginData), "sessionToken not stored in the login mgr json");
++ Assert.ok(!("verified" in loginData), "verified not stored in the login mgr json");
++
++ yield fxa.signOut(/* localOnly = */ true);
++ Assert.strictEqual(getLoginMgrData(), null, "login mgr data deleted on logout");
++});
++
+ add_task(function test_consistentWithMPEdgeCases() {
+ let fxa = new FxAccounts({});
+
+ let creds1 = {
+ email: "test@example.com",
+ sessionToken: "sessionToken",
+ kA: "the kA value",
+ kB: "the kB value",
+--
+1.8.3.msysgit.0
+
diff --git a/data/patches/0004-Bug-1013064-part-4b-browserid_identity-and-sync-chang.patch b/data/patches/0004-Bug-1013064-part-4b-browserid_identity-and-sync-chang.patch
new file mode 100644
index 0000000..369bde6
--- /dev/null
+++ b/data/patches/0004-Bug-1013064-part-4b-browserid_identity-and-sync-chang.patch
@@ -0,0 +1,286 @@
+From 9717484083e66b78eedfa14e539d51382aba760f Mon Sep 17 00:00:00 2001
+From: Mark Hammond <mhammond@skippinet.com.au>
+Date: Sat, 14 Jun 2014 14:33:56 +1000
+Subject: Bug 1013064 (part 4) - browserid_identity and sync changes to support
+ FxA credentials in the login manager. r=ckarlof,rnewman
+
+---
+ services/sync/modules/browserid_identity.js | 61 ++++++++++++++++++++--
+ services/sync/modules/identity.js | 19 +++++++
+ services/sync/modules/service.js | 20 ++++---
+ .../sync/tests/unit/test_browserid_identity.js | 15 ++++++
+ 4 files changed, 102 insertions(+), 13 deletions(-)
+
+diff --git a/services/sync/modules/browserid_identity.js b/services/sync/modules/browserid_identity.js
+--- a/services/sync/modules/browserid_identity.js
++++ b/services/sync/modules/browserid_identity.js
+@@ -394,41 +394,83 @@ this.BrowserIDManager.prototype = {
+ this._syncKeyUpdated = true;
+ this._shouldHaveSyncKeyBundle = false;
+ },
+
+ /**
+ * The current state of the auth credentials.
+ *
+ * This essentially validates that enough credentials are available to use
+- * Sync.
++ * Sync, although it effectively ignores the state of the master-password -
++ * if that's locked and that's the only problem we can see, say everything
++ * is OK - unlockAndVerifyAuthState will be used to perform the unlock
++ * and re-verification if necessary.
+ */
+ get currentAuthState() {
+ if (this._authFailureReason) {
+ this._log.info("currentAuthState returning " + this._authFailureReason +
+ " due to previous failure");
+ return this._authFailureReason;
+ }
+ // TODO: need to revisit this. Currently this isn't ready to go until
+ // both the username and syncKeyBundle are both configured and having no
+ // username seems to make things fail fast so that's good.
+ if (!this.username) {
+ return LOGIN_FAILED_NO_USERNAME;
+ }
+
+ // No need to check this.syncKey as our getter for that attribute
+ // uses this.syncKeyBundle
+- // If bundle creation started, but failed.
+- if (this._shouldHaveSyncKeyBundle && !this.syncKeyBundle) {
+- return LOGIN_FAILED_NO_PASSPHRASE;
++ // If bundle creation started, but failed due to any reason other than
++ // the MP being locked...
++ if (this._shouldHaveSyncKeyBundle && !this.syncKeyBundle && !Utils.mpLocked()) {
++ // Return a state that says a re-auth is necessary so we can get keys.
++ return LOGIN_FAILED_LOGIN_REJECTED;
+ }
+
+ return STATUS_OK;
+ },
+
++ // Do we currently have keys, or do we have enough that we should be able
++ // to successfully fetch them?
++ _canFetchKeys: function() {
++ let userData = this._signedInUser;
++ // a keyFetchToken means we can almost certainly grab them.
++ // kA and kB means we already have them.
++ return userData && (userData.keyFetchToken || (userData.kA && userData.kB));
++ },
++
++ /**
++ * Verify the current auth state, unlocking the master-password if necessary.
++ *
++ * Returns a promise that resolves with the current auth state after
++ * attempting to unlock.
++ */
++ unlockAndVerifyAuthState: function() {
++ if (this._canFetchKeys()) {
++ return Promise.resolve(STATUS_OK);
++ }
++ // so no keys - ensure MP unlocked.
++ if (!Utils.ensureMPUnlocked()) {
++ // user declined to unlock, so we don't know if they are stored there.
++ return Promise.resolve(MASTER_PASSWORD_LOCKED);
++ }
++ // now we are unlocked we must re-fetch the user data as we may now have
++ // the details that were previously locked away.
++ return this._fxaService.getSignedInUser().then(
++ accountData => {
++ this._updateSignedInUser(accountData);
++ // If we still can't get keys it probably means the user authenticated
++ // without unlocking the MP or cleared the saved logins, so we've now
++ // lost them - the user will need to reauth before continuing.
++ return this._canFetchKeys() ? STATUS_OK : LOGIN_FAILED_LOGIN_REJECTED;
++ }
++ );
++ },
++
+ /**
+ * Do we have a non-null, not yet expired token for the user currently
+ * signed in?
+ */
+ hasValidToken: function() {
+ if (!this._token) {
+ return false;
+ }
+@@ -444,16 +486,24 @@ this.BrowserIDManager.prototype = {
+ if (tokenServerURI.endsWith("/")) { // trailing slashes cause problems...
+ tokenServerURI = tokenServerURI.slice(0, -1);
+ }
+ let log = this._log;
+ let client = this._tokenServerClient;
+ let fxa = this._fxaService;
+ let userData = this._signedInUser;
+
++ // We need kA and kB for things to work. If we don't have them, just
++ // return null for the token - sync calling unlockAndVerifyAuthState()
++ // before actually syncing will setup the error states if necessary.
++ if (!this._canFetchKeys()) {
++ log.info("_fetchTokenForUser has no keys to use.");
++ return null;
++ }
++
+ log.info("Fetching assertion and token from: " + tokenServerURI);
+
+ let maybeFetchKeys = () => {
+ // This is called at login time and every time we need a new token - in
+ // the latter case we already have kA and kB, so optimise that case.
+ if (userData.kA && userData.kB) {
+ return;
+ }
+@@ -519,17 +569,18 @@ this.BrowserIDManager.prototype = {
+ // TODO: write tests to make sure that different auth error cases are handled here
+ // properly: auth error getting assertion, auth error getting token (invalid generation
+ // and client-state error)
+ if (err instanceof AuthenticationError) {
+ this._log.error("Authentication error in _fetchTokenForUser: " + err);
+ // set it to the "fatal" LOGIN_FAILED_LOGIN_REJECTED reason.
+ this._authFailureReason = LOGIN_FAILED_LOGIN_REJECTED;
+ } else {
+- this._log.error("Non-authentication error in _fetchTokenForUser: " + err.message);
++ this._log.error("Non-authentication error in _fetchTokenForUser: "
++ + (err.message || err));
+ // for now assume it is just a transient network related problem.
+ this._authFailureReason = LOGIN_FAILED_NETWORK_ERROR;
+ }
+ // Drop the sync key bundle, but still expect to have one.
+ // This will arrange for us to be in the right 'currentAuthState'
+ // such that UI will show the right error.
+ this._shouldHaveSyncKeyBundle = true;
+ Weave.Status.login = this._authFailureReason;
+diff --git a/services/sync/modules/identity.js b/services/sync/modules/identity.js
+--- a/services/sync/modules/identity.js
++++ b/services/sync/modules/identity.js
+@@ -373,16 +373,35 @@ IdentityManager.prototype = {
+ if (!this.syncKeyBundle) {
+ return LOGIN_FAILED_INVALID_PASSPHRASE;
+ }
+
+ return STATUS_OK;
+ },
+
+ /**
++ * Verify the current auth state, unlocking the master-password if necessary.
++ *
++ * Returns a promise that resolves with the current auth state after
++ * attempting to unlock.
++ */
++ unlockAndVerifyAuthState: function() {
++ // Try to fetch the passphrase - this will prompt for MP unlock as a
++ // side-effect...
++ try {
++ this.syncKey;
++ } catch (ex) {
++ this._log.debug("Fetching passphrase threw " + ex +
++ "; assuming master password locked.");
++ return Promise.resolve(MASTER_PASSWORD_LOCKED);
++ }
++ return Promise.resolve(STATUS_OK);
++ },
++
++ /**
+ * Persist credentials to password store.
+ *
+ * When credentials are updated, they are changed in memory only. This will
+ * need to be called to save them to the underlying password store.
+ *
+ * If the password store is locked (e.g. if the master password hasn't been
+ * entered), this could throw an exception.
+ */
+diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js
+--- a/services/sync/modules/service.js
++++ b/services/sync/modules/service.js
+@@ -679,27 +679,31 @@ Sync11Service.prototype = {
+ }
+
+ if (!this.identity.username) {
+ this._log.warn("No username in verifyLogin.");
+ this.status.login = LOGIN_FAILED_NO_USERNAME;
+ return false;
+ }
+
+- // Unlock master password, or return.
+ // Attaching auth credentials to a request requires access to
+ // passwords, which means that Resource.get can throw MP-related
+ // exceptions!
+- // Try to fetch the passphrase first, while we still have control.
+- try {
+- this.identity.syncKey;
+- } catch (ex) {
+- this._log.debug("Fetching passphrase threw " + ex +
+- "; assuming master password locked.");
+- this.status.login = MASTER_PASSWORD_LOCKED;
++ // So we ask the identity to verify the login state after unlocking the
++ // master password (ie, this call is expected to prompt for MP unlock
++ // if necessary) while we still have control.
++ let cb = Async.makeSpinningCallback();
++ this.identity.unlockAndVerifyAuthState().then(
++ result => cb(null, result),
++ cb
++ );
++ let unlockedState = cb.wait();
++ this._log.debug("Fetching unlocked auth state returned " + unlockedState);
++ if (unlockedState != STATUS_OK) {
++ this.status.login = unlockedState;
+ return false;
+ }
+
+ try {
+ // Make sure we have a cluster to verify against.
+ // This is a little weird, if we don't get a node we pretend
+ // to succeed, since that probably means we just don't have storage.
+ if (this.clusterURL == "" && !this._clusterManager.setCluster()) {
+diff --git a/services/sync/tests/unit/test_browserid_identity.js b/services/sync/tests/unit/test_browserid_identity.js
+--- a/services/sync/tests/unit/test_browserid_identity.js
++++ b/services/sync/tests/unit/test_browserid_identity.js
+@@ -78,19 +78,33 @@ add_task(function test_initialializeWith
+ browseridManager.initializeWithCurrentIdentity();
+ yield browseridManager.whenReadyToAuthenticate.promise;
+ do_check_true(!!browseridManager._token);
+ do_check_true(browseridManager.hasValidToken());
+ do_check_eq(browseridManager.account, identityConfig.fxaccount.user.email);
+ }
+ );
+
++add_task(function test_initialializeWithNoKeys() {
++ _("Verify start after initializeWithCurrentIdentity without kA, kB or keyFetchToken");
++ let identityConfig = makeIdentityConfig();
++ delete identityConfig.fxaccount.user.kA;
++ delete identityConfig.fxaccount.user.kB;
++ // there's no keyFetchToken by default, so the initialize should fail.
++ configureFxAccountIdentity(browseridManager, identityConfig);
++
++ yield browseridManager.initializeWithCurrentIdentity();
++ yield browseridManager.whenReadyToAuthenticate.promise;
++ do_check_eq(Status.login, LOGIN_SUCCEEDED, "login succeeded even without keys");
++ do_check_false(browseridManager._canFetchKeys(), "_canFetchKeys reflects lack of keys");
++});
+
+ add_test(function test_getResourceAuthenticator() {
+ _("BrowserIDManager supplies a Resource Authenticator callback which returns a Hawk header.");
++ configureFxAccountIdentity(browseridManager);
+ let authenticator = browseridManager.getResourceAuthenticator();
+ do_check_true(!!authenticator);
+ let req = {uri: CommonUtils.makeURI(
+ "https://example.net/somewhere/over/the/rainbow"),
+ method: 'GET'};
+ let output = authenticator(req, 'GET');
+ do_check_true('headers' in output);
+ do_check_true('authorization' in output.headers);
+@@ -235,16 +249,17 @@ add_test(function test_RESTResourceAuthe
+ (getTimestampDelta(authHeader, now) - 12 * HOUR_MS) < 2 * MINUTE_MS);
+
+ run_next_test();
+ });
+
+ add_task(function test_ensureLoggedIn() {
+ configureFxAccountIdentity(browseridManager);
+ yield browseridManager.initializeWithCurrentIdentity();
++ yield browseridManager.whenReadyToAuthenticate.promise;
+ Assert.equal(Status.login, LOGIN_SUCCEEDED, "original initialize worked");
+ yield browseridManager.ensureLoggedIn();
+ Assert.equal(Status.login, LOGIN_SUCCEEDED, "original ensureLoggedIn worked");
+ Assert.ok(browseridManager._shouldHaveSyncKeyBundle,
+ "_shouldHaveSyncKeyBundle should always be true after ensureLogin completes.");
+
+ // arrange for no logged in user.
+ let fxa = browseridManager._fxaService
diff --git a/data/patches/0005-Bug-1013064-part-5-stop-disabling-the-password-engin.patch b/data/patches/0005-Bug-1013064-part-5-stop-disabling-the-password-engin.patch
new file mode 100644
index 0000000..394d27c
--- /dev/null
+++ b/data/patches/0005-Bug-1013064-part-5-stop-disabling-the-password-engin.patch
@@ -0,0 +1,519 @@
+From 07aa9cc1fcd5479976effe29f6adf5ad5ba7a8f8 Mon Sep 17 00:00:00 2001
+From: Mark Hammond <mhammond@skippinet.com.au>
+Date: Thu, 12 Jun 2014 18:19:49 +1000
+Subject: Bug 1013064 (part 5) - stop disabling the password engine when MP
+ enabled. r=ttaubert
+
+diff --git a/browser/base/content/sync/customize.js b/browser/base/content/sync/customize.js
+--- a/browser/base/content/sync/customize.js
++++ b/browser/base/content/sync/customize.js
+@@ -1,25 +1,11 @@
+ /* 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");
+-
+-let service = Cc["@mozilla.org/weave/service;1"]
+- .getService(Ci.nsISupports)
+- .wrappedJSObject;
+-
+-if (!service.allowPasswordsEngine) {
+- let checkbox = document.getElementById("fxa-pweng-chk");
+- checkbox.checked = false;
+- checkbox.disabled = true;
+-}
+-
+ addEventListener("dialogaccept", function () {
+ let pane = document.getElementById("sync-customize-pane");
+ pane.writePreferences(true);
+ window.arguments[0].accepted = true;
+ });
+diff --git a/browser/base/content/sync/customize.xul b/browser/base/content/sync/customize.xul
+--- a/browser/base/content/sync/customize.xul
++++ b/browser/base/content/sync/customize.xul
+@@ -40,18 +40,17 @@
+
+ <vbox align="start">
+ <checkbox label="&engine.tabs.label;"
+ accesskey="&engine.tabs.accesskey;"
+ preference="engine.tabs"/>
+ <checkbox label="&engine.bookmarks.label;"
+ accesskey="&engine.bookmarks.accesskey;"
+ preference="engine.bookmarks"/>
+- <checkbox id="fxa-pweng-chk"
+- label="&engine.passwords.label;"
++ <checkbox label="&engine.passwords.label;"
+ accesskey="&engine.passwords.accesskey;"
+ preference="engine.passwords"/>
+ <checkbox label="&engine.history.label;"
+ accesskey="&engine.history.accesskey;"
+ preference="engine.history"/>
+ <checkbox label="&engine.addons.label;"
+ accesskey="&engine.addons.accesskey;"
+ preference="engine.addons"/>
+diff --git a/browser/base/content/sync/utils.js b/browser/base/content/sync/utils.js
+--- a/browser/base/content/sync/utils.js
++++ b/browser/base/content/sync/utils.js
+@@ -82,22 +82,16 @@ let gSyncUtils = {
+ this._openLink(Weave.Svc.Prefs.get(root + "termsURL"));
+ },
+
+ openPrivacyPolicy: function () {
+ let root = this.fxAccountsEnabled ? "fxa." : "";
+ this._openLink(Weave.Svc.Prefs.get(root + "privacyURL"));
+ },
+
+- openMPInfoPage: function (event) {
+- event.stopPropagation();
+- let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
+- this._openLink(baseURL + "sync-master-password");
+- },
+-
+ openFirstSyncProgressPage: function () {
+ this._openLink("about:sync-progress");
+ },
+
+ /**
+ * Prepare an invisible iframe with the passphrase backup document.
+ * Used by both the print and saving methods.
+ *
+diff --git a/browser/components/preferences/in-content/sync.js b/browser/components/preferences/in-content/sync.js
+--- a/browser/components/preferences/in-content/sync.js
++++ b/browser/components/preferences/in-content/sync.js
+@@ -149,27 +149,16 @@ let gSyncPane = {
+ document.getElementById("fxaEmailAddress1").textContent = data.email;
+ document.getElementById("fxaEmailAddress2").textContent = data.email;
+ document.getElementById("fxaEmailAddress3").textContent = data.email;
+ document.getElementById("fxaSyncComputerName").value = Weave.Service.clientsEngine.localName;
+ let engines = document.getElementById("fxaSyncEngines")
+ for (let checkbox of engines.querySelectorAll("checkbox")) {
+ checkbox.disabled = enginesListDisabled;
+ }
+-
+- let checkbox = document.getElementById("fxa-pweng-chk");
+- let help = document.getElementById("fxa-pweng-help");
+- let allowPasswordsEngine = service.allowPasswordsEngine;
+-
+- if (!allowPasswordsEngine) {
+- checkbox.checked = false;
+- }
+-
+- checkbox.disabled = !allowPasswordsEngine || enginesListDisabled;
+- help.hidden = allowPasswordsEngine || enginesListDisabled;
+ });
+ // If fxAccountEnabled is false and we are in a "not configured" state,
+ // then fxAccounts is probably fully disabled rather than just unconfigured,
+ // so handle this case. This block can be removed once we remove support
+ // for fxAccounts being disabled.
+ } else if (Weave.Status.service == Weave.CLIENT_NOT_CONFIGURED ||
+ Weave.Svc.Prefs.get("firstSync", "") == "notReady") {
+ this.page = PAGE_NO_ACCOUNT;
+diff --git a/browser/components/preferences/in-content/sync.xul b/browser/components/preferences/in-content/sync.xul
+--- a/browser/components/preferences/in-content/sync.xul
++++ b/browser/components/preferences/in-content/sync.xul
+@@ -278,30 +278,19 @@
+ <hbox id="fxaSyncEngines">
+ <vbox>
+ <checkbox label="&engine.tabs.label;"
+ accesskey="&engine.tabs.accesskey;"
+ preference="engine.tabs"/>
+ <checkbox label="&engine.bookmarks.label;"
+ accesskey="&engine.bookmarks.accesskey;"
+ preference="engine.bookmarks"/>
+- <hbox>
+- <checkbox id="fxa-pweng-chk"
+- label="&engine.passwords.label;"
+- accesskey="&engine.passwords.accesskey;"
+- preference="engine.passwords"/>
+-
+- <vbox id="fxa-pweng-help">
+- <spacer flex="1"/>
+- <hbox id="fxa-pweng-help-link">
+- <image onclick="gSyncUtils.openMPInfoPage(event);" />
+- </hbox>
+- <spacer flex="1"/>
+- </vbox>
+- </hbox>
++ <checkbox label="&engine.passwords.label;"
++ accesskey="&engine.passwords.accesskey;"
++ preference="engine.passwords"/>
+ <checkbox label="&engine.history.label;"
+ accesskey="&engine.history.accesskey;"
+ preference="engine.history"/>
+ <checkbox label="&engine.addons.label;"
+ accesskey="&engine.addons.accesskey;"
+ preference="engine.addons"/>
+ <checkbox label="&engine.prefs.label;"
+ accesskey="&engine.prefs.accesskey;"
+diff --git a/browser/components/preferences/sync.js b/browser/components/preferences/sync.js
+--- a/browser/components/preferences/sync.js
++++ b/browser/components/preferences/sync.js
+@@ -149,27 +149,16 @@ let gSyncPane = {
+ document.getElementById("fxaEmailAddress1").textContent = data.email;
+ document.getElementById("fxaEmailAddress2").textContent = data.email;
+ document.getElementById("fxaEmailAddress3").textContent = data.email;
+ document.getElementById("fxaSyncComputerName").value = Weave.Service.clientsEngine.localName;
+ let engines = document.getElementById("fxaSyncEngines")
+ for (let checkbox of engines.querySelectorAll("checkbox")) {
+ checkbox.disabled = enginesListDisabled;
+ }
+-
+- let checkbox = document.getElementById("fxa-pweng-chk");
+- let help = document.getElementById("fxa-pweng-help");
+- let allowPasswordsEngine = service.allowPasswordsEngine;
+-
+- if (!allowPasswordsEngine) {
+- checkbox.checked = false;
+- }
+-
+- checkbox.disabled = !allowPasswordsEngine || enginesListDisabled;
+- help.hidden = allowPasswordsEngine || enginesListDisabled;
+ });
+ // If fxAccountEnabled is false and we are in a "not configured" state,
+ // then fxAccounts is probably fully disabled rather than just unconfigured,
+ // so handle this case. This block can be removed once we remove support
+ // for fxAccounts being disabled.
+ } else if (Weave.Status.service == Weave.CLIENT_NOT_CONFIGURED ||
+ Weave.Svc.Prefs.get("firstSync", "") == "notReady") {
+ this.page = PAGE_NO_ACCOUNT;
+diff --git a/browser/components/preferences/sync.xul b/browser/components/preferences/sync.xul
+--- a/browser/components/preferences/sync.xul
++++ b/browser/components/preferences/sync.xul
+@@ -260,30 +260,19 @@
+ <hbox id="fxaSyncEngines">
+ <vbox>
+ <checkbox label="&engine.tabs.label;"
+ accesskey="&engine.tabs.accesskey;"
+ preference="engine.tabs"/>
+ <checkbox label="&engine.bookmarks.label;"
+ accesskey="&engine.bookmarks.accesskey;"
+ preference="engine.bookmarks"/>
+- <hbox>
+- <checkbox id="fxa-pweng-chk"
+- label="&engine.passwords.label;"
+- accesskey="&engine.passwords.accesskey;"
+- preference="engine.passwords"/>
+-
+- <vbox id="fxa-pweng-help">
+- <spacer flex="1"/>
+- <hbox id="fxa-pweng-help-link">
+- <image onclick="gSyncUtils.openMPInfoPage(event);" />
+- </hbox>
+- <spacer flex="1"/>
+- </vbox>
+- </hbox>
++ <checkbox label="&engine.passwords.label;"
++ accesskey="&engine.passwords.accesskey;"
++ preference="engine.passwords"/>
+ <checkbox label="&engine.history.label;"
+ accesskey="&engine.history.accesskey;"
+ preference="engine.history"/>
+ <checkbox label="&engine.addons.label;"
+ accesskey="&engine.addons.accesskey;"
+ preference="engine.addons"/>
+ <checkbox label="&engine.prefs.label;"
+ accesskey="&engine.prefs.accesskey;"
+diff --git a/browser/themes/linux/preferences/preferences.css b/browser/themes/linux/preferences/preferences.css
+--- a/browser/themes/linux/preferences/preferences.css
++++ b/browser/themes/linux/preferences/preferences.css
+@@ -166,17 +166,9 @@ label.small {
+ margin: 5px;
+ line-height: 1.2em;
+ }
+
+ #noFxaAccount > label:first-child {
+ margin-bottom: 0.6em;
+ }
+
+-#fxa-pweng-help-link > label {
+- margin: 0;
+-}
+-
+-#fxa-pweng-help-link > image {
+- list-style-image: url("chrome://global/skin/icons/question-16.png");
+-}
+-
+ %endif
+diff --git a/browser/themes/osx/preferences/preferences.css b/browser/themes/osx/preferences/preferences.css
+--- a/browser/themes/osx/preferences/preferences.css
++++ b/browser/themes/osx/preferences/preferences.css
+@@ -228,25 +228,9 @@ html|a.inline-link:-moz-focusring {
+ margin: 12px 4px;
+ line-height: 1.2em;
+ }
+
+ #noFxaAccount > label:first-child {
+ margin-bottom: 0.6em;
+ }
+
+-#fxa-pweng-help-link > label {
+- margin: 0;
+-}
+-
+-#fxa-pweng-help-link > image {
+- width: 16px;
+- height: 16px;
+- list-style-image: url("chrome://global/skin/icons/question-16.png");
+-}
+-
+-@media (min-resolution: 2dppx) {
+- #fxa-pweng-help-link > image {
+- list-style-image: url("chrome://global/skin/icons/question-32.png");
+- }
+-}
+-
+ %endif
+diff --git a/browser/themes/windows/preferences/preferences.css b/browser/themes/windows/preferences/preferences.css
+--- a/browser/themes/windows/preferences/preferences.css
++++ b/browser/themes/windows/preferences/preferences.css
+@@ -156,17 +156,9 @@ label.small {
+ margin: 6px;
+ line-height: 1.2em;
+ }
+
+ #noFxaAccount > label:first-child {
+ margin-bottom: 0.6em;
+ }
+
+-#fxa-pweng-help-link > label {
+- margin: 0;
+-}
+-
+-#fxa-pweng-help-link > image {
+- list-style-image: url("chrome://global/skin/icons/question-16.png");
+-}
+-
+ %endif
+diff --git a/services/sync/Weave.js b/services/sync/Weave.js
+--- a/services/sync/Weave.js
++++ b/services/sync/Weave.js
+@@ -104,26 +104,16 @@ WeaveService.prototype = {
+ let username = Services.prefs.getCharPref(SYNC_PREFS_BRANCH + "username");
+ return !username || username.contains('@');
+ } catch (_) {
+ return true; // No username == only allow FxA to be configured.
+ }
+ },
+
+ /**
+- * Returns whether the password engine is allowed. We explicitly disallow
+- * the password engine when a master password is used to ensure those can't
+- * be accessed without the master key.
+- */
+- get allowPasswordsEngine() {
+- // This doesn't apply to old-style sync, it's only an issue for FxA.
+- return !this.fxAccountsEnabled || !Utils.mpEnabled();
+- },
+-
+- /**
+ * Whether Sync appears to be enabled.
+ *
+ * This returns true if all the Sync preferences for storing account
+ * and server configuration are populated.
+ *
+ * It does *not* perform a robust check to see if the client is working.
+ * For that, you'll want to check Weave.Status.checkSetup().
+ */
+diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js
+--- a/services/sync/modules/engines/passwords.js
++++ b/services/sync/modules/engines/passwords.js
+@@ -31,38 +31,16 @@ this.PasswordEngine = function PasswordE
+ }
+ PasswordEngine.prototype = {
+ __proto__: SyncEngine.prototype,
+ _storeObj: PasswordStore,
+ _trackerObj: PasswordTracker,
+ _recordObj: LoginRec,
+ applyIncomingBatchSize: PASSWORDS_STORE_BATCH_SIZE,
+
+- get isAllowed() {
+- return Cc["@mozilla.org/weave/service;1"]
+- .getService(Ci.nsISupports)
+- .wrappedJSObject
+- .allowPasswordsEngine;
+- },
+-
+- get enabled() {
+- // If we are disabled due to !isAllowed(), we must take care to ensure the
+- // engine has actually had the enabled setter called which reflects this state.
+- let prefVal = SyncEngine.prototype.__lookupGetter__("enabled").call(this);
+- let newVal = this.isAllowed && prefVal;
+- if (newVal != prefVal) {
+- this.enabled = newVal;
+- }
+- return newVal;
+- },
+-
+- set enabled(val) {
+- SyncEngine.prototype.__lookupSetter__("enabled").call(this, this.isAllowed && val);
+- },
+-
+ _syncFinish: function _syncFinish() {
+ SyncEngine.prototype._syncFinish.call(this);
+
+ // Delete the weave credentials from the server once
+ if (!Svc.Prefs.get("deletePwdFxA", false)) {
+ try {
+ let ids = [];
+ for (let host of Utils.getSyncCredentialsHosts()) {
+diff --git a/services/sync/tests/unit/test_password_mpenabled.js b/services/sync/tests/unit/test_password_mpenabled.js
+deleted file mode 100644
+--- a/services/sync/tests/unit/test_password_mpenabled.js
++++ /dev/null
+@@ -1,137 +0,0 @@
+-/* Any copyright is dedicated to the Public Domain.
+- http://creativecommons.org/publicdomain/zero/1.0/ */
+-
+-Cu.import("resource://gre/modules/Log.jsm");
+-Cu.import("resource://services-sync/stages/enginesync.js");
+-Cu.import("resource://services-sync/util.js");
+-Cu.import("resource://services-sync/engines/passwords.js");
+-Cu.import("resource://services-sync/service.js");
+-Cu.import("resource://testing-common/services/sync/utils.js");
+-
+-function run_test() {
+- initTestLogging("Trace");
+- run_next_test();
+-}
+-
+-add_test(function test_simple() {
+- ensureLegacyIdentityManager();
+- // Stub fxAccountsEnabled
+- let xpcs = Cc["@mozilla.org/weave/service;1"]
+- .getService(Components.interfaces.nsISupports)
+- .wrappedJSObject;
+- let fxaEnabledGetter = xpcs.__lookupGetter__("fxAccountsEnabled");
+- xpcs.__defineGetter__("fxAccountsEnabled", () => true);
+-
+- // Stub mpEnabled.
+- let mpEnabledF = Utils.mpEnabled;
+- let mpEnabled = false;
+- Utils.mpEnabled = function() mpEnabled;
+-
+- let manager = Service.engineManager;
+-
+- Service.engineManager.register(PasswordEngine);
+- let engine = Service.engineManager.get("passwords");
+- let wipeCount = 0;
+- let engineWipeServerF = engine.wipeServer;
+- engine.wipeServer = function() {
+- ++wipeCount;
+- }
+-
+- // A server for the metadata.
+- let server = new SyncServer();
+- let johndoe = server.registerUser("johndoe", "password");
+- johndoe.createContents({
+- meta: {global: {engines: {passwords: {version: engine.version,
+- syncID: engine.syncID}}}},
+- crypto: {},
+- clients: {}
+- });
+- server.start();
+- setBasicCredentials("johndoe", "password", "abcdeabcdeabcdeabcdeabcdea");
+- Service.serverURL = server.baseURI;
+- Service.clusterURL = server.baseURI;
+-
+- let engineSync = new EngineSynchronizer(Service);
+- engineSync._log.level = Log.Level.Trace;
+-
+- function assertEnabled(expected, message) {
+- Assert.strictEqual(engine.enabled, expected, message);
+- // The preference *must* reflect the actual state.
+- Assert.strictEqual(Svc.Prefs.get("engine." + engine.prefName), expected,
+- message + " (pref should match enabled state)");
+- }
+-
+- try {
+- assertEnabled(true, "password engine should be enabled by default")
+- let engineMeta = Service.recordManager.get(engine.metaURL);
+- // This engine should be in the meta/global
+- Assert.notStrictEqual(engineMeta.payload.engines[engine.name], undefined,
+- "The engine should appear in the metadata");
+- Assert.ok(!engineMeta.changed, "the metadata for the password engine hasn't changed");
+-
+- // (pretend to) enable a master-password
+- mpEnabled = true;
+- // The password engine should be locally disabled...
+- assertEnabled(false, "if mp is locked the engine should be disabled");
+- // ...but not declined.
+- Assert.ok(!manager.isDeclined("passwords"), "password engine is not declined");
+- // Next time a sync would happen, we call _updateEnabledEngines(), which
+- // would remove the engine from the metadata - call that now.
+- engineSync._updateEnabledEngines();
+- // The global meta should no longer list the engine.
+- engineMeta = Service.recordManager.get(engine.metaURL);
+- Assert.strictEqual(engineMeta.payload.engines[engine.name], undefined,
+- "The engine should have vanished");
+- // And we should have wiped the server data.
+- Assert.strictEqual(wipeCount, 1, "wipeServer should have been called");
+-
+- // Now simulate an incoming meta/global indicating the engine should be
+- // enabled. We should fail to actually enable it - the pref should remain
+- // false and we wipe the server for anything another device might have
+- // stored.
+- let meta = {
+- payload: {
+- engines: {
+- "passwords": {"version":1,"syncID":"yfBi2v7PpFO2"},
+- },
+- },
+- };
+- engineSync._updateEnabledFromMeta(meta, 3, manager);
+- Assert.strictEqual(wipeCount, 2, "wipeServer should have been called");
+- Assert.ok(!manager.isDeclined("passwords"), "password engine is not declined");
+- assertEnabled(false, "engine still not enabled locally");
+-
+- // Let's turn the MP off - but *not* re-enable it locally.
+- mpEnabled = false;
+- // Just disabling the MP isn't enough to force it back to enabled.
+- assertEnabled(false, "engine still not enabled locally");
+- // Another incoming metadata record with the engine enabled should cause
+- // it to be enabled locally.
+- meta = {
+- payload: {
+- engines: {
+- "passwords": 1,
+- },
+- },
+- };
+- engineSync._updateEnabledFromMeta(meta, 3, manager);
+- Assert.strictEqual(wipeCount, 2, "wipeServer should *not* have been called again");
+- Assert.ok(!manager.isDeclined("passwords"), "password engine is not declined");
+- // It should be enabled locally.
+- assertEnabled(true, "engine now enabled locally");
+- // Next time a sync starts it should magically re-appear in our meta/global
+- engine._syncStartup();
+- //engineSync._updateEnabledEngines();
+- engineMeta = Service.recordManager.get(engine.metaURL);
+- Assert.equal(engineMeta.payload.engines[engine.name].version, engine.version,
+- "The engine should re-appear in the metadata");
+- } finally {
+- // restore the damage we did above...
+- engine.wipeServer = engineWipeServerF;
+- engine._store.wipe();
+- // Un-stub mpEnabled and fxAccountsEnabled
+- Utils.mpEnabled = mpEnabledF;
+- xpcs.__defineGetter__("fxAccountsEnabled", fxaEnabledGetter);
+- server.stop(run_next_test);
+- }
+-});
+diff --git a/services/sync/tests/unit/xpcshell.ini b/services/sync/tests/unit/xpcshell.ini
+--- a/services/sync/tests/unit/xpcshell.ini
++++ b/services/sync/tests/unit/xpcshell.ini
+@@ -164,10 +164,8 @@ skip-if = debug
+ [test_prefs_store.js]
+ [test_prefs_tracker.js]
+ [test_tab_engine.js]
+ [test_tab_store.js]
+ [test_tab_tracker.js]
+
+ [test_healthreport.js]
+ skip-if = ! healthreport
+-
+-[test_password_mpenabled.js]
diff --git a/data/patches/disable/0001-upgrade-mobile-fxa-to-v32.patch b/data/patches/disable/0001-upgrade-mobile-fxa-to-v32.patch
new file mode 100644
index 0000000..e420727
--- /dev/null
+++ b/data/patches/disable/0001-upgrade-mobile-fxa-to-v32.patch
@@ -0,0 +1,992 @@
+diff -ruN mozilla-esr31/mobile/android/base/fxa/FirefoxAccounts.java mozilla-release/mobile/android/base/fxa/FirefoxAccounts.java
+--- mozilla-esr31/mobile/android/base/fxa/FirefoxAccounts.java 2015-02-25 19:21:06.000000000 +0100
++++ mozilla-release/mobile/android/base/fxa/FirefoxAccounts.java 2014-09-24 03:05:32.000000000 +0200
+@@ -6,13 +6,18 @@
+
+ import java.io.File;
+ import java.util.EnumSet;
++import java.util.Locale;
+ import java.util.concurrent.CountDownLatch;
+
++import org.mozilla.gecko.AppConstants;
++import org.mozilla.gecko.R;
+ import org.mozilla.gecko.background.common.log.Logger;
+ import org.mozilla.gecko.fxa.authenticator.AccountPickler;
+ import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
++import org.mozilla.gecko.fxa.login.State;
+ import org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter;
+ import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper;
++import org.mozilla.gecko.fxa.tasks.FxAccountCodeResender;
+ import org.mozilla.gecko.sync.ThreadPool;
+ import org.mozilla.gecko.sync.Utils;
+
+@@ -20,6 +25,7 @@
+ import android.accounts.AccountManager;
+ import android.content.ContentResolver;
+ import android.content.Context;
++import android.content.res.Resources;
+ import android.os.Bundle;
+
+ /**
+@@ -152,6 +158,38 @@
+ return null;
+ }
+
++ /**
++ * @return
++ * the {@link State} instance associated with the current account, or <code>null</code> if
++ * no accounts exist.
++ */
++ public static State getFirefoxAccountState(final Context context) {
++ final Account account = getFirefoxAccount(context);
++ if (account == null) {
++ return null;
++ }
++
++ final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
++ try {
++ return fxAccount.getState();
++ } catch (final Exception ex) {
++ Logger.warn(LOG_TAG, "Could not get FX account state.", ex);
++ return null;
++ }
++ }
++
++ /*
++ * @param context Android context
++ * @return the email address associated with the configured Firefox account if one exists; null otherwise.
++ */
++ public static String getFirefoxAccountEmail(final Context context) {
++ final Account account = getFirefoxAccount(context);
++ if (account == null) {
++ return null;
++ }
++ return account.name;
++ }
++
+ protected static void putHintsToSync(final Bundle extras, EnumSet<SyncHint> syncHints) {
+ // stagesToSync and stagesToSkip are allowed to be null.
+ if (syncHints == null) {
+@@ -264,4 +302,33 @@
+ // stopObserving null-checks its argument.
+ FxAccountSyncStatusHelper.getInstance().stopObserving(syncStatusListener);
+ }
++
++ public static String getOldSyncUpgradeURL(final Resources res, final Locale locale) {
++ final String VERSION = AppConstants.MOZ_APP_VERSION;
++ final String OS = AppConstants.OS_TARGET;
++ final String LOCALE = Utils.getLanguageTag(locale);
++ return res.getString(R.string.fxaccount_link_old_firefox, VERSION, OS, LOCALE);
++ }
++
++ /**
++ * Resends the account verification email, and displays an appropriate
++ * toast on both send success and failure. Note that because the underlying implementation
++ * uses {@link AsyncTask}, the provided context must be UI-capable, and this
++ * method called from the UI thread (see
++ * {@link org.mozilla.gecko.fxa.tasks.FxAccountCodeResender#resendCode(Context, AndroidFxAccount)}
++ * for more).
++ *
++ * @param context a UI-capable Android context.
++ * @return true if an account exists, false otherwise.
++ */
++ public static boolean resendVerificationEmail(final Context context) {
++ final Account account = getFirefoxAccount(context);
++ if (account == null) {
++ return false;
++ }
++
++ final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
++ FxAccountCodeResender.resendCode(context, fxAccount);
++ return true;
++ }
+ }
+diff -ruN mozilla-esr31/mobile/android/base/fxa/activities/FxAccountAbstractSetupActivity.java mozilla-release/mobile/android/base/fxa/activities/FxAccountAbstractSetupActivity.java
+--- mozilla-esr31/mobile/android/base/fxa/activities/FxAccountAbstractSetupActivity.java 2015-02-25 19:21:06.000000000 +0100
++++ mozilla-release/mobile/android/base/fxa/activities/FxAccountAbstractSetupActivity.java 2014-09-24 03:05:32.000000000 +0200
+@@ -19,10 +19,10 @@
+ import org.mozilla.gecko.background.fxa.PasswordStretcher;
+ import org.mozilla.gecko.background.fxa.QuickPasswordStretcher;
+ import org.mozilla.gecko.fxa.FxAccountConstants;
+-import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.ProgressDisplay;
+ import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+ import org.mozilla.gecko.fxa.login.Engaged;
+ import org.mozilla.gecko.fxa.login.State;
++import org.mozilla.gecko.fxa.tasks.FxAccountSetupTask.ProgressDisplay;
+ import org.mozilla.gecko.sync.SyncConfiguration;
+ import org.mozilla.gecko.sync.setup.Constants;
+ import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
+diff -ruN mozilla-esr31/mobile/android/base/fxa/activities/FxAccountConfirmAccountActivity.java mozilla-release/mobile/android/base/fxa/activities/FxAccountConfirmAccountActivity.java
+--- mozilla-esr31/mobile/android/base/fxa/activities/FxAccountConfirmAccountActivity.java 2015-02-25 19:21:06.000000000 +0100
++++ mozilla-release/mobile/android/base/fxa/activities/FxAccountConfirmAccountActivity.java 2014-09-24 03:05:32.000000000 +0200
+@@ -4,21 +4,15 @@
+
+ package org.mozilla.gecko.fxa.activities;
+
+-import java.util.concurrent.Executor;
+-import java.util.concurrent.Executors;
+-
+ import org.mozilla.gecko.R;
+ import org.mozilla.gecko.background.common.log.Logger;
+-import org.mozilla.gecko.background.fxa.FxAccountClient;
+-import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
+-import org.mozilla.gecko.background.fxa.FxAccountClient20;
+-import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
+ import org.mozilla.gecko.fxa.FirefoxAccounts;
+ import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+ import org.mozilla.gecko.fxa.login.Engaged;
+ import org.mozilla.gecko.fxa.login.State;
+ import org.mozilla.gecko.fxa.login.State.Action;
+ import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper;
++import org.mozilla.gecko.fxa.tasks.FxAccountCodeResender;
+ import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
+
+ import android.accounts.Account;
+@@ -28,7 +22,6 @@
+ import android.view.View;
+ import android.view.View.OnClickListener;
+ import android.widget.TextView;
+-import android.widget.Toast;
+
+ /**
+ * Activity which displays account created successfully screen to the user, and
+@@ -164,76 +157,8 @@
+ resendLink.setClickable(resendLinkShouldBeEnabled);
+ }
+
+- public static class FxAccountResendCodeTask extends FxAccountSetupTask<Void> {
+- protected static final String LOG_TAG = FxAccountResendCodeTask.class.getSimpleName();
+-
+- protected final byte[] sessionToken;
+-
+- public FxAccountResendCodeTask(Context context, byte[] sessionToken, FxAccountClient client, RequestDelegate<Void> delegate) {
+- super(context, null, client, delegate);
+- this.sessionToken = sessionToken;
+- }
+-
+- @Override
+- protected InnerRequestDelegate<Void> doInBackground(Void... arg0) {
+- try {
+- client.resendCode(sessionToken, innerDelegate);
+- latch.await();
+- return innerDelegate;
+- } catch (Exception e) {
+- Logger.error(LOG_TAG, "Got exception signing in.", e);
+- delegate.handleError(e);
+- }
+- return null;
+- }
+- }
+-
+- protected static class ResendCodeDelegate implements RequestDelegate<Void> {
+- public final Context context;
+-
+- public ResendCodeDelegate(Context context) {
+- this.context = context;
+- }
+-
+- @Override
+- public void handleError(Exception e) {
+- Logger.warn(LOG_TAG, "Got exception requesting fresh confirmation link; ignoring.", e);
+- Toast.makeText(context, R.string.fxaccount_confirm_account_verification_link_not_sent, Toast.LENGTH_LONG).show();
+- }
+-
+- @Override
+- public void handleFailure(FxAccountClientRemoteException e) {
+- handleError(e);
+- }
+-
+- @Override
+- public void handleSuccess(Void result) {
+- Toast.makeText(context, R.string.fxaccount_confirm_account_verification_link_sent, Toast.LENGTH_SHORT).show();
+- }
+- }
+-
+- public static void resendCode(Context context, AndroidFxAccount fxAccount) {
+- RequestDelegate<Void> delegate = new ResendCodeDelegate(context);
+-
+- byte[] sessionToken;
+- try {
+- sessionToken = ((Engaged) fxAccount.getState()).getSessionToken();
+- } catch (Exception e) {
+- delegate.handleError(e);
+- return;
+- }
+- if (sessionToken == null) {
+- delegate.handleError(new IllegalStateException("sessionToken should not be null"));
+- return;
+- }
+-
+- Executor executor = Executors.newSingleThreadExecutor();
+- FxAccountClient client = new FxAccountClient20(fxAccount.getAccountServerURI(), executor);
+- new FxAccountResendCodeTask(context, sessionToken, client, delegate).execute();
+- }
+-
+ @Override
+ public void onClick(View v) {
+- resendCode(this, fxAccount);
++ FxAccountCodeResender.resendCode(this, fxAccount);
+ }
+ }
+diff -ruN mozilla-esr31/mobile/android/base/fxa/activities/FxAccountCreateAccountActivity.java mozilla-release/mobile/android/base/fxa/activities/FxAccountCreateAccountActivity.java
+--- mozilla-esr31/mobile/android/base/fxa/activities/FxAccountCreateAccountActivity.java 2015-02-25 19:21:06.000000000 +0100
++++ mozilla-release/mobile/android/base/fxa/activities/FxAccountCreateAccountActivity.java 2014-09-24 03:05:32.000000000 +0200
+@@ -21,7 +21,7 @@
+ import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
+ import org.mozilla.gecko.background.fxa.PasswordStretcher;
+ import org.mozilla.gecko.fxa.FxAccountConstants;
+-import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.FxAccountCreateAccountTask;
++import org.mozilla.gecko.fxa.tasks.FxAccountCreateAccountTask;
+
+ import android.app.AlertDialog;
+ import android.app.Dialog;
+diff -ruN mozilla-esr31/mobile/android/base/fxa/activities/FxAccountGetStartedActivity.java mozilla-release/mobile/android/base/fxa/activities/FxAccountGetStartedActivity.java
+--- mozilla-esr31/mobile/android/base/fxa/activities/FxAccountGetStartedActivity.java 2015-02-25 19:21:06.000000000 +0100
++++ mozilla-release/mobile/android/base/fxa/activities/FxAccountGetStartedActivity.java 2014-09-24 03:05:32.000000000 +0200
+@@ -6,13 +6,11 @@
+
+ import java.util.Locale;
+
+-import org.mozilla.gecko.AppConstants;
+ import org.mozilla.gecko.R;
+ import org.mozilla.gecko.background.common.log.Logger;
+ import org.mozilla.gecko.background.fxa.FxAccountAgeLockoutHelper;
+ import org.mozilla.gecko.fxa.FirefoxAccounts;
+ import org.mozilla.gecko.fxa.FxAccountConstants;
+-import org.mozilla.gecko.sync.Utils;
+ import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
+ import org.mozilla.gecko.sync.setup.activities.LocaleAware;
+
+@@ -109,11 +107,7 @@
+ protected void linkifyOldFirefoxLink() {
+ TextView oldFirefox = (TextView) findViewById(R.id.old_firefox);
+ String text = getResources().getString(R.string.fxaccount_getting_started_old_firefox);
+- String VERSION = AppConstants.MOZ_APP_VERSION;
+- String OS = AppConstants.OS_TARGET;
+-
+- String LOCALE = Utils.getLanguageTag(Locale.getDefault());
+- String url = getResources().getString(R.string.fxaccount_link_old_firefox, VERSION, OS, LOCALE);
++ final String url = FirefoxAccounts.getOldSyncUpgradeURL(getResources(), Locale.getDefault());
+ FxAccountConstants.pii(LOG_TAG, "Old Firefox url is: " + url); // Don't want to leak locale in particular.
+ ActivityUtils.linkTextView(oldFirefox, text, url);
+ }
+diff -ruN mozilla-esr31/mobile/android/base/fxa/activities/FxAccountSetupTask.java mozilla-release/mobile/android/base/fxa/activities/FxAccountSetupTask.java
+--- mozilla-esr31/mobile/android/base/fxa/activities/FxAccountSetupTask.java 2015-02-25 19:21:06.000000000 +0100
++++ mozilla-release/mobile/android/base/fxa/activities/FxAccountSetupTask.java 1970-01-01 01:00:00.000000000 +0100
+@@ -1,172 +0,0 @@
+-/* 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.fxa.activities;
+-
+-import java.io.UnsupportedEncodingException;
+-import java.util.concurrent.CountDownLatch;
+-
+-import org.mozilla.gecko.background.common.log.Logger;
+-import org.mozilla.gecko.background.fxa.FxAccountClient;
+-import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
+-import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
+-import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
+-import org.mozilla.gecko.background.fxa.PasswordStretcher;
+-import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.InnerRequestDelegate;
+-
+-import android.content.Context;
+-import android.os.AsyncTask;
+-
+-/**
+- * An <code>AsyncTask</code> wrapper around signing up for, and signing in to, a
+- * Firefox Account.
+- * <p>
+- * It's strange to add explicit blocking to callback-threading code, but we do
+- * it here to take advantage of Android's built in support for background work.
+- * We really want to avoid making a threading mistake that brings down the whole
+- * process.
+- */
+-abstract class FxAccountSetupTask<T> extends AsyncTask<Void, Void, InnerRequestDelegate<T>> {
+- private static final String LOG_TAG = FxAccountSetupTask.class.getSimpleName();
+-
+- public interface ProgressDisplay {
+- public void showProgress();
+- public void dismissProgress();
+- }
+-
+- protected final Context context;
+- protected final FxAccountClient client;
+- protected final ProgressDisplay progressDisplay;
+-
+- // Initialized lazily.
+- protected byte[] quickStretchedPW;
+-
+- // AsyncTask's are one-time-use, so final members are fine.
+- protected final CountDownLatch latch = new CountDownLatch(1);
+- protected final InnerRequestDelegate<T> innerDelegate = new InnerRequestDelegate<T>(latch);
+-
+- protected final RequestDelegate<T> delegate;
+-
+- public FxAccountSetupTask(Context context, ProgressDisplay progressDisplay, FxAccountClient client, RequestDelegate<T> delegate) {
+- this.context = context;
+- this.client = client;
+- this.delegate = delegate;
+- this.progressDisplay = progressDisplay;
+- }
+-
+- @Override
+- protected void onPreExecute() {
+- if (progressDisplay != null) {
+- progressDisplay.showProgress();
+- }
+- }
+-
+- @Override
+- protected void onPostExecute(InnerRequestDelegate<T> result) {
+- if (progressDisplay != null) {
+- progressDisplay.dismissProgress();
+- }
+-
+- // We are on the UI thread, and need to invoke these callbacks here to allow UI updating.
+- if (innerDelegate.failure != null) {
+- delegate.handleFailure(innerDelegate.failure);
+- } else if (innerDelegate.exception != null) {
+- delegate.handleError(innerDelegate.exception);
+- } else {
+- delegate.handleSuccess(result.response);
+- }
+- }
+-
+- @Override
+- protected void onCancelled(InnerRequestDelegate<T> result) {
+- if (progressDisplay != null) {
+- progressDisplay.dismissProgress();
+- }
+- delegate.handleError(new IllegalStateException("Task was cancelled."));
+- }
+-
+- protected static class InnerRequestDelegate<T> implements RequestDelegate<T> {
+- protected final CountDownLatch latch;
+- public T response = null;
+- public Exception exception = null;
+- public FxAccountClientRemoteException failure = null;
+-
+- protected InnerRequestDelegate(CountDownLatch latch) {
+- this.latch = latch;
+- }
+-
+- @Override
+- public void handleError(Exception e) {
+- Logger.error(LOG_TAG, "Got exception.");
+- this.exception = e;
+- latch.countDown();
+- }
+-
+- @Override
+- public void handleFailure(FxAccountClientRemoteException e) {
+- Logger.warn(LOG_TAG, "Got failure.");
+- this.failure = e;
+- latch.countDown();
+- }
+-
+- @Override
+- public void handleSuccess(T result) {
+- Logger.info(LOG_TAG, "Got success.");
+- this.response = result;
+- latch.countDown();
+- }
+- }
+-
+- public static class FxAccountCreateAccountTask extends FxAccountSetupTask<LoginResponse> {
+- private static final String LOG_TAG = FxAccountCreateAccountTask.class.getSimpleName();
+-
+- protected final byte[] emailUTF8;
+- protected final PasswordStretcher passwordStretcher;
+-
+- public FxAccountCreateAccountTask(Context context, ProgressDisplay progressDisplay, String email, PasswordStretcher passwordStretcher, FxAccountClient client, RequestDelegate<LoginResponse> delegate) throws UnsupportedEncodingException {
+- super(context, progressDisplay, client, delegate);
+- this.emailUTF8 = email.getBytes("UTF-8");
+- this.passwordStretcher = passwordStretcher;
+- }
+-
+- @Override
+- protected InnerRequestDelegate<LoginResponse> doInBackground(Void... arg0) {
+- try {
+- client.createAccountAndGetKeys(emailUTF8, passwordStretcher, innerDelegate);
+- latch.await();
+- return innerDelegate;
+- } catch (Exception e) {
+- Logger.error(LOG_TAG, "Got exception logging in.", e);
+- delegate.handleError(e);
+- }
+- return null;
+- }
+- }
+-
+- public static class FxAccountSignInTask extends FxAccountSetupTask<LoginResponse> {
+- protected static final String LOG_TAG = FxAccountSignInTask.class.getSimpleName();
+-
+- protected final byte[] emailUTF8;
+- protected final PasswordStretcher passwordStretcher;
+-
+- public FxAccountSignInTask(Context context, ProgressDisplay progressDisplay, String email, PasswordStretcher passwordStretcher, FxAccountClient client, RequestDelegate<LoginResponse> delegate) throws UnsupportedEncodingException {
+- super(context, progressDisplay, client, delegate);
+- this.emailUTF8 = email.getBytes("UTF-8");
+- this.passwordStretcher = passwordStretcher;
+- }
+-
+- @Override
+- protected InnerRequestDelegate<LoginResponse> doInBackground(Void... arg0) {
+- try {
+- client.loginAndGetKeys(emailUTF8, passwordStretcher, innerDelegate);
+- latch.await();
+- return innerDelegate;
+- } catch (Exception e) {
+- Logger.error(LOG_TAG, "Got exception signing in.", e);
+- delegate.handleError(e);
+- }
+- return null;
+- }
+- }
+-}
+diff -ruN mozilla-esr31/mobile/android/base/fxa/activities/FxAccountSignInActivity.java mozilla-release/mobile/android/base/fxa/activities/FxAccountSignInActivity.java
+--- mozilla-esr31/mobile/android/base/fxa/activities/FxAccountSignInActivity.java 2015-02-25 19:21:06.000000000 +0100
++++ mozilla-release/mobile/android/base/fxa/activities/FxAccountSignInActivity.java 2014-09-24 03:05:32.000000000 +0200
+@@ -16,7 +16,7 @@
+ import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
+ import org.mozilla.gecko.background.fxa.PasswordStretcher;
+ import org.mozilla.gecko.fxa.FxAccountConstants;
+-import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.FxAccountSignInTask;
++import org.mozilla.gecko.fxa.tasks.FxAccountSignInTask;
+ import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
+
+ import android.content.Intent;
+diff -ruN mozilla-esr31/mobile/android/base/fxa/activities/FxAccountStatusFragment.java mozilla-release/mobile/android/base/fxa/activities/FxAccountStatusFragment.java
+--- mozilla-esr31/mobile/android/base/fxa/activities/FxAccountStatusFragment.java 2015-02-25 19:21:06.000000000 +0100
++++ mozilla-release/mobile/android/base/fxa/activities/FxAccountStatusFragment.java 2014-09-24 03:05:32.000000000 +0200
+@@ -17,6 +17,8 @@
+ import org.mozilla.gecko.fxa.login.Married;
+ import org.mozilla.gecko.fxa.login.State;
+ import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper;
++import org.mozilla.gecko.fxa.tasks.FxAccountCodeResender;
++import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
+ import org.mozilla.gecko.sync.SyncConfiguration;
+
+ import android.accounts.Account;
+@@ -27,10 +29,13 @@
+ import android.os.Bundle;
+ import android.os.Handler;
+ import android.preference.CheckBoxPreference;
++import android.preference.EditTextPreference;
+ import android.preference.Preference;
++import android.preference.Preference.OnPreferenceChangeListener;
+ import android.preference.Preference.OnPreferenceClickListener;
+ import android.preference.PreferenceCategory;
+ import android.preference.PreferenceScreen;
++import android.text.TextUtils;
+
+ /**
+ * A fragment that displays the status of an AndroidFxAccount.
+@@ -38,7 +43,9 @@
+ * The owning activity is responsible for providing an AndroidFxAccount at
+ * appropriate times.
+ */
+-public class FxAccountStatusFragment extends PreferenceFragment implements OnPreferenceClickListener {
++public class FxAccountStatusFragment
++ extends PreferenceFragment
++ implements OnPreferenceClickListener, OnPreferenceChangeListener {
+ private static final String LOG_TAG = FxAccountStatusFragment.class.getSimpleName();
+
+ // When a checkbox is toggled, wait 5 seconds (for other checkbox actions)
+@@ -64,7 +71,12 @@
+ protected CheckBoxPreference tabsPreference;
+ protected CheckBoxPreference passwordsPreference;
+
++ protected EditTextPreference deviceNamePreference;
++
+ protected volatile AndroidFxAccount fxAccount;
++ // The contract is: when fxAccount is non-null, then clientsDataDelegate is
++ // non-null. If violated then an IllegalStateException is thrown.
++ protected volatile SharedPreferencesClientsDataDelegate clientsDataDelegate;
+
+ // Used to post delayed sync requests.
+ protected Handler handler;
+@@ -87,6 +99,10 @@
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
++ addPreferences();
++ }
++
++ protected void addPreferences() {
+ addPreferencesFromResource(R.xml.fxaccount_status_prefscreen);
+
+ emailPreference = ensureFindPreference("email");
+@@ -118,6 +134,9 @@
+ historyPreference.setOnPreferenceClickListener(this);
+ tabsPreference.setOnPreferenceClickListener(this);
+ passwordsPreference.setOnPreferenceClickListener(this);
++
++ deviceNamePreference = (EditTextPreference) ensureFindPreference("device_name");
++ deviceNamePreference.setOnPreferenceChangeListener(this);
+ }
+
+ /**
+@@ -142,7 +161,7 @@
+ }
+
+ if (preference == needsVerificationPreference) {
+- FxAccountConfirmAccountActivity.resendCode(getActivity().getApplicationContext(), fxAccount);
++ FxAccountCodeResender.resendCode(getActivity().getApplicationContext(), fxAccount);
+
+ Intent intent = new Intent(getActivity(), FxAccountConfirmAccountActivity.class);
+ // Per http://stackoverflow.com/a/8992365, this triggers a known bug with
+@@ -176,6 +195,8 @@
+ historyPreference.setEnabled(enabled);
+ tabsPreference.setEnabled(enabled);
+ passwordsPreference.setEnabled(enabled);
++ // Since we can't sync, we can't update our remote client record.
++ deviceNamePreference.setEnabled(enabled);
+ }
+
+ /**
+@@ -293,6 +314,14 @@
+ throw new IllegalArgumentException("fxAccount must not be null");
+ }
+ this.fxAccount = fxAccount;
++ try {
++ this.clientsDataDelegate = new SharedPreferencesClientsDataDelegate(fxAccount.getSyncPrefs());
++ } catch (Exception e) {
++ Logger.error(LOG_TAG, "Got exception fetching Sync prefs associated to Firefox Account; aborting.", e);
++ // Something is terribly wrong; best to get a stack trace rather than
++ // continue with a null clients delegate.
++ throw new IllegalStateException(e);
++ }
+
+ handler = new Handler(); // Attached to current (assumed to be UI) thread.
+
+@@ -318,6 +347,17 @@
+ FxAccountSyncStatusHelper.getInstance().stopObserving(syncStatusDelegate);
+ }
+
++ protected void hardRefresh() {
++ // This is the only way to guarantee that the EditText dialogs created by
++ // EditTextPreferences are re-created. This works around the issue described
++ // at http://androiddev.orkitra.com/?p=112079.
++ final PreferenceScreen statusScreen = (PreferenceScreen) ensureFindPreference("status_screen");
++ statusScreen.removeAll();
++ addPreferences();
++
++ refresh();
++ }
++
+ protected void refresh() {
+ // refresh is called from our onResume, which can happen before the owning
+ // Activity tells us about an account (via our public
+@@ -371,6 +411,10 @@
+ // No matter our state, we should update the checkboxes.
+ updateSelectedEngines();
+ }
++
++ final String clientName = clientsDataDelegate.getClientName();
++ deviceNamePreference.setSummary(clientName);
++ deviceNamePreference.setText(clientName);
+ }
+
+ /**
+@@ -570,4 +614,22 @@
+ button.setOnPreferenceClickListener(listener);
+ }
+ }
++
++ @Override
++ public boolean onPreferenceChange(Preference preference, Object newValue) {
++ if (preference == deviceNamePreference) {
++ String newClientName = (String) newValue;
++ if (TextUtils.isEmpty(newClientName)) {
++ newClientName = clientsDataDelegate.getDefaultClientName();
++ }
++ final long now = System.currentTimeMillis();
++ clientsDataDelegate.setClientName(newClientName, now);
++ requestDelayedSync(); // Try to update our remote client record.
++ hardRefresh(); // Updates the value displayed to the user, among other things.
++ return true;
++ }
++
++ // For everything else, accept the change.
++ return true;
++ }
+ }
+diff -ruN mozilla-esr31/mobile/android/base/fxa/activities/FxAccountUpdateCredentialsActivity.java mozilla-release/mobile/android/base/fxa/activities/FxAccountUpdateCredentialsActivity.java
+--- mozilla-esr31/mobile/android/base/fxa/activities/FxAccountUpdateCredentialsActivity.java 2015-02-25 19:21:06.000000000 +0100
++++ mozilla-release/mobile/android/base/fxa/activities/FxAccountUpdateCredentialsActivity.java 2014-09-24 03:05:32.000000000 +0200
+@@ -18,11 +18,11 @@
+ import org.mozilla.gecko.background.fxa.PasswordStretcher;
+ import org.mozilla.gecko.fxa.FirefoxAccounts;
+ import org.mozilla.gecko.fxa.FxAccountConstants;
+-import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.FxAccountSignInTask;
+ import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+ import org.mozilla.gecko.fxa.login.Engaged;
+ import org.mozilla.gecko.fxa.login.State;
+ import org.mozilla.gecko.fxa.login.State.StateLabel;
++import org.mozilla.gecko.fxa.tasks.FxAccountSignInTask;
+ import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
+
+ import android.os.Bundle;
+diff -ruN mozilla-esr31/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java mozilla-release/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
+--- mozilla-esr31/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java 2015-02-25 19:21:06.000000000 +0100
++++ mozilla-release/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java 2014-09-24 03:05:33.000000000 +0200
+@@ -357,7 +357,11 @@
+
+ FxAccountGlobalSession globalSession = null;
+ try {
+- ClientsDataDelegate clientsDataDelegate = new SharedPreferencesClientsDataDelegate(sharedPrefs);
++ final ClientsDataDelegate clientsDataDelegate = new SharedPreferencesClientsDataDelegate(sharedPrefs);
++ if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
++ FxAccountConstants.pii(LOG_TAG, "Client device name is: '" + clientsDataDelegate.getClientName() + "'.");
++ FxAccountConstants.pii(LOG_TAG, "Client device data last modified: " + clientsDataDelegate.getLastModifiedTimestamp());
++ }
+
+ // We compute skew over time using SkewHandler. This yields an unchanging
+ // skew adjustment that the HawkAuthHeaderProvider uses to adjust its
+diff -ruN mozilla-esr31/mobile/android/base/fxa/tasks/FxAccountCodeResender.java mozilla-release/mobile/android/base/fxa/tasks/FxAccountCodeResender.java
+--- mozilla-esr31/mobile/android/base/fxa/tasks/FxAccountCodeResender.java 1970-01-01 01:00:00.000000000 +0100
++++ mozilla-release/mobile/android/base/fxa/tasks/FxAccountCodeResender.java 2014-09-24 03:05:33.000000000 +0200
+@@ -0,0 +1,108 @@
++/* 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.fxa.tasks;
++
++import java.util.concurrent.Executor;
++import java.util.concurrent.Executors;
++
++import org.mozilla.gecko.R;
++import org.mozilla.gecko.background.common.log.Logger;
++import org.mozilla.gecko.background.fxa.FxAccountClient;
++import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
++import org.mozilla.gecko.background.fxa.FxAccountClient20;
++import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
++import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
++import org.mozilla.gecko.fxa.login.Engaged;
++
++import android.content.Context;
++import android.widget.Toast;
++
++/**
++ * A helper class that provides a simple interface for requesting
++ * a Firefox Account verification email to be resent.
++ */
++public class FxAccountCodeResender {
++ private static final String LOG_TAG = FxAccountCodeResender.class.getSimpleName();
++
++ private static class FxAccountResendCodeTask extends FxAccountSetupTask<Void> {
++ protected static final String LOG_TAG = FxAccountResendCodeTask.class.getSimpleName();
++
++ protected final byte[] sessionToken;
++
++ public FxAccountResendCodeTask(Context context, byte[] sessionToken, FxAccountClient client, RequestDelegate<Void> delegate) {
++ super(context, null, client, delegate);
++ this.sessionToken = sessionToken;
++ }
++
++ @Override
++ protected InnerRequestDelegate<Void> doInBackground(Void... arg0) {
++ try {
++ client.resendCode(sessionToken, innerDelegate);
++ latch.await();
++ return innerDelegate;
++ } catch (Exception e) {
++ Logger.error(LOG_TAG, "Got exception signing in.", e);
++ delegate.handleError(e);
++ }
++ return null;
++ }
++ }
++
++ private static class ResendCodeDelegate implements RequestDelegate<Void> {
++ public final Context context;
++
++ public ResendCodeDelegate(Context context) {
++ this.context = context;
++ }
++
++ @Override
++ public void handleError(Exception e) {
++ Logger.warn(LOG_TAG, "Got exception requesting fresh confirmation link; ignoring.", e);
++ Toast.makeText(context, R.string.fxaccount_confirm_account_verification_link_not_sent, Toast.LENGTH_LONG).show();
++ }
++
++ @Override
++ public void handleFailure(FxAccountClientRemoteException e) {
++ handleError(e);
++ }
++
++ @Override
++ public void handleSuccess(Void result) {
++ Toast.makeText(context, R.string.fxaccount_confirm_account_verification_link_sent, Toast.LENGTH_SHORT).show();
++ }
++ }
++
++ /**
++ * Resends the account verification email, and displays an appropriate
++ * toast on both send success and failure. Note that because the underlying implementation
++ * uses {@link AsyncTask}, the provided context must be UI-capable and
++ * this method called from the UI thread.
++ *
++ * Note that it may actually be possible to run this (and the {@link AsyncTask}) method
++ * from a background thread - but this hasn't been tested.
++ *
++ * @param context A UI-capable Android context.
++ * @param fxAccount The Firefox Account to resend the code to.
++ */
++ public static void resendCode(Context context, AndroidFxAccount fxAccount) {
++ RequestDelegate<Void> delegate = new ResendCodeDelegate(context);
++
++ byte[] sessionToken;
++ try {
++ sessionToken = ((Engaged) fxAccount.getState()).getSessionToken();
++ } catch (Exception e) {
++ delegate.handleError(e);
++ return;
++ }
++ if (sessionToken == null) {
++ delegate.handleError(new IllegalStateException("sessionToken should not be null"));
++ return;
++ }
++
++ Executor executor = Executors.newSingleThreadExecutor();
++ FxAccountClient client = new FxAccountClient20(fxAccount.getAccountServerURI(), executor);
++ new FxAccountResendCodeTask(context, sessionToken, client, delegate).execute();
++ }
++}
+diff -ruN mozilla-esr31/mobile/android/base/fxa/tasks/FxAccountCreateAccountTask.java mozilla-release/mobile/android/base/fxa/tasks/FxAccountCreateAccountTask.java
+--- mozilla-esr31/mobile/android/base/fxa/tasks/FxAccountCreateAccountTask.java 1970-01-01 01:00:00.000000000 +0100
++++ mozilla-release/mobile/android/base/fxa/tasks/FxAccountCreateAccountTask.java 2014-09-24 03:05:33.000000000 +0200
+@@ -0,0 +1,41 @@
++/* 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.fxa.tasks;
++
++import java.io.UnsupportedEncodingException;
++
++import org.mozilla.gecko.background.common.log.Logger;
++import org.mozilla.gecko.background.fxa.FxAccountClient;
++import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
++import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
++import org.mozilla.gecko.background.fxa.PasswordStretcher;
++
++import android.content.Context;
++
++public class FxAccountCreateAccountTask extends FxAccountSetupTask<LoginResponse> {
++ private static final String LOG_TAG = FxAccountCreateAccountTask.class.getSimpleName();
++
++ protected final byte[] emailUTF8;
++ protected final PasswordStretcher passwordStretcher;
++
++ public FxAccountCreateAccountTask(Context context, ProgressDisplay progressDisplay, String email, PasswordStretcher passwordStretcher, FxAccountClient client, RequestDelegate<LoginResponse> delegate) throws UnsupportedEncodingException {
++ super(context, progressDisplay, client, delegate);
++ this.emailUTF8 = email.getBytes("UTF-8");
++ this.passwordStretcher = passwordStretcher;
++ }
++
++ @Override
++ protected InnerRequestDelegate<LoginResponse> doInBackground(Void... arg0) {
++ try {
++ client.createAccountAndGetKeys(emailUTF8, passwordStretcher, innerDelegate);
++ latch.await();
++ return innerDelegate;
++ } catch (Exception e) {
++ Logger.error(LOG_TAG, "Got exception logging in.", e);
++ delegate.handleError(e);
++ }
++ return null;
++ }
++}
+diff -ruN mozilla-esr31/mobile/android/base/fxa/tasks/FxAccountSetupTask.java mozilla-release/mobile/android/base/fxa/tasks/FxAccountSetupTask.java
+--- mozilla-esr31/mobile/android/base/fxa/tasks/FxAccountSetupTask.java 1970-01-01 01:00:00.000000000 +0100
++++ mozilla-release/mobile/android/base/fxa/tasks/FxAccountSetupTask.java 2014-09-24 03:05:33.000000000 +0200
+@@ -0,0 +1,117 @@
++/* 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.fxa.tasks;
++
++import java.util.concurrent.CountDownLatch;
++
++import org.mozilla.gecko.background.common.log.Logger;
++import org.mozilla.gecko.background.fxa.FxAccountClient;
++import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
++import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
++import org.mozilla.gecko.fxa.tasks.FxAccountSetupTask.InnerRequestDelegate;
++
++import android.content.Context;
++import android.os.AsyncTask;
++
++/**
++ * An <code>AsyncTask</code> wrapper around signing up for, and signing in to, a
++ * Firefox Account.
++ * <p>
++ * It's strange to add explicit blocking to callback-threading code, but we do
++ * it here to take advantage of Android's built in support for background work.
++ * We really want to avoid making a threading mistake that brings down the whole
++ * process.
++ */
++public abstract class FxAccountSetupTask<T> extends AsyncTask<Void, Void, InnerRequestDelegate<T>> {
++ private static final String LOG_TAG = FxAccountSetupTask.class.getSimpleName();
++
++ public interface ProgressDisplay {
++ public void showProgress();
++ public void dismissProgress();
++ }
++
++ protected final Context context;
++ protected final FxAccountClient client;
++ protected final ProgressDisplay progressDisplay;
++
++ // Initialized lazily.
++ protected byte[] quickStretchedPW;
++
++ // AsyncTask's are one-time-use, so final members are fine.
++ protected final CountDownLatch latch = new CountDownLatch(1);
++ protected final InnerRequestDelegate<T> innerDelegate = new InnerRequestDelegate<T>(latch);
++
++ protected final RequestDelegate<T> delegate;
++
++ public FxAccountSetupTask(Context context, ProgressDisplay progressDisplay, FxAccountClient client, RequestDelegate<T> delegate) {
++ this.context = context;
++ this.client = client;
++ this.delegate = delegate;
++ this.progressDisplay = progressDisplay;
++ }
++
++ @Override
++ protected void onPreExecute() {
++ if (progressDisplay != null) {
++ progressDisplay.showProgress();
++ }
++ }
++
++ @Override
++ protected void onPostExecute(InnerRequestDelegate<T> result) {
++ if (progressDisplay != null) {
++ progressDisplay.dismissProgress();
++ }
++
++ // We are on the UI thread, and need to invoke these callbacks here to allow UI updating.
++ if (innerDelegate.failure != null) {
++ delegate.handleFailure(innerDelegate.failure);
++ } else if (innerDelegate.exception != null) {
++ delegate.handleError(innerDelegate.exception);
++ } else {
++ delegate.handleSuccess(result.response);
++ }
++ }
++
++ @Override
++ protected void onCancelled(InnerRequestDelegate<T> result) {
++ if (progressDisplay != null) {
++ progressDisplay.dismissProgress();
++ }
++ delegate.handleError(new IllegalStateException("Task was cancelled."));
++ }
++
++ protected static class InnerRequestDelegate<T> implements RequestDelegate<T> {
++ protected final CountDownLatch latch;
++ public T response = null;
++ public Exception exception = null;
++ public FxAccountClientRemoteException failure = null;
++
++ protected InnerRequestDelegate(CountDownLatch latch) {
++ this.latch = latch;
++ }
++
++ @Override
++ public void handleError(Exception e) {
++ Logger.error(LOG_TAG, "Got exception.");
++ this.exception = e;
++ latch.countDown();
++ }
++
++ @Override
++ public void handleFailure(FxAccountClientRemoteException e) {
++ Logger.warn(LOG_TAG, "Got failure.");
++ this.failure = e;
++ latch.countDown();
++ }
++
++ @Override
++ public void handleSuccess(T result) {
++ Logger.info(LOG_TAG, "Got success.");
++ this.response = result;
++ latch.countDown();
++ }
++ }
++}
+diff -ruN mozilla-esr31/mobile/android/base/fxa/tasks/FxAccountSignInTask.java mozilla-release/mobile/android/base/fxa/tasks/FxAccountSignInTask.java
+--- mozilla-esr31/mobile/android/base/fxa/tasks/FxAccountSignInTask.java 1970-01-01 01:00:00.000000000 +0100
++++ mozilla-release/mobile/android/base/fxa/tasks/FxAccountSignInTask.java 2014-09-24 03:05:33.000000000 +0200
+@@ -0,0 +1,41 @@
++/* 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.fxa.tasks;
++
++import java.io.UnsupportedEncodingException;
++
++import org.mozilla.gecko.background.common.log.Logger;
++import org.mozilla.gecko.background.fxa.FxAccountClient;
++import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
++import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
++import org.mozilla.gecko.background.fxa.PasswordStretcher;
++
++import android.content.Context;
++
++public class FxAccountSignInTask extends FxAccountSetupTask<LoginResponse> {
++ protected static final String LOG_TAG = FxAccountSignInTask.class.getSimpleName();
++
++ protected final byte[] emailUTF8;
++ protected final PasswordStretcher passwordStretcher;
++
++ public FxAccountSignInTask(Context context, ProgressDisplay progressDisplay, String email, PasswordStretcher passwordStretcher, FxAccountClient client, RequestDelegate<LoginResponse> delegate) throws UnsupportedEncodingException {
++ super(context, progressDisplay, client, delegate);
++ this.emailUTF8 = email.getBytes("UTF-8");
++ this.passwordStretcher = passwordStretcher;
++ }
++
++ @Override
++ protected InnerRequestDelegate<LoginResponse> doInBackground(Void... arg0) {
++ try {
++ client.loginAndGetKeys(emailUTF8, passwordStretcher, innerDelegate);
++ latch.await();
++ return innerDelegate;
++ } catch (Exception e) {
++ Logger.error(LOG_TAG, "Got exception signing in.", e);
++ delegate.handleError(e);
++ }
++ return null;
++ }
++}
+--- mozilla-esr31/mobile/android/base/android-services.mozbuild 2015-02-25 19:21:05.000000000 +0100
++++ mozilla-release/mobile/android/base/android-services.mozbuild 2014-09-24 03:05:32.000000000 +0200
+@@ -555,7 +557,6 @@
+ 'fxa/activities/FxAccountCreateAccountActivity.java',
+ 'fxa/activities/FxAccountCreateAccountNotAllowedActivity.java',
+ 'fxa/activities/FxAccountGetStartedActivity.java',
+- 'fxa/activities/FxAccountSetupTask.java',
+ 'fxa/activities/FxAccountSignInActivity.java',
+ 'fxa/activities/FxAccountStatusActivity.java',
+ 'fxa/activities/FxAccountStatusFragment.java',
+@@ -589,6 +590,10 @@
+ 'fxa/sync/FxAccountSyncService.java',
+ 'fxa/sync/FxAccountSyncStatusHelper.java',
+ 'fxa/sync/SchedulePolicy.java',
++ 'fxa/tasks/FxAccountCodeResender.java',
++ 'fxa/tasks/FxAccountCreateAccountTask.java',
++ 'fxa/tasks/FxAccountSetupTask.java',
++ 'fxa/tasks/FxAccountSignInTask.java',
+ 'sync/AlreadySyncingException.java',
+ 'sync/BackoffHandler.java',
+ 'sync/BadRequiredFieldJSONException.java',
diff --git a/data/patches/disable/0002-update-mobile-sqlite-to-v35.patch b/data/patches/disable/0002-update-mobile-sqlite-to-v35.patch
new file mode 100644
index 0000000..9a99ea2
--- /dev/null
+++ b/data/patches/disable/0002-update-mobile-sqlite-to-v35.patch
@@ -0,0 +1,246 @@
+diff -ru mobile/android/base/sqlite/ByteBufferInputStream.java /tmp/mozilla-release/mobile/android/base/sqlite/ByteBufferInputStream.java
+--- a/mobile/android/base/sqlite/ByteBufferInputStream.java 2015-02-23 22:40:38.708934982 +0100
++++ b/mobile/android/base/sqlite/ByteBufferInputStream.java 2015-01-23 07:00:04.000000000 +0100
+@@ -14,7 +14,7 @@
+ * easier to use.
+ */
+ public class ByteBufferInputStream extends InputStream {
+- private ByteBuffer mByteBuffer;
++ private final ByteBuffer mByteBuffer;
+
+ public ByteBufferInputStream(ByteBuffer aByteBuffer) {
+ mByteBuffer = aByteBuffer;
+diff -ru mobile/android/base/sqlite/MatrixBlobCursor.java /tmp/mozilla-release/mobile/android/base/sqlite/MatrixBlobCursor.java
+--- a/mobile/android/base/sqlite/MatrixBlobCursor.java 2015-02-23 22:40:38.720934982 +0100
++++ b/mobile/android/base/sqlite/MatrixBlobCursor.java 2015-01-23 07:00:04.000000000 +0100
+@@ -17,34 +17,37 @@
+
+ package org.mozilla.gecko.sqlite;
+
++import java.nio.ByteBuffer;
++import java.util.ArrayList;
++
++import org.mozilla.gecko.AppConstants;
+ import org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI;
+
+ import android.database.AbstractCursor;
+ import android.database.CursorIndexOutOfBoundsException;
++import android.util.Log;
+
+-import java.nio.ByteBuffer;
+-import java.util.ArrayList;
+-
+-/*
+- * Android's AbstractCursor throws on getBlob()
+- * and MatrixCursor forgot to override it. This was fixed
+- * at some point but old devices are still SOL.
+- * Oh, and everything in MatrixCursor is private instead of
+- * protected, so we need to entirely duplicate it here,
+- * instad of just being able to add the missing method.
+- */
+ /**
+ * A mutable cursor implementation backed by an array of {@code Object}s. Use
+ * {@link #newRow()} to add rows. Automatically expands internal capacity
+ * as needed.
++ *
++ * This class provides one missing feature from Android's MatrixCursor:
++ * the implementation of getBlob that was inadvertently omitted from API 9 (and
++ * perhaps later; it's present in 14).
++ *
++ * MatrixCursor is all private, so we entirely duplicate it here.
+ */
+ public class MatrixBlobCursor extends AbstractCursor {
++ private static final String LOGTAG = "GeckoMatrixCursor";
+
+ private final String[] columnNames;
+- private Object[] data;
+- private int rowCount = 0;
+ private final int columnCount;
+
++ private int rowCount;
++
++ Object[] data;
++
+ /**
+ * Constructs a new cursor with the given initial capacity.
+ *
+@@ -140,17 +143,18 @@
+ */
+ @WrapElementForJNI
+ public void addRow(Iterable<?> columnValues) {
+- int start = rowCount * columnCount;
+- int end = start + columnCount;
+- ensureCapacity(end);
++ final int start = rowCount * columnCount;
+
+ if (columnValues instanceof ArrayList<?>) {
+ addRow((ArrayList<?>) columnValues, start);
+ return;
+ }
+
++ final int end = start + columnCount;
+ int current = start;
+- Object[] localData = data;
++
++ ensureCapacity(end);
++ final Object[] localData = data;
+ for (Object columnValue : columnValues) {
+ if (current == end) {
+ // TODO: null out row?
+@@ -173,39 +177,47 @@
+ /** Optimization for {@link ArrayList}. */
+ @WrapElementForJNI
+ private void addRow(ArrayList<?> columnValues, int start) {
+- int size = columnValues.size();
++ final int size = columnValues.size();
+ if (size != columnCount) {
+ throw new IllegalArgumentException("columnNames.length = "
+ + columnCount + ", columnValues.size() = " + size);
+ }
+
+- rowCount++;
+- Object[] localData = data;
++ final int end = start + columnCount;
++ ensureCapacity(end);
++
++ // Take a reference just in case someone calls ensureCapacity
++ // and `data` gets replaced by a new array!
++ final Object[] localData = data;
+ for (int i = 0; i < size; i++) {
+ localData[start + i] = columnValues.get(i);
+ }
++
++ rowCount++;
+ }
+
+- /** Ensures that this cursor has enough capacity. */
+- private void ensureCapacity(int size) {
+- if (size > data.length) {
+- Object[] oldData = this.data;
+- int newSize = data.length * 2;
+- if (newSize < size) {
+- newSize = size;
+- }
+- this.data = new Object[newSize];
+- System.arraycopy(oldData, 0, this.data, 0, oldData.length);
++ /**
++ * Ensures that this cursor has enough capacity. If it needs to allocate
++ * a new array, the existing capacity will be at least doubled.
++ */
++ private void ensureCapacity(final int size) {
++ if (size <= data.length) {
++ return;
+ }
++
++ final Object[] oldData = this.data;
++ this.data = new Object[Math.max(size, data.length * 2)];
++ System.arraycopy(oldData, 0, this.data, 0, oldData.length);
+ }
+
+ /**
+ * Builds a row, starting from the left-most column and adding one column
+ * value at a time. Follows the same ordering as the column names specified
+ * at cursor construction time.
++ *
++ * Not thread-safe.
+ */
+ public class RowBuilder {
+-
+ private int index;
+ private final int endIndex;
+
+@@ -221,10 +233,9 @@
+ * values
+ * @return this builder to support chaining
+ */
+- public RowBuilder add(Object columnValue) {
++ public RowBuilder add(final Object columnValue) {
+ if (index == endIndex) {
+- throw new CursorIndexOutOfBoundsException(
+- "No more columns left.");
++ throw new CursorIndexOutOfBoundsException("No more columns left.");
+ }
+
+ data[index++] = columnValue;
+@@ -232,6 +243,9 @@
+ }
+ }
+
++ /**
++ * Not thread safe.
++ */
+ public void set(int column, Object value) {
+ if (column < 0 || column >= columnCount) {
+ throw new CursorIndexOutOfBoundsException("Requested column: "
+@@ -266,7 +280,7 @@
+
+ @Override
+ public short getShort(int column) {
+- Object value = get(column);
++ final Object value = get(column);
+ if (value == null) return 0;
+ if (value instanceof Number) return ((Number) value).shortValue();
+ return Short.parseShort(value.toString());
+@@ -311,10 +325,11 @@
+ if (value instanceof byte[]) {
+ return (byte[]) value;
+ }
++
+ if (value instanceof ByteBuffer) {
+- ByteBuffer data = (ByteBuffer)value;
+- byte[] byteArray = new byte[data.remaining()];
+- data.get(byteArray);
++ final ByteBuffer bytes = (ByteBuffer) value;
++ byte[] byteArray = new byte[bytes.remaining()];
++ bytes.get(byteArray);
+ return byteArray;
+ }
+ throw new UnsupportedOperationException("BLOB Object not of known type");
+@@ -324,4 +339,15 @@
+ public boolean isNull(int column) {
+ return get(column) == null;
+ }
++
++ @Override
++ protected void finalize() {
++ if (AppConstants.DEBUG_BUILD) {
++ if (!isClosed()) {
++ Log.e(LOGTAG, "Cursor finalized without being closed", new RuntimeException("stack"));
++ }
++ }
++
++ super.finalize();
++ }
+ }
+diff -ru mobile/android/base/sqlite/SQLiteBridge.java /tmp/mozilla-release/mobile/android/base/sqlite/SQLiteBridge.java
+--- a/mobile/android/base/sqlite/SQLiteBridge.java 2015-02-23 22:40:38.716934982 +0100
++++ b/mobile/android/base/sqlite/SQLiteBridge.java 2015-01-23 07:00:04.000000000 +0100
+@@ -26,16 +26,16 @@
+ private static final String LOGTAG = "SQLiteBridge";
+
+ // Path to the database. If this database was not opened with openDatabase, we reopen it every query.
+- private String mDb;
++ private final String mDb;
+
+ // Pointer to the database if it was opened with openDatabase. 0 implies closed.
+- protected volatile long mDbPointer = 0L;
++ protected volatile long mDbPointer;
+
+ // Values remembered after a query.
+ private long[] mQueryResults;
+
+- private boolean mTransactionSuccess = false;
+- private boolean mInTransaction = false;
++ private boolean mTransactionSuccess;
++ private boolean mInTransaction;
+
+ private static final int RESULT_INSERT_ROW_ID = 0;
+ private static final int RESULT_ROWS_CHANGED = 1;
+@@ -227,6 +227,7 @@
+ cursor.moveToFirst();
+ String version = cursor.getString(0);
+ ret = Integer.parseInt(version);
++ cursor.close();
+ }
+ return ret;
+ }
diff --git a/data/patches/disable/0003-upgrade-mobile-sync-to-v32.patch b/data/patches/disable/0003-upgrade-mobile-sync-to-v32.patch
new file mode 100644
index 0000000..e420727
--- /dev/null
+++ b/data/patches/disable/0003-upgrade-mobile-sync-to-v32.patch
@@ -0,0 +1,992 @@
+diff -ruN mozilla-esr31/mobile/android/base/fxa/FirefoxAccounts.java mozilla-release/mobile/android/base/fxa/FirefoxAccounts.java
+--- mozilla-esr31/mobile/android/base/fxa/FirefoxAccounts.java 2015-02-25 19:21:06.000000000 +0100
++++ mozilla-release/mobile/android/base/fxa/FirefoxAccounts.java 2014-09-24 03:05:32.000000000 +0200
+@@ -6,13 +6,18 @@
+
+ import java.io.File;
+ import java.util.EnumSet;
++import java.util.Locale;
+ import java.util.concurrent.CountDownLatch;
+
++import org.mozilla.gecko.AppConstants;
++import org.mozilla.gecko.R;
+ import org.mozilla.gecko.background.common.log.Logger;
+ import org.mozilla.gecko.fxa.authenticator.AccountPickler;
+ import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
++import org.mozilla.gecko.fxa.login.State;
+ import org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter;
+ import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper;
++import org.mozilla.gecko.fxa.tasks.FxAccountCodeResender;
+ import org.mozilla.gecko.sync.ThreadPool;
+ import org.mozilla.gecko.sync.Utils;
+
+@@ -20,6 +25,7 @@
+ import android.accounts.AccountManager;
+ import android.content.ContentResolver;
+ import android.content.Context;
++import android.content.res.Resources;
+ import android.os.Bundle;
+
+ /**
+@@ -152,6 +158,38 @@
+ return null;
+ }
+
++ /**
++ * @return
++ * the {@link State} instance associated with the current account, or <code>null</code> if
++ * no accounts exist.
++ */
++ public static State getFirefoxAccountState(final Context context) {
++ final Account account = getFirefoxAccount(context);
++ if (account == null) {
++ return null;
++ }
++
++ final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
++ try {
++ return fxAccount.getState();
++ } catch (final Exception ex) {
++ Logger.warn(LOG_TAG, "Could not get FX account state.", ex);
++ return null;
++ }
++ }
++
++ /*
++ * @param context Android context
++ * @return the email address associated with the configured Firefox account if one exists; null otherwise.
++ */
++ public static String getFirefoxAccountEmail(final Context context) {
++ final Account account = getFirefoxAccount(context);
++ if (account == null) {
++ return null;
++ }
++ return account.name;
++ }
++
+ protected static void putHintsToSync(final Bundle extras, EnumSet<SyncHint> syncHints) {
+ // stagesToSync and stagesToSkip are allowed to be null.
+ if (syncHints == null) {
+@@ -264,4 +302,33 @@
+ // stopObserving null-checks its argument.
+ FxAccountSyncStatusHelper.getInstance().stopObserving(syncStatusListener);
+ }
++
++ public static String getOldSyncUpgradeURL(final Resources res, final Locale locale) {
++ final String VERSION = AppConstants.MOZ_APP_VERSION;
++ final String OS = AppConstants.OS_TARGET;
++ final String LOCALE = Utils.getLanguageTag(locale);
++ return res.getString(R.string.fxaccount_link_old_firefox, VERSION, OS, LOCALE);
++ }
++
++ /**
++ * Resends the account verification email, and displays an appropriate
++ * toast on both send success and failure. Note that because the underlying implementation
++ * uses {@link AsyncTask}, the provided context must be UI-capable, and this
++ * method called from the UI thread (see
++ * {@link org.mozilla.gecko.fxa.tasks.FxAccountCodeResender#resendCode(Context, AndroidFxAccount)}
++ * for more).
++ *
++ * @param context a UI-capable Android context.
++ * @return true if an account exists, false otherwise.
++ */
++ public static boolean resendVerificationEmail(final Context context) {
++ final Account account = getFirefoxAccount(context);
++ if (account == null) {
++ return false;
++ }
++
++ final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
++ FxAccountCodeResender.resendCode(context, fxAccount);
++ return true;
++ }
+ }
+diff -ruN mozilla-esr31/mobile/android/base/fxa/activities/FxAccountAbstractSetupActivity.java mozilla-release/mobile/android/base/fxa/activities/FxAccountAbstractSetupActivity.java
+--- mozilla-esr31/mobile/android/base/fxa/activities/FxAccountAbstractSetupActivity.java 2015-02-25 19:21:06.000000000 +0100
++++ mozilla-release/mobile/android/base/fxa/activities/FxAccountAbstractSetupActivity.java 2014-09-24 03:05:32.000000000 +0200
+@@ -19,10 +19,10 @@
+ import org.mozilla.gecko.background.fxa.PasswordStretcher;
+ import org.mozilla.gecko.background.fxa.QuickPasswordStretcher;
+ import org.mozilla.gecko.fxa.FxAccountConstants;
+-import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.ProgressDisplay;
+ import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+ import org.mozilla.gecko.fxa.login.Engaged;
+ import org.mozilla.gecko.fxa.login.State;
++import org.mozilla.gecko.fxa.tasks.FxAccountSetupTask.ProgressDisplay;
+ import org.mozilla.gecko.sync.SyncConfiguration;
+ import org.mozilla.gecko.sync.setup.Constants;
+ import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
+diff -ruN mozilla-esr31/mobile/android/base/fxa/activities/FxAccountConfirmAccountActivity.java mozilla-release/mobile/android/base/fxa/activities/FxAccountConfirmAccountActivity.java
+--- mozilla-esr31/mobile/android/base/fxa/activities/FxAccountConfirmAccountActivity.java 2015-02-25 19:21:06.000000000 +0100
++++ mozilla-release/mobile/android/base/fxa/activities/FxAccountConfirmAccountActivity.java 2014-09-24 03:05:32.000000000 +0200
+@@ -4,21 +4,15 @@
+
+ package org.mozilla.gecko.fxa.activities;
+
+-import java.util.concurrent.Executor;
+-import java.util.concurrent.Executors;
+-
+ import org.mozilla.gecko.R;
+ import org.mozilla.gecko.background.common.log.Logger;
+-import org.mozilla.gecko.background.fxa.FxAccountClient;
+-import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
+-import org.mozilla.gecko.background.fxa.FxAccountClient20;
+-import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
+ import org.mozilla.gecko.fxa.FirefoxAccounts;
+ import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+ import org.mozilla.gecko.fxa.login.Engaged;
+ import org.mozilla.gecko.fxa.login.State;
+ import org.mozilla.gecko.fxa.login.State.Action;
+ import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper;
++import org.mozilla.gecko.fxa.tasks.FxAccountCodeResender;
+ import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
+
+ import android.accounts.Account;
+@@ -28,7 +22,6 @@
+ import android.view.View;
+ import android.view.View.OnClickListener;
+ import android.widget.TextView;
+-import android.widget.Toast;
+
+ /**
+ * Activity which displays account created successfully screen to the user, and
+@@ -164,76 +157,8 @@
+ resendLink.setClickable(resendLinkShouldBeEnabled);
+ }
+
+- public static class FxAccountResendCodeTask extends FxAccountSetupTask<Void> {
+- protected static final String LOG_TAG = FxAccountResendCodeTask.class.getSimpleName();
+-
+- protected final byte[] sessionToken;
+-
+- public FxAccountResendCodeTask(Context context, byte[] sessionToken, FxAccountClient client, RequestDelegate<Void> delegate) {
+- super(context, null, client, delegate);
+- this.sessionToken = sessionToken;
+- }
+-
+- @Override
+- protected InnerRequestDelegate<Void> doInBackground(Void... arg0) {
+- try {
+- client.resendCode(sessionToken, innerDelegate);
+- latch.await();
+- return innerDelegate;
+- } catch (Exception e) {
+- Logger.error(LOG_TAG, "Got exception signing in.", e);
+- delegate.handleError(e);
+- }
+- return null;
+- }
+- }
+-
+- protected static class ResendCodeDelegate implements RequestDelegate<Void> {
+- public final Context context;
+-
+- public ResendCodeDelegate(Context context) {
+- this.context = context;
+- }
+-
+- @Override
+- public void handleError(Exception e) {
+- Logger.warn(LOG_TAG, "Got exception requesting fresh confirmation link; ignoring.", e);
+- Toast.makeText(context, R.string.fxaccount_confirm_account_verification_link_not_sent, Toast.LENGTH_LONG).show();
+- }
+-
+- @Override
+- public void handleFailure(FxAccountClientRemoteException e) {
+- handleError(e);
+- }
+-
+- @Override
+- public void handleSuccess(Void result) {
+- Toast.makeText(context, R.string.fxaccount_confirm_account_verification_link_sent, Toast.LENGTH_SHORT).show();
+- }
+- }
+-
+- public static void resendCode(Context context, AndroidFxAccount fxAccount) {
+- RequestDelegate<Void> delegate = new ResendCodeDelegate(context);
+-
+- byte[] sessionToken;
+- try {
+- sessionToken = ((Engaged) fxAccount.getState()).getSessionToken();
+- } catch (Exception e) {
+- delegate.handleError(e);
+- return;
+- }
+- if (sessionToken == null) {
+- delegate.handleError(new IllegalStateException("sessionToken should not be null"));
+- return;
+- }
+-
+- Executor executor = Executors.newSingleThreadExecutor();
+- FxAccountClient client = new FxAccountClient20(fxAccount.getAccountServerURI(), executor);
+- new FxAccountResendCodeTask(context, sessionToken, client, delegate).execute();
+- }
+-
+ @Override
+ public void onClick(View v) {
+- resendCode(this, fxAccount);
++ FxAccountCodeResender.resendCode(this, fxAccount);
+ }
+ }
+diff -ruN mozilla-esr31/mobile/android/base/fxa/activities/FxAccountCreateAccountActivity.java mozilla-release/mobile/android/base/fxa/activities/FxAccountCreateAccountActivity.java
+--- mozilla-esr31/mobile/android/base/fxa/activities/FxAccountCreateAccountActivity.java 2015-02-25 19:21:06.000000000 +0100
++++ mozilla-release/mobile/android/base/fxa/activities/FxAccountCreateAccountActivity.java 2014-09-24 03:05:32.000000000 +0200
+@@ -21,7 +21,7 @@
+ import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
+ import org.mozilla.gecko.background.fxa.PasswordStretcher;
+ import org.mozilla.gecko.fxa.FxAccountConstants;
+-import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.FxAccountCreateAccountTask;
++import org.mozilla.gecko.fxa.tasks.FxAccountCreateAccountTask;
+
+ import android.app.AlertDialog;
+ import android.app.Dialog;
+diff -ruN mozilla-esr31/mobile/android/base/fxa/activities/FxAccountGetStartedActivity.java mozilla-release/mobile/android/base/fxa/activities/FxAccountGetStartedActivity.java
+--- mozilla-esr31/mobile/android/base/fxa/activities/FxAccountGetStartedActivity.java 2015-02-25 19:21:06.000000000 +0100
++++ mozilla-release/mobile/android/base/fxa/activities/FxAccountGetStartedActivity.java 2014-09-24 03:05:32.000000000 +0200
+@@ -6,13 +6,11 @@
+
+ import java.util.Locale;
+
+-import org.mozilla.gecko.AppConstants;
+ import org.mozilla.gecko.R;
+ import org.mozilla.gecko.background.common.log.Logger;
+ import org.mozilla.gecko.background.fxa.FxAccountAgeLockoutHelper;
+ import org.mozilla.gecko.fxa.FirefoxAccounts;
+ import org.mozilla.gecko.fxa.FxAccountConstants;
+-import org.mozilla.gecko.sync.Utils;
+ import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
+ import org.mozilla.gecko.sync.setup.activities.LocaleAware;
+
+@@ -109,11 +107,7 @@
+ protected void linkifyOldFirefoxLink() {
+ TextView oldFirefox = (TextView) findViewById(R.id.old_firefox);
+ String text = getResources().getString(R.string.fxaccount_getting_started_old_firefox);
+- String VERSION = AppConstants.MOZ_APP_VERSION;
+- String OS = AppConstants.OS_TARGET;
+-
+- String LOCALE = Utils.getLanguageTag(Locale.getDefault());
+- String url = getResources().getString(R.string.fxaccount_link_old_firefox, VERSION, OS, LOCALE);
++ final String url = FirefoxAccounts.getOldSyncUpgradeURL(getResources(), Locale.getDefault());
+ FxAccountConstants.pii(LOG_TAG, "Old Firefox url is: " + url); // Don't want to leak locale in particular.
+ ActivityUtils.linkTextView(oldFirefox, text, url);
+ }
+diff -ruN mozilla-esr31/mobile/android/base/fxa/activities/FxAccountSetupTask.java mozilla-release/mobile/android/base/fxa/activities/FxAccountSetupTask.java
+--- mozilla-esr31/mobile/android/base/fxa/activities/FxAccountSetupTask.java 2015-02-25 19:21:06.000000000 +0100
++++ mozilla-release/mobile/android/base/fxa/activities/FxAccountSetupTask.java 1970-01-01 01:00:00.000000000 +0100
+@@ -1,172 +0,0 @@
+-/* 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.fxa.activities;
+-
+-import java.io.UnsupportedEncodingException;
+-import java.util.concurrent.CountDownLatch;
+-
+-import org.mozilla.gecko.background.common.log.Logger;
+-import org.mozilla.gecko.background.fxa.FxAccountClient;
+-import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
+-import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
+-import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
+-import org.mozilla.gecko.background.fxa.PasswordStretcher;
+-import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.InnerRequestDelegate;
+-
+-import android.content.Context;
+-import android.os.AsyncTask;
+-
+-/**
+- * An <code>AsyncTask</code> wrapper around signing up for, and signing in to, a
+- * Firefox Account.
+- * <p>
+- * It's strange to add explicit blocking to callback-threading code, but we do
+- * it here to take advantage of Android's built in support for background work.
+- * We really want to avoid making a threading mistake that brings down the whole
+- * process.
+- */
+-abstract class FxAccountSetupTask<T> extends AsyncTask<Void, Void, InnerRequestDelegate<T>> {
+- private static final String LOG_TAG = FxAccountSetupTask.class.getSimpleName();
+-
+- public interface ProgressDisplay {
+- public void showProgress();
+- public void dismissProgress();
+- }
+-
+- protected final Context context;
+- protected final FxAccountClient client;
+- protected final ProgressDisplay progressDisplay;
+-
+- // Initialized lazily.
+- protected byte[] quickStretchedPW;
+-
+- // AsyncTask's are one-time-use, so final members are fine.
+- protected final CountDownLatch latch = new CountDownLatch(1);
+- protected final InnerRequestDelegate<T> innerDelegate = new InnerRequestDelegate<T>(latch);
+-
+- protected final RequestDelegate<T> delegate;
+-
+- public FxAccountSetupTask(Context context, ProgressDisplay progressDisplay, FxAccountClient client, RequestDelegate<T> delegate) {
+- this.context = context;
+- this.client = client;
+- this.delegate = delegate;
+- this.progressDisplay = progressDisplay;
+- }
+-
+- @Override
+- protected void onPreExecute() {
+- if (progressDisplay != null) {
+- progressDisplay.showProgress();
+- }
+- }
+-
+- @Override
+- protected void onPostExecute(InnerRequestDelegate<T> result) {
+- if (progressDisplay != null) {
+- progressDisplay.dismissProgress();
+- }
+-
+- // We are on the UI thread, and need to invoke these callbacks here to allow UI updating.
+- if (innerDelegate.failure != null) {
+- delegate.handleFailure(innerDelegate.failure);
+- } else if (innerDelegate.exception != null) {
+- delegate.handleError(innerDelegate.exception);
+- } else {
+- delegate.handleSuccess(result.response);
+- }
+- }
+-
+- @Override
+- protected void onCancelled(InnerRequestDelegate<T> result) {
+- if (progressDisplay != null) {
+- progressDisplay.dismissProgress();
+- }
+- delegate.handleError(new IllegalStateException("Task was cancelled."));
+- }
+-
+- protected static class InnerRequestDelegate<T> implements RequestDelegate<T> {
+- protected final CountDownLatch latch;
+- public T response = null;
+- public Exception exception = null;
+- public FxAccountClientRemoteException failure = null;
+-
+- protected InnerRequestDelegate(CountDownLatch latch) {
+- this.latch = latch;
+- }
+-
+- @Override
+- public void handleError(Exception e) {
+- Logger.error(LOG_TAG, "Got exception.");
+- this.exception = e;
+- latch.countDown();
+- }
+-
+- @Override
+- public void handleFailure(FxAccountClientRemoteException e) {
+- Logger.warn(LOG_TAG, "Got failure.");
+- this.failure = e;
+- latch.countDown();
+- }
+-
+- @Override
+- public void handleSuccess(T result) {
+- Logger.info(LOG_TAG, "Got success.");
+- this.response = result;
+- latch.countDown();
+- }
+- }
+-
+- public static class FxAccountCreateAccountTask extends FxAccountSetupTask<LoginResponse> {
+- private static final String LOG_TAG = FxAccountCreateAccountTask.class.getSimpleName();
+-
+- protected final byte[] emailUTF8;
+- protected final PasswordStretcher passwordStretcher;
+-
+- public FxAccountCreateAccountTask(Context context, ProgressDisplay progressDisplay, String email, PasswordStretcher passwordStretcher, FxAccountClient client, RequestDelegate<LoginResponse> delegate) throws UnsupportedEncodingException {
+- super(context, progressDisplay, client, delegate);
+- this.emailUTF8 = email.getBytes("UTF-8");
+- this.passwordStretcher = passwordStretcher;
+- }
+-
+- @Override
+- protected InnerRequestDelegate<LoginResponse> doInBackground(Void... arg0) {
+- try {
+- client.createAccountAndGetKeys(emailUTF8, passwordStretcher, innerDelegate);
+- latch.await();
+- return innerDelegate;
+- } catch (Exception e) {
+- Logger.error(LOG_TAG, "Got exception logging in.", e);
+- delegate.handleError(e);
+- }
+- return null;
+- }
+- }
+-
+- public static class FxAccountSignInTask extends FxAccountSetupTask<LoginResponse> {
+- protected static final String LOG_TAG = FxAccountSignInTask.class.getSimpleName();
+-
+- protected final byte[] emailUTF8;
+- protected final PasswordStretcher passwordStretcher;
+-
+- public FxAccountSignInTask(Context context, ProgressDisplay progressDisplay, String email, PasswordStretcher passwordStretcher, FxAccountClient client, RequestDelegate<LoginResponse> delegate) throws UnsupportedEncodingException {
+- super(context, progressDisplay, client, delegate);
+- this.emailUTF8 = email.getBytes("UTF-8");
+- this.passwordStretcher = passwordStretcher;
+- }
+-
+- @Override
+- protected InnerRequestDelegate<LoginResponse> doInBackground(Void... arg0) {
+- try {
+- client.loginAndGetKeys(emailUTF8, passwordStretcher, innerDelegate);
+- latch.await();
+- return innerDelegate;
+- } catch (Exception e) {
+- Logger.error(LOG_TAG, "Got exception signing in.", e);
+- delegate.handleError(e);
+- }
+- return null;
+- }
+- }
+-}
+diff -ruN mozilla-esr31/mobile/android/base/fxa/activities/FxAccountSignInActivity.java mozilla-release/mobile/android/base/fxa/activities/FxAccountSignInActivity.java
+--- mozilla-esr31/mobile/android/base/fxa/activities/FxAccountSignInActivity.java 2015-02-25 19:21:06.000000000 +0100
++++ mozilla-release/mobile/android/base/fxa/activities/FxAccountSignInActivity.java 2014-09-24 03:05:32.000000000 +0200
+@@ -16,7 +16,7 @@
+ import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
+ import org.mozilla.gecko.background.fxa.PasswordStretcher;
+ import org.mozilla.gecko.fxa.FxAccountConstants;
+-import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.FxAccountSignInTask;
++import org.mozilla.gecko.fxa.tasks.FxAccountSignInTask;
+ import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
+
+ import android.content.Intent;
+diff -ruN mozilla-esr31/mobile/android/base/fxa/activities/FxAccountStatusFragment.java mozilla-release/mobile/android/base/fxa/activities/FxAccountStatusFragment.java
+--- mozilla-esr31/mobile/android/base/fxa/activities/FxAccountStatusFragment.java 2015-02-25 19:21:06.000000000 +0100
++++ mozilla-release/mobile/android/base/fxa/activities/FxAccountStatusFragment.java 2014-09-24 03:05:32.000000000 +0200
+@@ -17,6 +17,8 @@
+ import org.mozilla.gecko.fxa.login.Married;
+ import org.mozilla.gecko.fxa.login.State;
+ import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper;
++import org.mozilla.gecko.fxa.tasks.FxAccountCodeResender;
++import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
+ import org.mozilla.gecko.sync.SyncConfiguration;
+
+ import android.accounts.Account;
+@@ -27,10 +29,13 @@
+ import android.os.Bundle;
+ import android.os.Handler;
+ import android.preference.CheckBoxPreference;
++import android.preference.EditTextPreference;
+ import android.preference.Preference;
++import android.preference.Preference.OnPreferenceChangeListener;
+ import android.preference.Preference.OnPreferenceClickListener;
+ import android.preference.PreferenceCategory;
+ import android.preference.PreferenceScreen;
++import android.text.TextUtils;
+
+ /**
+ * A fragment that displays the status of an AndroidFxAccount.
+@@ -38,7 +43,9 @@
+ * The owning activity is responsible for providing an AndroidFxAccount at
+ * appropriate times.
+ */
+-public class FxAccountStatusFragment extends PreferenceFragment implements OnPreferenceClickListener {
++public class FxAccountStatusFragment
++ extends PreferenceFragment
++ implements OnPreferenceClickListener, OnPreferenceChangeListener {
+ private static final String LOG_TAG = FxAccountStatusFragment.class.getSimpleName();
+
+ // When a checkbox is toggled, wait 5 seconds (for other checkbox actions)
+@@ -64,7 +71,12 @@
+ protected CheckBoxPreference tabsPreference;
+ protected CheckBoxPreference passwordsPreference;
+
++ protected EditTextPreference deviceNamePreference;
++
+ protected volatile AndroidFxAccount fxAccount;
++ // The contract is: when fxAccount is non-null, then clientsDataDelegate is
++ // non-null. If violated then an IllegalStateException is thrown.
++ protected volatile SharedPreferencesClientsDataDelegate clientsDataDelegate;
+
+ // Used to post delayed sync requests.
+ protected Handler handler;
+@@ -87,6 +99,10 @@
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
++ addPreferences();
++ }
++
++ protected void addPreferences() {
+ addPreferencesFromResource(R.xml.fxaccount_status_prefscreen);
+
+ emailPreference = ensureFindPreference("email");
+@@ -118,6 +134,9 @@
+ historyPreference.setOnPreferenceClickListener(this);
+ tabsPreference.setOnPreferenceClickListener(this);
+ passwordsPreference.setOnPreferenceClickListener(this);
++
++ deviceNamePreference = (EditTextPreference) ensureFindPreference("device_name");
++ deviceNamePreference.setOnPreferenceChangeListener(this);
+ }
+
+ /**
+@@ -142,7 +161,7 @@
+ }
+
+ if (preference == needsVerificationPreference) {
+- FxAccountConfirmAccountActivity.resendCode(getActivity().getApplicationContext(), fxAccount);
++ FxAccountCodeResender.resendCode(getActivity().getApplicationContext(), fxAccount);
+
+ Intent intent = new Intent(getActivity(), FxAccountConfirmAccountActivity.class);
+ // Per http://stackoverflow.com/a/8992365, this triggers a known bug with
+@@ -176,6 +195,8 @@
+ historyPreference.setEnabled(enabled);
+ tabsPreference.setEnabled(enabled);
+ passwordsPreference.setEnabled(enabled);
++ // Since we can't sync, we can't update our remote client record.
++ deviceNamePreference.setEnabled(enabled);
+ }
+
+ /**
+@@ -293,6 +314,14 @@
+ throw new IllegalArgumentException("fxAccount must not be null");
+ }
+ this.fxAccount = fxAccount;
++ try {
++ this.clientsDataDelegate = new SharedPreferencesClientsDataDelegate(fxAccount.getSyncPrefs());
++ } catch (Exception e) {
++ Logger.error(LOG_TAG, "Got exception fetching Sync prefs associated to Firefox Account; aborting.", e);
++ // Something is terribly wrong; best to get a stack trace rather than
++ // continue with a null clients delegate.
++ throw new IllegalStateException(e);
++ }
+
+ handler = new Handler(); // Attached to current (assumed to be UI) thread.
+
+@@ -318,6 +347,17 @@
+ FxAccountSyncStatusHelper.getInstance().stopObserving(syncStatusDelegate);
+ }
+
++ protected void hardRefresh() {
++ // This is the only way to guarantee that the EditText dialogs created by
++ // EditTextPreferences are re-created. This works around the issue described
++ // at http://androiddev.orkitra.com/?p=112079.
++ final PreferenceScreen statusScreen = (PreferenceScreen) ensureFindPreference("status_screen");
++ statusScreen.removeAll();
++ addPreferences();
++
++ refresh();
++ }
++
+ protected void refresh() {
+ // refresh is called from our onResume, which can happen before the owning
+ // Activity tells us about an account (via our public
+@@ -371,6 +411,10 @@
+ // No matter our state, we should update the checkboxes.
+ updateSelectedEngines();
+ }
++
++ final String clientName = clientsDataDelegate.getClientName();
++ deviceNamePreference.setSummary(clientName);
++ deviceNamePreference.setText(clientName);
+ }
+
+ /**
+@@ -570,4 +614,22 @@
+ button.setOnPreferenceClickListener(listener);
+ }
+ }
++
++ @Override
++ public boolean onPreferenceChange(Preference preference, Object newValue) {
++ if (preference == deviceNamePreference) {
++ String newClientName = (String) newValue;
++ if (TextUtils.isEmpty(newClientName)) {
++ newClientName = clientsDataDelegate.getDefaultClientName();
++ }
++ final long now = System.currentTimeMillis();
++ clientsDataDelegate.setClientName(newClientName, now);
++ requestDelayedSync(); // Try to update our remote client record.
++ hardRefresh(); // Updates the value displayed to the user, among other things.
++ return true;
++ }
++
++ // For everything else, accept the change.
++ return true;
++ }
+ }
+diff -ruN mozilla-esr31/mobile/android/base/fxa/activities/FxAccountUpdateCredentialsActivity.java mozilla-release/mobile/android/base/fxa/activities/FxAccountUpdateCredentialsActivity.java
+--- mozilla-esr31/mobile/android/base/fxa/activities/FxAccountUpdateCredentialsActivity.java 2015-02-25 19:21:06.000000000 +0100
++++ mozilla-release/mobile/android/base/fxa/activities/FxAccountUpdateCredentialsActivity.java 2014-09-24 03:05:32.000000000 +0200
+@@ -18,11 +18,11 @@
+ import org.mozilla.gecko.background.fxa.PasswordStretcher;
+ import org.mozilla.gecko.fxa.FirefoxAccounts;
+ import org.mozilla.gecko.fxa.FxAccountConstants;
+-import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.FxAccountSignInTask;
+ import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+ import org.mozilla.gecko.fxa.login.Engaged;
+ import org.mozilla.gecko.fxa.login.State;
+ import org.mozilla.gecko.fxa.login.State.StateLabel;
++import org.mozilla.gecko.fxa.tasks.FxAccountSignInTask;
+ import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
+
+ import android.os.Bundle;
+diff -ruN mozilla-esr31/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java mozilla-release/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
+--- mozilla-esr31/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java 2015-02-25 19:21:06.000000000 +0100
++++ mozilla-release/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java 2014-09-24 03:05:33.000000000 +0200
+@@ -357,7 +357,11 @@
+
+ FxAccountGlobalSession globalSession = null;
+ try {
+- ClientsDataDelegate clientsDataDelegate = new SharedPreferencesClientsDataDelegate(sharedPrefs);
++ final ClientsDataDelegate clientsDataDelegate = new SharedPreferencesClientsDataDelegate(sharedPrefs);
++ if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
++ FxAccountConstants.pii(LOG_TAG, "Client device name is: '" + clientsDataDelegate.getClientName() + "'.");
++ FxAccountConstants.pii(LOG_TAG, "Client device data last modified: " + clientsDataDelegate.getLastModifiedTimestamp());
++ }
+
+ // We compute skew over time using SkewHandler. This yields an unchanging
+ // skew adjustment that the HawkAuthHeaderProvider uses to adjust its
+diff -ruN mozilla-esr31/mobile/android/base/fxa/tasks/FxAccountCodeResender.java mozilla-release/mobile/android/base/fxa/tasks/FxAccountCodeResender.java
+--- mozilla-esr31/mobile/android/base/fxa/tasks/FxAccountCodeResender.java 1970-01-01 01:00:00.000000000 +0100
++++ mozilla-release/mobile/android/base/fxa/tasks/FxAccountCodeResender.java 2014-09-24 03:05:33.000000000 +0200
+@@ -0,0 +1,108 @@
++/* 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.fxa.tasks;
++
++import java.util.concurrent.Executor;
++import java.util.concurrent.Executors;
++
++import org.mozilla.gecko.R;
++import org.mozilla.gecko.background.common.log.Logger;
++import org.mozilla.gecko.background.fxa.FxAccountClient;
++import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
++import org.mozilla.gecko.background.fxa.FxAccountClient20;
++import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
++import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
++import org.mozilla.gecko.fxa.login.Engaged;
++
++import android.content.Context;
++import android.widget.Toast;
++
++/**
++ * A helper class that provides a simple interface for requesting
++ * a Firefox Account verification email to be resent.
++ */
++public class FxAccountCodeResender {
++ private static final String LOG_TAG = FxAccountCodeResender.class.getSimpleName();
++
++ private static class FxAccountResendCodeTask extends FxAccountSetupTask<Void> {
++ protected static final String LOG_TAG = FxAccountResendCodeTask.class.getSimpleName();
++
++ protected final byte[] sessionToken;
++
++ public FxAccountResendCodeTask(Context context, byte[] sessionToken, FxAccountClient client, RequestDelegate<Void> delegate) {
++ super(context, null, client, delegate);
++ this.sessionToken = sessionToken;
++ }
++
++ @Override
++ protected InnerRequestDelegate<Void> doInBackground(Void... arg0) {
++ try {
++ client.resendCode(sessionToken, innerDelegate);
++ latch.await();
++ return innerDelegate;
++ } catch (Exception e) {
++ Logger.error(LOG_TAG, "Got exception signing in.", e);
++ delegate.handleError(e);
++ }
++ return null;
++ }
++ }
++
++ private static class ResendCodeDelegate implements RequestDelegate<Void> {
++ public final Context context;
++
++ public ResendCodeDelegate(Context context) {
++ this.context = context;
++ }
++
++ @Override
++ public void handleError(Exception e) {
++ Logger.warn(LOG_TAG, "Got exception requesting fresh confirmation link; ignoring.", e);
++ Toast.makeText(context, R.string.fxaccount_confirm_account_verification_link_not_sent, Toast.LENGTH_LONG).show();
++ }
++
++ @Override
++ public void handleFailure(FxAccountClientRemoteException e) {
++ handleError(e);
++ }
++
++ @Override
++ public void handleSuccess(Void result) {
++ Toast.makeText(context, R.string.fxaccount_confirm_account_verification_link_sent, Toast.LENGTH_SHORT).show();
++ }
++ }
++
++ /**
++ * Resends the account verification email, and displays an appropriate
++ * toast on both send success and failure. Note that because the underlying implementation
++ * uses {@link AsyncTask}, the provided context must be UI-capable and
++ * this method called from the UI thread.
++ *
++ * Note that it may actually be possible to run this (and the {@link AsyncTask}) method
++ * from a background thread - but this hasn't been tested.
++ *
++ * @param context A UI-capable Android context.
++ * @param fxAccount The Firefox Account to resend the code to.
++ */
++ public static void resendCode(Context context, AndroidFxAccount fxAccount) {
++ RequestDelegate<Void> delegate = new ResendCodeDelegate(context);
++
++ byte[] sessionToken;
++ try {
++ sessionToken = ((Engaged) fxAccount.getState()).getSessionToken();
++ } catch (Exception e) {
++ delegate.handleError(e);
++ return;
++ }
++ if (sessionToken == null) {
++ delegate.handleError(new IllegalStateException("sessionToken should not be null"));
++ return;
++ }
++
++ Executor executor = Executors.newSingleThreadExecutor();
++ FxAccountClient client = new FxAccountClient20(fxAccount.getAccountServerURI(), executor);
++ new FxAccountResendCodeTask(context, sessionToken, client, delegate).execute();
++ }
++}
+diff -ruN mozilla-esr31/mobile/android/base/fxa/tasks/FxAccountCreateAccountTask.java mozilla-release/mobile/android/base/fxa/tasks/FxAccountCreateAccountTask.java
+--- mozilla-esr31/mobile/android/base/fxa/tasks/FxAccountCreateAccountTask.java 1970-01-01 01:00:00.000000000 +0100
++++ mozilla-release/mobile/android/base/fxa/tasks/FxAccountCreateAccountTask.java 2014-09-24 03:05:33.000000000 +0200
+@@ -0,0 +1,41 @@
++/* 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.fxa.tasks;
++
++import java.io.UnsupportedEncodingException;
++
++import org.mozilla.gecko.background.common.log.Logger;
++import org.mozilla.gecko.background.fxa.FxAccountClient;
++import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
++import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
++import org.mozilla.gecko.background.fxa.PasswordStretcher;
++
++import android.content.Context;
++
++public class FxAccountCreateAccountTask extends FxAccountSetupTask<LoginResponse> {
++ private static final String LOG_TAG = FxAccountCreateAccountTask.class.getSimpleName();
++
++ protected final byte[] emailUTF8;
++ protected final PasswordStretcher passwordStretcher;
++
++ public FxAccountCreateAccountTask(Context context, ProgressDisplay progressDisplay, String email, PasswordStretcher passwordStretcher, FxAccountClient client, RequestDelegate<LoginResponse> delegate) throws UnsupportedEncodingException {
++ super(context, progressDisplay, client, delegate);
++ this.emailUTF8 = email.getBytes("UTF-8");
++ this.passwordStretcher = passwordStretcher;
++ }
++
++ @Override
++ protected InnerRequestDelegate<LoginResponse> doInBackground(Void... arg0) {
++ try {
++ client.createAccountAndGetKeys(emailUTF8, passwordStretcher, innerDelegate);
++ latch.await();
++ return innerDelegate;
++ } catch (Exception e) {
++ Logger.error(LOG_TAG, "Got exception logging in.", e);
++ delegate.handleError(e);
++ }
++ return null;
++ }
++}
+diff -ruN mozilla-esr31/mobile/android/base/fxa/tasks/FxAccountSetupTask.java mozilla-release/mobile/android/base/fxa/tasks/FxAccountSetupTask.java
+--- mozilla-esr31/mobile/android/base/fxa/tasks/FxAccountSetupTask.java 1970-01-01 01:00:00.000000000 +0100
++++ mozilla-release/mobile/android/base/fxa/tasks/FxAccountSetupTask.java 2014-09-24 03:05:33.000000000 +0200
+@@ -0,0 +1,117 @@
++/* 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.fxa.tasks;
++
++import java.util.concurrent.CountDownLatch;
++
++import org.mozilla.gecko.background.common.log.Logger;
++import org.mozilla.gecko.background.fxa.FxAccountClient;
++import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
++import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
++import org.mozilla.gecko.fxa.tasks.FxAccountSetupTask.InnerRequestDelegate;
++
++import android.content.Context;
++import android.os.AsyncTask;
++
++/**
++ * An <code>AsyncTask</code> wrapper around signing up for, and signing in to, a
++ * Firefox Account.
++ * <p>
++ * It's strange to add explicit blocking to callback-threading code, but we do
++ * it here to take advantage of Android's built in support for background work.
++ * We really want to avoid making a threading mistake that brings down the whole
++ * process.
++ */
++public abstract class FxAccountSetupTask<T> extends AsyncTask<Void, Void, InnerRequestDelegate<T>> {
++ private static final String LOG_TAG = FxAccountSetupTask.class.getSimpleName();
++
++ public interface ProgressDisplay {
++ public void showProgress();
++ public void dismissProgress();
++ }
++
++ protected final Context context;
++ protected final FxAccountClient client;
++ protected final ProgressDisplay progressDisplay;
++
++ // Initialized lazily.
++ protected byte[] quickStretchedPW;
++
++ // AsyncTask's are one-time-use, so final members are fine.
++ protected final CountDownLatch latch = new CountDownLatch(1);
++ protected final InnerRequestDelegate<T> innerDelegate = new InnerRequestDelegate<T>(latch);
++
++ protected final RequestDelegate<T> delegate;
++
++ public FxAccountSetupTask(Context context, ProgressDisplay progressDisplay, FxAccountClient client, RequestDelegate<T> delegate) {
++ this.context = context;
++ this.client = client;
++ this.delegate = delegate;
++ this.progressDisplay = progressDisplay;
++ }
++
++ @Override
++ protected void onPreExecute() {
++ if (progressDisplay != null) {
++ progressDisplay.showProgress();
++ }
++ }
++
++ @Override
++ protected void onPostExecute(InnerRequestDelegate<T> result) {
++ if (progressDisplay != null) {
++ progressDisplay.dismissProgress();
++ }
++
++ // We are on the UI thread, and need to invoke these callbacks here to allow UI updating.
++ if (innerDelegate.failure != null) {
++ delegate.handleFailure(innerDelegate.failure);
++ } else if (innerDelegate.exception != null) {
++ delegate.handleError(innerDelegate.exception);
++ } else {
++ delegate.handleSuccess(result.response);
++ }
++ }
++
++ @Override
++ protected void onCancelled(InnerRequestDelegate<T> result) {
++ if (progressDisplay != null) {
++ progressDisplay.dismissProgress();
++ }
++ delegate.handleError(new IllegalStateException("Task was cancelled."));
++ }
++
++ protected static class InnerRequestDelegate<T> implements RequestDelegate<T> {
++ protected final CountDownLatch latch;
++ public T response = null;
++ public Exception exception = null;
++ public FxAccountClientRemoteException failure = null;
++
++ protected InnerRequestDelegate(CountDownLatch latch) {
++ this.latch = latch;
++ }
++
++ @Override
++ public void handleError(Exception e) {
++ Logger.error(LOG_TAG, "Got exception.");
++ this.exception = e;
++ latch.countDown();
++ }
++
++ @Override
++ public void handleFailure(FxAccountClientRemoteException e) {
++ Logger.warn(LOG_TAG, "Got failure.");
++ this.failure = e;
++ latch.countDown();
++ }
++
++ @Override
++ public void handleSuccess(T result) {
++ Logger.info(LOG_TAG, "Got success.");
++ this.response = result;
++ latch.countDown();
++ }
++ }
++}
+diff -ruN mozilla-esr31/mobile/android/base/fxa/tasks/FxAccountSignInTask.java mozilla-release/mobile/android/base/fxa/tasks/FxAccountSignInTask.java
+--- mozilla-esr31/mobile/android/base/fxa/tasks/FxAccountSignInTask.java 1970-01-01 01:00:00.000000000 +0100
++++ mozilla-release/mobile/android/base/fxa/tasks/FxAccountSignInTask.java 2014-09-24 03:05:33.000000000 +0200
+@@ -0,0 +1,41 @@
++/* 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.fxa.tasks;
++
++import java.io.UnsupportedEncodingException;
++
++import org.mozilla.gecko.background.common.log.Logger;
++import org.mozilla.gecko.background.fxa.FxAccountClient;
++import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
++import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
++import org.mozilla.gecko.background.fxa.PasswordStretcher;
++
++import android.content.Context;
++
++public class FxAccountSignInTask extends FxAccountSetupTask<LoginResponse> {
++ protected static final String LOG_TAG = FxAccountSignInTask.class.getSimpleName();
++
++ protected final byte[] emailUTF8;
++ protected final PasswordStretcher passwordStretcher;
++
++ public FxAccountSignInTask(Context context, ProgressDisplay progressDisplay, String email, PasswordStretcher passwordStretcher, FxAccountClient client, RequestDelegate<LoginResponse> delegate) throws UnsupportedEncodingException {
++ super(context, progressDisplay, client, delegate);
++ this.emailUTF8 = email.getBytes("UTF-8");
++ this.passwordStretcher = passwordStretcher;
++ }
++
++ @Override
++ protected InnerRequestDelegate<LoginResponse> doInBackground(Void... arg0) {
++ try {
++ client.loginAndGetKeys(emailUTF8, passwordStretcher, innerDelegate);
++ latch.await();
++ return innerDelegate;
++ } catch (Exception e) {
++ Logger.error(LOG_TAG, "Got exception signing in.", e);
++ delegate.handleError(e);
++ }
++ return null;
++ }
++}
+--- mozilla-esr31/mobile/android/base/android-services.mozbuild 2015-02-25 19:21:05.000000000 +0100
++++ mozilla-release/mobile/android/base/android-services.mozbuild 2014-09-24 03:05:32.000000000 +0200
+@@ -555,7 +557,6 @@
+ 'fxa/activities/FxAccountCreateAccountActivity.java',
+ 'fxa/activities/FxAccountCreateAccountNotAllowedActivity.java',
+ 'fxa/activities/FxAccountGetStartedActivity.java',
+- 'fxa/activities/FxAccountSetupTask.java',
+ 'fxa/activities/FxAccountSignInActivity.java',
+ 'fxa/activities/FxAccountStatusActivity.java',
+ 'fxa/activities/FxAccountStatusFragment.java',
+@@ -589,6 +590,10 @@
+ 'fxa/sync/FxAccountSyncService.java',
+ 'fxa/sync/FxAccountSyncStatusHelper.java',
+ 'fxa/sync/SchedulePolicy.java',
++ 'fxa/tasks/FxAccountCodeResender.java',
++ 'fxa/tasks/FxAccountCreateAccountTask.java',
++ 'fxa/tasks/FxAccountSetupTask.java',
++ 'fxa/tasks/FxAccountSignInTask.java',
+ 'sync/AlreadySyncingException.java',
+ 'sync/BackoffHandler.java',
+ 'sync/BadRequiredFieldJSONException.java',
diff --git a/data/patches/gnuzilla-bug-1010972.patch b/data/patches/gnuzilla-bug-1010972.patch
new file mode 100644
index 0000000..94eaf2b
--- /dev/null
+++ b/data/patches/gnuzilla-bug-1010972.patch
@@ -0,0 +1,11 @@
+--- a/media/libyuv/source/cpu_id.cc 2015-01-27 19:15:52.069520584 +0100
++++ b/media/libyuv/source/cpu_id.cc 2015-01-30 00:50:10.425853894 +0100
+@@ -97,7 +97,7 @@
+ uint32 xcr0 = 0u;
+ #if defined(_MSC_VER) && defined(_XCR_XFEATURE_ENABLED_MASK)
+ xcr0 = (uint32)(_xgetbv(_XCR_XFEATURE_ENABLED_MASK));
+-#elif defined(_M_IX86)
++#elif defined(_MSC_VER) && defined(_M_IX86)
+ __asm {
+ xor ecx, ecx // xcr 0
+ _asm _emit 0x0f _asm _emit 0x01 _asm _emit 0xd0 // For VS2010 and earlier.
diff --git a/data/patches/gnuzilla-bug-1025689.patch b/data/patches/gnuzilla-bug-1025689.patch
new file mode 100644
index 0000000..9364624
--- /dev/null
+++ b/data/patches/gnuzilla-bug-1025689.patch
@@ -0,0 +1,19 @@
+diff -ru mozilla-esr31/media/libopus/moz.build icecat-31.4.0/media/libopus/moz.build
+--- a/media/libopus/moz.build 2015-01-06 06:08:03.000000000 +0100
++++ b/media/libopus/moz.build 2015-02-02 02:37:44.416803188 +0100
+@@ -23,10 +23,11 @@
+ DEFINES['OPUS_ARM_ASM'] = True
+ DEFINES['OPUS_ARM_EXTERNAL_ASM'] = True
+ DEFINES['OPUS_ARM_INLINE_ASM'] = True
+- DEFINES['OPUS_ARM_INLINE_EDSP'] = True
+- DEFINES['OPUS_ARM_MAY_HAVE_EDSP'] = True
+- DEFINES['OPUS_ARM_MAY_HAVE_MEDIA'] = True
+- DEFINES['OPUS_ARM_MAY_HAVE_NEON'] = True
++ if int(CONFIG['ARM_ARCH']) >= 6:
++ DEFINES['OPUS_ARM_INLINE_EDSP'] = True
++ DEFINES['OPUS_ARM_MAY_HAVE_EDSP'] = True
++ DEFINES['OPUS_ARM_MAY_HAVE_MEDIA'] = True
++ DEFINES['OPUS_ARM_MAY_HAVE_NEON'] = True
+
+ if CONFIG['MOZ_DEBUG']:
+ DEFINES['ENABLE_ASSERTIONS'] = True
diff --git a/data/patches/gnuzilla-bug-1030899-1.patch b/data/patches/gnuzilla-bug-1030899-1.patch
new file mode 100644
index 0000000..5f3d3ba
--- /dev/null
+++ b/data/patches/gnuzilla-bug-1030899-1.patch
@@ -0,0 +1,60 @@
+--- a/ipc/chromium/src/third_party/libevent-dont-use-issetugid-on-android.patch
++++ a/ipc/chromium/src/third_party/libevent-dont-use-issetugid-on-android.patch
+@@ -0,0 +1,22 @@
++diff --git a/ipc/chromium/src/third_party/libevent/android/event2/event-config.h b/ipc/chromium/src/third_party/libevent/android/event2/event-config.h
++--- a/ipc/chromium/src/third_party/libevent/android/event2/event-config.h
+++++ b/ipc/chromium/src/third_party/libevent/android/event2/event-config.h
++@@ -119,17 +119,17 @@
++
++ /* Define to 1 if you have the `inet_pton' function. */
++ #define _EVENT_HAVE_INET_PTON 1
++
++ /* Define to 1 if you have the <inttypes.h> header file. */
++ #define _EVENT_HAVE_INTTYPES_H 1
++
++ /* Define to 1 if you have the `issetugid' function. */
++-#define _EVENT_HAVE_ISSETUGID 1
+++/* #undef _EVENT_HAVE_ISSETUGID */
++
++ /* Define to 1 if you have the `kqueue' function. */
++ /* #undef _EVENT_HAVE_KQUEUE */
++
++ /* Define if the system has zlib */
++ #define _EVENT_HAVE_LIBZ 1
++
++ /* Define to 1 if you have the <memory.h> header file. */
+--- a/ipc/chromium/src/third_party/libevent/README.mozilla
++++ a/ipc/chromium/src/third_party/libevent/README.mozilla
+@@ -10,8 +10,10 @@ These files are taken from libevent-2.0.
+
+ 2. This is ugly, prepare yourself. OS X has a weird problem with how the "TAILQ_END(head)" is used, causing a linking error. Just replace all use of the "TAILQ_END(head)" macro with "NULL".
+
+ 3. Apply "add mac-arc4random-buf.patch", which removes some bad OS X compatibility code. This will allow libevent to compile on all supported versions of OS X.
+
+ 4. Apply "openbsd-no-arc4random_addrandom.patch", which fixes the build on OpenBSD (which doesnt provide arc4random_addrandom anymore, see #931354)
+
+ 5. Apply "libevent-use-non-deprecated-syscalls.patch", which fixes the build on AArch64 architecture (which does not provide deprecated syscalls)
++
++6. Apply "libevent-dont-use-issetugid-on-android.patch'. which fixes the build on Android L preview
+--- a/ipc/chromium/src/third_party/libevent/android/event2/event-config.h
++++ a/ipc/chromium/src/third_party/libevent/android/event2/event-config.h
+@@ -119,17 +119,17 @@
+
+ /* Define to 1 if you have the `inet_pton' function. */
+ #define _EVENT_HAVE_INET_PTON 1
+
+ /* Define to 1 if you have the <inttypes.h> header file. */
+ #define _EVENT_HAVE_INTTYPES_H 1
+
+ /* Define to 1 if you have the `issetugid' function. */
+-#define _EVENT_HAVE_ISSETUGID 1
++/* #undef _EVENT_HAVE_ISSETUGID */
+
+ /* Define to 1 if you have the `kqueue' function. */
+ /* #undef _EVENT_HAVE_KQUEUE */
+
+ /* Define if the system has zlib */
+ #define _EVENT_HAVE_LIBZ 1
+
+ /* Define to 1 if you have the <memory.h> header file. */
+
diff --git a/data/patches/gnuzilla-bug-1030899-2.patch b/data/patches/gnuzilla-bug-1030899-2.patch
new file mode 100644
index 0000000..55728ab
--- /dev/null
+++ b/data/patches/gnuzilla-bug-1030899-2.patch
@@ -0,0 +1,59 @@
+--- a/memory/mozjemalloc/jemalloc.c
++++ a/memory/mozjemalloc/jemalloc.c
+@@ -220,17 +220,16 @@
+
+ #ifdef MALLOC_PAGEFILE
+ /* Write size when initializing a page file. */
+ # define MALLOC_PAGEFILE_WRITE_SIZE 512
+ #endif
+
+ #if defined(MOZ_MEMORY_LINUX) && !defined(MOZ_MEMORY_ANDROID)
+ #define _GNU_SOURCE /* For mremap(2). */
+-#define issetugid() 0
+ #if 0 /* Enable in order to test decommit code on Linux. */
+ # define MALLOC_DECOMMIT
+ #endif
+ #endif
+
+ #include <sys/types.h>
+
+ #include <errno.h>
+@@ -261,17 +260,16 @@
+ #define vsnprintf _vsnprintf
+
+ #ifndef NO_TLS
+ static unsigned long tlsIndex = 0xffffffff;
+ #endif
+
+ #define __thread
+ #define _pthread_self() __threadid()
+-#define issetugid() 0
+
+ /* use MSVC intrinsics */
+ #pragma intrinsic(_BitScanForward)
+ static __forceinline int
+ ffs(int x)
+ {
+ unsigned long i;
+
+@@ -5675,18 +5673,17 @@ malloc_init_hard(void)
+ #endif
+ {
+ /* No configuration specified. */
+ buf[0] = '\0';
+ opts = buf;
+ }
+ break;
+ case 1:
+- if (issetugid() == 0 && (opts =
+- getenv("MALLOC_OPTIONS")) != NULL) {
++ if ((opts = getenv("MALLOC_OPTIONS")) != NULL) {
+ /*
+ * Do nothing; opts is already initialized to
+ * the value of the MALLOC_OPTIONS environment
+ * variable.
+ */
+ } else {
+ /* No configuration specified. */
+ buf[0] = '\0';
+
diff --git a/data/patches/gnuzilla-bug-1032460.patch b/data/patches/gnuzilla-bug-1032460.patch
new file mode 100644
index 0000000..08f37c3
--- /dev/null
+++ b/data/patches/gnuzilla-bug-1032460.patch
@@ -0,0 +1,16 @@
+https://bugzilla.mozilla.org/show_bug.cgi?id=1032460
+
+diff -ru icecat-31.4.0.orig/widget/android/AndroidBridge.cpp icecat-31.4.0/widget/android/AndroidBridge.cpp
+--- icecat-31.4.0.orig/widget/android/AndroidBridge.cpp 2015-02-22 19:11:11.834816575 +0100
++++ icecat-31.4.0/widget/android/AndroidBridge.cpp 2015-02-22 19:16:16.226810788 +0100
+@@ -199,7 +199,9 @@
+
+ jclass eglClass = getClassGlobalRef("com/google/android/gles_jni/EGLSurfaceImpl");
+ if (eglClass) {
+- jEGLSurfacePointerField = getField("mEGLSurface", "I");
++ // The pointer type moved to a 'long' in Android L, API version 20
++ const char* jniType = mAPIVersion >= 20 ? "J" : "I";
++ jEGLSurfacePointerField = getField("mEGLSurface", jniType);
+ } else {
+ jEGLSurfacePointerField = 0;
+ }
diff --git a/data/patches/gnuzilla-bug-1034167-1.patch b/data/patches/gnuzilla-bug-1034167-1.patch
new file mode 100644
index 0000000..410b2cf
--- /dev/null
+++ b/data/patches/gnuzilla-bug-1034167-1.patch
@@ -0,0 +1,93 @@
+# HG changeset patch
+# User Lucas Rocha <lucasr@mozilla.com>
+
+Bug 1034167 - Part 2: Explicit set scrollbars in TabsTray (r=mfinkle)
+
+diff --git a/mobile/android/base/resources/values-land/styles.xml b/mobile/android/base/resources/values-land/styles.xml
+index 774c48e..6769b1e 100644
+--- a/mobile/android/base/resources/values-land/styles.xml
++++ b/mobile/android/base/resources/values-land/styles.xml
+@@ -2,16 +2,17 @@
+ <!-- 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/. -->
+
+ <resources>
+
+ <style name="TabsList" parent="TabsListBase">
+ <item name="android:orientation">horizontal</item>
++ <item name="android:scrollbars">horizontal</item>
+ </style>
+
+ <style name="TabsItem">
+ <item name="android:nextFocusDown">@+id/close</item>
+ </style>
+
+ <style name="TabsItemClose">
+ <item name="android:nextFocusUp">@+id/info</item>
+diff --git a/mobile/android/base/resources/values-large-land-v11/styles.xml b/mobile/android/base/resources/values-large-land-v11/styles.xml
+index 557b659..1468914 100644
+--- a/mobile/android/base/resources/values-large-land-v11/styles.xml
++++ b/mobile/android/base/resources/values-large-land-v11/styles.xml
+@@ -2,16 +2,17 @@
+ <!-- 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/. -->
+
+ <resources>
+
+ <style name="TabsList" parent="TabsListBase">
+ <item name="android:orientation">vertical</item>
++ <item name="android:scrollbars">vertical</item>
+ </style>
+
+ <style name="Widget.BookmarkFolderView" parent="Widget.TwoLinePageRow.Title">
+ <item name="android:paddingLeft">60dip</item>
+ <item name="android:drawablePadding">10dip</item>
+ <item name="android:drawableLeft">@drawable/bookmark_folder</item>
+ </style>
+
+diff --git a/mobile/android/base/resources/values-large-v11/styles.xml b/mobile/android/base/resources/values-large-v11/styles.xml
+index 7a22789..a6a6221 100644
+--- a/mobile/android/base/resources/values-large-v11/styles.xml
++++ b/mobile/android/base/resources/values-large-v11/styles.xml
+@@ -25,16 +25,17 @@
+ <item name="android:layout_marginTop">6dp</item>
+ <item name="android:layout_marginBottom">6dp</item>
+ <!-- Start with forward hidden -->
+ <item name="android:orientation">horizontal</item>
+ </style>
+
+ <style name="TabsList" parent="TabsListBase">
+ <item name="android:orientation">horizontal</item>
++ <item name="android:scrollbars">horizontal</item>
+ </style>
+
+ <style name="TabsItem">
+ <item name="android:nextFocusDown">@+id/close</item>
+ </style>
+
+ <style name="TabsItemClose">
+ <item name="android:nextFocusUp">@+id/info</item>
+diff --git a/mobile/android/base/resources/values/styles.xml b/mobile/android/base/resources/values/styles.xml
+index a15f72e..f479180 100644
+--- a/mobile/android/base/resources/values/styles.xml
++++ b/mobile/android/base/resources/values/styles.xml
+@@ -430,16 +430,17 @@
+ <!-- TabsTray List -->
+ <style name="TabsListBase">
+ <item name="android:background">@android:color/transparent</item>
+ <item name="android:listSelector">@android:color/transparent</item>
+ </style>
+
+ <style name="TabsList" parent="TabsListBase">
+ <item name="android:orientation">vertical</item>
++ <item name="android:scrollbars">vertical</item>
+ </style>
+
+ <style name="TabsItem">
+ <item name="android:nextFocusRight">@+id/close</item>
+ </style>
+
+ <style name="TabsItemClose">
+ <item name="android:nextFocusLeft">@+id/info</item>
diff --git a/data/patches/gnuzilla-bug-1034167-2.patch b/data/patches/gnuzilla-bug-1034167-2.patch
new file mode 100644
index 0000000..3df67a0
--- /dev/null
+++ b/data/patches/gnuzilla-bug-1034167-2.patch
@@ -0,0 +1,1205 @@
+# HG changeset patch
+# User Lucas Rocha <lucasr@mozilla.com>
+
+Bug 1034167 - Part 1: Update TwoWayView from upstream (r=mfinkle)
+
+diff --git a/mobile/android/base/resources/values/attrs.xml b/mobile/android/base/resources/values/attrs.xml
+index 0c27b87..7140292 100644
+--- a/mobile/android/base/resources/values/attrs.xml
++++ b/mobile/android/base/resources/values/attrs.xml
+@@ -117,91 +117,20 @@
+
+ <declare-styleable name="LightweightTheme">
+ <attr name="state_light" format="boolean"/>
+ <attr name="state_dark" format="boolean"/>
+ <attr name="autoUpdateTheme" format="boolean"/>
+ </declare-styleable>
+
+ <declare-styleable name="TwoWayView">
+- <!-- Imported from View -->
+- <attr name="android:id"/>
+- <attr name="android:tag"/>
+- <attr name="android:scrollX"/>
+- <attr name="android:scrollY"/>
+- <attr name="android:background"/>
+- <attr name="android:padding"/>
+- <attr name="android:paddingLeft"/>
+- <attr name="android:paddingTop"/>
+- <attr name="android:paddingRight"/>
+- <attr name="android:paddingBottom"/>
+- <attr name="android:paddingStart"/>
+- <attr name="android:paddingEnd"/>
+- <attr name="android:focusable"/>
+- <attr name="android:focusableInTouchMode"/>
+- <attr name="android:visibility"/>
+- <attr name="android:fitsSystemWindows"/>
+- <attr name="android:scrollbars"/>
+- <attr name="android:scrollbarStyle"/>
+- <attr name="android:isScrollContainer"/>
+- <attr name="android:fadeScrollbars"/>
+- <attr name="android:scrollbarFadeDuration"/>
+- <attr name="android:scrollbarDefaultDelayBeforeFade"/>
+- <attr name="android:scrollbarSize"/>
+- <attr name="android:scrollbarThumbHorizontal"/>
+- <attr name="android:scrollbarThumbVertical"/>
+- <attr name="android:scrollbarTrackHorizontal"/>
+- <attr name="android:scrollbarTrackVertical"/>
+- <attr name="android:scrollbarAlwaysDrawHorizontalTrack"/>
+- <attr name="android:scrollbarAlwaysDrawVerticalTrack"/>
+- <attr name="android:fadingEdge"/>
+- <attr name="android:requiresFadingEdge"/>
+- <attr name="android:fadingEdgeLength"/>
+- <attr name="android:nextFocusLeft"/>
+- <attr name="android:nextFocusRight"/>
+- <attr name="android:nextFocusUp"/>
+- <attr name="android:nextFocusDown"/>
+- <attr name="android:nextFocusForward"/>
+- <attr name="android:clickable"/>
+- <attr name="android:longClickable"/>
+- <attr name="android:saveEnabled"/>
+- <attr name="android:filterTouchesWhenObscured"/>
+- <attr name="android:drawingCacheQuality"/>
+- <attr name="android:keepScreenOn"/>
+- <attr name="android:duplicateParentState"/>
+- <attr name="android:minHeight"/>
+- <attr name="android:minWidth"/>
+- <attr name="android:soundEffectsEnabled"/>
+- <attr name="android:hapticFeedbackEnabled"/>
+- <attr name="android:contentDescription"/>
+- <attr name="android:onClick"/>
+- <attr name="android:overScrollMode"/>
+- <attr name="android:alpha"/>
+- <attr name="android:translationX"/>
+- <attr name="android:translationY"/>
+- <attr name="android:transformPivotX"/>
+- <attr name="android:transformPivotY"/>
+- <attr name="android:rotation"/>
+- <attr name="android:rotationX"/>
+- <attr name="android:rotationY"/>
+- <attr name="android:scaleX"/>
+- <attr name="android:scaleY"/>
+- <attr name="android:verticalScrollbarPosition"/>
+- <attr name="android:layerType"/>
+- <attr name="android:layoutDirection"/>
+- <attr name="android:textDirection"/>
+- <attr name="android:textAlignment"/>
+-
+- <!-- Imported from Android -->
+ <attr name="android:orientation"/>
+-
+- <!-- Imported from AbsListView -->
+ <attr name="android:choiceMode"/>
+- <attr name="android:drawSelectorOnTop"/>
+ <attr name="android:listSelector"/>
++ <attr name="android:drawSelectorOnTop"/>
+ </declare-styleable>
+
+ <declare-styleable name="HomeListView">
+ <!-- Draws a divider on top of the list, if true. Defaults to false. -->
+ <attr name="topDivider" format="boolean"/>
+ </declare-styleable>
+
+ <declare-styleable name="HomePagerTabStrip">
+--- a/mobile/android/base/widget/TwoWayView.java 2015-01-06 05:07:55.000000000 +0000
++++ b/mobile/android/base/widget/TwoWayView.java 2015-02-23 19:46:01.737065730 +0000
+@@ -23,6 +23,9 @@
+
+ import org.mozilla.gecko.R;
+
++import java.util.ArrayList;
++import java.util.List;
++
+ import android.annotation.TargetApi;
+ import android.content.Context;
+ import android.content.res.TypedArray;
+@@ -67,8 +70,7 @@
+ import android.widget.ListAdapter;
+ import android.widget.Scroller;
+
+-import java.util.ArrayList;
+-import java.util.List;
++import static android.os.Build.VERSION_CODES.HONEYCOMB;
+
+ /*
+ * Implementation Notes:
+@@ -136,8 +138,8 @@
+
+ public static enum Orientation {
+ HORIZONTAL,
+- VERTICAL;
+- };
++ VERTICAL
++ }
+
+ private ListAdapter mAdapter;
+
+@@ -266,7 +268,7 @@
+ * Callback method to be invoked while the list view or grid view is being scrolled. If the
+ * view is being scrolled, this method will be called before the next frame of the scroll is
+ * rendered. In particular, it will be called before any calls to
+- * {@link Adapter#getView(int, View, ViewGroup)}.
++ * {@link android.widget.Adapter#getView(int, View, ViewGroup)}.
+ *
+ * @param view The view whose scroll state is being reported
+ *
+@@ -388,7 +390,6 @@
+ ViewCompat.setOverScrollMode(this, ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS);
+
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TwoWayView, defStyle, 0);
+- initializeScrollbars(a);
+
+ mDrawSelectorOnTop = a.getBoolean(
+ R.styleable.TwoWayView_android_drawSelectorOnTop, false);
+@@ -409,19 +410,16 @@
+ }
+
+ a.recycle();
+-
+- updateScrollbarsDirection();
+ }
+
+ public void setOrientation(Orientation orientation) {
+- final boolean isVertical = (orientation.compareTo(Orientation.VERTICAL) == 0);
++ final boolean isVertical = (orientation == Orientation.VERTICAL);
+ if (mIsVertical == isVertical) {
+ return;
+ }
+
+ mIsVertical = isVertical;
+
+- updateScrollbarsDirection();
+ resetState();
+ mRecycler.clear();
+
+@@ -441,6 +439,7 @@
+ requestLayout();
+ }
+
++ @SuppressWarnings("unused")
+ public int getItemMargin() {
+ return mItemMargin;
+ }
+@@ -451,6 +450,7 @@
+ *
+ * @param itemsCanFocus true if items can get focus, false otherwise
+ */
++ @SuppressWarnings("unused")
+ public void setItemsCanFocus(boolean itemsCanFocus) {
+ mItemsCanFocus = itemsCanFocus;
+ if (!itemsCanFocus) {
+@@ -462,6 +462,7 @@
+ * @return Whether the views created by the ListAdapter can contain focusable
+ * items.
+ */
++ @SuppressWarnings("unused")
+ public boolean getItemsCanFocus() {
+ return mItemsCanFocus;
+ }
+@@ -481,7 +482,7 @@
+ * the recycler for later reuse. This listener can be used to free resources
+ * associated to the View.
+ *
+- * @param listener The recycler listener to be notified of views set aside
++ * @param l The recycler listener to be notified of views set aside
+ * in the recycler.
+ *
+ * @see TwoWayView.RecycleBin
+@@ -495,11 +496,12 @@
+ * Controls whether the selection highlight drawable should be drawn on top of the item or
+ * behind it.
+ *
+- * @param onTop If true, the selector will be drawn on the item it is highlighting. The default
+- * is false.
++ * @param drawSelectorOnTop If true, the selector will be drawn on the item it is highlighting.
++ * The default is false.
+ *
+ * @attr ref android.R.styleable#AbsListView_drawSelectorOnTop
+ */
++ @SuppressWarnings("unused")
+ public void setDrawSelectorOnTop(boolean drawSelectorOnTop) {
+ mDrawSelectorOnTop = drawSelectorOnTop;
+ }
+@@ -511,6 +513,7 @@
+ *
+ * @attr ref android.R.styleable#AbsListView_listSelector
+ */
++ @SuppressWarnings("unused")
+ public void setSelector(int resID) {
+ setSelector(getResources().getDrawable(resID));
+ }
+@@ -542,6 +545,7 @@
+ *
+ * @return the drawable used to display the selector
+ */
++ @SuppressWarnings("unused")
+ public Drawable getSelector() {
+ return mSelector;
+ }
+@@ -564,7 +568,7 @@
+
+ /**
+ * Returns the number of items currently selected. This will only be valid
+- * if the choice mode is not {@link #CHOICE_MODE_NONE} (default).
++ * if the choice mode is not {@link ChoiceMode#NONE} (default).
+ *
+ * <p>To determine the specific items that are currently selected, use one of
+ * the <code>getChecked*</code> methods.
+@@ -575,23 +579,24 @@
+ * @see #getCheckedItemPositions()
+ * @see #getCheckedItemIds()
+ */
++ @SuppressWarnings("unused")
+ public int getCheckedItemCount() {
+ return mCheckedItemCount;
+ }
+
+ /**
+ * Returns the checked state of the specified position. The result is only
+- * valid if the choice mode has been set to {@link #CHOICE_MODE_SINGLE}
+- * or {@link #CHOICE_MODE_MULTIPLE}.
++ * valid if the choice mode has been set to {@link ChoiceMode#SINGLE}
++ * or {@link ChoiceMode#MULTIPLE}.
+ *
+ * @param position The item whose checked state to return
+ * @return The item's checked state or <code>false</code> if choice mode
+ * is invalid
+ *
+- * @see #setChoiceMode(int)
++ * @see #setChoiceMode(ChoiceMode)
+ */
+ public boolean isItemChecked(int position) {
+- if (mChoiceMode.compareTo(ChoiceMode.NONE) == 0 && mCheckStates != null) {
++ if (mChoiceMode == ChoiceMode.NONE && mCheckStates != null) {
+ return mCheckStates.get(position);
+ }
+
+@@ -600,16 +605,15 @@
+
+ /**
+ * Returns the currently checked item. The result is only valid if the choice
+- * mode has been set to {@link #CHOICE_MODE_SINGLE}.
++ * mode has been set to {@link ChoiceMode#SINGLE}.
+ *
+ * @return The position of the currently checked item or
+ * {@link #INVALID_POSITION} if nothing is selected
+ *
+- * @see #setChoiceMode(int)
++ * @see #setChoiceMode(ChoiceMode)
+ */
+ public int getCheckedItemPosition() {
+- if (mChoiceMode.compareTo(ChoiceMode.SINGLE) == 0 &&
+- mCheckStates != null && mCheckStates.size() == 1) {
++ if (mChoiceMode == ChoiceMode.SINGLE && mCheckStates != null && mCheckStates.size() == 1) {
+ return mCheckStates.keyAt(0);
+ }
+
+@@ -618,15 +622,15 @@
+
+ /**
+ * Returns the set of checked items in the list. The result is only valid if
+- * the choice mode has not been set to {@link #CHOICE_MODE_NONE}.
++ * the choice mode has not been set to {@link ChoiceMode#NONE}.
+ *
+ * @return A SparseBooleanArray which will return true for each call to
+ * get(int position) where position is a position in the list,
+ * or <code>null</code> if the choice mode is set to
+- * {@link #CHOICE_MODE_NONE}.
++ * {@link ChoiceMode#NONE}.
+ */
+ public SparseBooleanArray getCheckedItemPositions() {
+- if (mChoiceMode.compareTo(ChoiceMode.NONE) != 0) {
++ if (mChoiceMode != ChoiceMode.NONE) {
+ return mCheckStates;
+ }
+
+@@ -635,15 +639,14 @@
+
+ /**
+ * Returns the set of checked items ids. The result is only valid if the
+- * choice mode has not been set to {@link #CHOICE_MODE_NONE} and the adapter
++ * choice mode has not been set to {@link ChoiceMode#NONE} and the adapter
+ * has stable IDs. ({@link ListAdapter#hasStableIds()} == {@code true})
+ *
+ * @return A new array which contains the id of each checked item in the
+ * list.
+ */
+ public long[] getCheckedItemIds() {
+- if (mChoiceMode.compareTo(ChoiceMode.NONE) == 0 ||
+- mCheckedIdStates == null || mAdapter == null) {
++ if (mChoiceMode == ChoiceMode.NONE || mCheckedIdStates == null || mAdapter == null) {
+ return new long[0];
+ }
+
+@@ -660,18 +663,19 @@
+
+ /**
+ * Sets the checked state of the specified position. The is only valid if
+- * the choice mode has been set to {@link #CHOICE_MODE_SINGLE} or
+- * {@link #CHOICE_MODE_MULTIPLE}.
++ * the choice mode has been set to {@link ChoiceMode#SINGLE} or
++ * {@link ChoiceMode#MULTIPLE}.
+ *
+ * @param position The item whose checked state is to be checked
+ * @param value The new checked state for the item
+ */
++ @SuppressWarnings("unused")
+ public void setItemChecked(int position, boolean value) {
+- if (mChoiceMode.compareTo(ChoiceMode.NONE) == 0) {
++ if (mChoiceMode == ChoiceMode.NONE) {
+ return;
+ }
+
+- if (mChoiceMode.compareTo(ChoiceMode.MULTIPLE) == 0) {
++ if (mChoiceMode == ChoiceMode.MULTIPLE) {
+ boolean oldValue = mCheckStates.get(position);
+ mCheckStates.put(position, value);
+
+@@ -729,6 +733,7 @@
+ /**
+ * Clear any choices previously set
+ */
++ @SuppressWarnings("unused")
+ public void clearChoices() {
+ if (mCheckStates != null) {
+ mCheckStates.clear();
+@@ -742,27 +747,28 @@
+ }
+
+ /**
+- * @see #setChoiceMode(int)
++ * @see #setChoiceMode(ChoiceMode)
+ *
+ * @return The current choice mode
+ */
++ @SuppressWarnings("unused")
+ public ChoiceMode getChoiceMode() {
+ return mChoiceMode;
+ }
+
+ /**
+ * Defines the choice behavior for the List. By default, Lists do not have any choice behavior
+- * ({@link #CHOICE_MODE_NONE}). By setting the choiceMode to {@link #CHOICE_MODE_SINGLE}, the
++ * ({@link ChoiceMode#NONE}). By setting the choiceMode to {@link ChoiceMode#SINGLE}, the
+ * List allows up to one item to be in a chosen state. By setting the choiceMode to
+- * {@link #CHOICE_MODE_MULTIPLE}, the list allows any number of items to be chosen.
++ * {@link ChoiceMode#MULTIPLE}, the list allows any number of items to be chosen.
+ *
+- * @param choiceMode One of {@link #CHOICE_MODE_NONE}, {@link #CHOICE_MODE_SINGLE}, or
+- * {@link #CHOICE_MODE_MULTIPLE}
++ * @param choiceMode One of {@link ChoiceMode#NONE}, {@link ChoiceMode#SINGLE}, or
++ * {@link ChoiceMode#MULTIPLE}
+ */
+ public void setChoiceMode(ChoiceMode choiceMode) {
+ mChoiceMode = choiceMode;
+
+- if (mChoiceMode.compareTo(ChoiceMode.NONE) != 0) {
++ if (mChoiceMode != ChoiceMode.NONE) {
+ if (mCheckStates == null) {
+ mCheckStates = new SparseBooleanArray();
+ }
+@@ -813,8 +819,7 @@
+ mHasStableIds = adapter.hasStableIds();
+ mAreAllItemsSelectable = adapter.areAllItemsEnabled();
+
+- if (mChoiceMode.compareTo(ChoiceMode.NONE) != 0 && mHasStableIds &&
+- mCheckedIdStates == null) {
++ if (mChoiceMode != ChoiceMode.NONE && mHasStableIds && mCheckedIdStates == null) {
+ mCheckedIdStates = new LongSparseArray<Integer>();
+ }
+
+@@ -1268,7 +1273,7 @@
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+- if (!mIsAttached) {
++ if (!mIsAttached || mAdapter == null) {
+ return false;
+ }
+
+@@ -1354,7 +1359,7 @@
+ return isClickable() || isLongClickable();
+ }
+
+- if (!mIsAttached) {
++ if (!mIsAttached || mAdapter == null) {
+ return false;
+ }
+
+@@ -1477,7 +1482,7 @@
+ final float x = ev.getX();
+ final float y = ev.getY();
+
+- boolean inList = false;
++ final boolean inList;
+ if (mIsVertical) {
+ inList = x > getPaddingLeft() && x < getWidth() - getPaddingRight();
+ } else {
+@@ -1731,7 +1736,7 @@
+ }
+
+ // TODO: Use some form of smooth scroll instead
+- trackMotionScroll(viewportSize);
++ scrollListItemsBy(viewportSize);
+ return true;
+ }
+ return false;
+@@ -1746,7 +1751,7 @@
+ }
+
+ // TODO: Use some form of smooth scroll instead
+- trackMotionScroll(-viewportSize);
++ scrollListItemsBy(-viewportSize);
+ return true;
+ }
+ return false;
+@@ -2077,7 +2082,7 @@
+ newFocus = FocusFinder.getInstance().findNextFocus(this, oldFocus, direction);
+ } else {
+ if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) {
+- final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft());
++ final int start = getStartEdge();
+
+ final int selectedStart;
+ if (selectedView != null) {
+@@ -2088,12 +2093,11 @@
+
+ searchPoint = Math.max(selectedStart, start);
+ } else {
+- final int end = (mIsVertical ? getHeight() - getPaddingBottom() :
+- getWidth() - getPaddingRight());
++ final int end = getEndEdge();
+
+ final int selectedEnd;
+ if (selectedView != null) {
+- selectedEnd = (mIsVertical ? selectedView.getBottom() : selectedView.getRight());
++ selectedEnd = getChildEndEdge(selectedView);
+ } else {
+ selectedEnd = end;
+ }
+@@ -2242,7 +2246,7 @@
+ }
+
+ if (amountToScroll > 0) {
+- trackMotionScroll(direction == View.FOCUS_UP || direction == View.FOCUS_LEFT ?
++ scrollListItemsBy(direction == View.FOCUS_UP || direction == View.FOCUS_LEFT ?
+ amountToScroll : -amountToScroll);
+ needToRedraw = true;
+ }
+@@ -2304,8 +2308,7 @@
+ final int numChildren = getChildCount();
+
+ if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) {
+- final int end = (mIsVertical ? getHeight() - getPaddingBottom() :
+- getWidth() - getPaddingRight());
++ final int end = getEndEdge();
+
+ int indexToMakeVisible = numChildren - 1;
+ if (nextSelectedPosition != INVALID_POSITION) {
+@@ -2320,10 +2323,8 @@
+ goalEnd -= getArrowScrollPreviewLength();
+ }
+
+- final int viewToMakeVisibleStart =
+- (mIsVertical ? viewToMakeVisible.getTop() : viewToMakeVisible.getLeft());
+- final int viewToMakeVisibleEnd =
+- (mIsVertical ? viewToMakeVisible.getBottom() : viewToMakeVisible.getRight());
++ final int viewToMakeVisibleStart = getChildStartEdge(viewToMakeVisible);
++ final int viewToMakeVisibleEnd = getChildEndEdge(viewToMakeVisible);
+
+ if (viewToMakeVisibleEnd <= goalEnd) {
+ // Target item is fully visible
+@@ -2339,8 +2340,7 @@
+ int amountToScroll = (viewToMakeVisibleEnd - goalEnd);
+
+ if (mFirstPosition + numChildren == mItemCount) {
+- final View lastChild = getChildAt(numChildren - 1);
+- final int lastChildEnd = (mIsVertical ? lastChild.getBottom() : lastChild.getRight());
++ final int lastChildEnd = getChildEndEdge(getChildAt(numChildren - 1));
+
+ // Last is last in list -> Make sure we don't scroll past it
+ final int max = lastChildEnd - end;
+@@ -2349,7 +2349,7 @@
+
+ return Math.min(amountToScroll, getMaxScrollAmount());
+ } else {
+- final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft());
++ final int start = getStartEdge();
+
+ int indexToMakeVisible = 0;
+ if (nextSelectedPosition != INVALID_POSITION) {
+@@ -2364,10 +2364,8 @@
+ goalStart += getArrowScrollPreviewLength();
+ }
+
+- final int viewToMakeVisibleStart =
+- (mIsVertical ? viewToMakeVisible.getTop() : viewToMakeVisible.getLeft());
+- final int viewToMakeVisibleEnd =
+- (mIsVertical ? viewToMakeVisible.getBottom() : viewToMakeVisible.getRight());
++ final int viewToMakeVisibleStart = getChildStartEdge(viewToMakeVisible);
++ final int viewToMakeVisibleEnd = getChildEndEdge(viewToMakeVisible);
+
+ if (viewToMakeVisibleStart >= goalStart) {
+ // Item is fully visible
+@@ -2383,8 +2381,7 @@
+ int amountToScroll = (goalStart - viewToMakeVisibleStart);
+
+ if (mFirstPosition == 0) {
+- final View firstChild = getChildAt(0);
+- final int firstChildStart = (mIsVertical ? firstChild.getTop() : firstChild.getLeft());
++ final int firstChildStart = getChildStartEdge(getChildAt(0));
+
+ // First is first in list -> make sure we don't scroll past it
+ final int max = start - firstChildStart;
+@@ -2416,7 +2413,7 @@
+ offsetDescendantRectToMyCoords(newFocus, mTempRect);
+
+ if (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT) {
+- final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft());
++ final int start = getStartEdge();
+ final int newFocusStart = (mIsVertical ? mTempRect.top : mTempRect.left);
+
+ if (newFocusStart < start) {
+@@ -2426,8 +2423,7 @@
+ }
+ }
+ } else {
+- final int end = (mIsVertical ? getHeight() - getPaddingBottom() :
+- getWidth() - getPaddingRight());
++ final int end = getEndEdge();
+ final int newFocusEnd = (mIsVertical ? mTempRect.bottom : mTempRect.right);
+
+ if (newFocusEnd > end) {
+@@ -2452,9 +2448,8 @@
+ descendant.getDrawingRect(mTempRect);
+ offsetDescendantRectToMyCoords(descendant, mTempRect);
+
+- final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft());
+- final int end = (mIsVertical ? getHeight() - getPaddingBottom() :
+- getWidth() - getPaddingRight());
++ final int start = getStartEdge();
++ final int end = getEndEdge();
+
+ final int viewStart = (mIsVertical ? mTempRect.top : mTempRect.left);
+ final int viewEnd = (mIsVertical ? mTempRect.bottom : mTempRect.right);
+@@ -2741,7 +2736,7 @@
+ motionViewPrevStart = (mIsVertical ? motionView.getTop() : motionView.getLeft());
+ }
+
+- boolean atEdge = trackMotionScroll(delta);
++ boolean atEdge = scrollListItemsBy(delta);
+
+ motionView = this.getChildAt(motionIndex);
+ if (motionView != null) {
+@@ -2821,7 +2816,7 @@
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+
+- trackMotionScroll(delta);
++ scrollListItemsBy(delta);
+ mTouchMode = TOUCH_MODE_DRAGGING;
+
+ // We did not scroll the full amount. Treat this essentially like the
+@@ -2901,10 +2896,8 @@
+ }
+
+ for (int i = 0; i < childCount; i++) {
+- View v = getChildAt(i);
+-
+- if ((mIsVertical && motionPos <= v.getBottom()) ||
+- (!mIsVertical && motionPos <= v.getRight())) {
++ final View v = getChildAt(i);
++ if (motionPos <= getChildEndEdge(v)) {
+ return mFirstPosition + i;
+ }
+ }
+@@ -2935,6 +2928,26 @@
+ return vc.getScaledOverscrollDistance();
+ }
+
++ private int getStartEdge() {
++ return (mIsVertical ? getPaddingTop() : getPaddingLeft());
++ }
++
++ private int getEndEdge() {
++ if (mIsVertical) {
++ return (getHeight() - getPaddingBottom());
++ } else {
++ return (getWidth() - getPaddingRight());
++ }
++ }
++
++ private int getChildStartEdge(View child) {
++ return (mIsVertical ? child.getTop() : child.getLeft());
++ }
++
++ private int getChildEndEdge(View child) {
++ return (mIsVertical ? child.getBottom() : child.getRight());
++ }
++
+ private boolean contentFits() {
+ final int childCount = getChildCount();
+ if (childCount == 0) {
+@@ -2948,18 +2961,8 @@
+ View first = getChildAt(0);
+ View last = getChildAt(childCount - 1);
+
+- if (mIsVertical) {
+- return first.getTop() >= getPaddingTop() &&
+- last.getBottom() <= getHeight() - getPaddingBottom();
+- } else {
+- return first.getLeft() >= getPaddingLeft() &&
+- last.getRight() <= getWidth() - getPaddingRight();
+- }
+- }
+-
+- private void updateScrollbarsDirection() {
+- setHorizontalScrollBarEnabled(!mIsVertical);
+- setVerticalScrollBarEnabled(mIsVertical);
++ return (getChildStartEdge(first) >= getStartEdge() &&
++ getChildEndEdge(last) <= getEndEdge());
+ }
+
+ private void triggerCheckForTap() {
+@@ -2997,17 +3000,14 @@
+ removeCallbacks(mPendingCheckForLongPress);
+ }
+
+- boolean trackMotionScroll(int incrementalDelta) {
++ private boolean scrollListItemsBy(int incrementalDelta) {
+ final int childCount = getChildCount();
+ if (childCount == 0) {
+ return true;
+ }
+
+- final View first = getChildAt(0);
+- final int firstStart = (mIsVertical ? first.getTop() : first.getLeft());
+-
+- final View last = getChildAt(childCount - 1);
+- final int lastEnd = (mIsVertical ? last.getBottom() : last.getRight());
++ final int firstStart = getChildStartEdge(getChildAt(0));
++ final int lastEnd = getChildEndEdge(getChildAt(childCount - 1));
+
+ final int paddingTop = getPaddingTop();
+ final int paddingBottom = getPaddingBottom();
+@@ -3017,8 +3017,7 @@
+ final int paddingStart = (mIsVertical ? paddingTop : paddingLeft);
+
+ final int spaceBefore = paddingStart - firstStart;
+- final int end = (mIsVertical ? getHeight() - paddingBottom :
+- getWidth() - paddingRight);
++ final int end = getEndEdge();
+ final int spaceAfter = lastEnd - end;
+
+ final int size;
+@@ -3059,7 +3058,7 @@
+
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+- final int childEnd = (mIsVertical ? child.getBottom() : child.getRight());
++ final int childEnd = getChildEndEdge(child);
+
+ if (childEnd >= childrenStart) {
+ break;
+@@ -3073,7 +3072,7 @@
+
+ for (int i = childCount - 1; i >= 0; i--) {
+ final View child = getChildAt(i);
+- final int childStart = (mIsVertical ? child.getTop() : child.getLeft());
++ final int childStart = getChildStartEdge(child);
+
+ if (childStart <= childrenEnd) {
+ break;
+@@ -3140,11 +3139,7 @@
+
+ @TargetApi(5)
+ private boolean awakenScrollbarsInternal() {
+- if (Build.VERSION.SDK_INT >= 5) {
+- return super.awakenScrollBars();
+- } else {
+- return false;
+- }
++ return (Build.VERSION.SDK_INT >= 5) && super.awakenScrollBars();
+ }
+
+ @Override
+@@ -3163,7 +3158,7 @@
+ final int diff = (int) (pos - mLastTouchPos);
+ mLastTouchPos = pos;
+
+- final boolean stopped = trackMotionScroll(diff);
++ final boolean stopped = scrollListItemsBy(diff);
+
+ if (!stopped && !mScroller.isFinished()) {
+ ViewCompat.postInvalidateOnAnimation(this);
+@@ -3210,7 +3205,7 @@
+ }
+
+ final int restoreCount = canvas.save();
+- final int height = getHeight() - getPaddingTop() - getPaddingBottom();
++ final int height = getHeight();
+
+ canvas.translate(0, height);
+ canvas.rotate(270);
+@@ -3226,8 +3221,8 @@
+ }
+
+ final int restoreCount = canvas.save();
+- final int width = getWidth() - getPaddingLeft() - getPaddingRight();
+- final int height = getHeight() - getPaddingTop() - getPaddingBottom();
++ final int width = getWidth();
++ final int height = getHeight();
+
+ if (mIsVertical) {
+ canvas.translate(-width, height);
+@@ -3683,6 +3678,10 @@
+ }
+ }
+
++ public void scrollBy(int offset) {
++ scrollListItemsBy(offset);
++ }
++
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ // Dispatch in the normal way
+@@ -3814,9 +3813,8 @@
+ return;
+ }
+
+- final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft());
+- final int end =
+- (mIsVertical ? getHeight() - getPaddingBottom() : getWidth() - getPaddingRight());
++ final int start = getStartEdge();
++ final int end = getEndEdge();
+
+ int childCount = getChildCount();
+ int index = 0;
+@@ -4084,8 +4082,8 @@
+ int end) {
+ final int selectedPosition = mSelectedPosition;
+
+- final int oldSelectedStart = (mIsVertical ? oldSelected.getTop() : oldSelected.getLeft());
+- final int oldSelectedEnd = (mIsVertical ? oldSelected.getBottom() : oldSelected.getRight());
++ final int oldSelectedStart = getChildStartEdge(oldSelected);
++ final int oldSelectedEnd = getChildEndEdge(oldSelected);
+
+ View selected = null;
+
+@@ -4118,8 +4116,8 @@
+ // Now put the new selection (B) below that
+ selected = makeAndAddView(selectedPosition, oldSelectedEnd + itemMargin, true, true);
+
+- final int selectedStart = (mIsVertical ? selected.getTop() : selected.getLeft());
+- final int selectedEnd = (mIsVertical ? selected.getBottom() : selected.getRight());
++ final int selectedStart = getChildStartEdge(selected);
++ final int selectedEnd = getChildEndEdge(selected);
+
+ // Some of the newly selected item extends below the bottom of the list
+ if (selectedEnd > end) {
+@@ -4178,8 +4176,8 @@
+ selected = makeAndAddView(selectedPosition, oldSelectedStart, false, true);
+ }
+
+- final int selectedStart = (mIsVertical ? selected.getTop() : selected.getLeft());
+- final int selectedEnd = (mIsVertical ? selected.getBottom() : selected.getRight());
++ final int selectedStart = getChildStartEdge(selected);
++ final int selectedEnd = getChildEndEdge(selected);
+
+ // Some of the newly selected item extends above the top of the list
+ if (selectedStart < start) {
+@@ -4210,8 +4208,8 @@
+
+ selected = makeAndAddView(selectedPosition, oldSelectedStart, true, true);
+
+- final int selectedStart = (mIsVertical ? selected.getTop() : selected.getLeft());
+- final int selectedEnd = (mIsVertical ? selected.getBottom() : selected.getRight());
++ final int selectedStart = getChildStartEdge(selected);
++ final int selectedEnd = getChildEndEdge(selected);
+
+ // We're staying still...
+ if (oldSelectedStart < start) {
+@@ -4272,7 +4270,7 @@
+ }
+
+ private void handleDataChanged() {
+- if (mChoiceMode.compareTo(ChoiceMode.NONE) != 0 && mAdapter != null && mAdapter.hasStableIds()) {
++ if (mChoiceMode != ChoiceMode.NONE && mAdapter != null && mAdapter.hasStableIds()) {
+ confirmCheckedPositionsById();
+ }
+
+@@ -4406,9 +4404,8 @@
+ int selectedStart = 0;
+ int selectedPosition;
+
+- final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft());
+- final int end =
+- (mIsVertical ? getHeight() - getPaddingBottom() : getWidth() - getPaddingRight());
++ final int start = getStartEdge();
++ final int end = getEndEdge();
+
+ final int firstPosition = mFirstPosition;
+ final int toPosition = mResurrectToPosition;
+@@ -4445,8 +4442,8 @@
+
+ for (int i = childCount - 1; i >= 0; i--) {
+ final View child = getChildAt(i);
+- final int childStart = (mIsVertical ? child.getTop() : child.getLeft());
+- final int childEnd = (mIsVertical ? child.getBottom() : child.getRight());
++ final int childStart = getChildStartEdge(child);
++ final int childEnd = getChildEndEdge(child);
+
+ if (i == childCount - 1) {
+ selectedStart = childStart;
+@@ -4718,7 +4715,7 @@
+ recycleBin.addScrapView(child, -1);
+ }
+
+- returnedWidth += child.getMeasuredHeight();
++ returnedWidth += child.getMeasuredWidth();
+
+ if (returnedWidth >= maxWidth) {
+ // We went over, figure out which width to return. If returnedWidth > maxWidth,
+@@ -4810,11 +4807,10 @@
+ child.setPressed(isPressed);
+ }
+
+- if (mChoiceMode.compareTo(ChoiceMode.NONE) != 0 && mCheckStates != null) {
++ if (mChoiceMode != ChoiceMode.NONE && mCheckStates != null) {
+ if (child instanceof Checkable) {
+ ((Checkable) child).setChecked(mCheckStates.get(position));
+- } else if (getContext().getApplicationInfo().targetSdkVersion
+- >= Build.VERSION_CODES.HONEYCOMB) {
++ } else if (Build.VERSION.SDK_INT >= HONEYCOMB) {
+ child.setActivated(mCheckStates.get(position));
+ }
+ }
+@@ -4847,26 +4843,17 @@
+
+ if (down) {
+ final int paddingStart = (mIsVertical ? getPaddingTop() : getPaddingLeft());
+-
+- final int lastEnd;
+- if (mIsVertical) {
+- lastEnd = getChildAt(childCount - 1).getBottom();
+- } else {
+- lastEnd = getChildAt(childCount - 1).getRight();
+- }
++ final int lastEnd = getChildEndEdge(getChildAt(childCount - 1));
+
+ final int offset = (childCount > 0 ? lastEnd + mItemMargin : paddingStart);
+ fillAfter(mFirstPosition + childCount, offset);
+ correctTooHigh(getChildCount());
+ } else {
+- final int end;
++ final int end = getEndEdge();
+ final int firstStart;
+-
+ if (mIsVertical) {
+- end = getHeight() - getPaddingBottom();
+ firstStart = getChildAt(0).getTop();
+ } else {
+- end = getWidth() - getPaddingRight();
+ firstStart = getChildAt(0).getLeft();
+ }
+
+@@ -4879,7 +4866,7 @@
+ private View fillBefore(int pos, int nextOffset) {
+ View selectedView = null;
+
+- final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft());
++ final int start = getStartEdge();
+
+ while (nextOffset > start && pos >= 0) {
+ boolean isSelected = (pos == mSelectedPosition);
+@@ -4906,19 +4893,13 @@
+ private View fillAfter(int pos, int nextOffset) {
+ View selectedView = null;
+
+- final int end =
+- (mIsVertical ? getHeight() - getPaddingBottom() : getWidth() - getPaddingRight());
++ final int end = getEndEdge();
+
+ while (nextOffset < end && pos < mItemCount) {
+ boolean selected = (pos == mSelectedPosition);
+
+ View child = makeAndAddView(pos, nextOffset, true, selected);
+-
+- if (mIsVertical) {
+- nextOffset = child.getBottom() + mItemMargin;
+- } else {
+- nextOffset = child.getRight() + mItemMargin;
+- }
++ nextOffset = getChildEndEdge(child) + mItemMargin;
+
+ if (selected) {
+ selectedView = child;
+@@ -4937,25 +4918,13 @@
+ // Possibly changed again in fillBefore if we add rows above this one.
+ mFirstPosition = position;
+
+- final int itemMargin = mItemMargin;
+-
+- final int offsetBefore;
+- if (mIsVertical) {
+- offsetBefore = temp.getTop() - itemMargin;
+- } else {
+- offsetBefore = temp.getLeft() - itemMargin;
+- }
++ final int offsetBefore = getChildStartEdge(temp) + mItemMargin;
+ final View before = fillBefore(position - 1, offsetBefore);
+
+ // This will correct for the top of the first view not touching the top of the list
+ adjustViewsStartOrEnd();
+
+- final int offsetAfter;
+- if (mIsVertical) {
+- offsetAfter = temp.getBottom() + itemMargin;
+- } else {
+- offsetAfter = temp.getRight() + itemMargin;
+- }
++ final int offsetAfter = getChildEndEdge(temp) + mItemMargin;
+ final View after = fillAfter(position + 1, offsetAfter);
+
+ final int childCount = getChildCount();
+@@ -5009,37 +4978,22 @@
+ }
+
+ private void fillBeforeAndAfter(View selected, int position) {
+- final int itemMargin = mItemMargin;
+-
+- final int offsetBefore;
+- if (mIsVertical) {
+- offsetBefore = selected.getTop() - itemMargin;
+- } else {
+- offsetBefore = selected.getLeft() - itemMargin;
+- }
+-
++ final int offsetBefore = getChildStartEdge(selected) + mItemMargin;
+ fillBefore(position - 1, offsetBefore);
+
+ adjustViewsStartOrEnd();
+
+- final int offsetAfter;
+- if (mIsVertical) {
+- offsetAfter = selected.getBottom() + itemMargin;
+- } else {
+- offsetAfter = selected.getRight() + itemMargin;
+- }
+-
++ final int offsetAfter = getChildEndEdge(selected) + mItemMargin;
+ fillAfter(position + 1, offsetAfter);
+ }
+
+ private View fillFromSelection(int selectedTop, int start, int end) {
+ final int selectedPosition = mSelectedPosition;
+- View selected;
+
+- selected = makeAndAddView(selectedPosition, selectedTop, true, true);
++ View selected = makeAndAddView(selectedPosition, selectedTop, true, true);
+
+- final int selectedStart = (mIsVertical ? selected.getTop() : selected.getLeft());
+- final int selectedEnd = (mIsVertical ? selected.getBottom() : selected.getRight());
++ final int selectedStart = getChildStartEdge(selected);
++ final int selectedEnd = getChildEndEdge(selected);
+
+ // Some of the newly selected item extends below the bottom of the list
+ if (selectedEnd > end) {
+@@ -5085,28 +5039,19 @@
+ return;
+ }
+
+- // Get the last child ...
+- final View lastChild = getChildAt(childCount - 1);
+-
+- // ... and its end edge
+- final int lastEnd;
+- if (mIsVertical) {
+- lastEnd = lastChild.getBottom();
+- } else {
+- lastEnd = lastChild.getRight();
+- }
++ // Get the last child end edge
++ final int lastEnd = getChildEndEdge(getChildAt(childCount - 1));
+
+ // This is bottom of our drawable area
+- final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft());
+- final int end =
+- (mIsVertical ? getHeight() - getPaddingBottom() : getWidth() - getPaddingRight());
++ final int start = getStartEdge();
++ final int end = getEndEdge();
+
+ // This is how far the end edge of the last view is from the end of the
+ // drawable area
+ int endOffset = end - lastEnd;
+
+ View firstChild = getChildAt(0);
+- int firstStart = (mIsVertical ? firstChild.getTop() : firstChild.getLeft());
++ int firstStart = getChildStartEdge(firstChild);
+
+ // Make sure we are 1) Too high, and 2) Either there are more rows above the
+ // first row or the first row is scrolled off the top of the drawable area
+@@ -5120,7 +5065,7 @@
+ offsetChildren(endOffset);
+
+ if (mFirstPosition > 0) {
+- firstStart = (mIsVertical ? firstChild.getTop() : firstChild.getLeft());
++ firstStart = getChildStartEdge(firstChild);
+
+ // Fill the gap that was opened above mFirstPosition with more rows, if
+ // possible
+@@ -5142,21 +5087,15 @@
+ final View first = getChildAt(0);
+ final int firstStart = (mIsVertical ? first.getTop() : first.getLeft());
+
+- final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft());
+-
+- final int end;
+- if (mIsVertical) {
+- end = getHeight() - getPaddingBottom();
+- } else {
+- end = getWidth() - getPaddingRight();
+- }
++ final int start = getStartEdge();
++ final int end = getEndEdge();
+
+ // This is how far the start edge of the first view is from the start of the
+ // drawable area
+ int startOffset = firstStart - start;
+
+ View last = getChildAt(childCount - 1);
+- int lastEnd = (mIsVertical ? last.getBottom() : last.getRight());
++ int lastEnd = getChildEndEdge(last);
+
+ int lastPosition = mFirstPosition + childCount - 1;
+
+@@ -5174,7 +5113,7 @@
+ offsetChildren(-startOffset);
+
+ if (lastPosition < mItemCount - 1) {
+- lastEnd = (mIsVertical ? last.getBottom() : last.getRight());
++ lastEnd = getChildEndEdge(last);
+
+ // Fill the gap that was opened below the last position with more rows, if
+ // possible
+@@ -5366,6 +5305,8 @@
+ }
+
+ void resetState() {
++ mScroller.forceFinished(true);
++
+ removeAllViewsInLayout();
+
+ mSelectedStart = 0;
+@@ -5419,7 +5360,7 @@
+ mSyncPosition = mFirstPosition;
+
+ if (child != null) {
+- mSpecificStart = child.getTop();
++ mSpecificStart = (mIsVertical ? child.getTop() : child.getLeft());
+ }
+
+ mSyncMode = SYNC_FIRST_POSITION;
+@@ -5435,16 +5376,13 @@
+ final int firstPos = mFirstPosition;
+ final int count = getChildCount();
+
+- final boolean useActivated = getContext().getApplicationInfo().targetSdkVersion
+- >= Build.VERSION_CODES.HONEYCOMB;
+-
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ final int position = firstPos + i;
+
+ if (child instanceof Checkable) {
+ ((Checkable) child).setChecked(mCheckStates.get(position));
+- } else if (useActivated) {
++ } else if (Build.VERSION.SDK_INT >= HONEYCOMB) {
+ child.setActivated(mCheckStates.get(position));
+ }
+ }
+@@ -5454,7 +5392,7 @@
+ public boolean performItemClick(View view, int position, long id) {
+ boolean checkedStateChanged = false;
+
+- if (mChoiceMode.compareTo(ChoiceMode.MULTIPLE) == 0) {
++ if (mChoiceMode == ChoiceMode.MULTIPLE) {
+ boolean checked = !mCheckStates.get(position, false);
+ mCheckStates.put(position, checked);
+
+@@ -5473,7 +5411,7 @@
+ }
+
+ checkedStateChanged = true;
+- } else if (mChoiceMode.compareTo(ChoiceMode.SINGLE) == 0) {
++ } else if (mChoiceMode == ChoiceMode.SINGLE) {
+ boolean checked = !mCheckStates.get(position, false);
+ if (checked) {
+ mCheckStates.clear();
+@@ -5699,14 +5637,14 @@
+ super(width, height);
+
+ if (this.width == MATCH_PARENT) {
+- Log.w(LOGTAG, "Constructing LayoutParams with width FILL_PARENT " +
++ Log.w(LOGTAG, "Constructing LayoutParams with width MATCH_PARENT " +
+ "does not make much sense as the view might change orientation. " +
+ "Falling back to WRAP_CONTENT");
+ this.width = WRAP_CONTENT;
+ }
+
+ if (this.height == MATCH_PARENT) {
+- Log.w(LOGTAG, "Constructing LayoutParams with height FILL_PARENT " +
++ Log.w(LOGTAG, "Constructing LayoutParams with height MATCH_PARENT " +
+ "does not make much sense as the view might change orientation. " +
+ "Falling back to WRAP_CONTENT");
+ this.height = WRAP_CONTENT;
+@@ -5735,7 +5673,7 @@
+ super(other);
+
+ if (this.width == MATCH_PARENT) {
+- Log.w(LOGTAG, "Constructing LayoutParams with height MATCH_PARENT - " +
++ Log.w(LOGTAG, "Constructing LayoutParams with width MATCH_PARENT - " +
+ "does not make much sense as the view might change orientation. " +
+ "Falling back to WRAP_CONTENT");
+ this.width = WRAP_CONTENT;
+@@ -5786,11 +5724,8 @@
+ } else {
+ final int typeCount = mViewTypeCount;
+ for (int i = 0; i < typeCount; i++) {
+- final ArrayList<View> scrap = mScrapViews[i];
+- final int scrapCount = scrap.size();
+-
+- for (int j = 0; j < scrapCount; j++) {
+- scrap.get(j).forceLayout();
++ for (View scrap : mScrapViews[i]) {
++ scrap.forceLayout();
+ }
+ }
+ }
+@@ -6120,7 +6055,7 @@
+ // Force one here to make sure that the state of the list matches
+ // the state of the adapter.
+ if (mDataChanged) {
+- onLayout(false, getLeft(), getTop(), getRight(), getBottom());
++ layout(getLeft(), getTop(), getRight(), getBottom());
+ }
+ } else {
+ if (mEmptyView != null) {
+@@ -6525,16 +6460,10 @@
+ return false;
+
+ case AccessibilityNodeInfoCompat.ACTION_CLICK:
+- if (isClickable()) {
+- return performItemClick(host, position, id);
+- }
+- return false;
++ return isClickable() && performItemClick(host, position, id);
+
+ case AccessibilityNodeInfoCompat.ACTION_LONG_CLICK:
+- if (isLongClickable()) {
+- return performLongPress(host, position, id);
+- }
+- return false;
++ return isLongClickable() && performLongPress(host, position, id);
+ }
+
+ return false;
diff --git a/data/patches/gnuzilla-bug-1036286-1.patch b/data/patches/gnuzilla-bug-1036286-1.patch
new file mode 100644
index 0000000..a5d3110
--- /dev/null
+++ b/data/patches/gnuzilla-bug-1036286-1.patch
@@ -0,0 +1,188 @@
+--- a/mozglue/linker/ElfLoader.cpp
++++ a/mozglue/linker/ElfLoader.cpp
+@@ -960,28 +960,28 @@ static uint64_t ProcessTimeStamp_Now()
+ /* Data structure used to pass data to the temporary signal handler,
+ * as well as triggering a test crash. */
+ struct TmpData {
+ volatile int crash_int;
+ volatile uint64_t crash_timestamp;
+ };
+
+ SEGVHandler::SEGVHandler()
+-: registeredHandler(false), signalHandlingBroken(false)
+-, signalHandlingSlow(false)
++: initialized(false), registeredHandler(false), signalHandlingBroken(true)
++, signalHandlingSlow(true)
+ {
+ /* Initialize oldStack.ss_flags to an invalid value when used to set
+ * an alternative stack, meaning we haven't got information about the
+- * original alternative stack and thus don't mean to restore it */
++ * original alternative stack and thus don't mean to restore it in
++ * the desctructor. */
+ oldStack.ss_flags = SS_ONSTACK;
+- if (!Divert(sigaction, __wrap_sigaction))
+- return;
+
+ /* Get the current segfault signal handler. */
+- sys_sigaction(SIGSEGV, nullptr, &this->action);
++ struct sigaction old_action;
++ sys_sigaction(SIGSEGV, nullptr, &old_action);
+
+ /* Some devices don't provide useful information to their SIGSEGV handlers,
+ * making it impossible for on-demand decompression to work. To check if
+ * we're on such a device, setup a temporary handler and deliberately
+ * trigger a segfault. The handler will set signalHandlingBroken if the
+ * provided information is bogus.
+ * Some other devices have a kernel option enabled that makes SIGSEGV handler
+ * have an overhead so high that it affects how on-demand decompression
+@@ -1000,21 +1000,33 @@ SEGVHandler::SEGVHandler()
+ if (sys_sigaction(SIGSEGV, &action, nullptr))
+ return;
+
+ TmpData *data = reinterpret_cast<TmpData*>(stackPtr.get());
+ data->crash_timestamp = ProcessTimeStamp_Now();
+ mprotect(stackPtr, stackPtr.GetLength(), PROT_NONE);
+ data->crash_int = 123;
+ /* Restore the original segfault signal handler. */
+- sys_sigaction(SIGSEGV, &this->action, nullptr);
++ sys_sigaction(SIGSEGV, &old_action, nullptr);
+ stackPtr.Assign(MAP_FAILED, 0);
++}
++
++void
++SEGVHandler::FinishInitialization()
++{
++ /* Ideally, we'd need some locking here, but in practice, we're not
++ * going to race with another thread. */
++ initialized = true;
++
+ if (signalHandlingBroken || signalHandlingSlow)
+ return;
+
++ if (!Divert(sigaction, __wrap_sigaction))
++ return;
++
+ /* Setup an alternative stack if the already existing one is not big
+ * enough, or if there is none. */
+ if (sigaltstack(nullptr, &oldStack) == 0) {
+ if (oldStack.ss_flags == SS_ONSTACK)
+ oldStack.ss_flags = 0;
+ if (!oldStack.ss_sp || oldStack.ss_size < stackSize) {
+ stackPtr.Assign(MemoryRange::mmap(nullptr, stackSize,
+ PROT_READ | PROT_WRITE,
+@@ -1028,17 +1040,17 @@ SEGVHandler::SEGVHandler()
+ if (sigaltstack(&stack, nullptr) != 0)
+ return;
+ }
+ }
+ /* Register our own handler, and store the already registered one in
+ * SEGVHandler's struct sigaction member */
+ action.sa_sigaction = &SEGVHandler::handler;
+ action.sa_flags = SA_SIGINFO | SA_NODEFER | SA_ONSTACK;
+- registeredHandler = !sys_sigaction(SIGSEGV, &action, nullptr);
++ registeredHandler = !sys_sigaction(SIGSEGV, &action, &this->action);
+ }
+
+ SEGVHandler::~SEGVHandler()
+ {
+ /* Restore alternative stack for signals */
+ if (oldStack.ss_flags != SS_ONSTACK)
+ sigaltstack(&oldStack, nullptr);
+ /* Restore original signal handler */
+@@ -1048,28 +1060,28 @@ SEGVHandler::~SEGVHandler()
+
+ /* Test handler for a deliberately triggered SIGSEGV that determines whether
+ * useful information is provided to signal handlers, particularly whether
+ * si_addr is filled in properly, and whether the segfault handler is called
+ * quickly enough. */
+ void SEGVHandler::test_handler(int signum, siginfo_t *info, void *context)
+ {
+ SEGVHandler &that = ElfLoader::Singleton;
+- if (signum != SIGSEGV ||
+- info == nullptr || info->si_addr != that.stackPtr.get())
+- that.signalHandlingBroken = true;
++ if (signum == SIGSEGV && info &&
++ info->si_addr == that.stackPtr.get())
++ that.signalHandlingBroken = false;
+ mprotect(that.stackPtr, that.stackPtr.GetLength(), PROT_READ | PROT_WRITE);
+ TmpData *data = reinterpret_cast<TmpData*>(that.stackPtr.get());
+ uint64_t latency = ProcessTimeStamp_Now() - data->crash_timestamp;
+ DEBUG_LOG("SEGVHandler latency: %" PRIu64, latency);
+ /* See bug 886736 for timings on different devices, 150 µs is reasonably above
+- * the latency on "working" devices and seems to be reasonably fast to incur
+- * a huge overhead to on-demand decompression. */
+- if (latency > 150000)
+- that.signalHandlingSlow = true;
++ * the latency on "working" devices and seems to be reasonably fast not to
++ * incur a huge overhead to on-demand decompression. */
++ if (latency <= 150000)
++ that.signalHandlingSlow = false;
+ }
+
+ /* TODO: "properly" handle signal masks and flags */
+ void SEGVHandler::handler(int signum, siginfo_t *info, void *context)
+ {
+ //ASSERT(signum == SIGSEGV);
+ DEBUG_LOG("Caught segmentation fault @%p", info->si_addr);
+
+--- a/mozglue/linker/ElfLoader.h
++++ a/mozglue/linker/ElfLoader.h
+@@ -307,32 +307,40 @@ private:
+ * faults within the address space of the loaded libraries. It however
+ * allows a handler to be set for faults in other places, and redispatches
+ * to the handler set through signal() or sigaction().
+ */
+ class SEGVHandler
+ {
+ public:
+ bool hasRegisteredHandler() {
++ if (! initialized)
++ FinishInitialization();
+ return registeredHandler;
+ }
+
+ bool isSignalHandlingBroken() {
+ return signalHandlingBroken;
+ }
+
+ protected:
+ SEGVHandler();
+ ~SEGVHandler();
+
+ private:
+ static int __wrap_sigaction(int signum, const struct sigaction *act,
+ struct sigaction *oldact);
+
+ /**
++ * The constructor doesn't do all initialization, and the tail is done
++ * at a later time.
++ */
++ void FinishInitialization();
++
++ /**
+ * SIGSEGV handler registered with __wrap_signal or __wrap_sigaction.
+ */
+ struct sigaction action;
+
+ /**
+ * ElfLoader SIGSEGV handler.
+ */
+ static void handler(int signum, siginfo_t *info, void *context);
+@@ -354,16 +362,17 @@ private:
+ stack_t oldStack;
+
+ /**
+ * Pointer to an alternative stack for signals. Only set if oldStack is
+ * not set or not big enough.
+ */
+ MappedPtr stackPtr;
+
++ bool initialized;
+ bool registeredHandler;
+ bool signalHandlingBroken;
+ bool signalHandlingSlow;
+ };
+
+ /**
+ * Elf Loader class in charge of loading and bookkeeping libraries.
+ */
diff --git a/data/patches/gnuzilla-bug-1036286-2.patch b/data/patches/gnuzilla-bug-1036286-2.patch
new file mode 100644
index 0000000..6708062
--- /dev/null
+++ b/data/patches/gnuzilla-bug-1036286-2.patch
@@ -0,0 +1,105 @@
+--- a/mozglue/linker/CustomElf.cpp
++++ a/mozglue/linker/CustomElf.cpp
+@@ -1,16 +1,17 @@
+ /* 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/. */
+
+ #include <cstring>
+ #include <sys/mman.h>
+ #include <vector>
+ #include <dlfcn.h>
++#include <signal.h>
+ #include "CustomElf.h"
+ #include "Mappable.h"
+ #include "Logging.h"
+
+ using namespace Elf;
+ using namespace mozilla;
+
+ /* TODO: Fill ElfLoader::Singleton.lastError on errors. */
+@@ -348,16 +349,21 @@ CustomElf::GetSymbolPtrInDeps(const char
+ if (strcmp(symbol + 2, "dso_handle") == 0)
+ return const_cast<CustomElf *>(this);
+ if (strcmp(symbol + 2, "moz_linker_stats") == 0)
+ return FunctionPtr(&ElfLoader::stats);
+ #ifdef __ARM_EABI__
+ if (strcmp(symbol + 2, "gnu_Unwind_Find_exidx") == 0)
+ return FunctionPtr(__wrap___gnu_Unwind_Find_exidx);
+ #endif
++ } else if (symbol[0] == 's' && symbol[1] == 'i') {
++ if (strcmp(symbol + 2, "gnal") == 0)
++ return FunctionPtr(signal);
++ if (strcmp(symbol + 2, "gaction") == 0)
++ return FunctionPtr(sigaction);
+ }
+
+ #define MISSING_FLASH_SYMNAME_START "_ZN7android10VectorImpl19reservedVectorImpl"
+
+ // Android changed some symbols that Flash depended on in 4.4,
+ // so stub those out here
+ if (strncmp(symbol,
+ MISSING_FLASH_SYMNAME_START,
+--- a/mozglue/linker/ElfLoader.cpp
++++ a/mozglue/linker/ElfLoader.cpp
+@@ -1014,17 +1014,59 @@ SEGVHandler::FinishInitialization()
+ {
+ /* Ideally, we'd need some locking here, but in practice, we're not
+ * going to race with another thread. */
+ initialized = true;
+
+ if (signalHandlingBroken || signalHandlingSlow)
+ return;
+
+- if (!Divert(sigaction, __wrap_sigaction))
++ typedef int (*sigaction_func)(int, const struct sigaction *,
++ struct sigaction *);
++
++ sigaction_func libc_sigaction;
++
++#if defined(ANDROID)
++ /* Android > 4.4 comes with a sigaction wrapper in a LD_PRELOADed library
++ * (libsigchain) for ART. That wrapper kind of does the same trick as we
++ * do, so we need extra care in handling it.
++ * - Divert the libc's sigaction, assuming the LD_PRELOADed library uses
++ * it under the hood (which is more or less true according to the source
++ * of that library, since it's doing a lookup in RTLD_NEXT)
++ * - With the LD_PRELOADed library in place, all calls to sigaction from
++ * from system libraries will go to the LD_PRELOADed library.
++ * - The LD_PRELOADed library calls to sigaction go to our __wrap_sigaction.
++ * - The calls to sigaction from libraries faulty.lib loads are sent to
++ * the LD_PRELOADed library.
++ * In practice, for signal handling, this means:
++ * - The signal handler registered to the kernel is ours.
++ * - Our handler redispatches to the LD_PRELOADed library's if there's a
++ * segfault we don't handle.
++ * - The LD_PRELOADed library redispatches according to whatever system
++ * library or faulty.lib-loaded library set with sigaction.
++ *
++ * When there is no sigaction wrapper in place:
++ * - Divert the libc's sigaction.
++ * - Calls to sigaction from system library and faulty.lib-loaded libraries
++ * all go to the libc's sigaction, which end up in our __wrap_sigaction.
++ * - The signal handler registered to the kernel is ours.
++ * - Our handler redispatches according to whatever system library or
++ * faulty.lib-loaded library set with sigaction.
++ */
++ void *libc = dlopen("libc.so", RTLD_GLOBAL | RTLD_LAZY);
++ if (libc) {
++ libc_sigaction =
++ reinterpret_cast<sigaction_func>(dlsym(libc, "sigaction"));
++ } else
++#endif
++ {
++ libc_sigaction = sigaction;
++ }
++
++ if (!Divert(libc_sigaction, __wrap_sigaction))
+ return;
+
+ /* Setup an alternative stack if the already existing one is not big
+ * enough, or if there is none. */
+ if (sigaltstack(nullptr, &oldStack) == 0) {
+ if (oldStack.ss_flags == SS_ONSTACK)
+ oldStack.ss_flags = 0;
+ if (!oldStack.ss_sp || oldStack.ss_size < stackSize) {
diff --git a/data/patches/gnuzilla-bug-1050780.patch b/data/patches/gnuzilla-bug-1050780.patch
new file mode 100644
index 0000000..b142dc7
--- /dev/null
+++ b/data/patches/gnuzilla-bug-1050780.patch
@@ -0,0 +1,53 @@
+# HG changeset patch
+# User Lucas Rocha <lucasr@mozilla.com>
+
+Bug 1050780 - Avoid disabled items in GeckoMenu's adapter (r=margaret)
+
+diff --git a/mobile/android/base/menu/GeckoMenu.java b/mobile/android/base/menu/GeckoMenu.java
+index eff2a51..cd2404b 100644
+--- a/mobile/android/base/menu/GeckoMenu.java
++++ b/mobile/android/base/menu/GeckoMenu.java
+@@ -781,17 +781,19 @@ public class GeckoMenu extends ListView
+ public boolean areAllItemsEnabled() {
+ // Setting this to true is a workaround to fix disappearing
+ // dividers in the menu (bug 963249).
+ return true;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+- return getItem(position).isEnabled();
++ // Setting this to true is a workaround to fix disappearing
++ // dividers in the menu in L (bug 1050780).
++ return true;
+ }
+
+ public void addMenuItem(GeckoMenuItem menuItem) {
+ if (mItems.contains(menuItem))
+ return;
+
+ // Insert it in proper order.
+ int index = 0;
+diff --git a/mobile/android/base/resources/drawable/action_bar_button.xml b/mobile/android/base/resources/drawable/action_bar_button.xml
+index 9cb7aa8..fe36bc4 100644
+--- a/mobile/android/base/resources/drawable/action_bar_button.xml
++++ b/mobile/android/base/resources/drawable/action_bar_button.xml
+@@ -1,16 +1,17 @@
+ <?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/. -->
+
+ <selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+- <item android:state_pressed="true">
++ <item android:state_pressed="true"
++ android:state_enabled="true">
+ <shape>
+ <solid android:color="@color/highlight" />
+ </shape>
+ </item>
+
+ <item android:state_focused="true"
+ android:state_pressed="false">
+ <shape>
diff --git a/data/patches/gnuzilla-bug-1055166.patch b/data/patches/gnuzilla-bug-1055166.patch
new file mode 100644
index 0000000..51b8ee5
--- /dev/null
+++ b/data/patches/gnuzilla-bug-1055166.patch
@@ -0,0 +1,33 @@
+From: James Willcox <snorp@snorp.net>
+Bug 1055166 - Catch IllegalStateException when recycling Message
+
+
+
+diff --git a/mobile/android/base/GeckoAppShell.java b/mobile/android/base/GeckoAppShell.java
+index b06547f8c8135542d27713bb8e4c5d0e7496bb17..a50538e1b3449d3767ad78ed3fb8e596bc1acfbb 100644
+--- a/mobile/android/base/GeckoAppShell.java
++++ b/mobile/android/base/GeckoAppShell.java
+@@ -2478,17 +2478,22 @@ public class GeckoAppShell
+ // Our "queue is empty" message; see runGecko()
+ msg.recycle();
+ return false;
+ }
+ if (msg.getTarget() == null)
+ Looper.myLooper().quit();
+ else
+ msg.getTarget().dispatchMessage(msg);
+- msg.recycle();
++
++ try {
++ msg.recycle();
++ } catch (IllegalStateException e) {
++ // There is nothing we can do here so just eat it
++ }
+ return true;
+ }
+
+ @WrapElementForJNI
+ public static void notifyWakeLockChanged(String topic, String state) {
+ if (getGeckoInterface() != null)
+ getGeckoInterface().notifyWakeLockChanged(topic, state);
+ }
diff --git a/data/patches/gnuzilla-bug-1058778.patch b/data/patches/gnuzilla-bug-1058778.patch
new file mode 100644
index 0000000..27d5097
--- /dev/null
+++ b/data/patches/gnuzilla-bug-1058778.patch
@@ -0,0 +1,10 @@
+--- a/security/build/moz.build 2015-01-06 05:07:59.000000000 +0000
++++ b/security/build/moz.build 2015-01-30 21:55:40.988341831 +0000
+@@ -10,5 +10,5 @@
+
+ FORCE_SHARED_LIB = True
+
+-if CONFIG['OS_TARGET'] == 'WINNT':
+- DEFFILE = 'nss3.def'
++ if CONFIG['OS_TARGET'] == 'WINNT':
++ DEFFILE = 'nss3.def'
diff --git a/data/patches/gnuzilla-bug-1089931.patch b/data/patches/gnuzilla-bug-1089931.patch
new file mode 100644
index 0000000..cfd019f
--- /dev/null
+++ b/data/patches/gnuzilla-bug-1089931.patch
@@ -0,0 +1,40 @@
+https://bugzilla.mozilla.org/show_bug.cgi?id=1089931
+--- a/toolkit/crashreporter/google-breakpad/src/common/android/include/ucontext.h
++++ a/toolkit/crashreporter/google-breakpad/src/common/android/include/ucontext.h
+@@ -26,31 +26,30 @@
+ // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+ #ifndef GOOGLE_BREAKPAD_COMMON_ANDROID_INCLUDE_UCONTEXT_H
+ #define GOOGLE_BREAKPAD_COMMON_ANDROID_INCLUDE_UCONTEXT_H
+
+ #include <sys/cdefs.h>
++#include <signal.h>
+
+-#ifdef __BIONIC_UCONTEXT_H
+-#include <ucontext.h>
++#ifdef __BIONIC_HAVE_UCONTEXT_H
++# include_next <ucontext.h>
+ #else
+-
+-#include <sys/ucontext.h>
++# include <sys/ucontext.h>
++#endif // __BIONIC_UCONTEXT_H
+
+ #ifdef __cplusplus
+ extern "C" {
+ #endif // __cplusplus
+
+ // Provided by src/android/common/breakpad_getcontext.S
+ int breakpad_getcontext(ucontext_t* ucp);
+
+ #define getcontext(x) breakpad_getcontext(x)
+
+ #ifdef __cplusplus
+ } // extern "C"
+ #endif // __cplusplus
+
+-#endif // __BIONIC_UCONTEXT_H
+-
+ #endif // GOOGLE_BREAKPAD_COMMON_ANDROID_INCLUDE_UCONTEXT_H
diff --git a/data/patches/gnuzilla-bug-1091987.patch b/data/patches/gnuzilla-bug-1091987.patch
new file mode 100644
index 0000000..d42b5df
--- /dev/null
+++ b/data/patches/gnuzilla-bug-1091987.patch
@@ -0,0 +1,40 @@
+https://bugzilla.mozilla.org/show_bug.cgi?id=1091987
+
+--- a/widget/android/AndroidJavaWrappers.h
++++ a/widget/android/AndroidJavaWrappers.h
+@@ -228,16 +228,17 @@ private:
+ static jmethodID jBeginDrawingMethod;
+ static jmethodID jDrawBackgroundMethod;
+ static jmethodID jDrawForegroundMethod;
+ static jmethodID jEndDrawingMethod;
+ };
+
+ enum {
+ // These keycode masks are not defined in android/keycodes.h:
++#if __ANDROID_API__ < 15
+ AKEYCODE_ESCAPE = 111,
+ AKEYCODE_FORWARD_DEL = 112,
+ AKEYCODE_CTRL_LEFT = 113,
+ AKEYCODE_CTRL_RIGHT = 114,
+ AKEYCODE_CAPS_LOCK = 115,
+ AKEYCODE_SCROLL_LOCK = 116,
+ AKEYCODE_META_LEFT = 117,
+ AKEYCODE_META_RIGHT = 118,
+@@ -337,16 +338,17 @@ enum {
+ AKEYCODE_EISU = 212,
+ AKEYCODE_MUHENKAN = 213,
+ AKEYCODE_HENKAN = 214,
+ AKEYCODE_KATAKANA_HIRAGANA = 215,
+ AKEYCODE_YEN = 216,
+ AKEYCODE_RO = 217,
+ AKEYCODE_KANA = 218,
+ AKEYCODE_ASSIST = 219,
++#endif
+
+ AMETA_FUNCTION_ON = 0x00000008,
+ AMETA_CTRL_ON = 0x00001000,
+ AMETA_CTRL_LEFT_ON = 0x00002000,
+ AMETA_CTRL_RIGHT_ON = 0x00004000,
+ AMETA_META_ON = 0x00010000,
+ AMETA_META_LEFT_ON = 0x00020000,
+ AMETA_META_RIGHT_ON = 0x00040000,
diff --git a/data/patches/gnuzilla-bug-1095298.patch b/data/patches/gnuzilla-bug-1095298.patch
new file mode 100644
index 0000000..aa075a2
--- /dev/null
+++ b/data/patches/gnuzilla-bug-1095298.patch
@@ -0,0 +1,30 @@
+--- a/intl/locale/src/nsLocaleService.cpp
++++ a/intl/locale/src/nsLocaleService.cpp
+@@ -135,17 +135,25 @@ nsLocaleService::nsLocaleService(void)
+
+ for( i = 0; i < LocaleListLength; i++ ) {
+ nsresult result;
+ // setlocale( , "") evaluates LC_* and LANG
+ char* lc_temp = setlocale(posix_locale_category[i], "");
+ CopyASCIItoUTF16(LocaleList[i], category);
+ category_platform = category;
+ category_platform.AppendLiteral("##PLATFORM");
+- if (lc_temp != nullptr) {
++
++ bool lc_temp_valid = lc_temp != nullptr;
++
++#if defined(MOZ_WIDGET_ANDROID)
++ // Treat the "C" env as nothing useful. See Bug 1095298.
++ lc_temp_valid = lc_temp_valid && strcmp(lc_temp, "C") != 0;
++#endif
++
++ if (lc_temp_valid) {
+ result = nsPosixLocale::GetXPLocale(lc_temp, xpLocale);
+ CopyASCIItoUTF16(lc_temp, platformLocale);
+ } else {
+ if ( lang == nullptr ) {
+ platformLocale.AssignLiteral("en_US");
+ result = nsPosixLocale::GetXPLocale("en-US", xpLocale);
+ } else {
+ CopyASCIItoUTF16(lang, platformLocale);
+
diff --git a/data/patches/reduceGeckoPriority.patch b/data/patches/reduceGeckoPriority.patch
new file mode 100644
index 0000000..843d2c8
--- /dev/null
+++ b/data/patches/reduceGeckoPriority.patch
@@ -0,0 +1,18 @@
+diff -ur icecat-31.4.0.orig/mobile/android/base/util/ThreadUtils.java icecat-31.4.0/mobile/android/base/util/ThreadUtils.java
+--- icecat-31.4.0.orig/mobile/android/base/util/ThreadUtils.java 2015-02-22 18:35:21.022789023 +0000
++++ icecat-31.4.0/mobile/android/base/util/ThreadUtils.java 2015-02-22 22:09:50.518544343 +0000
+@@ -179,7 +179,13 @@
+ * @param timeout Timeout in ms after which the priority will be reset
+ */
+ public static void reduceGeckoPriority(long timeout) {
+- if (!sIsGeckoPriorityReduced) {
++ if (Runtime.getRuntime().availableProcessors() > 1) {
++ // Don't reduce priority for multicore devices. We use availableProcessors()
++ // for its fast performance. It may give false negatives (i.e. multicore
++ // detected as single-core), but we can tolerate this behavior.
++ return;
++ }
++ if (!sIsGeckoPriorityReduced && sGeckoThread != null) {
+ sIsGeckoPriorityReduced = true;
+ sGeckoThread.setPriority(Thread.MIN_PRIORITY);
+ getUiHandler().postDelayed(sPriorityResetRunnable, timeout);
diff --git a/data/patches/reorder-mobile_android_base_android-services_mozbuild.patch b/data/patches/reorder-mobile_android_base_android-services_mozbuild.patch
new file mode 100644
index 0000000..dd38815
--- /dev/null
+++ b/data/patches/reorder-mobile_android_base_android-services_mozbuild.patch
@@ -0,0 +1,34 @@
+--- a/mobile/android/base/android-services.mozbuild 2015-01-06 05:07:54.000000000 +0000
++++ b/mobile/android/base/android-services.mozbuild 2015-02-02 02:59:30.180709918 +0000
+@@ -567,7 +567,7 @@
+ 'fxa/authenticator/FxAccountAuthenticatorService.java',
+ 'fxa/authenticator/FxAccountLoginDelegate.java',
+ 'fxa/authenticator/FxAccountLoginException.java',
+- 'fxa/FirefoxAccounts.java',
++ 'fxa/IceCatAccounts.java',
+ 'fxa/login/BaseRequestDelegate.java',
+ 'fxa/login/Cohabiting.java',
+ 'fxa/login/Doghouse.java',
+@@ -709,8 +709,8 @@
+ 'sync/repositories/android/CachedSQLiteOpenHelper.java',
+ 'sync/repositories/android/ClientsDatabase.java',
+ 'sync/repositories/android/ClientsDatabaseAccessor.java',
+- 'sync/repositories/android/FennecTabsRepository.java',
+ 'sync/repositories/android/FormHistoryRepositorySession.java',
++ 'sync/repositories/android/IceCatMobileTabsRepository.java',
+ 'sync/repositories/android/PasswordsRepositorySession.java',
+ 'sync/repositories/android/RepoUtils.java',
+ 'sync/repositories/BookmarkNeedsReparentingException.java',
+@@ -803,11 +803,11 @@
+ 'sync/stage/CompletedStage.java',
+ 'sync/stage/EnsureClusterURLStage.java',
+ 'sync/stage/EnsureCrypto5KeysStage.java',
+- 'sync/stage/FennecTabsServerSyncStage.java',
+ 'sync/stage/FetchInfoCollectionsStage.java',
+ 'sync/stage/FetchMetaGlobalStage.java',
+ 'sync/stage/FormHistoryServerSyncStage.java',
+ 'sync/stage/GlobalSyncStage.java',
++ 'sync/stage/IceCatMobileTabsServerSyncStage.java',
+ 'sync/stage/NoSuchStageException.java',
+ 'sync/stage/NoSyncIDException.java',
+ 'sync/stage/PasswordsServerSyncStage.java',
diff --git a/data/patches/revert-gnuzilla-bug-973138.patch b/data/patches/revert-gnuzilla-bug-973138.patch
new file mode 100644
index 0000000..ffd3a68
--- /dev/null
+++ b/data/patches/revert-gnuzilla-bug-973138.patch
@@ -0,0 +1,25 @@
+This patch reverts fix for gnuzilla bug:
+https://bugzilla.mozilla.org/show_bug.cgi?id=973138
+which introduces:
+https://trac.torproject.org/projects/tor/ticket/12811
+https://bugzilla.mozilla.org/show_bug.cgi?id=1049366
+
+--- a/mozglue/build/WindowsDllBlocklist.cpp 2015-01-27 19:17:08.585519129 +0100
++++ b/mozglue/build/WindowsDllBlocklist.cpp 2015-01-30 00:42:10.817863012 +0100
+@@ -551,7 +551,7 @@
+ printf_stderr("LdrLoadDll: dll name '%s'\n", dllName);
+ #endif
+
+- // Block a suspicious binary that uses various 12-digit hex strings
++/* // Block a suspicious binary that uses various 12-digit hex strings
+ // e.g. MovieMode.48CA2AEFA22D.dll (bug 973138)
+ char * dot = strchr(dllName, '.');
+ if (dot && (strchr(dot+1, '.') == dot+13)) {
+@@ -561,6 +561,7 @@
+ return STATUS_DLL_NOT_FOUND;
+ }
+ }
++*/
+
+ // then compare to everything on the blocklist
+ info = &sWindowsDllBlocklist[0];
diff --git a/data/patches/torbrowser-bug-12811-2.patch b/data/patches/torbrowser-bug-12811-2.patch
new file mode 100644
index 0000000..e80e228
--- /dev/null
+++ b/data/patches/torbrowser-bug-12811-2.patch
@@ -0,0 +1,33 @@
+commit 73a628a5cebfa6dae1fca4c5fde072376ce4d06f
+Author: Georg Koppen <gk at torproject.org>
+Date: Fri Sep 12 09:37:39 2014 +0000
+
+ Bug 12811: Add MOZ_D3DCOMPILER_DLL iff available.
+
+ It seems MOZ_D3DCOMPILER_DLL is not avilable when compiling with
+ mingw-w64. Trying to include it nevertheless breaks the packaging step
+ as `@BINPATH@/@MOZ_D3DCOMPILER_DLL@` resolves to `bin/` which results
+ in including all the things beginning with a `bin/` (again) which is
+ treated as an error in case they got already included.
+
+ This got fixed more or less by accident by bug 999260 which did not make
+ it into ESR 31 (https://hg.mozilla.org/mozilla-central/rev/8b48386fc226).
+---
+ browser/installer/package-manifest.in | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in
+index 521538f..c9117cf 100644
+--- a/browser/installer/package-manifest.in
++++ b/browser/installer/package-manifest.in
+@@ -597,8 +597,10 @@
+ #ifdef MOZ_ANGLE_RENDERER
+ @BINPATH@/libEGL.dll
+ @BINPATH@/libGLESv2.dll
++#ifdef MOZ_D3DCOMPILER_DLL_PATH
+ @BINPATH@/@MOZ_D3DCOMPILER_DLL@
+ #endif
++#endif
+
+ ; [Browser Chrome Files]
+ @BINPATH@/browser/chrome.manifest
diff --git a/data/patches/torbrowser-bug-12811.patch b/data/patches/torbrowser-bug-12811.patch
new file mode 100644
index 0000000..6b062f4
--- /dev/null
+++ b/data/patches/torbrowser-bug-12811.patch
@@ -0,0 +1,19 @@
+ Bug 12811: WTypes.h -> wtypes.h
+---
+ .../google-breakpad/src/google_breakpad/common/breakpad_types.h | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/toolkit/crashreporter/google-breakpad/src/google_breakpad/common/breakpad_types.h b/toolkit/crashreporter/google-breakpad/src/google_breakpad/common/breakpad_types.h
+index a60c5f3..e92436f 100644
+--- a/toolkit/crashreporter/google-breakpad/src/google_breakpad/common/breakpad_types.h
++++ b/toolkit/crashreporter/google-breakpad/src/google_breakpad/common/breakpad_types.h
+@@ -58,7 +58,7 @@
+ */
+ #include BREAKPAD_CUSTOM_STDINT_H
+ #else
+-#include <WTypes.h>
++#include <wtypes.h>
+
+ typedef unsigned __int8 uint8_t;
+ typedef unsigned __int16 uint16_t;
+
diff --git a/data/patches/torbrowser-bug-9837.patch b/data/patches/torbrowser-bug-9837.patch
new file mode 100644
index 0000000..f62f0cb
--- /dev/null
+++ b/data/patches/torbrowser-bug-9837.patch
@@ -0,0 +1,22 @@
+ Disable uninstall helper.exe on Win32.
+
+ Reworked for Icecat
+
+ Fixes 'make package' breakage on Windows builds. See
+ https://trac.torproject.org/projects/tor/ticket/9837
+---
+ browser/installer/package-manifest.in | 6 +++---
+ 1 file changed, 3 insertions(+), 3 deletions(-)
+
+--- a/browser/installer/package-manifest.in 2015-01-30 23:33:42.296298458 +0100
++++ b/browser/installer/package-manifest.in 2015-02-02 03:22:28.480752158 +0100
+@@ -43,7 +43,7 @@
+ @BINPATH@/browser/@PREF_DIR@/firefox-l10n.js
+ @BINPATH@/browser/searchplugins/*
+ #ifdef XP_WIN32
+-@BINPATH@/uninstall/helper.exe
++;@BINPATH@/uninstall/helper.exe
+ #endif
+ #ifdef MOZ_UPDATER
+ @BINPATH@/update.locale
+