Remove Firefox Accounts service and tie-ins.

See previous commit for removal of browser identity module.
This commit is contained in:
wolfbeast 2019-04-19 02:02:56 +02:00 committed by Roy Tam
commit c64dc935f9
420 changed files with 9 additions and 51958 deletions

View file

@ -1,23 +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.background;
import org.mozilla.gecko.AppConstants;
/**
* This is in 'background' not 'reading' so that it's still usable even when the
* Reading List feature is build-time disabled.
*/
public class ReadingListConstants {
public static final String GLOBAL_LOG_TAG = "FxReadingList";
public static final String USER_AGENT = "Firefox-Android-FxReader/" + AppConstants.MOZ_APP_VERSION + " (" + AppConstants.MOZ_APP_UA_NAME + ")";
public static final String DEFAULT_DEV_ENDPOINT = "https://readinglist.dev.mozaws.net/v1/";
public static final String DEFAULT_PROD_ENDPOINT = "https://readinglist.services.mozilla.com/v1/";
public static final String OAUTH_SCOPE_READINGLIST = "readinglist";
public static final String AUTH_TOKEN_TYPE = "oauth::" + OAUTH_SCOPE_READINGLIST;
public static boolean DEBUG = false;
}

View file

@ -1,82 +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.background.common;
import java.util.Set;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
public class EditorBranch implements Editor {
private final String prefix;
private Editor editor;
public EditorBranch(final SharedPreferences prefs, final String prefix) {
if (!prefix.endsWith(".")) {
throw new IllegalArgumentException("No trailing period in prefix.");
}
this.prefix = prefix;
this.editor = prefs.edit();
}
@Override
public void apply() {
this.editor.apply();
}
@Override
public Editor clear() {
this.editor = this.editor.clear();
return this;
}
@Override
public boolean commit() {
return this.editor.commit();
}
@Override
public Editor putBoolean(String key, boolean value) {
this.editor = this.editor.putBoolean(prefix + key, value);
return this;
}
@Override
public Editor putFloat(String key, float value) {
this.editor = this.editor.putFloat(prefix + key, value);
return this;
}
@Override
public Editor putInt(String key, int value) {
this.editor = this.editor.putInt(prefix + key, value);
return this;
}
@Override
public Editor putLong(String key, long value) {
this.editor = this.editor.putLong(prefix + key, value);
return this;
}
@Override
public Editor putString(String key, String value) {
this.editor = this.editor.putString(prefix + key, value);
return this;
}
// Not marking as Override, because Android <= 10 doesn't have
// putStringSet. Neither can we implement it.
public Editor putStringSet(String key, Set<String> value) {
throw new RuntimeException("putStringSet not available.");
}
@Override
public Editor remove(String key) {
this.editor = this.editor.remove(prefix + key);
return this;
}
}

View file

@ -1,90 +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.background.common;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.AppConstants.Versions;
/**
* Constant values common to all Android services.
*/
public class GlobalConstants {
public static final String BROWSER_INTENT_PACKAGE = AppConstants.ANDROID_PACKAGE_NAME;
public static final String BROWSER_INTENT_CLASS = AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS;
public static final int SHARED_PREFERENCES_MODE = 0;
// Common time values.
public static final long MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
public static final long MILLISECONDS_PER_SIX_MONTHS = 180 * MILLISECONDS_PER_DAY;
// Acceptable cipher suites.
/**
* We support only a very limited range of strong cipher suites and protocols:
* no SSLv3 or TLSv1.0 (if we can), no DHE ciphers that might be vulnerable to Logjam
* (https://weakdh.org/), no RC4.
*
* Backstory: Bug 717691 (we no longer support Android 2.2, so the name
* workaround is unnecessary), Bug 1081953, Bug 1061273, Bug 1166839.
*
* See <http://developer.android.com/reference/javax/net/ssl/SSLSocket.html> for
* supported Android versions for each set of protocols and cipher suites.
*
* Note that currently we need to support connections to Sync 1.1 on Mozilla-hosted infra,
* as well as connections to FxA and Sync 1.5 on AWS.
*
* ELB cipher suites:
* <http://docs.aws.amazon.com/ElasticLoadBalancing/latest/DeveloperGuide/elb-security-policy-table.html>
*/
public static final String[] DEFAULT_CIPHER_SUITES;
public static final String[] DEFAULT_PROTOCOLS;
static {
// Prioritize 128 over 256 as a tradeoff between device CPU/battery and the minor
// increase in strength.
if (Versions.feature20Plus) {
DEFAULT_CIPHER_SUITES = new String[]
{
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", // 20+
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", // 20+
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", // 20+
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", // 11+
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", // 20+
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", // 20+
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", // 11+
// For Sync 1.1.
"TLS_DHE_RSA_WITH_AES_128_CBC_SHA", // 9+
"TLS_RSA_WITH_AES_128_CBC_SHA", // 9+
};
} else {
DEFAULT_CIPHER_SUITES = new String[]
{
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", // 11+
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", // 11+
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", // 11+
// For Sync 1.1.
"TLS_DHE_RSA_WITH_AES_128_CBC_SHA", // 9+
"TLS_RSA_WITH_AES_128_CBC_SHA", // 9+
};
}
if (Versions.feature16Plus) {
DEFAULT_PROTOCOLS = new String[]
{
"TLSv1.2",
"TLSv1.1",
"TLSv1", // We would like to remove this, and will do so when we can.
};
} else {
// Fall back to TLSv1 if there's nothing better.
DEFAULT_PROTOCOLS = new String[]
{
"TLSv1",
};
}
}
}

View file

@ -1,83 +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.background.common;
import java.util.Map;
import java.util.Set;
import android.content.SharedPreferences;
/**
* A wrapper around a portion of the SharedPreferences space.
*/
public class PrefsBranch implements SharedPreferences {
private final SharedPreferences prefs;
private final String prefix; // Including trailing period.
public PrefsBranch(SharedPreferences prefs, String prefix) {
if (!prefix.endsWith(".")) {
throw new IllegalArgumentException("No trailing period in prefix.");
}
this.prefs = prefs;
this.prefix = prefix;
}
@Override
public boolean contains(String key) {
return prefs.contains(prefix + key);
}
@Override
public Editor edit() {
return new EditorBranch(prefs, prefix);
}
@Override
public Map<String, ?> getAll() {
// Not implemented. TODO
return null;
}
@Override
public boolean getBoolean(String key, boolean defValue) {
return prefs.getBoolean(prefix + key, defValue);
}
@Override
public float getFloat(String key, float defValue) {
return prefs.getFloat(prefix + key, defValue);
}
@Override
public int getInt(String key, int defValue) {
return prefs.getInt(prefix + key, defValue);
}
@Override
public long getLong(String key, long defValue) {
return prefs.getLong(prefix + key, defValue);
}
@Override
public String getString(String key, String defValue) {
return prefs.getString(prefix + key, defValue);
}
// Not marking as Override, because Android <= 10 doesn't have
// getStringSet. Neither can we implement it.
public Set<String> getStringSet(String key, Set<String> defValue) {
throw new RuntimeException("getStringSet not available.");
}
@Override
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
prefs.registerOnSharedPreferenceChangeListener(listener);
}
@Override
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
prefs.unregisterOnSharedPreferenceChangeListener(listener);
}
}

View file

@ -1,232 +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.background.common.log;
import java.io.PrintWriter;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;
import org.mozilla.gecko.background.common.GlobalConstants;
import org.mozilla.gecko.background.common.log.writers.AndroidLevelCachingLogWriter;
import org.mozilla.gecko.background.common.log.writers.AndroidLogWriter;
import org.mozilla.gecko.background.common.log.writers.LogWriter;
import org.mozilla.gecko.background.common.log.writers.PrintLogWriter;
import org.mozilla.gecko.background.common.log.writers.SimpleTagLogWriter;
import org.mozilla.gecko.background.common.log.writers.ThreadLocalTagLogWriter;
import android.util.Log;
/**
* Logging helper class. Serializes all log operations (by synchronizing).
*/
public class Logger {
public static final String LOGGER_TAG = "Logger";
public static final String DEFAULT_LOG_TAG = "GeckoLogger";
// For extra debugging.
public static boolean LOG_PERSONAL_INFORMATION = false;
/**
* Allow each thread to use its own global log tag. This allows
* independent services to log as different sources.
*
* When your thread sets up logging, it should do something like the following:
*
* Logger.setThreadLogTag("MyTag");
*
* The value is inheritable, so worker threads and such do not need to
* set the same log tag as their parent.
*/
private static final InheritableThreadLocal<String> logTag = new InheritableThreadLocal<String>() {
@Override
protected String initialValue() {
return DEFAULT_LOG_TAG;
}
};
public static void setThreadLogTag(final String logTag) {
Logger.logTag.set(logTag);
}
public static String getThreadLogTag() {
return Logger.logTag.get();
}
/**
* Current set of writers to which we will log.
* <p>
* We want logging to be available while running tests, so we initialize
* this set statically.
*/
protected final static Set<LogWriter> logWriters;
static {
final Set<LogWriter> defaultWriters = Logger.defaultLogWriters();
logWriters = new LinkedHashSet<LogWriter>(defaultWriters);
}
/**
* Default set of log writers to log to.
*/
public final static Set<LogWriter> defaultLogWriters() {
final String processedPackage = GlobalConstants.BROWSER_INTENT_PACKAGE.replace("org.mozilla.", "");
final Set<LogWriter> defaultLogWriters = new LinkedHashSet<LogWriter>();
final LogWriter log = new AndroidLogWriter();
final LogWriter cache = new AndroidLevelCachingLogWriter(log);
final LogWriter single = new SimpleTagLogWriter(processedPackage, new ThreadLocalTagLogWriter(Logger.logTag, cache));
defaultLogWriters.add(single);
return defaultLogWriters;
}
public static synchronized void startLoggingTo(LogWriter logWriter) {
logWriters.add(logWriter);
}
public static synchronized void startLoggingToWriters(Set<LogWriter> writers) {
logWriters.addAll(writers);
}
public static synchronized void stopLoggingTo(LogWriter logWriter) {
try {
logWriter.close();
} catch (Exception e) {
Log.e(LOGGER_TAG, "Got exception closing and removing LogWriter " + logWriter + ".", e);
}
logWriters.remove(logWriter);
}
public static synchronized void stopLoggingToAll() {
for (LogWriter logWriter : logWriters) {
try {
logWriter.close();
} catch (Exception e) {
Log.e(LOGGER_TAG, "Got exception closing and removing LogWriter " + logWriter + ".", e);
}
}
logWriters.clear();
}
/**
* Write to only the default log writers.
*/
public static synchronized void resetLogging() {
stopLoggingToAll();
logWriters.addAll(Logger.defaultLogWriters());
}
/**
* Start writing log output to stdout.
* <p>
* Use <code>resetLogging</code> to stop logging to stdout.
*/
public static synchronized void startLoggingToConsole() {
setThreadLogTag("Test");
startLoggingTo(new PrintLogWriter(new PrintWriter(System.out, true)));
}
// Synchronized version for other classes to use.
public static synchronized boolean shouldLogVerbose(String logTag) {
for (LogWriter logWriter : logWriters) {
if (logWriter.shouldLogVerbose(logTag)) {
return true;
}
}
return false;
}
public static void error(String tag, String message) {
Logger.error(tag, message, null);
}
public static void warn(String tag, String message) {
Logger.warn(tag, message, null);
}
public static void info(String tag, String message) {
Logger.info(tag, message, null);
}
public static void debug(String tag, String message) {
Logger.debug(tag, message, null);
}
public static void trace(String tag, String message) {
Logger.trace(tag, message, null);
}
public static void pii(String tag, String message) {
if (LOG_PERSONAL_INFORMATION) {
Logger.debug(tag, "$$PII$$: " + message);
}
}
public static synchronized void error(String tag, String message, Throwable error) {
Iterator<LogWriter> it = logWriters.iterator();
while (it.hasNext()) {
LogWriter writer = it.next();
try {
writer.error(tag, message, error);
} catch (Exception e) {
Log.e(LOGGER_TAG, "Got exception logging; removing LogWriter " + writer + ".", e);
it.remove();
}
}
}
public static synchronized void warn(String tag, String message, Throwable error) {
Iterator<LogWriter> it = logWriters.iterator();
while (it.hasNext()) {
LogWriter writer = it.next();
try {
writer.warn(tag, message, error);
} catch (Exception e) {
Log.e(LOGGER_TAG, "Got exception logging; removing LogWriter " + writer + ".", e);
it.remove();
}
}
}
public static synchronized void info(String tag, String message, Throwable error) {
Iterator<LogWriter> it = logWriters.iterator();
while (it.hasNext()) {
LogWriter writer = it.next();
try {
writer.info(tag, message, error);
} catch (Exception e) {
Log.e(LOGGER_TAG, "Got exception logging; removing LogWriter " + writer + ".", e);
it.remove();
}
}
}
public static synchronized void debug(String tag, String message, Throwable error) {
Iterator<LogWriter> it = logWriters.iterator();
while (it.hasNext()) {
LogWriter writer = it.next();
try {
writer.debug(tag, message, error);
} catch (Exception e) {
Log.e(LOGGER_TAG, "Got exception logging; removing LogWriter " + writer + ".", e);
it.remove();
}
}
}
public static synchronized void trace(String tag, String message, Throwable error) {
Iterator<LogWriter> it = logWriters.iterator();
while (it.hasNext()) {
LogWriter writer = it.next();
try {
writer.trace(tag, message, error);
} catch (Exception e) {
Log.e(LOGGER_TAG, "Got exception logging; removing LogWriter " + writer + ".", e);
it.remove();
}
}
}
}

View file

@ -1,132 +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.background.common.log.writers;
import java.util.IdentityHashMap;
import java.util.Map;
import android.util.Log;
/**
* Make a <code>LogWriter</code> only log when the Android log system says to.
*/
public class AndroidLevelCachingLogWriter extends LogWriter {
protected final LogWriter inner;
public AndroidLevelCachingLogWriter(LogWriter inner) {
this.inner = inner;
}
// I can't believe we have to implement this ourselves.
// These aren't synchronized (and neither are the setters) because
// the logging calls themselves are synchronized.
private Map<String, Boolean> isErrorLoggable = new IdentityHashMap<String, Boolean>();
private Map<String, Boolean> isWarnLoggable = new IdentityHashMap<String, Boolean>();
private Map<String, Boolean> isInfoLoggable = new IdentityHashMap<String, Boolean>();
private Map<String, Boolean> isDebugLoggable = new IdentityHashMap<String, Boolean>();
private Map<String, Boolean> isVerboseLoggable = new IdentityHashMap<String, Boolean>();
/**
* Empty the caches of log levels.
*/
public void refreshLogLevels() {
isErrorLoggable = new IdentityHashMap<String, Boolean>();
isWarnLoggable = new IdentityHashMap<String, Boolean>();
isInfoLoggable = new IdentityHashMap<String, Boolean>();
isDebugLoggable = new IdentityHashMap<String, Boolean>();
isVerboseLoggable = new IdentityHashMap<String, Boolean>();
}
private boolean shouldLogError(String logTag) {
Boolean out = isErrorLoggable.get(logTag);
if (out != null) {
return out;
}
out = Log.isLoggable(logTag, Log.ERROR);
isErrorLoggable.put(logTag, out);
return out;
}
private boolean shouldLogWarn(String logTag) {
Boolean out = isWarnLoggable.get(logTag);
if (out != null) {
return out;
}
out = Log.isLoggable(logTag, Log.WARN);
isWarnLoggable.put(logTag, out);
return out;
}
private boolean shouldLogInfo(String logTag) {
Boolean out = isInfoLoggable.get(logTag);
if (out != null) {
return out;
}
out = Log.isLoggable(logTag, Log.INFO);
isInfoLoggable.put(logTag, out);
return out;
}
private boolean shouldLogDebug(String logTag) {
Boolean out = isDebugLoggable.get(logTag);
if (out != null) {
return out;
}
out = Log.isLoggable(logTag, Log.DEBUG);
isDebugLoggable.put(logTag, out);
return out;
}
@Override
public boolean shouldLogVerbose(String logTag) {
Boolean out = isVerboseLoggable.get(logTag);
if (out != null) {
return out;
}
out = Log.isLoggable(logTag, Log.VERBOSE);
isVerboseLoggable.put(logTag, out);
return out;
}
@Override
public void error(String tag, String message, Throwable error) {
if (shouldLogError(tag)) {
inner.error(tag, message, error);
}
}
@Override
public void warn(String tag, String message, Throwable error) {
if (shouldLogWarn(tag)) {
inner.warn(tag, message, error);
}
}
@Override
public void info(String tag, String message, Throwable error) {
if (shouldLogInfo(tag)) {
inner.info(tag, message, error);
}
}
@Override
public void debug(String tag, String message, Throwable error) {
if (shouldLogDebug(tag)) {
inner.debug(tag, message, error);
}
}
@Override
public void trace(String tag, String message, Throwable error) {
if (shouldLogVerbose(tag)) {
inner.trace(tag, message, error);
}
}
@Override
public void close() {
inner.close();
}
}

View file

@ -1,46 +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.background.common.log.writers;
import android.util.Log;
/**
* Log to the Android log.
*/
public class AndroidLogWriter extends LogWriter {
@Override
public boolean shouldLogVerbose(String logTag) {
return true;
}
@Override
public void error(String tag, String message, Throwable error) {
Log.e(tag, message, error);
}
@Override
public void warn(String tag, String message, Throwable error) {
Log.w(tag, message, error);
}
@Override
public void info(String tag, String message, Throwable error) {
Log.i(tag, message, error);
}
@Override
public void debug(String tag, String message, Throwable error) {
Log.d(tag, message, error);
}
@Override
public void trace(String tag, String message, Throwable error) {
Log.v(tag, message, error);
}
@Override
public void close() {
}
}

View file

@ -1,67 +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.background.common.log.writers;
import android.util.Log;
/**
* A LogWriter that logs only if the message is as important as the specified
* level. For example, if the specified level is <code>Log.WARN</code>, only
* <code>warn</code> and <code>error</code> will log.
*/
public class LevelFilteringLogWriter extends LogWriter {
protected final LogWriter inner;
protected final int logLevel;
public LevelFilteringLogWriter(int logLevel, LogWriter inner) {
this.inner = inner;
this.logLevel = logLevel;
}
@Override
public void close() {
inner.close();
}
@Override
public void error(String tag, String message, Throwable error) {
if (logLevel <= Log.ERROR) {
inner.error(tag, message, error);
}
}
@Override
public void warn(String tag, String message, Throwable error) {
if (logLevel <= Log.WARN) {
inner.warn(tag, message, error);
}
}
@Override
public void info(String tag, String message, Throwable error) {
if (logLevel <= Log.INFO) {
inner.info(tag, message, error);
}
}
@Override
public void debug(String tag, String message, Throwable error) {
if (logLevel <= Log.DEBUG) {
inner.debug(tag, message, error);
}
}
@Override
public void trace(String tag, String message, Throwable error) {
if (logLevel <= Log.VERBOSE) {
inner.trace(tag, message, error);
}
}
@Override
public boolean shouldLogVerbose(String tag) {
return logLevel <= Log.VERBOSE;
}
}

View file

@ -1,29 +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.background.common.log.writers;
/**
* An abstract object that logs information in some way.
* <p>
* Intended to be composed with other log writers, for example a log
* writer could make all log entries have the same single log tag, or
* could ignore certain log levels, before delegating to an inner log
* writer.
*/
public abstract class LogWriter {
public abstract void error(String tag, String message, Throwable error);
public abstract void warn(String tag, String message, Throwable error);
public abstract void info(String tag, String message, Throwable error);
public abstract void debug(String tag, String message, Throwable error);
public abstract void trace(String tag, String message, Throwable error);
/**
* We expect <code>close</code> to be called only by static
* synchronized methods in class <code>Logger</code>.
*/
public abstract void close();
public abstract boolean shouldLogVerbose(String tag);
}

View file

@ -1,77 +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.background.common.log.writers;
import java.io.PrintWriter;
/**
* Log to a <code>PrintWriter</code>.
*/
public class PrintLogWriter extends LogWriter {
protected final PrintWriter pw;
protected boolean closed = false;
public static final String ERROR = " :: E :: ";
public static final String WARN = " :: W :: ";
public static final String INFO = " :: I :: ";
public static final String DEBUG = " :: D :: ";
public static final String VERBOSE = " :: V :: ";
public PrintLogWriter(PrintWriter pw) {
this.pw = pw;
}
protected void log(String tag, String message, Throwable error) {
if (closed) {
return;
}
pw.println(tag + message);
if (error != null) {
error.printStackTrace(pw);
}
}
@Override
public void error(String tag, String message, Throwable error) {
log(tag, ERROR + message, error);
}
@Override
public void warn(String tag, String message, Throwable error) {
log(tag, WARN + message, error);
}
@Override
public void info(String tag, String message, Throwable error) {
log(tag, INFO + message, error);
}
@Override
public void debug(String tag, String message, Throwable error) {
log(tag, DEBUG + message, error);
}
@Override
public void trace(String tag, String message, Throwable error) {
log(tag, VERBOSE + message, error);
}
@Override
public boolean shouldLogVerbose(String tag) {
return true;
}
@Override
public void close() {
if (closed) {
return;
}
if (pw != null) {
pw.close();
}
closed = true;
}
}

View file

@ -1,21 +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.background.common.log.writers;
/**
* Make a <code>LogWriter</code> only log with a single string tag.
*/
public class SimpleTagLogWriter extends TagLogWriter {
final String tag;
public SimpleTagLogWriter(String tag, LogWriter inner) {
super(inner);
this.tag = tag;
}
@Override
protected String getMainTag() {
return tag;
}
}

View file

@ -1,57 +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.background.common.log.writers;
import java.io.PrintWriter;
import java.io.StringWriter;
public class StringLogWriter extends LogWriter {
protected final StringWriter sw;
protected final PrintLogWriter inner;
public StringLogWriter() {
sw = new StringWriter();
inner = new PrintLogWriter(new PrintWriter(sw));
}
public String toString() {
return sw.toString();
}
@Override
public boolean shouldLogVerbose(String tag) {
return true;
}
@Override
public void error(String tag, String message, Throwable error) {
inner.error(tag, message, error);
}
@Override
public void warn(String tag, String message, Throwable error) {
inner.warn(tag, message, error);
}
@Override
public void info(String tag, String message, Throwable error) {
inner.info(tag, message, error);
}
@Override
public void debug(String tag, String message, Throwable error) {
inner.debug(tag, message, error);
}
@Override
public void trace(String tag, String message, Throwable error) {
inner.trace(tag, message, error);
}
@Override
public void close() {
inner.close();
}
}

View file

@ -1,55 +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.background.common.log.writers;
/**
* A @link{LogWriter} that logs each message under a parent tag.
*/
public abstract class TagLogWriter extends LogWriter {
protected final LogWriter inner;
public TagLogWriter(final LogWriter inner) {
super();
this.inner = inner;
}
protected abstract String getMainTag();
@Override
public void error(String tag, String message, Throwable error) {
inner.error(this.getMainTag(), tag + " :: " + message, error);
}
@Override
public void warn(String tag, String message, Throwable error) {
inner.warn(this.getMainTag(), tag + " :: " + message, error);
}
@Override
public void info(String tag, String message, Throwable error) {
inner.info(this.getMainTag(), tag + " :: " + message, error);
}
@Override
public void debug(String tag, String message, Throwable error) {
inner.debug(this.getMainTag(), tag + " :: " + message, error);
}
@Override
public void trace(String tag, String message, Throwable error) {
inner.trace(this.getMainTag(), tag + " :: " + message, error);
}
@Override
public boolean shouldLogVerbose(String tag) {
return inner.shouldLogVerbose(this.getMainTag());
}
@Override
public void close() {
inner.close();
}
}

View file

@ -1,25 +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.background.common.log.writers;
/**
* Log with a single global tag but that tag can be different for each thread.
*
* Takes a @link{ThreadLocal} as a constructor parameter.
*/
public class ThreadLocalTagLogWriter extends TagLogWriter {
private final ThreadLocal<String> tag;
public ThreadLocalTagLogWriter(ThreadLocal<String> tag, LogWriter inner) {
super(inner);
this.tag = tag;
}
@Override
protected String getMainTag() {
return this.tag.get();
}
}

View file

@ -1,56 +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.background.common.telemetry;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.mozilla.gecko.background.common.log.Logger;
/**
* Android Background Services are normally built into Fennec, but can also be
* built as a stand-alone APK for rapid local development. The current Telemetry
* implementation is coupled to Gecko, and Background Services should not
* interact with Gecko directly. To maintain this independence, Background
* Services lazily introspects the relevant Telemetry class from the enclosing
* package, warning but otherwise ignoring failures during introspection or
* invocation.
* <p>
* It is possible that Background Services will introspect and invoke the
* Telemetry implementation while Gecko is not running. In this case, the Fennec
* process itself buffers Telemetry events until such time as they can be
* flushed to disk and uploaded. <b>There is no guarantee that all Telemetry
* events will be uploaded!</b> Depending on the volume of data and the
* application lifecycle, Telemetry events may be dropped.
*/
public class TelemetryWrapper {
private static final String LOG_TAG = TelemetryWrapper.class.getSimpleName();
// Marking this volatile maintains thread safety cheaply.
private static volatile Method mAddToHistogram;
public static void addToHistogram(String key, int value) {
if (mAddToHistogram == null) {
try {
final Class<?> telemetry = Class.forName("org.mozilla.gecko.Telemetry");
mAddToHistogram = telemetry.getMethod("addToHistogram", String.class, int.class);
} catch (ClassNotFoundException e) {
Logger.warn(LOG_TAG, "org.mozilla.gecko.Telemetry class found!");
return;
} catch (NoSuchMethodException e) {
Logger.warn(LOG_TAG, "org.mozilla.gecko.Telemetry.addToHistogram(String, int) method not found!");
return;
}
}
if (mAddToHistogram != null) {
try {
mAddToHistogram.invoke(null, key, value);
} catch (IllegalArgumentException | InvocationTargetException | IllegalAccessException e) {
Logger.warn(LOG_TAG, "Got exception invoking telemetry!");
}
}
}
}

View file

@ -1,99 +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.background.db;
import android.database.Cursor;
/**
* A utility for dumping a cursor the debug log.
* <p>
* <b>For debugging only!</p>
*/
public class CursorDumper {
protected static String fixedWidth(int width, String s) {
if (s == null) {
return spaces(width);
}
int length = s.length();
if (width == length) {
return s;
}
if (width > length) {
return s + spaces(width - length);
}
return s.substring(0, width);
}
protected static String spaces(int i) {
return " ".substring(0, i);
}
protected static String dashes(int i) {
return "-------------------------------------".substring(0, i);
}
/**
* Dump a cursor to the debug log, ignoring any log level settings.
* <p>
* The position in the cursor is maintained. Caller is responsible for opening
* and closing cursor.
*
* @param cursor
* to dump.
*/
public static void dumpCursor(Cursor cursor) {
dumpCursor(cursor, 18, "records");
}
/**
* Dump a cursor to the debug log, ignoring any log level settings.
* <p>
* The position in the cursor is maintained. Caller is responsible for opening
* and closing cursor.
*
* @param cursor
* to dump.
* @param columnWidth
* how many characters per cursor column.
* @param tags
* a descriptor, printed like "(10 tags)", in the header row.
*/
protected static void dumpCursor(Cursor cursor, int columnWidth, String tags) {
int originalPosition = cursor.getPosition();
try {
String[] columnNames = cursor.getColumnNames();
int columnCount = cursor.getColumnCount();
for (int i = 0; i < columnCount; ++i) {
System.out.print(fixedWidth(columnWidth, columnNames[i]) + " | ");
}
System.out.println("(" + cursor.getCount() + " " + tags + ")");
for (int i = 0; i < columnCount; ++i) {
System.out.print(dashes(columnWidth) + " | ");
}
System.out.println("");
if (!cursor.moveToFirst()) {
System.out.println("EMPTY");
return;
}
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
for (int i = 0; i < columnCount; ++i) {
System.out.print(fixedWidth(columnWidth, cursor.getString(i)) + " | ");
}
System.out.println("");
cursor.moveToNext();
}
for (int i = 0; i < columnCount-1; ++i) {
System.out.print(dashes(columnWidth + 3));
}
System.out.print(dashes(columnWidth + 3 - 1));
System.out.println("");
} finally {
cursor.moveToPosition(originalPosition);
}
}
}

View file

@ -1,86 +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.background.db;
import org.json.simple.JSONArray;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.BrowserContract.Tabs;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.repositories.android.RepoUtils;
import android.content.ContentValues;
import android.database.Cursor;
// Immutable.
public class Tab {
public final String title;
public final String icon;
public final JSONArray history;
public final long lastUsed;
public Tab(String title, String icon, JSONArray history, long lastUsed) {
this.title = title;
this.icon = icon;
this.history = history;
this.lastUsed = lastUsed;
}
public ContentValues toContentValues(String clientGUID, int position) {
ContentValues out = new ContentValues();
out.put(BrowserContract.Tabs.POSITION, position);
out.put(BrowserContract.Tabs.CLIENT_GUID, clientGUID);
out.put(BrowserContract.Tabs.FAVICON, this.icon);
out.put(BrowserContract.Tabs.LAST_USED, this.lastUsed);
out.put(BrowserContract.Tabs.TITLE, this.title);
out.put(BrowserContract.Tabs.URL, (String) this.history.get(0));
out.put(BrowserContract.Tabs.HISTORY, this.history.toJSONString());
return out;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Tab)) {
return false;
}
final Tab other = (Tab) o;
if (!RepoUtils.stringsEqual(this.title, other.title)) {
return false;
}
if (!RepoUtils.stringsEqual(this.icon, other.icon)) {
return false;
}
if (!(this.lastUsed == other.lastUsed)) {
return false;
}
return Utils.sameArrays(this.history, other.history);
}
@Override
public int hashCode() {
return super.hashCode();
}
/**
* Extract a <code>Tab</code> from a cursor row.
* <p>
* Caller is responsible for creating, positioning, and closing the cursor.
*
* @param cursor
* to inspect.
* @return <code>Tab</code> instance.
*/
public static Tab fromCursor(final Cursor cursor) {
final String title = RepoUtils.getStringFromCursor(cursor, Tabs.TITLE);
final String icon = RepoUtils.getStringFromCursor(cursor, Tabs.FAVICON);
final JSONArray history = RepoUtils.getJSONArrayFromCursor(cursor, Tabs.HISTORY);
final long lastUsed = RepoUtils.getLongFromCursor(cursor, Tabs.LAST_USED);
return new Tab(title, icon, history, lastUsed);
}
}

View file

@ -1,52 +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.background.fxa;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.Utils;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
public class FxAccount20CreateDelegate {
protected final byte[] emailUTF8;
protected final byte[] authPW;
protected final boolean preVerified;
/**
* Make a new "create account" delegate.
*
* @param emailUTF8
* email as UTF-8 bytes.
* @param quickStretchedPW
* quick stretched password as bytes.
* @param preVerified
* true if account should be marked already verified; only effective
* for non-production auth servers.
* @throws UnsupportedEncodingException
* @throws GeneralSecurityException
*/
public FxAccount20CreateDelegate(byte[] emailUTF8, byte[] quickStretchedPW, boolean preVerified) throws UnsupportedEncodingException, GeneralSecurityException {
this.emailUTF8 = emailUTF8;
this.authPW = FxAccountUtils.generateAuthPW(quickStretchedPW);
this.preVerified = preVerified;
}
public ExtendedJSONObject getCreateBody() throws FxAccountClientException {
final ExtendedJSONObject body = new ExtendedJSONObject();
try {
body.put("email", new String(emailUTF8, "UTF-8"));
body.put("authPW", Utils.byte2Hex(authPW));
if (preVerified) {
// Production endpoints do not allow preVerified; this assumes we only
// set it when it's okay to send it.
body.put("preVerified", preVerified);
}
return body;
} catch (UnsupportedEncodingException e) {
throw new FxAccountClientException(e);
}
}
}

View file

@ -1,36 +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.background.fxa;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.Utils;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
/**
* An abstraction around providing an email and authorization token to the auth
* server.
*/
public class FxAccount20LoginDelegate {
protected final byte[] emailUTF8;
protected final byte[] authPW;
public FxAccount20LoginDelegate(byte[] emailUTF8, byte[] quickStretchedPW) throws UnsupportedEncodingException, GeneralSecurityException {
this.emailUTF8 = emailUTF8;
this.authPW = FxAccountUtils.generateAuthPW(quickStretchedPW);
}
public ExtendedJSONObject getCreateBody() throws FxAccountClientException {
final ExtendedJSONObject body = new ExtendedJSONObject();
try {
body.put("email", new String(emailUTF8, "UTF-8"));
body.put("authPW", Utils.byte2Hex(authPW));
return body;
} catch (UnsupportedEncodingException e) {
throw new FxAccountClientException(e);
}
}
}

View file

@ -1,24 +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.background.fxa;
import org.mozilla.gecko.background.fxa.FxAccountClient20.AccountStatusResponse;
import org.mozilla.gecko.background.fxa.FxAccountClient20.RecoveryEmailStatusResponse;
import org.mozilla.gecko.background.fxa.FxAccountClient20.RequestDelegate;
import org.mozilla.gecko.background.fxa.FxAccountClient20.TwoKeys;
import org.mozilla.gecko.fxa.FxAccountDevice;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import java.util.List;
public interface FxAccountClient {
public void accountStatus(String uid, RequestDelegate<AccountStatusResponse> requestDelegate);
public void recoveryEmailStatus(byte[] sessionToken, RequestDelegate<RecoveryEmailStatusResponse> requestDelegate);
public void keys(byte[] keyFetchToken, RequestDelegate<TwoKeys> requestDelegate);
public void sign(byte[] sessionToken, ExtendedJSONObject publicKey, long certificateDurationInMilliseconds, RequestDelegate<String> requestDelegate);
public void registerOrUpdateDevice(byte[] sessionToken, FxAccountDevice device, RequestDelegate<FxAccountDevice> requestDelegate);
public void deviceList(byte[] sessionToken, RequestDelegate<FxAccountDevice[]> requestDelegate);
public void notifyDevices(byte[] sessionToken, List<String> deviceIds, ExtendedJSONObject payload, Long TTL, RequestDelegate<ExtendedJSONObject> requestDelegate);
}

View file

@ -1,914 +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.background.fxa;
import android.support.annotation.NonNull;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientMalformedResponseException;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.Locales;
import org.mozilla.gecko.fxa.FxAccountDevice;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.crypto.HKDF;
import org.mozilla.gecko.sync.net.AuthHeaderProvider;
import org.mozilla.gecko.sync.net.BaseResource;
import org.mozilla.gecko.sync.net.BaseResourceDelegate;
import org.mozilla.gecko.sync.net.HawkAuthHeaderProvider;
import org.mozilla.gecko.sync.net.Resource;
import org.mozilla.gecko.sync.net.SyncResponse;
import org.mozilla.gecko.sync.net.SyncStorageResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Executor;
import javax.crypto.Mac;
import ch.boye.httpclientandroidlib.HttpEntity;
import ch.boye.httpclientandroidlib.HttpHeaders;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.client.ClientProtocolException;
import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
/**
* An HTTP client for talking to an FxAccount server.
* <p>
* <p>
* The delegate structure used is a little different from the rest of the code
* base. We add a <code>RequestDelegate</code> layer that processes a typed
* value extracted from the body of a successful response.
*/
public class FxAccountClient20 implements FxAccountClient {
protected static final String LOG_TAG = FxAccountClient20.class.getSimpleName();
protected static final String ACCEPT_HEADER = "application/json;charset=utf-8";
public static final String JSON_KEY_EMAIL = "email";
public static final String JSON_KEY_KEYFETCHTOKEN = "keyFetchToken";
public static final String JSON_KEY_SESSIONTOKEN = "sessionToken";
public static final String JSON_KEY_UID = "uid";
public static final String JSON_KEY_VERIFIED = "verified";
public static final String JSON_KEY_ERROR = "error";
public static final String JSON_KEY_MESSAGE = "message";
public static final String JSON_KEY_INFO = "info";
public static final String JSON_KEY_CODE = "code";
public static final String JSON_KEY_ERRNO = "errno";
public static final String JSON_KEY_EXISTS = "exists";
protected static final String[] requiredErrorStringFields = { JSON_KEY_ERROR, JSON_KEY_MESSAGE, JSON_KEY_INFO };
protected static final String[] requiredErrorLongFields = { JSON_KEY_CODE, JSON_KEY_ERRNO };
/**
* The server's URI.
* <p>
* We assume throughout that this ends with a trailing slash (and guarantee as
* much in the constructor).
*/
protected final String serverURI;
protected final Executor executor;
public FxAccountClient20(String serverURI, Executor executor) {
if (serverURI == null) {
throw new IllegalArgumentException("Must provide a server URI.");
}
if (executor == null) {
throw new IllegalArgumentException("Must provide a non-null executor.");
}
this.serverURI = serverURI.endsWith("/") ? serverURI : serverURI + "/";
if (!this.serverURI.endsWith("/")) {
throw new IllegalArgumentException("Constructed serverURI must end with a trailing slash: " + this.serverURI);
}
this.executor = executor;
}
protected BaseResource getBaseResource(String path, Map<String, String> queryParameters) throws UnsupportedEncodingException, URISyntaxException {
if (queryParameters == null || queryParameters.isEmpty()) {
return getBaseResource(path);
}
final String[] array = new String[2 * queryParameters.size()];
int i = 0;
for (Entry<String, String> entry : queryParameters.entrySet()) {
array[i++] = entry.getKey();
array[i++] = entry.getValue();
}
return getBaseResource(path, array);
}
/**
* Create <code>BaseResource</code>, encoding query parameters carefully.
* <p>
* This is equivalent to <code>android.net.Uri.Builder</code>, which is not
* present in our JUnit 4 tests.
*
* @param path fragment.
* @param queryParameters list of key/value query parameter pairs. Must be even length!
* @return <code>BaseResource<instance>
* @throws URISyntaxException
* @throws UnsupportedEncodingException
*/
protected BaseResource getBaseResource(String path, String... queryParameters) throws URISyntaxException, UnsupportedEncodingException {
final StringBuilder sb = new StringBuilder(serverURI);
sb.append(path);
if (queryParameters != null) {
int i = 0;
while (i < queryParameters.length) {
sb.append(i > 0 ? "&" : "?");
final String key = queryParameters[i++];
final String val = queryParameters[i++];
sb.append(URLEncoder.encode(key, "UTF-8"));
sb.append("=");
sb.append(URLEncoder.encode(val, "UTF-8"));
}
}
return new BaseResource(new URI(sb.toString()));
}
/**
* Process a typed value extracted from a successful response (in an
* endpoint-dependent way).
*/
public interface RequestDelegate<T> {
public void handleError(Exception e);
public void handleFailure(FxAccountClientRemoteException e);
public void handleSuccess(T result);
}
/**
* Thin container for two cryptographic keys.
*/
public static class TwoKeys {
public final byte[] kA;
public final byte[] wrapkB;
public TwoKeys(byte[] kA, byte[] wrapkB) {
this.kA = kA;
this.wrapkB = wrapkB;
}
}
protected <T> void invokeHandleError(final RequestDelegate<T> delegate, final Exception e) {
executor.execute(new Runnable() {
@Override
public void run() {
delegate.handleError(e);
}
});
}
enum ResponseType {
JSON_ARRAY,
JSON_OBJECT
}
/**
* Translate resource callbacks into request callbacks invoked on the provided
* executor.
* <p>
* Override <code>handleSuccess</code> to parse the body of the resource
* request and call the request callback. <code>handleSuccess</code> is
* invoked via the executor, so you don't need to delegate further.
*/
protected abstract class ResourceDelegate<T> extends BaseResourceDelegate {
protected void handleSuccess(final int status, HttpResponse response, final ExtendedJSONObject body) throws Exception {
throw new UnsupportedOperationException();
}
protected void handleSuccess(final int status, HttpResponse response, final JSONArray body) throws Exception {
throw new UnsupportedOperationException();
}
protected final RequestDelegate<T> delegate;
protected final byte[] tokenId;
protected final byte[] reqHMACKey;
protected final SkewHandler skewHandler;
protected final ResponseType responseType;
/**
* Create a delegate for an un-authenticated resource.
*/
public ResourceDelegate(final Resource resource, final RequestDelegate<T> delegate, ResponseType responseType) {
this(resource, delegate, responseType, null, null);
}
/**
* Create a delegate for a Hawk-authenticated resource.
* <p>
* Every Hawk request that encloses an entity (PATCH, POST, and PUT) will
* include the payload verification hash.
*/
public ResourceDelegate(final Resource resource, final RequestDelegate<T> delegate, ResponseType responseType, final byte[] tokenId, final byte[] reqHMACKey) {
super(resource);
this.delegate = delegate;
this.reqHMACKey = reqHMACKey;
this.tokenId = tokenId;
this.skewHandler = SkewHandler.getSkewHandlerForResource(resource);
this.responseType = responseType;
}
@Override
public AuthHeaderProvider getAuthHeaderProvider() {
if (tokenId != null && reqHMACKey != null) {
// We always include the payload verification hash for FxA Hawk-authenticated requests.
final boolean includePayloadVerificationHash = true;
return new HawkAuthHeaderProvider(Utils.byte2Hex(tokenId), reqHMACKey, includePayloadVerificationHash, skewHandler.getSkewInSeconds());
}
return super.getAuthHeaderProvider();
}
@Override
public String getUserAgent() {
return FxAccountConstants.USER_AGENT;
}
@Override
public void handleHttpResponse(HttpResponse response) {
try {
final int status = validateResponse(response);
skewHandler.updateSkew(response, now());
invokeHandleSuccess(status, response);
} catch (FxAccountClientRemoteException e) {
if (!skewHandler.updateSkew(response, now())) {
// If we couldn't update skew, but we got a failure, let's try clearing the skew.
skewHandler.resetSkew();
}
invokeHandleFailure(e);
}
}
protected void invokeHandleFailure(final FxAccountClientRemoteException e) {
executor.execute(new Runnable() {
@Override
public void run() {
delegate.handleFailure(e);
}
});
}
protected void invokeHandleSuccess(final int status, final HttpResponse response) {
executor.execute(new Runnable() {
@Override
public void run() {
try {
SyncResponse syncResponse = new SyncResponse(response);
if (responseType == ResponseType.JSON_ARRAY) {
JSONArray body = syncResponse.jsonArrayBody();
ResourceDelegate.this.handleSuccess(status, response, body);
} else {
ExtendedJSONObject body = syncResponse.jsonObjectBody();
ResourceDelegate.this.handleSuccess(status, response, body);
}
} catch (Exception e) {
delegate.handleError(e);
}
}
});
}
@Override
public void handleHttpProtocolException(final ClientProtocolException e) {
invokeHandleError(delegate, e);
}
@Override
public void handleHttpIOException(IOException e) {
invokeHandleError(delegate, e);
}
@Override
public void handleTransportException(GeneralSecurityException e) {
invokeHandleError(delegate, e);
}
@Override
public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
super.addHeaders(request, client);
// The basics.
final Locale locale = Locale.getDefault();
request.addHeader(HttpHeaders.ACCEPT_LANGUAGE, Locales.getLanguageTag(locale));
request.addHeader(HttpHeaders.ACCEPT, ACCEPT_HEADER);
}
}
protected <T> void post(BaseResource resource, final ExtendedJSONObject requestBody) {
if (requestBody == null) {
resource.post((HttpEntity) null);
} else {
resource.post(requestBody);
}
}
@SuppressWarnings("static-method")
public long now() {
return System.currentTimeMillis();
}
/**
* Intepret a response from the auth server.
* <p>
* Throw an appropriate exception on errors; otherwise, return the response's
* status code.
*
* @return response's HTTP status code.
* @throws FxAccountClientException
*/
public static int validateResponse(HttpResponse response) throws FxAccountClientRemoteException {
final int status = response.getStatusLine().getStatusCode();
if (status == 200) {
return status;
}
int code;
int errno;
String error;
String message;
String info;
ExtendedJSONObject body;
try {
body = new SyncStorageResponse(response).jsonObjectBody();
body.throwIfFieldsMissingOrMisTyped(requiredErrorStringFields, String.class);
body.throwIfFieldsMissingOrMisTyped(requiredErrorLongFields, Long.class);
code = body.getLong(JSON_KEY_CODE).intValue();
errno = body.getLong(JSON_KEY_ERRNO).intValue();
error = body.getString(JSON_KEY_ERROR);
message = body.getString(JSON_KEY_MESSAGE);
info = body.getString(JSON_KEY_INFO);
} catch (Exception e) {
throw new FxAccountClientMalformedResponseException(response);
}
throw new FxAccountClientRemoteException(response, code, errno, error, message, info, body);
}
/**
* Don't call this directly. Use <code>unbundleBody</code> instead.
*/
protected void unbundleBytes(byte[] bundleBytes, byte[] respHMACKey, byte[] respXORKey, byte[]... rest)
throws InvalidKeyException, NoSuchAlgorithmException, FxAccountClientException {
if (bundleBytes.length < 32) {
throw new IllegalArgumentException("input bundle must include HMAC");
}
int len = respXORKey.length;
if (bundleBytes.length != len + 32) {
throw new IllegalArgumentException("input bundle and XOR key with HMAC have different lengths");
}
int left = len;
for (byte[] array : rest) {
left -= array.length;
}
if (left != 0) {
throw new IllegalArgumentException("XOR key and total output arrays have different lengths");
}
byte[] ciphertext = new byte[len];
byte[] HMAC = new byte[32];
System.arraycopy(bundleBytes, 0, ciphertext, 0, len);
System.arraycopy(bundleBytes, len, HMAC, 0, 32);
Mac hmacHasher = HKDF.makeHMACHasher(respHMACKey);
byte[] computedHMAC = hmacHasher.doFinal(ciphertext);
if (!Arrays.equals(computedHMAC, HMAC)) {
throw new FxAccountClientException("Bad message HMAC");
}
int offset = 0;
for (byte[] array : rest) {
for (int i = 0; i < array.length; i++) {
array[i] = (byte) (respXORKey[offset + i] ^ ciphertext[offset + i]);
}
offset += array.length;
}
}
protected void unbundleBody(ExtendedJSONObject body, byte[] requestKey, byte[] ctxInfo, byte[]... rest) throws Exception {
int length = 0;
for (byte[] array : rest) {
length += array.length;
}
if (body == null) {
throw new FxAccountClientException("body must be non-null");
}
String bundle = body.getString("bundle");
if (bundle == null) {
throw new FxAccountClientException("bundle must be a non-null string");
}
byte[] bundleBytes = Utils.hex2Byte(bundle);
final byte[] respHMACKey = new byte[32];
final byte[] respXORKey = new byte[length];
HKDF.deriveMany(requestKey, new byte[0], ctxInfo, respHMACKey, respXORKey);
unbundleBytes(bundleBytes, respHMACKey, respXORKey, rest);
}
public void keys(byte[] keyFetchToken, final RequestDelegate<TwoKeys> delegate) {
final byte[] tokenId = new byte[32];
final byte[] reqHMACKey = new byte[32];
final byte[] requestKey = new byte[32];
try {
HKDF.deriveMany(keyFetchToken, new byte[0], FxAccountUtils.KW("keyFetchToken"), tokenId, reqHMACKey, requestKey);
} catch (Exception e) {
invokeHandleError(delegate, e);
return;
}
BaseResource resource;
try {
resource = getBaseResource("account/keys");
} catch (URISyntaxException | UnsupportedEncodingException e) {
invokeHandleError(delegate, e);
return;
}
resource.delegate = new ResourceDelegate<TwoKeys>(resource, delegate, ResponseType.JSON_OBJECT, tokenId, reqHMACKey) {
@Override
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) throws Exception {
byte[] kA = new byte[FxAccountUtils.CRYPTO_KEY_LENGTH_BYTES];
byte[] wrapkB = new byte[FxAccountUtils.CRYPTO_KEY_LENGTH_BYTES];
unbundleBody(body, requestKey, FxAccountUtils.KW("account/keys"), kA, wrapkB);
delegate.handleSuccess(new TwoKeys(kA, wrapkB));
}
};
resource.get();
}
/**
* Thin container for account status response.
*/
public static class AccountStatusResponse {
public final boolean exists;
public AccountStatusResponse(boolean exists) {
this.exists = exists;
}
}
/**
* Query the account status of an account given a uid.
*
* @param uid to query.
* @param delegate to invoke callbacks.
*/
public void accountStatus(String uid, final RequestDelegate<AccountStatusResponse> delegate) {
final BaseResource resource;
try {
final Map<String, String> params = new HashMap<>(1);
params.put("uid", uid);
resource = getBaseResource("account/status", params);
} catch (URISyntaxException | UnsupportedEncodingException e) {
invokeHandleError(delegate, e);
return;
}
resource.delegate = new ResourceDelegate<AccountStatusResponse>(resource, delegate, ResponseType.JSON_OBJECT) {
@Override
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) throws Exception {
boolean exists = body.getBoolean(JSON_KEY_EXISTS);
delegate.handleSuccess(new AccountStatusResponse(exists));
}
};
resource.get();
}
/**
* Thin container for recovery email status response.
*/
public static class RecoveryEmailStatusResponse {
public final String email;
public final boolean verified;
public RecoveryEmailStatusResponse(String email, boolean verified) {
this.email = email;
this.verified = verified;
}
}
/**
* Query the recovery email status of an account given a valid session token.
* <p>
* This API is a little odd: the auth server returns the email and
* verification state of the account that corresponds to the (opaque) session
* token. It might fail if the session token is unknown (or invalid, or
* revoked).
*
* @param sessionToken
* to query.
* @param delegate
* to invoke callbacks.
*/
public void recoveryEmailStatus(byte[] sessionToken, final RequestDelegate<RecoveryEmailStatusResponse> delegate) {
final byte[] tokenId = new byte[32];
final byte[] reqHMACKey = new byte[32];
final byte[] requestKey = new byte[32];
try {
HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey, requestKey);
} catch (Exception e) {
invokeHandleError(delegate, e);
return;
}
BaseResource resource;
try {
resource = getBaseResource("recovery_email/status");
} catch (URISyntaxException | UnsupportedEncodingException e) {
invokeHandleError(delegate, e);
return;
}
resource.delegate = new ResourceDelegate<RecoveryEmailStatusResponse>(resource, delegate, ResponseType.JSON_OBJECT, tokenId, reqHMACKey) {
@Override
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) throws Exception {
String[] requiredStringFields = new String[] { JSON_KEY_EMAIL };
body.throwIfFieldsMissingOrMisTyped(requiredStringFields, String.class);
String email = body.getString(JSON_KEY_EMAIL);
Boolean verified = body.getBoolean(JSON_KEY_VERIFIED);
delegate.handleSuccess(new RecoveryEmailStatusResponse(email, verified));
}
};
resource.get();
}
@SuppressWarnings("unchecked")
public void sign(final byte[] sessionToken, final ExtendedJSONObject publicKey, long durationInMilliseconds, final RequestDelegate<String> delegate) {
final ExtendedJSONObject body = new ExtendedJSONObject();
body.put("publicKey", publicKey);
body.put("duration", durationInMilliseconds);
final byte[] tokenId = new byte[32];
final byte[] reqHMACKey = new byte[32];
try {
HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey);
} catch (Exception e) {
invokeHandleError(delegate, e);
return;
}
BaseResource resource;
try {
resource = getBaseResource("certificate/sign");
} catch (URISyntaxException | UnsupportedEncodingException e) {
invokeHandleError(delegate, e);
return;
}
resource.delegate = new ResourceDelegate<String>(resource, delegate, ResponseType.JSON_OBJECT, tokenId, reqHMACKey) {
@Override
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) throws Exception {
String cert = body.getString("cert");
if (cert == null) {
delegate.handleError(new FxAccountClientException("cert must be a non-null string"));
return;
}
delegate.handleSuccess(cert);
}
};
post(resource, body);
}
protected static final String[] LOGIN_RESPONSE_REQUIRED_STRING_FIELDS = new String[] { JSON_KEY_UID, JSON_KEY_SESSIONTOKEN };
protected static final String[] LOGIN_RESPONSE_REQUIRED_STRING_FIELDS_KEYS = new String[] { JSON_KEY_UID, JSON_KEY_SESSIONTOKEN, JSON_KEY_KEYFETCHTOKEN, };
protected static final String[] LOGIN_RESPONSE_REQUIRED_BOOLEAN_FIELDS = new String[] { JSON_KEY_VERIFIED };
/**
* Thin container for login response.
* <p>
* The <code>remoteEmail</code> field is the email address as normalized by the
* server, and is <b>not necessarily</b> the email address delivered to the
* <code>login</code> or <code>create</code> call.
*/
public static class LoginResponse {
public final String remoteEmail;
public final String uid;
public final byte[] sessionToken;
public final boolean verified;
public final byte[] keyFetchToken;
public LoginResponse(String remoteEmail, String uid, boolean verified, byte[] sessionToken, byte[] keyFetchToken) {
this.remoteEmail = remoteEmail;
this.uid = uid;
this.verified = verified;
this.sessionToken = sessionToken;
this.keyFetchToken = keyFetchToken;
}
}
// Public for testing only; prefer login and loginAndGetKeys (without boolean parameter).
public void login(final byte[] emailUTF8, final byte[] quickStretchedPW, final boolean getKeys,
final Map<String, String> queryParameters,
final RequestDelegate<LoginResponse> delegate) {
final BaseResource resource;
final ExtendedJSONObject body;
try {
final String path = "account/login";
final Map<String, String> modifiedParameters = new HashMap<>();
if (queryParameters != null) {
modifiedParameters.putAll(queryParameters);
}
if (getKeys) {
modifiedParameters.put("keys", "true");
}
resource = getBaseResource(path, modifiedParameters);
body = new FxAccount20LoginDelegate(emailUTF8, quickStretchedPW).getCreateBody();
} catch (Exception e) {
invokeHandleError(delegate, e);
return;
}
resource.delegate = new ResourceDelegate<LoginResponse>(resource, delegate, ResponseType.JSON_OBJECT) {
@Override
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) throws Exception {
final String[] requiredStringFields = getKeys ? LOGIN_RESPONSE_REQUIRED_STRING_FIELDS_KEYS : LOGIN_RESPONSE_REQUIRED_STRING_FIELDS;
body.throwIfFieldsMissingOrMisTyped(requiredStringFields, String.class);
final String[] requiredBooleanFields = LOGIN_RESPONSE_REQUIRED_BOOLEAN_FIELDS;
body.throwIfFieldsMissingOrMisTyped(requiredBooleanFields, Boolean.class);
String uid = body.getString(JSON_KEY_UID);
boolean verified = body.getBoolean(JSON_KEY_VERIFIED);
byte[] sessionToken = Utils.hex2Byte(body.getString(JSON_KEY_SESSIONTOKEN));
byte[] keyFetchToken = null;
if (getKeys) {
keyFetchToken = Utils.hex2Byte(body.getString(JSON_KEY_KEYFETCHTOKEN));
}
LoginResponse loginResponse = new LoginResponse(new String(emailUTF8, "UTF-8"), uid, verified, sessionToken, keyFetchToken);
delegate.handleSuccess(loginResponse);
}
};
post(resource, body);
}
public void createAccount(final byte[] emailUTF8, final byte[] quickStretchedPW,
final boolean getKeys,
final boolean preVerified,
final Map<String, String> queryParameters,
final RequestDelegate<LoginResponse> delegate) {
final BaseResource resource;
final ExtendedJSONObject body;
try {
final String path = "account/create";
final Map<String, String> modifiedParameters = new HashMap<>();
if (queryParameters != null) {
modifiedParameters.putAll(queryParameters);
}
if (getKeys) {
modifiedParameters.put("keys", "true");
}
resource = getBaseResource(path, modifiedParameters);
body = new FxAccount20CreateDelegate(emailUTF8, quickStretchedPW, preVerified).getCreateBody();
} catch (Exception e) {
invokeHandleError(delegate, e);
return;
}
// This is very similar to login, except verified is not required.
resource.delegate = new ResourceDelegate<LoginResponse>(resource, delegate, ResponseType.JSON_OBJECT) {
@Override
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) throws Exception {
final String[] requiredStringFields = getKeys ? LOGIN_RESPONSE_REQUIRED_STRING_FIELDS_KEYS : LOGIN_RESPONSE_REQUIRED_STRING_FIELDS;
body.throwIfFieldsMissingOrMisTyped(requiredStringFields, String.class);
String uid = body.getString(JSON_KEY_UID);
boolean verified = false; // In production, we're definitely not verified immediately upon creation.
Boolean tempVerified = body.getBoolean(JSON_KEY_VERIFIED);
if (tempVerified != null) {
verified = tempVerified;
}
byte[] sessionToken = Utils.hex2Byte(body.getString(JSON_KEY_SESSIONTOKEN));
byte[] keyFetchToken = null;
if (getKeys) {
keyFetchToken = Utils.hex2Byte(body.getString(JSON_KEY_KEYFETCHTOKEN));
}
LoginResponse loginResponse = new LoginResponse(new String(emailUTF8, "UTF-8"), uid, verified, sessionToken, keyFetchToken);
delegate.handleSuccess(loginResponse);
}
};
post(resource, body);
}
/**
* We want users to be able to enter their email address case-insensitively.
* We stretch the password locally using the email address as a salt, to make
* dictionary attacks more expensive. This means that a client with a
* case-differing email address is unable to produce the correct
* authorization, even though it knows the password. In this case, the server
* returns the email that the account was created with, so that the client can
* re-stretch the password locally with the correct email salt. This version
* of <code>login</code> retries at most one time with a server provided email
* address.
* <p>
* Be aware that consumers will not see the initial error response from the
* server providing an alternate email (if there is one).
*
* @param emailUTF8
* user entered email address.
* @param stretcher
* delegate to stretch and re-stretch password.
* @param getKeys
* true if a <code>keyFetchToken</code> should be returned (in
* addition to the standard <code>sessionToken</code>).
* @param queryParameters
* @param delegate
* to invoke callbacks.
*/
public void login(final byte[] emailUTF8, final PasswordStretcher stretcher, final boolean getKeys,
final Map<String, String> queryParameters,
final RequestDelegate<LoginResponse> delegate) {
byte[] quickStretchedPW;
try {
FxAccountUtils.pii(LOG_TAG, "Trying user provided email: '" + new String(emailUTF8, "UTF-8") + "'" );
quickStretchedPW = stretcher.getQuickStretchedPW(emailUTF8);
} catch (Exception e) {
delegate.handleError(e);
return;
}
this.login(emailUTF8, quickStretchedPW, getKeys, queryParameters, new RequestDelegate<LoginResponse>() {
@Override
public void handleSuccess(LoginResponse result) {
delegate.handleSuccess(result);
}
@Override
public void handleError(Exception e) {
delegate.handleError(e);
}
@Override
public void handleFailure(FxAccountClientRemoteException e) {
String alternateEmail = e.body.getString(JSON_KEY_EMAIL);
if (!e.isBadEmailCase() || alternateEmail == null) {
delegate.handleFailure(e);
return;
};
Logger.info(LOG_TAG, "Server returned alternate email; retrying login with provided email.");
FxAccountUtils.pii(LOG_TAG, "Trying server provided email: '" + alternateEmail + "'" );
try {
// Nota bene: this is not recursive, since we call the fixed password
// signature here, which invokes a non-retrying version.
byte[] alternateEmailUTF8 = alternateEmail.getBytes("UTF-8");
byte[] alternateQuickStretchedPW = stretcher.getQuickStretchedPW(alternateEmailUTF8);
login(alternateEmailUTF8, alternateQuickStretchedPW, getKeys, queryParameters, delegate);
} catch (Exception innerException) {
delegate.handleError(innerException);
return;
}
}
});
}
/**
* Registers a device given a valid session token.
*
* @param sessionToken to query.
* @param delegate to invoke callbacks.
*/
@Override
public void registerOrUpdateDevice(byte[] sessionToken, FxAccountDevice device, RequestDelegate<FxAccountDevice> delegate) {
final byte[] tokenId = new byte[32];
final byte[] reqHMACKey = new byte[32];
final byte[] requestKey = new byte[32];
try {
HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey, requestKey);
} catch (Exception e) {
invokeHandleError(delegate, e);
return;
}
final BaseResource resource;
final ExtendedJSONObject body;
try {
resource = getBaseResource("account/device");
body = device.toJson();
} catch (URISyntaxException | UnsupportedEncodingException e) {
invokeHandleError(delegate, e);
return;
}
resource.delegate = new ResourceDelegate<FxAccountDevice>(resource, delegate, ResponseType.JSON_OBJECT, tokenId, reqHMACKey) {
@Override
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
try {
delegate.handleSuccess(FxAccountDevice.fromJson(body));
} catch (Exception e) {
delegate.handleError(e);
}
}
};
post(resource, body);
}
@Override
public void deviceList(byte[] sessionToken, RequestDelegate<FxAccountDevice[]> delegate) {
final byte[] tokenId = new byte[32];
final byte[] reqHMACKey = new byte[32];
final byte[] requestKey = new byte[32];
try {
HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey, requestKey);
} catch (Exception e) {
invokeHandleError(delegate, e);
return;
}
final BaseResource resource;
try {
resource = getBaseResource("account/devices");
} catch (URISyntaxException | UnsupportedEncodingException e) {
invokeHandleError(delegate, e);
return;
}
resource.delegate = new ResourceDelegate<FxAccountDevice[]>(resource, delegate, ResponseType.JSON_ARRAY, tokenId, reqHMACKey) {
@Override
public void handleSuccess(int status, HttpResponse response, JSONArray devicesJson) {
try {
FxAccountDevice[] devices = new FxAccountDevice[devicesJson.size()];
for (int i = 0; i < devices.length; i++) {
ExtendedJSONObject deviceJson = new ExtendedJSONObject((JSONObject) devicesJson.get(i));
devices[i] = FxAccountDevice.fromJson(deviceJson);
}
delegate.handleSuccess(devices);
} catch (Exception e) {
delegate.handleError(e);
}
}
};
resource.get();
}
@Override
public void notifyDevices(@NonNull byte[] sessionToken, @NonNull List<String> deviceIds, ExtendedJSONObject payload, Long TTL, RequestDelegate<ExtendedJSONObject> delegate) {
final byte[] tokenId = new byte[32];
final byte[] reqHMACKey = new byte[32];
final byte[] requestKey = new byte[32];
try {
HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey, requestKey);
} catch (Exception e) {
invokeHandleError(delegate, e);
return;
}
final BaseResource resource;
final ExtendedJSONObject body = createNotifyDevicesBody(deviceIds, payload, TTL);
try {
resource = getBaseResource("account/devices/notify");
} catch (URISyntaxException | UnsupportedEncodingException e) {
invokeHandleError(delegate, e);
return;
}
resource.delegate = new ResourceDelegate<ExtendedJSONObject>(resource, delegate, ResponseType.JSON_OBJECT, tokenId, reqHMACKey) {
@Override
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
try {
delegate.handleSuccess(body);
} catch (Exception e) {
delegate.handleError(e);
}
}
};
post(resource, body);
}
@NonNull
@SuppressWarnings("unchecked")
private ExtendedJSONObject createNotifyDevicesBody(@NonNull List<String> deviceIds, ExtendedJSONObject payload, Long TTL) {
final ExtendedJSONObject body = new ExtendedJSONObject();
final JSONArray to = new JSONArray();
to.addAll(deviceIds);
body.put("to", to);
if (payload != null) {
body.put("payload", payload);
}
if (TTL != null) {
body.put("TTL", TTL);
}
return body;
}
}

View file

@ -1,133 +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.background.fxa;
import org.mozilla.gecko.R;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.HTTPFailureException;
import org.mozilla.gecko.sync.net.SyncStorageResponse;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.HttpStatus;
/**
* From <a href="https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md">https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md</a>.
*/
public class FxAccountClientException extends Exception {
private static final long serialVersionUID = 7953459541558266597L;
public FxAccountClientException(String detailMessage) {
super(detailMessage);
}
public FxAccountClientException(Exception e) {
super(e);
}
public static class FxAccountClientRemoteException extends FxAccountClientException {
private static final long serialVersionUID = 2209313149952001097L;
public final HttpResponse response;
public final long httpStatusCode;
public final long apiErrorNumber;
public final String error;
public final String message;
public final String info;
public final ExtendedJSONObject body;
public FxAccountClientRemoteException(HttpResponse response, long httpStatusCode, long apiErrorNumber, String error, String message, String info, ExtendedJSONObject body) {
super(new HTTPFailureException(new SyncStorageResponse(response)));
if (body == null) {
throw new IllegalArgumentException("body must not be null");
}
this.response = response;
this.httpStatusCode = httpStatusCode;
this.apiErrorNumber = apiErrorNumber;
this.error = error;
this.message = message;
this.info = info;
this.body = body;
}
@Override
public String toString() {
return "<FxAccountClientRemoteException " + this.httpStatusCode + " [" + this.apiErrorNumber + "]: " + this.message + ">";
}
public boolean isInvalidAuthentication() {
return httpStatusCode == HttpStatus.SC_UNAUTHORIZED;
}
public boolean isAccountAlreadyExists() {
return apiErrorNumber == FxAccountRemoteError.ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS;
}
public boolean isAccountDoesNotExist() {
return apiErrorNumber == FxAccountRemoteError.ATTEMPT_TO_ACCESS_AN_ACCOUNT_THAT_DOES_NOT_EXIST;
}
public boolean isBadPassword() {
return apiErrorNumber == FxAccountRemoteError.INCORRECT_PASSWORD;
}
public boolean isUnverified() {
return apiErrorNumber == FxAccountRemoteError.ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT;
}
public boolean isUpgradeRequired() {
return
apiErrorNumber == FxAccountRemoteError.ENDPOINT_IS_NO_LONGER_SUPPORTED ||
apiErrorNumber == FxAccountRemoteError.INCORRECT_LOGIN_METHOD_FOR_THIS_ACCOUNT ||
apiErrorNumber == FxAccountRemoteError.INCORRECT_KEY_RETRIEVAL_METHOD_FOR_THIS_ACCOUNT ||
apiErrorNumber == FxAccountRemoteError.INCORRECT_API_VERSION_FOR_THIS_ACCOUNT;
}
public boolean isTooManyRequests() {
return apiErrorNumber == FxAccountRemoteError.CLIENT_HAS_SENT_TOO_MANY_REQUESTS;
}
public boolean isServerUnavailable() {
return apiErrorNumber == FxAccountRemoteError.SERVICE_TEMPORARILY_UNAVAILABLE_DUE_TO_HIGH_LOAD;
}
public boolean isBadEmailCase() {
return apiErrorNumber == FxAccountRemoteError.INCORRECT_EMAIL_CASE;
}
public boolean isAccountLocked() {
return apiErrorNumber == FxAccountRemoteError.ACCOUNT_LOCKED;
}
public int getErrorMessageStringResource() {
if (isUpgradeRequired()) {
return R.string.fxaccount_remote_error_UPGRADE_REQUIRED;
} else if (isAccountAlreadyExists()) {
return R.string.fxaccount_remote_error_ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS;
} else if (isAccountDoesNotExist()) {
return R.string.fxaccount_remote_error_ATTEMPT_TO_ACCESS_AN_ACCOUNT_THAT_DOES_NOT_EXIST;
} else if (isBadPassword()) {
return R.string.fxaccount_remote_error_INCORRECT_PASSWORD;
} else if (isUnverified()) {
return R.string.fxaccount_remote_error_ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT;
} else if (isTooManyRequests()) {
return R.string.fxaccount_remote_error_CLIENT_HAS_SENT_TOO_MANY_REQUESTS;
} else if (isServerUnavailable()) {
return R.string.fxaccount_remote_error_SERVICE_TEMPORARILY_UNAVAILABLE_TO_DUE_HIGH_LOAD;
} else if (isAccountLocked()) {
return R.string.fxaccount_remote_error_ACCOUNT_LOCKED;
} else {
return R.string.fxaccount_remote_error_UNKNOWN_ERROR;
}
}
}
public static class FxAccountClientMalformedResponseException extends FxAccountClientRemoteException {
private static final long serialVersionUID = 2209313149952001098L;
public FxAccountClientMalformedResponseException(HttpResponse response) {
super(response, 0, FxAccountRemoteError.UNKNOWN_ERROR, "Response malformed", "Response malformed", "Response malformed", new ExtendedJSONObject());
}
}
}

View file

@ -1,33 +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.background.fxa;
public interface FxAccountRemoteError {
public static final int ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS = 101;
public static final int ATTEMPT_TO_ACCESS_AN_ACCOUNT_THAT_DOES_NOT_EXIST = 102;
public static final int INCORRECT_PASSWORD = 103;
public static final int ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT = 104;
public static final int INVALID_VERIFICATION_CODE = 105;
public static final int REQUEST_BODY_WAS_NOT_VALID_JSON = 106;
public static final int REQUEST_BODY_CONTAINS_INVALID_PARAMETERS = 107;
public static final int REQUEST_BODY_MISSING_REQUIRED_PARAMETERS = 108;
public static final int INVALID_REQUEST_SIGNATURE = 109;
public static final int INVALID_AUTHENTICATION_TOKEN = 110;
public static final int INVALID_AUTHENTICATION_TIMESTAMP = 111;
public static final int CONTENT_LENGTH_HEADER_WAS_NOT_PROVIDED = 112;
public static final int REQUEST_BODY_TOO_LARGE = 113;
public static final int CLIENT_HAS_SENT_TOO_MANY_REQUESTS = 114;
public static final int INVALID_NONCE_IN_REQUEST_SIGNATURE = 115;
public static final int ENDPOINT_IS_NO_LONGER_SUPPORTED = 116;
public static final int INCORRECT_LOGIN_METHOD_FOR_THIS_ACCOUNT = 117;
public static final int INCORRECT_KEY_RETRIEVAL_METHOD_FOR_THIS_ACCOUNT = 118;
public static final int INCORRECT_API_VERSION_FOR_THIS_ACCOUNT = 119;
public static final int INCORRECT_EMAIL_CASE = 120;
public static final int ACCOUNT_LOCKED = 121;
public static final int UNKNOWN_DEVICE = 123;
public static final int DEVICE_SESSION_CONFLICT = 124;
public static final int SERVICE_TEMPORARILY_UNAVAILABLE_DUE_TO_HIGH_LOAD = 201;
public static final int UNKNOWN_ERROR = 999;
}

View file

@ -1,217 +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.background.fxa;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.R;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.nativecode.NativeCrypto;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.crypto.HKDF;
import org.mozilla.gecko.sync.crypto.KeyBundle;
import org.mozilla.gecko.sync.crypto.PBKDF2;
import android.content.Context;
public class FxAccountUtils {
private static final String LOG_TAG = FxAccountUtils.class.getSimpleName();
public static final int SALT_LENGTH_BYTES = 32;
public static final int SALT_LENGTH_HEX = 2 * SALT_LENGTH_BYTES;
public static final int HASH_LENGTH_BYTES = 16;
public static final int HASH_LENGTH_HEX = 2 * HASH_LENGTH_BYTES;
public static final int CRYPTO_KEY_LENGTH_BYTES = 32;
public static final int CRYPTO_KEY_LENGTH_HEX = 2 * CRYPTO_KEY_LENGTH_BYTES;
public static final String KW_VERSION_STRING = "identity.mozilla.com/picl/v1/";
public static final int NUMBER_OF_QUICK_STRETCH_ROUNDS = 1000;
// For extra debugging. Not final so it can be changed from Fennec, or from
// an add-on.
public static boolean LOG_PERSONAL_INFORMATION = false;
public static void pii(String tag, String message) {
if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
Logger.info(tag, "$$FxA PII$$: " + message);
}
}
public static String bytes(String string) throws UnsupportedEncodingException {
return Utils.byte2Hex(string.getBytes("UTF-8"));
}
public static byte[] KW(String name) throws UnsupportedEncodingException {
return Utils.concatAll(
KW_VERSION_STRING.getBytes("UTF-8"),
name.getBytes("UTF-8"));
}
public static byte[] KWE(String name, byte[] emailUTF8) throws UnsupportedEncodingException {
return Utils.concatAll(
KW_VERSION_STRING.getBytes("UTF-8"),
name.getBytes("UTF-8"),
":".getBytes("UTF-8"),
emailUTF8);
}
/**
* Calculate the SRP verifier <tt>x</tt> value.
*/
public static BigInteger srpVerifierLowercaseX(byte[] emailUTF8, byte[] srpPWBytes, byte[] srpSaltBytes)
throws NoSuchAlgorithmException, UnsupportedEncodingException {
byte[] inner = Utils.sha256(Utils.concatAll(emailUTF8, ":".getBytes("UTF-8"), srpPWBytes));
byte[] outer = Utils.sha256(Utils.concatAll(srpSaltBytes, inner));
return new BigInteger(1, outer);
}
/**
* Calculate the SRP verifier <tt>v</tt> value.
*/
public static BigInteger srpVerifierLowercaseV(byte[] emailUTF8, byte[] srpPWBytes, byte[] srpSaltBytes, BigInteger g, BigInteger N)
throws NoSuchAlgorithmException, UnsupportedEncodingException {
BigInteger x = srpVerifierLowercaseX(emailUTF8, srpPWBytes, srpSaltBytes);
BigInteger v = g.modPow(x, N);
return v;
}
/**
* Format x modulo N in hexadecimal, using as many characters as N takes (in hexadecimal).
* @param x to format.
* @param N modulus.
* @return x modulo N in hexadecimal.
*/
public static String hexModN(BigInteger x, BigInteger N) {
int byteLength = (N.bitLength() + 7) / 8;
int hexLength = 2 * byteLength;
return Utils.byte2Hex(Utils.hex2Byte((x.mod(N)).toString(16), byteLength), hexLength);
}
/**
* The first engineering milestone of PICL (Profile-in-the-Cloud) was
* comprised of Sync 1.1 fronted by a Firefox Account. The sync key was
* generated from the Firefox Account password-derived kB value using this
* method.
*/
public static KeyBundle generateSyncKeyBundle(final byte[] kB) throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
byte[] encryptionKey = new byte[32];
byte[] hmacKey = new byte[32];
byte[] derived = HKDF.derive(kB, new byte[0], FxAccountUtils.KW("oldsync"), 2*32);
System.arraycopy(derived, 0*32, encryptionKey, 0, 1*32);
System.arraycopy(derived, 1*32, hmacKey, 0, 1*32);
return new KeyBundle(encryptionKey, hmacKey);
}
/**
* Firefox Accounts are password authenticated, but clients should not store
* the plain-text password for any amount of time. Equivalent, but slightly
* more secure, is the quickly client-side stretched password.
* <p>
* We separate this since multiple login-time operations want it, and the
* PBKDF2 operation is computationally expensive.
*/
public static byte[] generateQuickStretchedPW(byte[] emailUTF8, byte[] passwordUTF8) throws GeneralSecurityException, UnsupportedEncodingException {
byte[] S = FxAccountUtils.KWE("quickStretch", emailUTF8);
try {
return NativeCrypto.pbkdf2SHA256(passwordUTF8, S, NUMBER_OF_QUICK_STRETCH_ROUNDS, 32);
} catch (final LinkageError e) {
// This will throw UnsatisfiedLinkError (missing mozglue) the first time it is called, and
// ClassNotDefFoundError, for the uninitialized NativeCrypto class, each subsequent time this
// is called; LinkageError is their common ancestor.
Logger.warn(LOG_TAG, "Got throwable stretching password using native pbkdf2SHA256 " +
"implementation; ignoring and using Java implementation.", e);
return PBKDF2.pbkdf2SHA256(passwordUTF8, S, NUMBER_OF_QUICK_STRETCH_ROUNDS, 32);
}
}
/**
* The password-derived credential used to authenticate to the Firefox Account
* auth server.
*/
public static byte[] generateAuthPW(byte[] quickStretchedPW) throws GeneralSecurityException, UnsupportedEncodingException {
return HKDF.derive(quickStretchedPW, new byte[0], FxAccountUtils.KW("authPW"), 32);
}
/**
* The password-derived credential used to unwrap keys managed by the Firefox
* Account auth server.
*/
public static byte[] generateUnwrapBKey(byte[] quickStretchedPW) throws GeneralSecurityException, UnsupportedEncodingException {
return HKDF.derive(quickStretchedPW, new byte[0], FxAccountUtils.KW("unwrapBkey"), 32);
}
public static byte[] unwrapkB(byte[] unwrapkB, byte[] wrapkB) {
if (unwrapkB == null) {
throw new IllegalArgumentException("unwrapkB must not be null");
}
if (wrapkB == null) {
throw new IllegalArgumentException("wrapkB must not be null");
}
if (unwrapkB.length != CRYPTO_KEY_LENGTH_BYTES || wrapkB.length != CRYPTO_KEY_LENGTH_BYTES) {
throw new IllegalArgumentException("unwrapkB and wrapkB must be " + CRYPTO_KEY_LENGTH_BYTES + " bytes long");
}
byte[] kB = new byte[CRYPTO_KEY_LENGTH_BYTES];
for (int i = 0; i < wrapkB.length; i++) {
kB[i] = (byte) (wrapkB[i] ^ unwrapkB[i]);
}
return kB;
}
/**
* The token server accepts an X-Client-State header, which is the
* lowercase-hex-encoded first 16 bytes of the SHA-256 hash of the
* bytes of kB.
* @param kB a byte array, expected to be 32 bytes long.
* @return a 32-character string.
* @throws NoSuchAlgorithmException
*/
public static String computeClientState(byte[] kB) throws NoSuchAlgorithmException {
if (kB == null ||
kB.length != 32) {
throw new IllegalArgumentException("Unexpected kB.");
}
byte[] sha256 = Utils.sha256(kB);
byte[] truncated = new byte[16];
System.arraycopy(sha256, 0, truncated, 0, 16);
return Utils.byte2Hex(truncated); // This is automatically lowercase.
}
/**
* Given an endpoint, calculate the corresponding BrowserID audience.
* <p>
* This is the domain, in web parlance.
*
* @param serverURI endpoint.
* @return BrowserID audience.
* @throws URISyntaxException
*/
public static String getAudienceForURL(String serverURI) throws URISyntaxException {
URI uri = new URI(serverURI);
return new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), null, null, null).toString();
}
public static String defaultClientName(Context context) {
String name = AppConstants.MOZ_APP_DISPLAYNAME; // The display name is never translated.
// Change "Firefox Aurora" or similar into "Aurora".
if (name.contains("Aurora")) {
name = "Aurora";
} else if (name.contains("Beta")) {
name = "Beta";
} else if (name.contains("Nightly")) {
name = "Nightly";
}
return context.getResources().getString(R.string.sync_default_client_name, name, android.os.Build.MODEL);
}
}

View file

@ -1,12 +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.background.fxa;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
public interface PasswordStretcher {
public byte[] getQuickStretchedPW(byte[] emailUTF8) throws UnsupportedEncodingException, GeneralSecurityException;
}

View file

@ -1,35 +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.background.fxa;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.Map;
import org.mozilla.gecko.sync.Utils;
public class QuickPasswordStretcher implements PasswordStretcher {
protected final String password;
protected final Map<String, String> cache = new HashMap<String, String>();
public QuickPasswordStretcher(String password) {
this.password = password;
}
@Override
public synchronized byte[] getQuickStretchedPW(byte[] emailUTF8) throws UnsupportedEncodingException, GeneralSecurityException {
if (emailUTF8 == null) {
throw new IllegalArgumentException("emailUTF8 must not be null");
}
String key = Utils.byte2Hex(emailUTF8);
if (!cache.containsKey(key)) {
byte[] value = FxAccountUtils.generateQuickStretchedPW(emailUTF8, password.getBytes("UTF-8"));
cache.put(key, Utils.byte2Hex(value));
return value;
}
return Utils.hex2Byte(cache.get(key));
}
}

View file

@ -1,111 +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.background.fxa;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.sync.net.Resource;
import ch.boye.httpclientandroidlib.Header;
import ch.boye.httpclientandroidlib.HttpHeaders;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.impl.cookie.DateParseException;
import ch.boye.httpclientandroidlib.impl.cookie.DateUtils;
public class SkewHandler {
private static final String LOG_TAG = "SkewHandler";
protected volatile long skewMillis = 0L;
protected final String hostname;
private static final HashMap<String, SkewHandler> skewHandlers = new HashMap<String, SkewHandler>();
public static SkewHandler getSkewHandlerForResource(final Resource resource) {
return getSkewHandlerForHostname(resource.getHostname());
}
public static SkewHandler getSkewHandlerFromEndpointString(final String url) throws URISyntaxException {
if (url == null) {
throw new IllegalArgumentException("url must not be null.");
}
URI u = new URI(url);
return getSkewHandlerForHostname(u.getHost());
}
public static synchronized SkewHandler getSkewHandlerForHostname(final String hostname) {
SkewHandler handler = skewHandlers.get(hostname);
if (handler == null) {
handler = new SkewHandler(hostname);
skewHandlers.put(hostname, handler);
}
return handler;
}
public static synchronized void clearSkewHandlers() {
skewHandlers.clear();
}
public SkewHandler(final String hostname) {
this.hostname = hostname;
}
public boolean updateSkewFromServerMillis(long millis, long now) {
skewMillis = millis - now;
Logger.debug(LOG_TAG, "Updated skew: " + skewMillis + "ms for hostname " + this.hostname);
return true;
}
public boolean updateSkewFromHTTPDateString(String date, long now) {
try {
final long millis = DateUtils.parseDate(date).getTime();
return updateSkewFromServerMillis(millis, now);
} catch (DateParseException e) {
Logger.warn(LOG_TAG, "Unexpected: invalid Date header from " + this.hostname);
return false;
}
}
public boolean updateSkewFromDateHeader(Header header, long now) {
String date = header.getValue();
if (null == date) {
Logger.warn(LOG_TAG, "Unexpected: null Date header from " + this.hostname);
return false;
}
return updateSkewFromHTTPDateString(date, now);
}
/**
* Update our tracked skew value to account for the local clock differing from
* the server's.
*
* @param response
* the received HTTP response.
* @param now
* the current time in milliseconds.
* @return true if the skew value was updated, false otherwise.
*/
public boolean updateSkew(HttpResponse response, long now) {
Header header = response.getFirstHeader(HttpHeaders.DATE);
if (null == header) {
Logger.warn(LOG_TAG, "Unexpected: missing Date header from " + this.hostname);
return false;
}
return updateSkewFromDateHeader(header, now);
}
public long getSkewInMillis() {
return skewMillis;
}
public long getSkewInSeconds() {
return skewMillis / 1000;
}
public void resetSkew() {
skewMillis = 0L;
}
}

View file

@ -1,224 +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.background.fxa.oauth;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Locale;
import java.util.concurrent.Executor;
import org.mozilla.gecko.background.fxa.FxAccountClientException;
import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException.FxAccountAbstractClientMalformedResponseException;
import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException.FxAccountAbstractClientRemoteException;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.Locales;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.net.AuthHeaderProvider;
import org.mozilla.gecko.sync.net.BaseResource;
import org.mozilla.gecko.sync.net.BaseResourceDelegate;
import org.mozilla.gecko.sync.net.Resource;
import org.mozilla.gecko.sync.net.SyncResponse;
import org.mozilla.gecko.sync.net.SyncStorageResponse;
import ch.boye.httpclientandroidlib.HttpEntity;
import ch.boye.httpclientandroidlib.HttpHeaders;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.client.ClientProtocolException;
import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
public abstract class FxAccountAbstractClient {
protected static final String LOG_TAG = FxAccountAbstractClient.class.getSimpleName();
protected static final String ACCEPT_HEADER = "application/json;charset=utf-8";
protected static final String AUTHORIZATION_RESPONSE_TYPE = "token";
public static final String JSON_KEY_ERROR = "error";
public static final String JSON_KEY_MESSAGE = "message";
public static final String JSON_KEY_CODE = "code";
public static final String JSON_KEY_ERRNO = "errno";
protected static final String[] requiredErrorStringFields = { JSON_KEY_ERROR, JSON_KEY_MESSAGE };
protected static final String[] requiredErrorLongFields = { JSON_KEY_CODE, JSON_KEY_ERRNO };
/**
* The server's URI.
* <p>
* We assume throughout that this ends with a trailing slash (and guarantee as
* much in the constructor).
*/
protected final String serverURI;
protected final Executor executor;
public FxAccountAbstractClient(String serverURI, Executor executor) {
if (serverURI == null) {
throw new IllegalArgumentException("Must provide a server URI.");
}
if (executor == null) {
throw new IllegalArgumentException("Must provide a non-null executor.");
}
this.serverURI = serverURI.endsWith("/") ? serverURI : serverURI + "/";
if (!this.serverURI.endsWith("/")) {
throw new IllegalArgumentException("Constructed serverURI must end with a trailing slash: " + this.serverURI);
}
this.executor = executor;
}
/**
* Process a typed value extracted from a successful response (in an
* endpoint-dependent way).
*/
public interface RequestDelegate<T> {
public void handleError(Exception e);
public void handleFailure(FxAccountAbstractClientRemoteException e);
public void handleSuccess(T result);
}
/**
* Intepret a response from the auth server.
* <p>
* Throw an appropriate exception on errors; otherwise, return the response's
* status code.
*
* @return response's HTTP status code.
* @throws FxAccountClientException
*/
public static int validateResponse(HttpResponse response) throws FxAccountAbstractClientRemoteException {
final int status = response.getStatusLine().getStatusCode();
if (status == 200) {
return status;
}
int code;
int errno;
String error;
String message;
ExtendedJSONObject body;
try {
body = new SyncStorageResponse(response).jsonObjectBody();
body.throwIfFieldsMissingOrMisTyped(requiredErrorStringFields, String.class);
body.throwIfFieldsMissingOrMisTyped(requiredErrorLongFields, Long.class);
code = body.getLong(JSON_KEY_CODE).intValue();
errno = body.getLong(JSON_KEY_ERRNO).intValue();
error = body.getString(JSON_KEY_ERROR);
message = body.getString(JSON_KEY_MESSAGE);
} catch (Exception e) {
throw new FxAccountAbstractClientMalformedResponseException(response);
}
throw new FxAccountAbstractClientRemoteException(response, code, errno, error, message, body);
}
protected <T> void invokeHandleError(final RequestDelegate<T> delegate, final Exception e) {
executor.execute(new Runnable() {
@Override
public void run() {
delegate.handleError(e);
}
});
}
protected <T> void post(BaseResource resource, final ExtendedJSONObject requestBody, final RequestDelegate<T> delegate) {
try {
if (requestBody == null) {
resource.post((HttpEntity) null);
} else {
resource.post(requestBody);
}
} catch (Exception e) {
invokeHandleError(delegate, e);
return;
}
}
/**
* Translate resource callbacks into request callbacks invoked on the provided
* executor.
* <p>
* Override <code>handleSuccess</code> to parse the body of the resource
* request and call the request callback. <code>handleSuccess</code> is
* invoked via the executor, so you don't need to delegate further.
*/
protected abstract class ResourceDelegate<T> extends BaseResourceDelegate {
protected abstract void handleSuccess(final int status, HttpResponse response, final ExtendedJSONObject body);
protected final RequestDelegate<T> delegate;
/**
* Create a delegate for an un-authenticated resource.
*/
public ResourceDelegate(final Resource resource, final RequestDelegate<T> delegate) {
super(resource);
this.delegate = delegate;
}
@Override
public AuthHeaderProvider getAuthHeaderProvider() {
return super.getAuthHeaderProvider();
}
@Override
public String getUserAgent() {
return FxAccountConstants.USER_AGENT;
}
@Override
public void handleHttpResponse(HttpResponse response) {
try {
final int status = validateResponse(response);
invokeHandleSuccess(status, response);
} catch (FxAccountAbstractClientRemoteException e) {
invokeHandleFailure(e);
}
}
protected void invokeHandleFailure(final FxAccountAbstractClientRemoteException e) {
executor.execute(new Runnable() {
@Override
public void run() {
delegate.handleFailure(e);
}
});
}
protected void invokeHandleSuccess(final int status, final HttpResponse response) {
executor.execute(new Runnable() {
@Override
public void run() {
try {
ExtendedJSONObject body = new SyncResponse(response).jsonObjectBody();
ResourceDelegate.this.handleSuccess(status, response, body);
} catch (Exception e) {
delegate.handleError(e);
}
}
});
}
@Override
public void handleHttpProtocolException(final ClientProtocolException e) {
invokeHandleError(delegate, e);
}
@Override
public void handleHttpIOException(IOException e) {
invokeHandleError(delegate, e);
}
@Override
public void handleTransportException(GeneralSecurityException e) {
invokeHandleError(delegate, e);
}
@Override
public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
super.addHeaders(request, client);
// The basics.
final Locale locale = Locale.getDefault();
request.addHeader(HttpHeaders.ACCEPT_LANGUAGE, Locales.getLanguageTag(locale));
request.addHeader(HttpHeaders.ACCEPT, ACCEPT_HEADER);
}
}
}

View file

@ -1,68 +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.background.fxa.oauth;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.HTTPFailureException;
import org.mozilla.gecko.sync.net.SyncStorageResponse;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.HttpStatus;
/**
* From <a href="https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md">https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md</a>.
*/
public class FxAccountAbstractClientException extends Exception {
private static final long serialVersionUID = 1953459541558266597L;
public FxAccountAbstractClientException(String detailMessage) {
super(detailMessage);
}
public FxAccountAbstractClientException(Exception e) {
super(e);
}
public static class FxAccountAbstractClientRemoteException extends FxAccountAbstractClientException {
private static final long serialVersionUID = 1209313149952001097L;
public final HttpResponse response;
public final long httpStatusCode;
public final long apiErrorNumber;
public final String error;
public final String message;
public final ExtendedJSONObject body;
public FxAccountAbstractClientRemoteException(HttpResponse response, long httpStatusCode, long apiErrorNumber, String error, String message, ExtendedJSONObject body) {
super(new HTTPFailureException(new SyncStorageResponse(response)));
if (body == null) {
throw new IllegalArgumentException("body must not be null");
}
this.response = response;
this.httpStatusCode = httpStatusCode;
this.apiErrorNumber = apiErrorNumber;
this.error = error;
this.message = message;
this.body = body;
}
@Override
public String toString() {
return "<FxAccountAbstractClientRemoteException " + this.httpStatusCode + " [" + this.apiErrorNumber + "]: " + this.message + ">";
}
public boolean isInvalidAuthentication() {
return this.httpStatusCode == HttpStatus.SC_UNAUTHORIZED;
}
}
public static class FxAccountAbstractClientMalformedResponseException extends FxAccountAbstractClientRemoteException {
private static final long serialVersionUID = 1209313149952001098L;
public FxAccountAbstractClientMalformedResponseException(HttpResponse response) {
super(response, 0, FxAccountOAuthRemoteError.UNKNOWN_ERROR, "Response malformed", "Response malformed", new ExtendedJSONObject());
}
}
}

View file

@ -1,129 +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.background.fxa.oauth;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.concurrent.Executor;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.net.BaseResource;
import ch.boye.httpclientandroidlib.HttpResponse;
/**
* Talk to an fxa-oauth-server to get "implicitly granted" OAuth tokens.
* <p>
* To use this client, you will need a pre-allocated fxa-oauth-server
* "client_id" with special "implicit grant" permissions.
* <p>
* This client was written against the API documented at <a href="https://github.com/mozilla/fxa-oauth-server/blob/41538990df9e91158558ae5a8115194383ac3b05/docs/api.md">https://github.com/mozilla/fxa-oauth-server/blob/41538990df9e91158558ae5a8115194383ac3b05/docs/api.md</a>.
*/
public class FxAccountOAuthClient10 extends FxAccountAbstractClient {
protected static final String LOG_TAG = FxAccountOAuthClient10.class.getSimpleName();
protected static final String AUTHORIZATION_RESPONSE_TYPE = "token";
protected static final String JSON_KEY_ACCESS_TOKEN = "access_token";
protected static final String JSON_KEY_ASSERTION = "assertion";
protected static final String JSON_KEY_CLIENT_ID = "client_id";
protected static final String JSON_KEY_RESPONSE_TYPE = "response_type";
protected static final String JSON_KEY_SCOPE = "scope";
protected static final String JSON_KEY_STATE = "state";
protected static final String JSON_KEY_TOKEN = "token";
protected static final String JSON_KEY_TOKEN_TYPE = "token_type";
// access_token: A string that can be used for authorized requests to service providers.
// scope: A string of space-separated permissions that this token has. May differ from requested scopes, since user can deny permissions.
// token_type: A string representing the token type. Currently will always be "bearer".
protected static final String[] AUTHORIZATION_RESPONSE_REQUIRED_STRING_FIELDS = new String[] { JSON_KEY_ACCESS_TOKEN, JSON_KEY_SCOPE, JSON_KEY_TOKEN_TYPE };
public FxAccountOAuthClient10(String serverURI, Executor executor) {
super(serverURI, executor);
}
/**
* Thin container for an authorization response.
*/
public static class AuthorizationResponse {
public final String access_token;
public final String token_type;
public final String scope;
public AuthorizationResponse(String access_token, String token_type, String scope) {
this.access_token = access_token;
this.token_type = token_type;
this.scope = scope;
}
}
public void authorization(String client_id, String assertion, String state, String scope,
RequestDelegate<AuthorizationResponse> delegate) {
final BaseResource resource;
try {
resource = new BaseResource(new URI(serverURI + "authorization"));
} catch (URISyntaxException e) {
invokeHandleError(delegate, e);
return;
}
resource.delegate = new ResourceDelegate<AuthorizationResponse>(resource, delegate) {
@Override
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
try {
body.throwIfFieldsMissingOrMisTyped(AUTHORIZATION_RESPONSE_REQUIRED_STRING_FIELDS, String.class);
String access_token = body.getString(JSON_KEY_ACCESS_TOKEN);
String token_type = body.getString(JSON_KEY_TOKEN_TYPE);
String scope = body.getString(JSON_KEY_SCOPE);
delegate.handleSuccess(new AuthorizationResponse(access_token, token_type, scope));
return;
} catch (Exception e) {
delegate.handleError(e);
return;
}
}
};
final ExtendedJSONObject requestBody = new ExtendedJSONObject();
requestBody.put(JSON_KEY_RESPONSE_TYPE, AUTHORIZATION_RESPONSE_TYPE);
requestBody.put(JSON_KEY_CLIENT_ID, client_id);
requestBody.put(JSON_KEY_ASSERTION, assertion);
if (scope != null) {
requestBody.put(JSON_KEY_SCOPE, scope);
}
if (state != null) {
requestBody.put(JSON_KEY_STATE, state);
}
post(resource, requestBody, delegate);
}
public void deleteToken(final String token, final RequestDelegate<Void> delegate) {
final BaseResource resource;
try {
resource = new BaseResource(new URI(serverURI + "destroy"));
} catch (URISyntaxException e) {
invokeHandleError(delegate, e);
return;
}
resource.delegate = new ResourceDelegate<Void>(resource, delegate) {
@Override
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
try {
delegate.handleSuccess(null);
return;
} catch (Exception e) {
delegate.handleError(e);
return;
}
}
};
final ExtendedJSONObject requestBody = new ExtendedJSONObject();
requestBody.put(JSON_KEY_TOKEN, token);
post(resource, requestBody, delegate);
}
}

View file

@ -1,19 +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.background.fxa.oauth;
public interface FxAccountOAuthRemoteError {
public static final int ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS = 101;
public static final int UNKNOWN_CLIENT_ID = 101;
public static final int INCORRECT_CLIENT_SECRET = 102;
public static final int REDIRECT_URI_DOES_NOT_MATCH_REGISTERED_VALUE = 103;
public static final int INVALID_FXA_ASSERTION = 104;
public static final int UNKNOWN_CODE = 105;
public static final int INCORRECT_CODE = 106;
public static final int EXPIRED_CODE = 107;
public static final int INVALID_TOKEN = 108;
public static final int INVALID_REQUEST_PARAMETER = 109;
public static final int UNKNOWN_ERROR = 999;
}

View file

@ -1,59 +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.background.fxa.profile;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.concurrent.Executor;
import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClient;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.net.AuthHeaderProvider;
import org.mozilla.gecko.sync.net.BaseResource;
import org.mozilla.gecko.sync.net.BearerAuthHeaderProvider;
import ch.boye.httpclientandroidlib.HttpResponse;
/**
* Talk to an fxa-profile-server to get profile information like name, age, gender, and avatar image.
* <p>
* This client was written against the API documented at <a href="https://github.com/mozilla/fxa-profile-server/blob/0c065619f5a2e867f813a343b4c67da3fe2c82a4/docs/API.md">https://github.com/mozilla/fxa-profile-server/blob/0c065619f5a2e867f813a343b4c67da3fe2c82a4/docs/API.md</a>.
*/
public class FxAccountProfileClient10 extends FxAccountAbstractClient {
public FxAccountProfileClient10(String serverURI, Executor executor) {
super(serverURI, executor);
}
public void profile(final String token, RequestDelegate<ExtendedJSONObject> delegate) {
BaseResource resource;
try {
resource = new BaseResource(new URI(serverURI + "profile"));
} catch (URISyntaxException e) {
invokeHandleError(delegate, e);
return;
}
resource.delegate = new ResourceDelegate<ExtendedJSONObject>(resource, delegate) {
@Override
public AuthHeaderProvider getAuthHeaderProvider() {
return new BearerAuthHeaderProvider(token);
}
@Override
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
try {
delegate.handleSuccess(body);
return;
} catch (Exception e) {
delegate.handleError(e);
return;
}
}
};
resource.get();
}
}

View file

@ -1,60 +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.background.nativecode;
import java.security.GeneralSecurityException;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.AppConstants;
import android.util.Log;
@RobocopTarget
public class NativeCrypto {
static {
try {
System.loadLibrary("mozglue");
} catch (UnsatisfiedLinkError e) {
Log.wtf("NativeCrypto", "Couldn't load mozglue. Trying /data/app-lib path.");
try {
System.load("/data/app-lib/" + AppConstants.ANDROID_PACKAGE_NAME + "/libmozglue.so");
} catch (Throwable ee) {
try {
Log.wtf("NativeCrypto", "Couldn't load mozglue: " + ee + ". Trying /data/data path.");
System.load("/data/data/" + AppConstants.ANDROID_PACKAGE_NAME + "/lib/libmozglue.so");
} catch (UnsatisfiedLinkError eee) {
Log.wtf("NativeCrypto", "Failed every attempt to load mozglue. Giving up.");
throw new RuntimeException("Unable to load mozglue", eee);
}
}
}
}
/**
* Wrapper to perform PBKDF2-HMAC-SHA-256 in native code.
*/
public native static byte[] pbkdf2SHA256(byte[] password, byte[] salt, int c, int dkLen)
throws GeneralSecurityException;
/**
* Wrapper to perform SHA-1 in native code.
*/
public native static byte[] sha1(byte[] str);
/**
* Wrapper to perform SHA-256 init in native code. Returns a SHA-256 context.
*/
public native static byte[] sha256init();
/**
* Wrapper to update a SHA-256 context in native code.
*/
public native static void sha256update(byte[] ctx, byte[] str, int len);
/**
* Wrapper to finalize a SHA-256 context in native code. Returns digest.
*/
public native static byte[] sha256finalize(byte[] ctx);
}

View file

@ -1,326 +0,0 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mozilla.gecko.background.preferences;
import org.mozilla.gecko.R;
import org.mozilla.gecko.util.WeakReferenceHandler;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.preference.Preference;
import android.preference.PreferenceGroup;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.support.v4.app.Fragment;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnKeyListener;
import android.view.ViewGroup;
import android.widget.ListView;
public abstract class PreferenceFragment extends Fragment implements PreferenceManagerCompat.OnPreferenceTreeClickListener {
private static final String PREFERENCES_TAG = "android:preferences";
private PreferenceManager mPreferenceManager;
private ListView mList;
private boolean mHavePrefs;
private boolean mInitDone;
/**
* The starting request code given out to preference framework.
*/
private static final int FIRST_REQUEST_CODE = 100;
private static final int MSG_BIND_PREFERENCES = 1;
private static class PreferenceFragmentHandler extends WeakReferenceHandler<PreferenceFragment> {
public PreferenceFragmentHandler(final PreferenceFragment that) {
super(that);
}
@Override
public void handleMessage(Message msg) {
final PreferenceFragment that = mTarget.get();
if (that == null) {
return;
}
switch (msg.what) {
case MSG_BIND_PREFERENCES:
that.bindPreferences();
break;
}
}
}
private final Handler mHandler = new PreferenceFragmentHandler(this);
final private Runnable mRequestFocus = new Runnable() {
@Override
public void run() {
mList.focusableViewAvailable(mList);
}
};
/**
* Interface that PreferenceFragment's containing activity should
* implement to be able to process preference items that wish to
* switch to a new fragment.
*/
public interface OnPreferenceStartFragmentCallback {
/**
* Called when the user has clicked on a Preference that has
* a fragment class name associated with it. The implementation
* to should instantiate and switch to an instance of the given
* fragment.
*/
boolean onPreferenceStartFragment(PreferenceFragment caller, Preference pref);
}
@Override
public void onCreate(Bundle paramBundle) {
super.onCreate(paramBundle);
mPreferenceManager = PreferenceManagerCompat.newInstance(getActivity(), FIRST_REQUEST_CODE);
PreferenceManagerCompat.setFragment(mPreferenceManager, this);
}
@Override
public View onCreateView(LayoutInflater paramLayoutInflater, ViewGroup paramViewGroup, Bundle paramBundle) {
return paramLayoutInflater.inflate(R.layout.fxaccount_preference_list_fragment, paramViewGroup,
false);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (mHavePrefs) {
bindPreferences();
}
mInitDone = true;
if (savedInstanceState != null) {
Bundle container = savedInstanceState.getBundle(PREFERENCES_TAG);
if (container != null) {
final PreferenceScreen preferenceScreen = getPreferenceScreen();
if (preferenceScreen != null) {
preferenceScreen.restoreHierarchyState(container);
}
}
}
}
@Override
public void onStart() {
super.onStart();
PreferenceManagerCompat.setOnPreferenceTreeClickListener(mPreferenceManager, this);
}
@Override
public void onStop() {
super.onStop();
PreferenceManagerCompat.dispatchActivityStop(mPreferenceManager);
PreferenceManagerCompat.setOnPreferenceTreeClickListener(mPreferenceManager, null);
}
@Override
public void onDestroyView() {
mList = null;
mHandler.removeCallbacks(mRequestFocus);
mHandler.removeMessages(MSG_BIND_PREFERENCES);
super.onDestroyView();
}
@Override
public void onDestroy() {
super.onDestroy();
PreferenceManagerCompat.dispatchActivityDestroy(mPreferenceManager);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
final PreferenceScreen preferenceScreen = getPreferenceScreen();
if (preferenceScreen != null) {
Bundle container = new Bundle();
preferenceScreen.saveHierarchyState(container);
outState.putBundle(PREFERENCES_TAG, container);
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
PreferenceManagerCompat.dispatchActivityResult(mPreferenceManager, requestCode, resultCode, data);
}
/**
* Returns the {@link PreferenceManager} used by this fragment.
* @return The {@link PreferenceManager}.
*/
public PreferenceManager getPreferenceManager() {
return mPreferenceManager;
}
/**
* Sets the root of the preference hierarchy that this fragment is showing.
*
* @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy.
*/
public void setPreferenceScreen(PreferenceScreen preferenceScreen) {
if (PreferenceManagerCompat.setPreferences(mPreferenceManager, preferenceScreen) && preferenceScreen != null) {
mHavePrefs = true;
if (mInitDone) {
postBindPreferences();
}
}
}
/**
* Gets the root of the preference hierarchy that this fragment is showing.
*
* @return The {@link PreferenceScreen} that is the root of the preference
* hierarchy.
*/
public PreferenceScreen getPreferenceScreen() {
return PreferenceManagerCompat.getPreferenceScreen(mPreferenceManager);
}
/**
* Adds preferences from activities that match the given {@link Intent}.
*
* @param intent The {@link Intent} to query activities.
*/
public void addPreferencesFromIntent(Intent intent) {
requirePreferenceManager();
setPreferenceScreen(PreferenceManagerCompat.inflateFromIntent(mPreferenceManager, intent, getPreferenceScreen()));
}
/**
* Inflates the given XML resource and adds the preference hierarchy to the current
* preference hierarchy.
*
* @param preferencesResId The XML resource ID to inflate.
*/
public void addPreferencesFromResource(int preferencesResId) {
requirePreferenceManager();
setPreferenceScreen(PreferenceManagerCompat.inflateFromResource(mPreferenceManager, getActivity(),
preferencesResId, getPreferenceScreen()));
}
/**
* {@inheritDoc}
*/
@Override
public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen,
Preference preference) {
//if (preference.getFragment() != null &&
if (
getActivity() instanceof OnPreferenceStartFragmentCallback) {
return ((OnPreferenceStartFragmentCallback)getActivity()).onPreferenceStartFragment(
this, preference);
}
return false;
}
/**
* Finds a {@link Preference} based on its key.
*
* @param key The key of the preference to retrieve.
* @return The {@link Preference} with the key, or null.
* @see PreferenceGroup#findPreference(CharSequence)
*/
public Preference findPreference(CharSequence key) {
if (mPreferenceManager == null) {
return null;
}
return mPreferenceManager.findPreference(key);
}
private void requirePreferenceManager() {
if (mPreferenceManager == null) {
throw new RuntimeException("This should be called after super.onCreate.");
}
}
private void postBindPreferences() {
if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) return;
mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget();
}
private void bindPreferences() {
final PreferenceScreen preferenceScreen = getPreferenceScreen();
if (preferenceScreen != null) {
preferenceScreen.bind(getListView());
}
}
public ListView getListView() {
ensureList();
return mList;
}
private void ensureList() {
if (mList != null) {
return;
}
View root = getView();
if (root == null) {
throw new IllegalStateException("Content view not yet created");
}
View rawListView = root.findViewById(android.R.id.list);
if (!(rawListView instanceof ListView)) {
throw new RuntimeException(
"Content has view with id attribute 'android.R.id.list' "
+ "that is not a ListView class");
}
mList = (ListView)rawListView;
if (mList == null) {
throw new RuntimeException(
"Your content must have a ListView whose id attribute is " +
"'android.R.id.list'");
}
mList.setOnKeyListener(mListOnKeyListener);
mHandler.post(mRequestFocus);
}
private final OnKeyListener mListOnKeyListener = new OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
Object selectedItem = mList.getSelectedItem();
if (selectedItem instanceof Preference) {
@SuppressWarnings("unused")
View selectedView = mList.getSelectedView();
//return ((Preference)selectedItem).onKey(
// selectedView, keyCode, event);
return false;
}
return false;
}
};
}

View file

@ -1,226 +0,0 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mozilla.gecko.background.preferences;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.preference.Preference;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.util.Log;
public class PreferenceManagerCompat {
private static final String TAG = PreferenceManagerCompat.class.getSimpleName();
/**
* Interface definition for a callback to be invoked when a {@link Preference} in the hierarchy
* rooted at this {@link PreferenceScreen} is clicked.
*/
interface OnPreferenceTreeClickListener {
/**
* Called when a preference in the tree rooted at this {@link PreferenceScreen} has been
* clicked.
*
* @param preferenceScreen The {@link PreferenceScreen} that the preference is located in.
* @param preference The preference that was clicked.
*
* @return Whether the click was handled.
*/
boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference);
}
static PreferenceManager newInstance(Activity activity, int firstRequestCode) {
try {
Constructor<PreferenceManager> c = PreferenceManager.class.getDeclaredConstructor(Activity.class, int.class);
c.setAccessible(true);
return c.newInstance(activity, firstRequestCode);
} catch (Exception e) {
Log.w(TAG, "Couldn't call constructor PreferenceManager by reflection", e);
}
return null;
}
/**
* Sets the owning preference fragment
*/
static void setFragment(PreferenceManager manager, PreferenceFragment fragment) {
// stub
}
/**
* Sets the callback to be invoked when a {@link Preference} in the hierarchy rooted at this
* {@link PreferenceManager} is clicked.
*
* @param listener The callback to be invoked.
*/
static void setOnPreferenceTreeClickListener(PreferenceManager manager, final OnPreferenceTreeClickListener listener) {
try {
Field onPreferenceTreeClickListener = PreferenceManager.class.getDeclaredField("mOnPreferenceTreeClickListener");
onPreferenceTreeClickListener.setAccessible(true);
if (listener != null) {
Object proxy = Proxy.newProxyInstance(
onPreferenceTreeClickListener.getType().getClassLoader(),
new Class<?>[] { onPreferenceTreeClickListener.getType() },
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
if (method.getName().equals("onPreferenceTreeClick")) {
return listener.onPreferenceTreeClick((PreferenceScreen) args[0], (Preference) args[1]);
} else {
return null;
}
}
});
onPreferenceTreeClickListener.set(manager, proxy);
} else {
onPreferenceTreeClickListener.set(manager, null);
}
} catch (Exception e) {
Log.w(TAG, "Couldn't set PreferenceManager.mOnPreferenceTreeClickListener by reflection", e);
}
}
/**
* Inflates a preference hierarchy from the preference hierarchies of {@link Activity Activities}
* that match the given {@link Intent}. An {@link Activity} defines its preference hierarchy with
* meta-data using the {@link #METADATA_KEY_PREFERENCES} key.
* <p/>
* If a preference hierarchy is given, the new preference hierarchies will be merged in.
*
* @param queryIntent The intent to match activities.
* @param rootPreferences Optional existing hierarchy to merge the new hierarchies into.
*
* @return The root hierarchy (if one was not provided, the new hierarchy's root).
*/
static PreferenceScreen inflateFromIntent(PreferenceManager manager, Intent intent, PreferenceScreen screen) {
try {
Method m = PreferenceManager.class.getDeclaredMethod("inflateFromIntent", Intent.class, PreferenceScreen.class);
m.setAccessible(true);
PreferenceScreen prefScreen = (PreferenceScreen) m.invoke(manager, intent, screen);
return prefScreen;
} catch (Exception e) {
Log.w(TAG, "Couldn't call PreferenceManager.inflateFromIntent by reflection", e);
}
return null;
}
/**
* Inflates a preference hierarchy from XML. If a preference hierarchy is given, the new
* preference hierarchies will be merged in.
*
* @param context The context of the resource.
* @param resId The resource ID of the XML to inflate.
* @param rootPreferences Optional existing hierarchy to merge the new hierarchies into.
*
* @return The root hierarchy (if one was not provided, the new hierarchy's root).
*
* @hide
*/
static PreferenceScreen inflateFromResource(PreferenceManager manager, Activity activity, int resId, PreferenceScreen screen) {
try {
Method m = PreferenceManager.class.getDeclaredMethod("inflateFromResource", Context.class, int.class, PreferenceScreen.class);
m.setAccessible(true);
PreferenceScreen prefScreen = (PreferenceScreen) m.invoke(manager, activity, resId, screen);
return prefScreen;
} catch (Exception e) {
Log.w(TAG, "Couldn't call PreferenceManager.inflateFromResource by reflection", e);
}
return null;
}
/**
* Returns the root of the preference hierarchy managed by this class.
*
* @return The {@link PreferenceScreen} object that is at the root of the hierarchy.
*/
static PreferenceScreen getPreferenceScreen(PreferenceManager manager) {
try {
Method m = PreferenceManager.class.getDeclaredMethod("getPreferenceScreen");
m.setAccessible(true);
return (PreferenceScreen) m.invoke(manager);
} catch (Exception e) {
Log.w(TAG, "Couldn't call PreferenceManager.getPreferenceScreen by reflection", e);
}
return null;
}
/**
* Called by the {@link PreferenceManager} to dispatch a subactivity result.
*/
static void dispatchActivityResult(PreferenceManager manager, int requestCode, int resultCode, Intent data) {
try {
Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityResult", int.class, int.class, Intent.class);
m.setAccessible(true);
m.invoke(manager, requestCode, resultCode, data);
} catch (Exception e) {
Log.w(TAG, "Couldn't call PreferenceManager.dispatchActivityResult by reflection", e);
}
}
/**
* Called by the {@link PreferenceManager} to dispatch the activity stop event.
*/
static void dispatchActivityStop(PreferenceManager manager) {
try {
Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityStop");
m.setAccessible(true);
m.invoke(manager);
} catch (Exception e) {
Log.w(TAG, "Couldn't call PreferenceManager.dispatchActivityStop by reflection", e);
}
}
/**
* Called by the {@link PreferenceManager} to dispatch the activity destroy event.
*/
static void dispatchActivityDestroy(PreferenceManager manager) {
try {
Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityDestroy");
m.setAccessible(true);
m.invoke(manager);
} catch (Exception e) {
Log.w(TAG, "Couldn't call PreferenceManager.dispatchActivityDestroy by reflection", e);
}
}
/**
* Sets the root of the preference hierarchy.
*
* @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy.
*
* @return Whether the {@link PreferenceScreen} given is different than the previous.
*/
static boolean setPreferences(PreferenceManager manager, PreferenceScreen screen) {
try {
Method m = PreferenceManager.class.getDeclaredMethod("setPreferences", PreferenceScreen.class);
m.setAccessible(true);
return ((Boolean) m.invoke(manager, screen));
} catch (Exception e) {
Log.w(TAG, "Couldn't call PreferenceManager.setPreferences by reflection", e);
}
return false;
}
}

View file

@ -1,82 +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.browserid;
/**
* Java produces signature in ASN.1 format. Here's some hard-coded encoding and decoding
* code, courtesy of a comment in
* <a href="http://stackoverflow.com/questions/10921733/how-sign-method-of-the-digital-signature-combines-the-r-s-values-in-to-array">http://stackoverflow.com/questions/10921733/how-sign-method-of-the-digital-signature-combines-the-r-s-values-in-to-array</a>.
*/
public class ASNUtils {
/**
* Decode two short arrays from ASN.1 bytes.
* @param input to extract.
* @return length 2 array of byte arrays.
*/
public static byte[][] decodeTwoArraysFromASN1(byte[] input) throws IllegalArgumentException {
if (input == null) {
throw new IllegalArgumentException("input must not be null");
}
if (input.length <= 3)
throw new IllegalArgumentException("bad length");
if (input[0] != 0x30)
throw new IllegalArgumentException("bad encoding");
if ((input[1] & ((byte) 0x80)) != 0)
throw new IllegalArgumentException("bad length encoding");
if (input[2] != 0x02)
throw new IllegalArgumentException("bad encoding");
if ((input[3] & ((byte) 0x80)) != 0)
throw new IllegalArgumentException("bad length encoding");
byte rLength = input[3];
if (input.length <= 5 + rLength)
throw new IllegalArgumentException("bad length");
if (input[4 + rLength] != 0x02)
throw new IllegalArgumentException("bad encoding");
if ((input[5 + rLength] & (byte) 0x80) !=0)
throw new IllegalArgumentException("bad length encoding");
byte sLength = input[5 + rLength];
if (input.length != 6 + sLength + rLength)
throw new IllegalArgumentException("bad length");
byte[] rArr = new byte[rLength];
byte[] sArr = new byte[sLength];
System.arraycopy(input, 4, rArr, 0, rLength);
System.arraycopy(input, 6 + rLength, sArr, 0, sLength);
return new byte[][] { rArr, sArr };
}
/**
* Encode two short arrays into ASN.1 bytes.
* @param first array to encode.
* @param second array to encode.
* @return array.
*/
public static byte[] encodeTwoArraysToASN1(byte[] first, byte[] second) throws IllegalArgumentException {
if (first == null) {
throw new IllegalArgumentException("first must not be null");
}
if (second == null) {
throw new IllegalArgumentException("second must not be null");
}
byte[] output = new byte[6 + first.length + second.length];
output[0] = 0x30;
if (4 + first.length + second.length > 255)
throw new IllegalArgumentException("bad length");
output[1] = (byte) (4 + first.length + second.length);
if ((output[1] & ((byte) 0x80)) != 0)
throw new IllegalArgumentException("bad length encoding");
output[2] = 0x02;
output[3] = (byte) first.length;
if ((output[3] & ((byte) 0x80)) != 0)
throw new IllegalArgumentException("bad length encoding");
System.arraycopy(first, 0, output, 4, first.length);
output[4 + first.length] = 0x02;
output[5 + first.length] = (byte) second.length;
if ((output[5 + first.length] & ((byte) 0x80)) != 0)
throw new IllegalArgumentException("bad length encoding");
System.arraycopy(second, 0, output, 6 + first.length, second.length);
return output;
}
}

View file

@ -1,35 +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.browserid;
import org.mozilla.gecko.sync.ExtendedJSONObject;
public class BrowserIDKeyPair {
public static final String JSON_KEY_PRIVATEKEY = "privateKey";
public static final String JSON_KEY_PUBLICKEY = "publicKey";
protected final SigningPrivateKey privateKey;
protected final VerifyingPublicKey publicKey;
public BrowserIDKeyPair(SigningPrivateKey privateKey, VerifyingPublicKey publicKey) {
this.privateKey = privateKey;
this.publicKey = publicKey;
}
public SigningPrivateKey getPrivate() {
return this.privateKey;
}
public VerifyingPublicKey getPublic() {
return this.publicKey;
}
public ExtendedJSONObject toJSONObject() {
ExtendedJSONObject o = new ExtendedJSONObject();
o.put(JSON_KEY_PRIVATEKEY, privateKey.toJSONObject());
o.put(JSON_KEY_PUBLICKEY, publicKey.toJSONObject());
return o;
}
}

View file

@ -1,255 +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.browserid;
import android.annotation.SuppressLint;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.NonObjectJSONException;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.util.PRNGFixes;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.security.interfaces.DSAParams;
import java.security.interfaces.DSAPrivateKey;
import java.security.interfaces.DSAPublicKey;
import java.security.spec.DSAPrivateKeySpec;
import java.security.spec.DSAPublicKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
public class DSACryptoImplementation {
private static final String LOG_TAG = DSACryptoImplementation.class.getSimpleName();
public static final String SIGNATURE_ALGORITHM = "SHA1withDSA";
public static final int SIGNATURE_LENGTH_BYTES = 40; // DSA signatures are always 40 bytes long.
/**
* Parameters are serialized as hex strings. Hex-versus-decimal was
* reverse-engineered from what the Persona public verifier accepted. We
* expect to follow the JOSE/JWT spec as it solidifies, and that will probably
* mean unifying this base.
*/
protected static final int SERIALIZATION_BASE = 16;
protected static class DSAVerifyingPublicKey implements VerifyingPublicKey {
protected final DSAPublicKey publicKey;
public DSAVerifyingPublicKey(DSAPublicKey publicKey) {
this.publicKey = publicKey;
}
/**
* Serialize to a JSON object.
* <p>
* Parameters are serialized as hex strings. Hex-versus-decimal was
* reverse-engineered from what the Persona public verifier accepted.
*/
@Override
public ExtendedJSONObject toJSONObject() {
DSAParams params = publicKey.getParams();
ExtendedJSONObject o = new ExtendedJSONObject();
o.put("algorithm", "DS");
o.put("y", publicKey.getY().toString(SERIALIZATION_BASE));
o.put("g", params.getG().toString(SERIALIZATION_BASE));
o.put("p", params.getP().toString(SERIALIZATION_BASE));
o.put("q", params.getQ().toString(SERIALIZATION_BASE));
return o;
}
@Override
public boolean verifyMessage(byte[] bytes, byte[] signature)
throws GeneralSecurityException {
if (bytes == null) {
throw new IllegalArgumentException("bytes must not be null");
}
if (signature == null) {
throw new IllegalArgumentException("signature must not be null");
}
if (signature.length != SIGNATURE_LENGTH_BYTES) {
return false;
}
byte[] first = new byte[signature.length / 2];
byte[] second = new byte[signature.length / 2];
System.arraycopy(signature, 0, first, 0, first.length);
System.arraycopy(signature, first.length, second, 0, second.length);
BigInteger r = new BigInteger(Utils.byte2Hex(first), 16);
BigInteger s = new BigInteger(Utils.byte2Hex(second), 16);
// This is awful, but encoding an extra 0 byte works better on devices.
byte[] encoded = ASNUtils.encodeTwoArraysToASN1(
Utils.hex2Byte(r.toString(16), 1 + SIGNATURE_LENGTH_BYTES / 2),
Utils.hex2Byte(s.toString(16), 1 + SIGNATURE_LENGTH_BYTES / 2));
final Signature signer = Signature.getInstance(SIGNATURE_ALGORITHM);
signer.initVerify(publicKey);
signer.update(bytes);
return signer.verify(encoded);
}
}
protected static class DSASigningPrivateKey implements SigningPrivateKey {
protected final DSAPrivateKey privateKey;
public DSASigningPrivateKey(DSAPrivateKey privateKey) {
this.privateKey = privateKey;
}
@Override
public String getAlgorithm() {
return "DS" + (privateKey.getParams().getP().bitLength() + 7)/8;
}
/**
* Serialize to a JSON object.
* <p>
* Parameters are serialized as decimal strings. Hex-versus-decimal was
* reverse-engineered from what the Persona public verifier accepted.
*/
@Override
public ExtendedJSONObject toJSONObject() {
DSAParams params = privateKey.getParams();
ExtendedJSONObject o = new ExtendedJSONObject();
o.put("algorithm", "DS");
o.put("x", privateKey.getX().toString(SERIALIZATION_BASE));
o.put("g", params.getG().toString(SERIALIZATION_BASE));
o.put("p", params.getP().toString(SERIALIZATION_BASE));
o.put("q", params.getQ().toString(SERIALIZATION_BASE));
return o;
}
@SuppressLint("TrulyRandom")
@Override
public byte[] signMessage(byte[] bytes)
throws GeneralSecurityException {
if (bytes == null) {
throw new IllegalArgumentException("bytes must not be null");
}
try {
PRNGFixes.apply();
} catch (Exception e) {
// Not much to be done here: it was weak before, and we couldn't patch it, so it's weak now. Not worth aborting.
Logger.error(LOG_TAG, "Got exception applying PRNGFixes! Cryptographic data produced on this device may be weak. Ignoring.", e);
}
final Signature signer = Signature.getInstance(SIGNATURE_ALGORITHM);
signer.initSign(privateKey);
signer.update(bytes);
final byte[] signature = signer.sign();
final byte[][] arrays = ASNUtils.decodeTwoArraysFromASN1(signature);
BigInteger r = new BigInteger(arrays[0]);
BigInteger s = new BigInteger(arrays[1]);
// This is awful, but signatures are always 40 bytes long.
byte[] decoded = Utils.concatAll(
Utils.hex2Byte(r.toString(16), SIGNATURE_LENGTH_BYTES / 2),
Utils.hex2Byte(s.toString(16), SIGNATURE_LENGTH_BYTES / 2));
return decoded;
}
}
public static BrowserIDKeyPair generateKeyPair(int keysize)
throws NoSuchAlgorithmException {
final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(keysize);
final KeyPair keyPair = keyPairGenerator.generateKeyPair();
DSAPrivateKey privateKey = (DSAPrivateKey) keyPair.getPrivate();
DSAPublicKey publicKey = (DSAPublicKey) keyPair.getPublic();
return new BrowserIDKeyPair(new DSASigningPrivateKey(privateKey), new DSAVerifyingPublicKey(publicKey));
}
public static SigningPrivateKey createPrivateKey(BigInteger x, BigInteger p, BigInteger q, BigInteger g) throws NoSuchAlgorithmException, InvalidKeySpecException {
if (x == null) {
throw new IllegalArgumentException("x must not be null");
}
if (p == null) {
throw new IllegalArgumentException("p must not be null");
}
if (q == null) {
throw new IllegalArgumentException("q must not be null");
}
if (g == null) {
throw new IllegalArgumentException("g must not be null");
}
KeySpec keySpec = new DSAPrivateKeySpec(x, p, q, g);
KeyFactory keyFactory = KeyFactory.getInstance("DSA");
DSAPrivateKey privateKey = (DSAPrivateKey) keyFactory.generatePrivate(keySpec);
return new DSASigningPrivateKey(privateKey);
}
public static VerifyingPublicKey createPublicKey(BigInteger y, BigInteger p, BigInteger q, BigInteger g) throws NoSuchAlgorithmException, InvalidKeySpecException {
if (y == null) {
throw new IllegalArgumentException("n must not be null");
}
if (p == null) {
throw new IllegalArgumentException("p must not be null");
}
if (q == null) {
throw new IllegalArgumentException("q must not be null");
}
if (g == null) {
throw new IllegalArgumentException("g must not be null");
}
KeySpec keySpec = new DSAPublicKeySpec(y, p, q, g);
KeyFactory keyFactory = KeyFactory.getInstance("DSA");
DSAPublicKey publicKey = (DSAPublicKey) keyFactory.generatePublic(keySpec);
return new DSAVerifyingPublicKey(publicKey);
}
public static SigningPrivateKey createPrivateKey(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
String algorithm = o.getString("algorithm");
if (!"DS".equals(algorithm)) {
throw new InvalidKeySpecException("algorithm must equal DS, was " + algorithm);
}
try {
BigInteger x = new BigInteger(o.getString("x"), SERIALIZATION_BASE);
BigInteger p = new BigInteger(o.getString("p"), SERIALIZATION_BASE);
BigInteger q = new BigInteger(o.getString("q"), SERIALIZATION_BASE);
BigInteger g = new BigInteger(o.getString("g"), SERIALIZATION_BASE);
return createPrivateKey(x, p, q, g);
} catch (NullPointerException | NumberFormatException e) {
throw new InvalidKeySpecException("x, p, q, and g must be integers encoded as strings, base " + SERIALIZATION_BASE);
}
}
public static VerifyingPublicKey createPublicKey(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
String algorithm = o.getString("algorithm");
if (!"DS".equals(algorithm)) {
throw new InvalidKeySpecException("algorithm must equal DS, was " + algorithm);
}
try {
BigInteger y = new BigInteger(o.getString("y"), SERIALIZATION_BASE);
BigInteger p = new BigInteger(o.getString("p"), SERIALIZATION_BASE);
BigInteger q = new BigInteger(o.getString("q"), SERIALIZATION_BASE);
BigInteger g = new BigInteger(o.getString("g"), SERIALIZATION_BASE);
return createPublicKey(y, p, q, g);
} catch (NullPointerException | NumberFormatException e) {
throw new InvalidKeySpecException("y, p, q, and g must be integers encoded as strings, base " + SERIALIZATION_BASE);
}
}
public static BrowserIDKeyPair fromJSONObject(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
try {
ExtendedJSONObject privateKey = o.getObject(BrowserIDKeyPair.JSON_KEY_PRIVATEKEY);
ExtendedJSONObject publicKey = o.getObject(BrowserIDKeyPair.JSON_KEY_PUBLICKEY);
if (privateKey == null) {
throw new InvalidKeySpecException("privateKey must not be null");
}
if (publicKey == null) {
throw new InvalidKeySpecException("publicKey must not be null");
}
return new BrowserIDKeyPair(createPrivateKey(privateKey), createPublicKey(publicKey));
} catch (NonObjectJSONException e) {
throw new InvalidKeySpecException("privateKey and publicKey must be JSON objects");
}
}
}

View file

@ -1,245 +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.browserid;
import org.json.simple.JSONObject;
import org.mozilla.apache.commons.codec.binary.Base64;
import org.mozilla.apache.commons.codec.binary.StringUtils;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.NonObjectJSONException;
import org.mozilla.gecko.sync.Utils;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.TreeMap;
/**
* Encode and decode JSON Web Tokens.
* <p>
* Reverse-engineered from the Node.js jwcrypto library at
* <a href="https://github.com/mozilla/jwcrypto">https://github.com/mozilla/jwcrypto</a>
* and informed by the informal draft standard "JSON Web Token (JWT)" at
* <a href="http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html">http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html</a>.
*/
public class JSONWebTokenUtils {
public static final long DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS = 60 * 60 * 1000;
public static final long DEFAULT_ASSERTION_DURATION_IN_MILLISECONDS = 60 * 60 * 1000;
public static final long DEFAULT_FUTURE_EXPIRES_AT_IN_MILLISECONDS = 9999999999999L;
public static final String DEFAULT_CERTIFICATE_ISSUER = "127.0.0.1";
public static final String DEFAULT_ASSERTION_ISSUER = "127.0.0.1";
public static String encode(String payload, SigningPrivateKey privateKey) throws UnsupportedEncodingException, GeneralSecurityException {
final ExtendedJSONObject header = new ExtendedJSONObject();
header.put("alg", privateKey.getAlgorithm());
String encodedHeader = Base64.encodeBase64URLSafeString(header.toJSONString().getBytes("UTF-8"));
String encodedPayload = Base64.encodeBase64URLSafeString(payload.getBytes("UTF-8"));
ArrayList<String> segments = new ArrayList<String>();
segments.add(encodedHeader);
segments.add(encodedPayload);
byte[] message = Utils.toDelimitedString(".", segments).getBytes("UTF-8");
byte[] signature = privateKey.signMessage(message);
segments.add(Base64.encodeBase64URLSafeString(signature));
return Utils.toDelimitedString(".", segments);
}
public static String decode(String token, VerifyingPublicKey publicKey) throws GeneralSecurityException, UnsupportedEncodingException {
if (token == null) {
throw new IllegalArgumentException("token must not be null");
}
String[] segments = token.split("\\.");
if (segments == null || segments.length != 3) {
throw new GeneralSecurityException("malformed token");
}
byte[] message = (segments[0] + "." + segments[1]).getBytes("UTF-8");
byte[] signature = Base64.decodeBase64(segments[2]);
boolean verifies = publicKey.verifyMessage(message, signature);
if (!verifies) {
throw new GeneralSecurityException("bad signature");
}
String payload = StringUtils.newStringUtf8(Base64.decodeBase64(segments[1]));
return payload;
}
/**
* Public for testing.
*/
@SuppressWarnings("unchecked")
public static String getPayloadString(String payloadString, String audience, String issuer,
Long issuedAt, long expiresAt) throws NonObjectJSONException, IOException {
ExtendedJSONObject payload;
if (payloadString != null) {
payload = new ExtendedJSONObject(payloadString);
} else {
payload = new ExtendedJSONObject();
}
if (audience != null) {
payload.put("aud", audience);
}
payload.put("iss", issuer);
if (issuedAt != null) {
payload.put("iat", issuedAt);
}
payload.put("exp", expiresAt);
// TreeMap so that keys are sorted. A small attempt to keep output stable over time.
return JSONObject.toJSONString(new TreeMap<Object, Object>(payload.object));
}
protected static String getCertificatePayloadString(VerifyingPublicKey publicKeyToSign, String email) throws NonObjectJSONException, IOException {
ExtendedJSONObject payload = new ExtendedJSONObject();
ExtendedJSONObject principal = new ExtendedJSONObject();
principal.put("email", email);
payload.put("principal", principal);
payload.put("public-key", publicKeyToSign.toJSONObject());
return payload.toJSONString();
}
public static String createCertificate(VerifyingPublicKey publicKeyToSign, String email,
String issuer, long issuedAt, long expiresAt, SigningPrivateKey privateKey) throws NonObjectJSONException, IOException, GeneralSecurityException {
String certificatePayloadString = getCertificatePayloadString(publicKeyToSign, email);
String payloadString = getPayloadString(certificatePayloadString, null, issuer, issuedAt, expiresAt);
return JSONWebTokenUtils.encode(payloadString, privateKey);
}
/**
* Create a Browser ID assertion.
*
* @param privateKeyToSignWith
* private key to sign assertion with.
* @param certificate
* to include in assertion; no attempt is made to ensure the
* certificate is valid, or corresponds to the private key, or any
* other condition.
* @param audience
* to produce assertion for.
* @param issuer
* to produce assertion for.
* @param issuedAt
* timestamp for assertion, in milliseconds since the epoch; if null,
* no timestamp is included.
* @param expiresAt
* expiration timestamp for assertion, in milliseconds since the epoch.
* @return assertion.
* @throws NonObjectJSONException
* @throws IOException
* @throws GeneralSecurityException
*/
public static String createAssertion(SigningPrivateKey privateKeyToSignWith, String certificate, String audience,
String issuer, Long issuedAt, long expiresAt) throws NonObjectJSONException, IOException, GeneralSecurityException {
String emptyAssertionPayloadString = "{}";
String payloadString = getPayloadString(emptyAssertionPayloadString, audience, issuer, issuedAt, expiresAt);
String signature = JSONWebTokenUtils.encode(payloadString, privateKeyToSignWith);
return certificate + "~" + signature;
}
/**
* For debugging only!
*
* @param input
* certificate to dump.
* @return non-null object with keys header, payload, signature if the
* certificate is well-formed.
*/
public static ExtendedJSONObject parseCertificate(String input) {
try {
String[] parts = input.split("\\.");
if (parts.length != 3) {
return null;
}
String cHeader = new String(Base64.decodeBase64(parts[0]));
String cPayload = new String(Base64.decodeBase64(parts[1]));
String cSignature = Utils.byte2Hex(Base64.decodeBase64(parts[2]));
ExtendedJSONObject o = new ExtendedJSONObject();
o.put("header", new ExtendedJSONObject(cHeader));
o.put("payload", new ExtendedJSONObject(cPayload));
o.put("signature", cSignature);
return o;
} catch (Exception e) {
return null;
}
}
/**
* For debugging only!
*
* @param input certificate to dump.
* @return true if the certificate is well-formed.
*/
public static boolean dumpCertificate(String input) {
ExtendedJSONObject c = parseCertificate(input);
try {
if (c == null) {
System.out.println("Malformed certificate -- got exception trying to dump contents.");
return false;
}
System.out.println("certificate header: " + c.getObject("header").toJSONString());
System.out.println("certificate payload: " + c.getObject("payload").toJSONString());
System.out.println("certificate signature: " + c.getString("signature"));
return true;
} catch (Exception e) {
System.out.println("Malformed certificate -- got exception trying to dump contents.");
return false;
}
}
/**
* For debugging only!
*
* @param input assertion to dump.
* @return true if the assertion is well-formed.
*/
public static ExtendedJSONObject parseAssertion(String input) {
try {
String[] parts = input.split("~");
if (parts.length != 2) {
return null;
}
String certificate = parts[0];
String assertion = parts[1];
parts = assertion.split("\\.");
if (parts.length != 3) {
return null;
}
String aHeader = new String(Base64.decodeBase64(parts[0]));
String aPayload = new String(Base64.decodeBase64(parts[1]));
String aSignature = Utils.byte2Hex(Base64.decodeBase64(parts[2]));
// We do all the assertion parsing *before* dumping the certificate in
// case there's a malformed assertion.
ExtendedJSONObject o = new ExtendedJSONObject();
o.put("header", new ExtendedJSONObject(aHeader));
o.put("payload", new ExtendedJSONObject(aPayload));
o.put("signature", aSignature);
o.put("certificate", certificate);
return o;
} catch (Exception e) {
return null;
}
}
/**
* For debugging only!
*
* @param input assertion to dump.
* @return true if the assertion is well-formed.
*/
public static boolean dumpAssertion(String input) {
ExtendedJSONObject a = parseAssertion(input);
try {
if (a == null) {
System.out.println("Malformed assertion -- got exception trying to dump contents.");
return false;
}
dumpCertificate(a.getString("certificate"));
System.out.println("assertion header: " + a.getObject("header").toJSONString());
System.out.println("assertion payload: " + a.getObject("payload").toJSONString());
System.out.println("assertion signature: " + a.getString("signature"));
return true;
} catch (Exception e) {
System.out.println("Malformed assertion -- got exception trying to dump contents.");
return false;
}
}
}

View file

@ -1,128 +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.browserid;
import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
/**
* Generate certificates and assertions backed by mockmyid.com's private key.
* <p>
* These artifacts are for testing only.
*/
public class MockMyIDTokenFactory {
public static final BigInteger MOCKMYID_x = new BigInteger("385cb3509f086e110c5e24bdd395a84b335a09ae", 16);
public static final BigInteger MOCKMYID_y = new BigInteger("738ec929b559b604a232a9b55a5295afc368063bb9c20fac4e53a74970a4db7956d48e4c7ed523405f629b4cc83062f13029c4d615bbacb8b97f5e56f0c7ac9bc1d4e23809889fa061425c984061fca1826040c399715ce7ed385c4dd0d402256912451e03452d3c961614eb458f188e3e8d2782916c43dbe2e571251ce38262", 16);
public static final BigInteger MOCKMYID_p = new BigInteger("ff600483db6abfc5b45eab78594b3533d550d9f1bf2a992a7a8daa6dc34f8045ad4e6e0c429d334eeeaaefd7e23d4810be00e4cc1492cba325ba81ff2d5a5b305a8d17eb3bf4a06a349d392e00d329744a5179380344e82a18c47933438f891e22aeef812d69c8f75e326cb70ea000c3f776dfdbd604638c2ef717fc26d02e17", 16);
public static final BigInteger MOCKMYID_q = new BigInteger("e21e04f911d1ed7991008ecaab3bf775984309c3", 16);
public static final BigInteger MOCKMYID_g = new BigInteger("c52a4a0ff3b7e61fdf1867ce84138369a6154f4afa92966e3c827e25cfa6cf508b90e5de419e1337e07a2e9e2a3cd5dea704d175f8ebf6af397d69e110b96afb17c7a03259329e4829b0d03bbc7896b15b4ade53e130858cc34d96269aa89041f409136c7242a38895c9d5bccad4f389af1d7a4bd1398bd072dffa896233397a", 16);
// Computed lazily by static <code>getMockMyIDPrivateKey</code>.
protected static SigningPrivateKey cachedMockMyIDPrivateKey;
public static SigningPrivateKey getMockMyIDPrivateKey() throws NoSuchAlgorithmException, InvalidKeySpecException {
if (cachedMockMyIDPrivateKey == null) {
cachedMockMyIDPrivateKey = DSACryptoImplementation.createPrivateKey(MOCKMYID_x, MOCKMYID_p, MOCKMYID_q, MOCKMYID_g);
}
return cachedMockMyIDPrivateKey;
}
/**
* Sign a public key asserting ownership of username@mockmyid.com with
* mockmyid.com's private key.
*
* @param publicKeyToSign
* public key to sign.
* @param username
* sign username@mockmyid.com
* @param issuedAt
* timestamp for certificate, in milliseconds since the epoch.
* @param expiresAt
* expiration timestamp for certificate, in milliseconds since the epoch.
* @return encoded certificate string.
* @throws Exception
*/
public String createMockMyIDCertificate(final VerifyingPublicKey publicKeyToSign, String username,
final long issuedAt, final long expiresAt)
throws Exception {
if (!username.endsWith("@mockmyid.com")) {
username = username + "@mockmyid.com";
}
SigningPrivateKey mockMyIdPrivateKey = getMockMyIDPrivateKey();
return JSONWebTokenUtils.createCertificate(publicKeyToSign, username, "mockmyid.com", issuedAt, expiresAt, mockMyIdPrivateKey);
}
/**
* Sign a public key asserting ownership of username@mockmyid.com with
* mockmyid.com's private key.
*
* @param publicKeyToSign
* public key to sign.
* @param username
* sign username@mockmyid.com
* @return encoded certificate string.
* @throws Exception
*/
public String createMockMyIDCertificate(final VerifyingPublicKey publicKeyToSign, final String username)
throws Exception {
long ciat = System.currentTimeMillis();
long cexp = ciat + JSONWebTokenUtils.DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS;
return createMockMyIDCertificate(publicKeyToSign, username, ciat, cexp);
}
/**
* Generate an assertion asserting ownership of username@mockmyid.com to a
* relying party. The underlying certificate is signed by mockymid.com's
* private key.
*
* @param keyPair
* to sign with.
* @param username
* sign username@mockmyid.com.
* @param certificateIssuedAt
* timestamp for certificate, in milliseconds since the epoch.
* @param certificateExpiresAt
* expiration timestamp for certificate, in milliseconds since the epoch.
* @param assertionIssuedAt
* timestamp for assertion, in milliseconds since the epoch; if null,
* no timestamp is included.
* @param assertionExpiresAt
* expiration timestamp for assertion, in milliseconds since the epoch.
* @return encoded assertion string.
* @throws Exception
*/
public String createMockMyIDAssertion(BrowserIDKeyPair keyPair, String username, String audience,
long certificateIssuedAt, long certificateExpiresAt,
Long assertionIssuedAt, long assertionExpiresAt)
throws Exception {
String certificate = createMockMyIDCertificate(keyPair.getPublic(), username,
certificateIssuedAt, certificateExpiresAt);
return JSONWebTokenUtils.createAssertion(keyPair.getPrivate(), certificate, audience,
JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER, assertionIssuedAt, assertionExpiresAt);
}
/**
* Generate an assertion asserting ownership of username@mockmyid.com to a
* relying party. The underlying certificate is signed by mockymid.com's
* private key.
*
* @param keyPair
* to sign with.
* @param username
* sign username@mockmyid.com.
* @return encoded assertion string.
* @throws Exception
*/
public String createMockMyIDAssertion(BrowserIDKeyPair keyPair, String username, String audience)
throws Exception {
long ciat = System.currentTimeMillis();
long cexp = ciat + JSONWebTokenUtils.DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS;
long aiat = ciat + 1;
long aexp = aiat + JSONWebTokenUtils.DEFAULT_ASSERTION_DURATION_IN_MILLISECONDS;
return createMockMyIDAssertion(keyPair, username, audience,
ciat, cexp, aiat, aexp);
}
}

View file

@ -1,182 +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.browserid;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.RSAPrivateKeySpec;
import java.security.spec.RSAPublicKeySpec;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.NonObjectJSONException;
public class RSACryptoImplementation {
public static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
/**
* Parameters are serialized as decimal strings. Hex-versus-decimal was
* reverse-engineered from what the Persona public verifier accepted. We
* expect to follow the JOSE/JWT spec as it solidifies, and that will probably
* mean unifying this base.
*/
protected static final int SERIALIZATION_BASE = 10;
protected static class RSAVerifyingPublicKey implements VerifyingPublicKey {
protected final RSAPublicKey publicKey;
public RSAVerifyingPublicKey(RSAPublicKey publicKey) {
this.publicKey = publicKey;
}
/**
* Serialize to a JSON object.
* <p>
* Parameters are serialized as decimal strings. Hex-versus-decimal was
* reverse-engineered from what the Persona public verifier accepted.
*/
@Override
public ExtendedJSONObject toJSONObject() {
ExtendedJSONObject o = new ExtendedJSONObject();
o.put("algorithm", "RS");
o.put("n", publicKey.getModulus().toString(SERIALIZATION_BASE));
o.put("e", publicKey.getPublicExponent().toString(SERIALIZATION_BASE));
return o;
}
@Override
public boolean verifyMessage(byte[] bytes, byte[] signature)
throws GeneralSecurityException {
final Signature signer = Signature.getInstance(SIGNATURE_ALGORITHM);
signer.initVerify(publicKey);
signer.update(bytes);
return signer.verify(signature);
}
}
protected static class RSASigningPrivateKey implements SigningPrivateKey {
protected final RSAPrivateKey privateKey;
public RSASigningPrivateKey(RSAPrivateKey privateKey) {
this.privateKey = privateKey;
}
@Override
public String getAlgorithm() {
return "RS" + (privateKey.getModulus().bitLength() + 7)/8;
}
/**
* Serialize to a JSON object.
* <p>
* Parameters are serialized as decimal strings. Hex-versus-decimal was
* reverse-engineered from what the Persona public verifier accepted.
*/
@Override
public ExtendedJSONObject toJSONObject() {
ExtendedJSONObject o = new ExtendedJSONObject();
o.put("algorithm", "RS");
o.put("n", privateKey.getModulus().toString(SERIALIZATION_BASE));
o.put("d", privateKey.getPrivateExponent().toString(SERIALIZATION_BASE));
return o;
}
@Override
public byte[] signMessage(byte[] bytes)
throws GeneralSecurityException {
final Signature signer = Signature.getInstance(SIGNATURE_ALGORITHM);
signer.initSign(privateKey);
signer.update(bytes);
return signer.sign();
}
}
public static BrowserIDKeyPair generateKeyPair(final int keysize) throws NoSuchAlgorithmException {
final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(keysize);
final KeyPair keyPair = keyPairGenerator.generateKeyPair();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
return new BrowserIDKeyPair(new RSASigningPrivateKey(privateKey), new RSAVerifyingPublicKey(publicKey));
}
public static SigningPrivateKey createPrivateKey(BigInteger n, BigInteger d) throws NoSuchAlgorithmException, InvalidKeySpecException {
if (n == null) {
throw new IllegalArgumentException("n must not be null");
}
if (d == null) {
throw new IllegalArgumentException("d must not be null");
}
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
KeySpec keySpec = new RSAPrivateKeySpec(n, d);
RSAPrivateKey privateKey = (RSAPrivateKey) keyFactory.generatePrivate(keySpec);
return new RSASigningPrivateKey(privateKey);
}
public static VerifyingPublicKey createPublicKey(BigInteger n, BigInteger e) throws NoSuchAlgorithmException, InvalidKeySpecException {
if (n == null) {
throw new IllegalArgumentException("n must not be null");
}
if (e == null) {
throw new IllegalArgumentException("e must not be null");
}
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
KeySpec keySpec = new RSAPublicKeySpec(n, e);
RSAPublicKey publicKey = (RSAPublicKey) keyFactory.generatePublic(keySpec);
return new RSAVerifyingPublicKey(publicKey);
}
public static SigningPrivateKey createPrivateKey(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
String algorithm = o.getString("algorithm");
if (!"RS".equals(algorithm)) {
throw new InvalidKeySpecException("algorithm must equal RS, was " + algorithm);
}
try {
BigInteger n = new BigInteger(o.getString("n"), SERIALIZATION_BASE);
BigInteger d = new BigInteger(o.getString("d"), SERIALIZATION_BASE);
return createPrivateKey(n, d);
} catch (NullPointerException | NumberFormatException e) {
throw new InvalidKeySpecException("n and d must be integers encoded as strings, base " + SERIALIZATION_BASE);
}
}
public static VerifyingPublicKey createPublicKey(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
String algorithm = o.getString("algorithm");
if (!"RS".equals(algorithm)) {
throw new InvalidKeySpecException("algorithm must equal RS, was " + algorithm);
}
try {
BigInteger n = new BigInteger(o.getString("n"), SERIALIZATION_BASE);
BigInteger e = new BigInteger(o.getString("e"), SERIALIZATION_BASE);
return createPublicKey(n, e);
} catch (NullPointerException | NumberFormatException e) {
throw new InvalidKeySpecException("n and e must be integers encoded as strings, base " + SERIALIZATION_BASE);
}
}
public static BrowserIDKeyPair fromJSONObject(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
try {
ExtendedJSONObject privateKey = o.getObject(BrowserIDKeyPair.JSON_KEY_PRIVATEKEY);
ExtendedJSONObject publicKey = o.getObject(BrowserIDKeyPair.JSON_KEY_PUBLICKEY);
if (privateKey == null) {
throw new InvalidKeySpecException("privateKey must not be null");
}
if (publicKey == null) {
throw new InvalidKeySpecException("publicKey must not be null");
}
return new BrowserIDKeyPair(createPrivateKey(privateKey), createPublicKey(publicKey));
} catch (NonObjectJSONException e) {
throw new InvalidKeySpecException("privateKey and publicKey must be JSON objects");
}
}
}

View file

@ -1,41 +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.browserid;
import java.security.GeneralSecurityException;
import org.mozilla.gecko.sync.ExtendedJSONObject;
public interface SigningPrivateKey {
/**
* Return the JSON Web Token "alg" header corresponding to this private key.
* <p>
* The header is used when formatting web tokens, and generally denotes the
* algorithm and an ad-hoc encoding of the key size.
*
* @return header.
*/
public String getAlgorithm();
/**
* Generate a JSON representation of a private key.
* <p>
* <b>This should only be used for debugging. No private keys should go over
* the wire at any time.</b>
*
* @param privateKey
* to represent.
* @return JSON representation.
*/
public ExtendedJSONObject toJSONObject();
/**
* Sign a message.
* @param message to sign.
* @return signature.
* @throws GeneralSecurityException
*/
public byte[] signMessage(byte[] message) throws GeneralSecurityException;
}

View file

@ -1,34 +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.browserid;
import java.security.GeneralSecurityException;
import org.mozilla.gecko.sync.ExtendedJSONObject;
public interface VerifyingPublicKey {
/**
* Generate a JSON representation of a public key.
*
* @param publicKey
* to represent.
* @return JSON representation.
*/
public ExtendedJSONObject toJSONObject();
/**
* Verify a signature.
*
* @param message
* to verify signature of.
* @param signature
* to verify.
* @return true if signature is a signature of message produced by the private
* key corresponding to this public key.
* @throws GeneralSecurityException
*/
public boolean verifyMessage(byte[] message, byte[] signature) throws GeneralSecurityException;
}

View file

@ -1,95 +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.browserid.verifier;
import java.io.IOException;
import java.net.URI;
import java.security.GeneralSecurityException;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.browserid.verifier.BrowserIDVerifierException.BrowserIDVerifierErrorResponseException;
import org.mozilla.gecko.browserid.verifier.BrowserIDVerifierException.BrowserIDVerifierMalformedResponseException;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.net.BaseResourceDelegate;
import org.mozilla.gecko.sync.net.Resource;
import org.mozilla.gecko.sync.net.SyncResponse;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.client.ClientProtocolException;
public abstract class AbstractBrowserIDRemoteVerifierClient implements BrowserIDVerifierClient {
public static final String LOG_TAG = AbstractBrowserIDRemoteVerifierClient.class.getSimpleName();
protected static class RemoteVerifierResourceDelegate extends BaseResourceDelegate {
private final BrowserIDVerifierDelegate delegate;
protected RemoteVerifierResourceDelegate(Resource resource, BrowserIDVerifierDelegate delegate) {
super(resource);
this.delegate = delegate;
}
@Override
public String getUserAgent() {
return null;
}
@Override
public void handleHttpResponse(HttpResponse response) {
SyncResponse res = new SyncResponse(response);
int statusCode = res.getStatusCode();
Logger.debug(LOG_TAG, "Got response with status code " + statusCode + ".");
if (statusCode != 200) {
delegate.handleError(new BrowserIDVerifierErrorResponseException("Expected status code 200."));
return;
}
ExtendedJSONObject o = null;
try {
o = res.jsonObjectBody();
} catch (Exception e) {
delegate.handleError(new BrowserIDVerifierMalformedResponseException(e));
return;
}
String status = o.getString("status");
if ("failure".equals(status)) {
delegate.handleFailure(o);
return;
}
if (!("okay".equals(status))) {
delegate.handleError(new BrowserIDVerifierMalformedResponseException("Expected status okay, got '" + status + "'."));
return;
}
delegate.handleSuccess(o);
}
@Override
public void handleTransportException(GeneralSecurityException e) {
Logger.warn(LOG_TAG, "Got transport exception.", e);
delegate.handleError(e);
}
@Override
public void handleHttpProtocolException(ClientProtocolException e) {
Logger.warn(LOG_TAG, "Got protocol exception.", e);
delegate.handleError(e);
}
@Override
public void handleHttpIOException(IOException e) {
Logger.warn(LOG_TAG, "Got IO exception.", e);
delegate.handleError(e);
}
}
protected final URI verifierUri;
public AbstractBrowserIDRemoteVerifierClient(URI verifierUri) {
this.verifierUri = verifierUri;
}
}

View file

@ -1,62 +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.browserid.verifier;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.List;
import org.mozilla.gecko.sync.net.BaseResource;
import ch.boye.httpclientandroidlib.NameValuePair;
import ch.boye.httpclientandroidlib.client.entity.UrlEncodedFormEntity;
import ch.boye.httpclientandroidlib.message.BasicNameValuePair;
/**
* The verifier protocol changed: version 1 posts form-encoded data; version 2
* posts JSON data.
*/
public class BrowserIDRemoteVerifierClient10 extends AbstractBrowserIDRemoteVerifierClient {
public static final String LOG_TAG = BrowserIDRemoteVerifierClient10.class.getSimpleName();
public static final String DEFAULT_VERIFIER_URL = "https://verifier.login.persona.org/verify";
public BrowserIDRemoteVerifierClient10() throws URISyntaxException {
super(new URI(DEFAULT_VERIFIER_URL));
}
public BrowserIDRemoteVerifierClient10(URI verifierUri) {
super(verifierUri);
}
@Override
public void verify(String audience, String assertion, final BrowserIDVerifierDelegate delegate) {
if (audience == null) {
throw new IllegalArgumentException("audience cannot be null.");
}
if (assertion == null) {
throw new IllegalArgumentException("assertion cannot be null.");
}
if (delegate == null) {
throw new IllegalArgumentException("delegate cannot be null.");
}
BaseResource r = new BaseResource(verifierUri);
r.delegate = new RemoteVerifierResourceDelegate(r, delegate);
List<NameValuePair> nvps = Arrays.asList(new NameValuePair[] {
new BasicNameValuePair("audience", audience),
new BasicNameValuePair("assertion", assertion) });
try {
r.post(new UrlEncodedFormEntity(nvps, "UTF-8"));
} catch (UnsupportedEncodingException e) {
delegate.handleError(e);
}
}
}

View file

@ -1,58 +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.browserid.verifier;
import java.net.URI;
import java.net.URISyntaxException;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.net.BaseResource;
/**
* The verifier protocol changed: version 1 posts form-encoded data; version 2
* posts JSON data.
*/
public class BrowserIDRemoteVerifierClient20 extends AbstractBrowserIDRemoteVerifierClient {
public static final String LOG_TAG = BrowserIDRemoteVerifierClient20.class.getSimpleName();
public static final String DEFAULT_VERIFIER_URL = "https://verifier.accounts.firefox.com/v2";
protected static final String JSON_KEY_ASSERTION = "assertion";
protected static final String JSON_KEY_AUDIENCE = "audience";
public BrowserIDRemoteVerifierClient20() throws URISyntaxException {
super(new URI(DEFAULT_VERIFIER_URL));
}
public BrowserIDRemoteVerifierClient20(URI verifierUri) {
super(verifierUri);
}
@Override
public void verify(String audience, String assertion, final BrowserIDVerifierDelegate delegate) {
if (audience == null) {
throw new IllegalArgumentException("audience cannot be null.");
}
if (assertion == null) {
throw new IllegalArgumentException("assertion cannot be null.");
}
if (delegate == null) {
throw new IllegalArgumentException("delegate cannot be null.");
}
BaseResource r = new BaseResource(verifierUri);
r.delegate = new RemoteVerifierResourceDelegate(r, delegate);
final ExtendedJSONObject requestBody = new ExtendedJSONObject();
requestBody.put(JSON_KEY_AUDIENCE, audience);
requestBody.put(JSON_KEY_ASSERTION, assertion);
try {
r.post(requestBody);
} catch (Exception e) {
delegate.handleError(e);
}
}
}

View file

@ -1,9 +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.browserid.verifier;
public interface BrowserIDVerifierClient {
public abstract void verify(String audience, String assertion, BrowserIDVerifierDelegate delegate);
}

View file

@ -1,13 +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.browserid.verifier;
import org.mozilla.gecko.sync.ExtendedJSONObject;
public interface BrowserIDVerifierDelegate {
void handleSuccess(ExtendedJSONObject response);
void handleFailure(ExtendedJSONObject response);
void handleError(Exception e);
}

View file

@ -1,41 +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.browserid.verifier;
public class BrowserIDVerifierException extends Exception {
private static final long serialVersionUID = 2228946910754889975L;
public BrowserIDVerifierException(String detailMessage) {
super(detailMessage);
}
public BrowserIDVerifierException(Throwable throwable) {
super(throwable);
}
public static class BrowserIDVerifierMalformedResponseException extends BrowserIDVerifierException {
private static final long serialVersionUID = 115377527009652839L;
public BrowserIDVerifierMalformedResponseException(String detailMessage) {
super(detailMessage);
}
public BrowserIDVerifierMalformedResponseException(Throwable throwable) {
super(throwable);
}
}
public static class BrowserIDVerifierErrorResponseException extends BrowserIDVerifierException {
private static final long serialVersionUID = 115377527009652840L;
public BrowserIDVerifierErrorResponseException(String detailMessage) {
super(detailMessage);
}
public BrowserIDVerifierErrorResponseException(Throwable throwable) {
super(throwable);
}
}
}

View file

@ -1,227 +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;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
import android.os.Looper;
import android.content.AsyncTaskLoader;
import android.support.v4.content.LocalBroadcastManager;
import java.lang.ref.WeakReference;
/**
* A Loader that queries and updates based on the existence of Firefox and
* legacy Sync Android Accounts.
*
* The loader returns an Android Account (of either Account type) if an account
* exists, and null to indicate no Account is present.
*
* The loader listens for Accounts added and deleted, and also Accounts being
* updated by Sync or another Activity, via the use of
* {@link AndroidFxAccount#setState(org.mozilla.gecko.fxa.login.State)}.
* Be careful of message loops if you update the account state from an activity
* that uses this loader.
*
* This implementation is based on
* <a href="http://www.androiddesignpatterns.com/2012/08/implementing-loaders.html">http://www.androiddesignpatterns.com/2012/08/implementing-loaders.html</a>.
*/
public class AccountLoader extends AsyncTaskLoader<Account> {
protected Account account = null;
protected BroadcastReceiver broadcastReceiver = null;
// Hold a weak reference to AccountLoader instance in this Runnable to avoid potentially leaking it
// after posting to a Handler in the BroadcastReceiver returned from makeNewObserver.
private final BroadcastReceiverRunnable broadcastReceiverRunnable = new BroadcastReceiverRunnable(this);
public AccountLoader(final Context context) {
super(context);
}
// Task that performs the asynchronous load.
@Override
public Account loadInBackground() {
return FirefoxAccounts.getFirefoxAccount(getContext());
}
// Deliver the results to the registered listener.
@Override
public void deliverResult(Account data) {
if (isReset()) {
// The Loader has been reset; ignore the result and invalidate the data.
releaseResources(data);
return;
}
// Hold a reference to the old data so it doesn't get garbage collected.
// We must protect it until the new data has been delivered.
Account oldData = account;
account = data;
if (isStarted()) {
// If the Loader is in a started state, deliver the results to the
// client. The superclass method does this for us.
super.deliverResult(data);
}
// Invalidate the old data as we don't need it any more.
if (oldData != null && oldData != data) {
releaseResources(oldData);
}
}
// The Loaders state-dependent behavior.
@Override
protected void onStartLoading() {
if (account != null) {
// Deliver any previously loaded data immediately.
deliverResult(account);
}
// Begin monitoring the underlying data source.
if (broadcastReceiver == null) {
broadcastReceiver = makeNewObserver();
registerLocalObserver(getContext(), broadcastReceiver);
registerSystemObserver(getContext(), broadcastReceiver);
}
if (takeContentChanged() || account == null) {
// When the observer detects a change, it should call onContentChanged()
// on the Loader, which will cause the next call to takeContentChanged()
// to return true. If this is ever the case (or if the current data is
// null), we force a new load.
forceLoad();
}
}
@Override
protected void onStopLoading() {
// The Loader is in a stopped state, so we should attempt to cancel the
// current load (if there is one).
cancelLoad();
// Note that we leave the observer as is. Loaders in a stopped state
// should still monitor the data source for changes so that the Loader
// will know to force a new load if it is ever started again.
}
@Override
protected void onReset() {
// Ensure the loader has been stopped. In CursorLoader and the template
// this code follows (see the class comment), this is onStopLoading, which
// appears to not set the started flag (see Loader itself).
stopLoading();
// At this point we can release the resources associated with 'mData'.
if (account != null) {
releaseResources(account);
account = null;
}
// The Loader is being reset, so we should stop monitoring for changes.
if (broadcastReceiver != null) {
final BroadcastReceiver observer = broadcastReceiver;
broadcastReceiver = null;
unregisterObserver(getContext(), observer);
}
}
@Override
public void onCanceled(final Account data) {
// Attempt to cancel the current asynchronous load.
super.onCanceled(data);
// The load has been canceled, so we should release the resources
// associated with 'data'.
releaseResources(data);
}
// Observer which receives notifications when the data changes.
protected BroadcastReceiver makeNewObserver() {
return new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// onContentChanged must be called on the main thread.
// If we're already on the main thread, call it directly.
if (Looper.myLooper() == Looper.getMainLooper()) {
onContentChanged();
return;
}
// Otherwise, post a Runnable to a Handler bound to the main thread's message loop.
final Handler mainHandler = new Handler(Looper.getMainLooper());
mainHandler.post(broadcastReceiverRunnable);
}
};
}
private static class BroadcastReceiverRunnable implements Runnable {
private final WeakReference<AccountLoader> accountLoaderWeakReference;
public BroadcastReceiverRunnable(final AccountLoader accountLoader) {
accountLoaderWeakReference = new WeakReference<>(accountLoader);
}
@Override
public void run() {
final AccountLoader accountLoader = accountLoaderWeakReference.get();
if (accountLoader != null) {
accountLoader.onContentChanged();
}
}
}
private void releaseResources(Account data) {
// For a simple List, there is nothing to do. For something like a Cursor, we
// would close it in this method. All resources associated with the Loader
// should be released here.
}
/**
* Register provided observer with the LocalBroadcastManager to listen for internal events.
*
* @param context <code>Context</code> to use for obtaining LocalBroadcastManager instance.
* @param observer <code>BroadcastReceiver</code> which will handle local events.
*/
protected static void registerLocalObserver(final Context context, final BroadcastReceiver observer) {
final IntentFilter intentFilter = new IntentFilter();
// Firefox Account internal state changed.
intentFilter.addAction(FxAccountConstants.ACCOUNT_STATE_CHANGED_ACTION);
// Firefox Account profile state changed.
intentFilter.addAction(FxAccountConstants.ACCOUNT_PROFILE_JSON_UPDATED_ACTION);
LocalBroadcastManager.getInstance(context).registerReceiver(observer, intentFilter);
}
/**
* Register provided observer for handling system-wide broadcasts.
*
* @param context <code>Context</code> to use for registering a receiver.
* @param observer <code>BroadcastReceiver</code> which will handle system events.
*/
protected static void registerSystemObserver(final Context context, final BroadcastReceiver observer) {
context.registerReceiver(observer,
// Android Account added or removed.
new IntentFilter(AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION),
// No broadcast permissions required.
null,
// Null handler ensures that broadcasts will be handled on the main thread.
null
);
}
protected static void unregisterObserver(final Context context, final BroadcastReceiver observer) {
LocalBroadcastManager.getInstance(context).unregisterReceiver(observer);
context.unregisterReceiver(observer);
}
}

View file

@ -1,222 +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;
import java.io.File;
import java.util.concurrent.CountDownLatch;
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.FxAccountSyncStatusHelper;
import org.mozilla.gecko.sync.ThreadPool;
import org.mozilla.gecko.sync.Utils;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.ContentResolver;
import android.content.Context;
import android.os.Bundle;
/**
* Simple public accessors for Firefox account objects.
*/
public class FirefoxAccounts {
private static final String LOG_TAG = FirefoxAccounts.class.getSimpleName();
/**
* Returns true if a FirefoxAccount exists, false otherwise.
*
* @param context Android context.
* @return true if at least one Firefox account exists.
*/
public static boolean firefoxAccountsExist(final Context context) {
return getFirefoxAccounts(context).length > 0;
}
/**
* Return Firefox accounts.
* <p>
* If no accounts exist in the AccountManager, one may be created
* via a pickled FirefoxAccount, if available, and that account
* will be added to the AccountManager and returned.
* <p>
* Note that this can be called from any thread.
*
* @param context Android context.
* @return Firefox account objects.
*/
public static Account[] getFirefoxAccounts(final Context context) {
final Account[] accounts =
AccountManager.get(context).getAccountsByType(FxAccountConstants.ACCOUNT_TYPE);
if (accounts.length > 0) {
return accounts;
}
final Account pickledAccount = getPickledAccount(context);
return (pickledAccount != null) ? new Account[] {pickledAccount} : new Account[0];
}
private static Account getPickledAccount(final Context context) {
// To avoid a StrictMode violation for disk access, we call this from a background thread.
// We do this every time, so the caller doesn't have to care.
final CountDownLatch latch = new CountDownLatch(1);
final Account[] accounts = new Account[1];
ThreadPool.run(new Runnable() {
@Override
public void run() {
try {
final File file = context.getFileStreamPath(FxAccountConstants.ACCOUNT_PICKLE_FILENAME);
if (!file.exists()) {
accounts[0] = null;
return;
}
// There is a small race window here: if the user creates a new Firefox account
// between our checks, this could erroneously report that no Firefox accounts
// exist.
final AndroidFxAccount fxAccount =
AccountPickler.unpickle(context, FxAccountConstants.ACCOUNT_PICKLE_FILENAME);
accounts[0] = fxAccount != null ? fxAccount.getAndroidAccount() : null;
} finally {
latch.countDown();
}
}
});
try {
latch.await(); // Wait for the background thread to return.
} catch (InterruptedException e) {
Logger.warn(LOG_TAG,
"Foreground thread unexpectedly interrupted while getting pickled account", e);
return null;
}
return accounts[0];
}
/**
* @param context Android context.
* @return the configured Firefox account if one exists, or null otherwise.
*/
public static Account getFirefoxAccount(final Context context) {
Account[] accounts = getFirefoxAccounts(context);
if (accounts.length > 0) {
return accounts[0];
}
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;
}
public static void logSyncOptions(Bundle syncOptions) {
final boolean scheduleNow = syncOptions.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false);
Logger.info(LOG_TAG, "Sync options -- scheduling now: " + scheduleNow);
}
public static void requestImmediateSync(final Account account, String[] stagesToSync, String[] stagesToSkip) {
final Bundle syncOptions = new Bundle();
syncOptions.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true);
syncOptions.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
requestSync(account, syncOptions, stagesToSync, stagesToSkip);
}
public static void requestEventualSync(final Account account, String[] stagesToSync, String[] stagesToSkip) {
requestSync(account, Bundle.EMPTY, stagesToSync, stagesToSkip);
}
/**
* Request a sync for the given Android Account.
* <p>
* Any hints are strictly optional: the actual requested sync is scheduled by
* the Android sync scheduler, and the sync mechanism may ignore hints as it
* sees fit.
* <p>
* It is safe to call this method from any thread.
*
* @param account to sync.
* @param syncOptions to pass to sync.
* @param stagesToSync stage names to sync.
* @param stagesToSkip stage names to skip.
*/
protected static void requestSync(final Account account, final Bundle syncOptions, String[] stagesToSync, String[] stagesToSkip) {
if (account == null) {
throw new IllegalArgumentException("account must not be null");
}
if (syncOptions == null) {
throw new IllegalArgumentException("syncOptions must not be null");
}
Utils.putStageNamesToSync(syncOptions, stagesToSync, stagesToSkip);
Logger.info(LOG_TAG, "Requesting sync.");
logSyncOptions(syncOptions);
// We get strict mode warnings on some devices, so make the request on a
// background thread.
ThreadPool.run(new Runnable() {
@Override
public void run() {
for (String authority : AndroidFxAccount.DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP.keySet()) {
ContentResolver.requestSync(account, authority, syncOptions);
}
}
});
}
/**
* Start notifying <code>syncStatusListener</code> of sync status changes.
* <p>
* Only a weak reference to <code>syncStatusListener</code> is held.
*
* @param syncStatusListener to start notifying.
*/
public static void addSyncStatusListener(SyncStatusListener syncStatusListener) {
// startObserving null-checks its argument.
FxAccountSyncStatusHelper.getInstance().startObserving(syncStatusListener);
}
/**
* Stop notifying <code>syncStatusListener</code> of sync status changes.
*
* @param syncStatusListener to stop notifying.
*/
public static void removeSyncStatusListener(SyncStatusListener syncStatusListener) {
// stopObserving null-checks its argument.
FxAccountSyncStatusHelper.getInstance().stopObserving(syncStatusListener);
}
}

View file

@ -1,75 +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;
import org.mozilla.gecko.AppConstants;
public class FxAccountConstants {
public static final String GLOBAL_LOG_TAG = "FxAccounts";
public static final String ACCOUNT_TYPE = AppConstants.MOZ_ANDROID_SHARED_FXACCOUNT_TYPE;
// Must be a client ID allocated with "canGrant" privileges!
public static final String OAUTH_CLIENT_ID_FENNEC = "3332a18d142636cb";
public static final String DEFAULT_AUTH_SERVER_ENDPOINT = "https://api.accounts.firefox.com/v1";
public static final String DEFAULT_TOKEN_SERVER_ENDPOINT = "https://token.services.mozilla.com/1.0/sync/1.5";
public static final String DEFAULT_OAUTH_SERVER_ENDPOINT = "https://oauth.accounts.firefox.com/v1";
public static final String DEFAULT_PROFILE_SERVER_ENDPOINT = "https://profile.accounts.firefox.com/v1";
public static final String STAGE_AUTH_SERVER_ENDPOINT = "https://stable.dev.lcip.org/auth/v1";
public static final String STAGE_TOKEN_SERVER_ENDPOINT = "https://stable.dev.lcip.org/syncserver/token/1.0/sync/1.5";
public static final String STAGE_OAUTH_SERVER_ENDPOINT = "https://oauth-stable.dev.lcip.org/v1";
public static final String STAGE_PROFILE_SERVER_ENDPOINT = "https://latest.dev.lcip.org/profile/v1";
// Action to update on cached profile information.
public static final String ACCOUNT_PROFILE_JSON_UPDATED_ACTION = "org.mozilla.gecko.fxa.profile.JSON.updated";
// You must be at least 13 years old, on the day of creation, to create a Firefox Account.
public static final int MINIMUM_AGE_TO_CREATE_AN_ACCOUNT = 13;
// Key for avatar URI in profile JSON.
public static final String KEY_PROFILE_JSON_AVATAR = "avatar";
// Key for username in profile JSON.
public static final String KEY_PROFILE_JSON_USERNAME = "displayName";
// You must wait 15 minutes after failing an age check before trying to create a different account.
public static final long MINIMUM_TIME_TO_WAIT_AFTER_AGE_CHECK_FAILED_IN_MILLISECONDS = 15 * 60 * 1000;
public static final String USER_AGENT = "Firefox-Android-FxAccounts/" + AppConstants.MOZ_APP_VERSION + " (" + AppConstants.MOZ_APP_UA_NAME + ")";
public static final String ACCOUNT_PICKLE_FILENAME = "fxa.account.json";
/**
* Version number of contents of SYNC_ACCOUNT_DELETED_ACTION intent.
*/
public static final long ACCOUNT_DELETED_INTENT_VERSION = 1;
public static final String ACCOUNT_DELETED_INTENT_VERSION_KEY = "account_deleted_intent_version";
public static final String ACCOUNT_DELETED_INTENT_ACCOUNT_KEY = "account_deleted_intent_account";
public static final String ACCOUNT_DELETED_INTENT_ACCOUNT_PROFILE = "account_deleted_intent_profile";
public static final String ACCOUNT_OAUTH_SERVICE_ENDPOINT_KEY = "account_oauth_service_endpoint";
public static final String ACCOUNT_DELETED_INTENT_ACCOUNT_AUTH_TOKENS = "account_deleted_intent_auth_tokens";
/**
* This action is broadcast when an Android Firefox Account's internal state
* is changed.
* <p>
* It is protected by signing-level permission PER_ACCOUNT_TYPE_PERMISSION and
* can be received only by Firefox versions sharing the same Android Firefox
* Account type.
*/
public static final String ACCOUNT_STATE_CHANGED_ACTION = AppConstants.MOZ_ANDROID_SHARED_FXACCOUNT_TYPE + ".accounts.ACCOUNT_STATE_CHANGED_ACTION";
public static final String ACTION_FXA_CONFIRM_ACCOUNT = AppConstants.ANDROID_PACKAGE_NAME + ".ACTION_FXA_CONFIRM_ACCOUNT";
public static final String ACTION_FXA_FINISH_MIGRATING = AppConstants.ANDROID_PACKAGE_NAME + ".ACTION_FXA_FINISH_MIGRATING";
public static final String ACTION_FXA_GET_STARTED = AppConstants.ANDROID_PACKAGE_NAME + ".ACTION_FXA_GET_STARTED";
public static final String ACTION_FXA_STATUS = AppConstants.ANDROID_PACKAGE_NAME + ".ACTION_FXA_STATUS";
public static final String ACTION_FXA_UPDATE_CREDENTIALS = AppConstants.ANDROID_PACKAGE_NAME + ".ACTION_FXA_UPDATE_CREDENTIALS";
public static final String ENDPOINT_PREFERENCES = "preferences";
public static final String ENDPOINT_NOTIFICATION = "notification";
public static final String ENDPOINT_FIRSTRUN = "firstrun";
}

View file

@ -1,81 +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;
import org.mozilla.gecko.sync.ExtendedJSONObject;
public class FxAccountDevice {
public static final String JSON_KEY_NAME = "name";
public static final String JSON_KEY_ID = "id";
public static final String JSON_KEY_TYPE = "type";
public static final String JSON_KEY_ISCURRENTDEVICE = "isCurrentDevice";
public static final String JSON_KEY_PUSH_CALLBACK = "pushCallback";
public static final String JSON_KEY_PUSH_PUBLICKEY = "pushPublicKey";
public static final String JSON_KEY_PUSH_AUTHKEY = "pushAuthKey";
public final String id;
public final String name;
public final String type;
public final Boolean isCurrentDevice;
public final String pushCallback;
public final String pushPublicKey;
public final String pushAuthKey;
public FxAccountDevice(String name, String id, String type, Boolean isCurrentDevice,
String pushCallback, String pushPublicKey, String pushAuthKey) {
this.name = name;
this.id = id;
this.type = type;
this.isCurrentDevice = isCurrentDevice;
this.pushCallback = pushCallback;
this.pushPublicKey = pushPublicKey;
this.pushAuthKey = pushAuthKey;
}
public static FxAccountDevice forRegister(String name, String type, String pushCallback,
String pushPublicKey, String pushAuthKey) {
return new FxAccountDevice(name, null, type, null, pushCallback, pushPublicKey, pushAuthKey);
}
public static FxAccountDevice forUpdate(String id, String name, String pushCallback,
String pushPublicKey, String pushAuthKey) {
return new FxAccountDevice(name, id, null, null, pushCallback, pushPublicKey, pushAuthKey);
}
public static FxAccountDevice fromJson(ExtendedJSONObject json) {
String name = json.getString(JSON_KEY_NAME);
String id = json.getString(JSON_KEY_ID);
String type = json.getString(JSON_KEY_TYPE);
Boolean isCurrentDevice = json.getBoolean(JSON_KEY_ISCURRENTDEVICE);
String pushCallback = json.getString(JSON_KEY_PUSH_CALLBACK);
String pushPublicKey = json.getString(JSON_KEY_PUSH_PUBLICKEY);
String pushAuthKey = json.getString(JSON_KEY_PUSH_AUTHKEY);
return new FxAccountDevice(name, id, type, isCurrentDevice, pushCallback, pushPublicKey, pushAuthKey);
}
public ExtendedJSONObject toJson() {
final ExtendedJSONObject body = new ExtendedJSONObject();
if (this.name != null) {
body.put(JSON_KEY_NAME, this.name);
}
if (this.id != null) {
body.put(JSON_KEY_ID, this.id);
}
if (this.type != null) {
body.put(JSON_KEY_TYPE, this.type);
}
if (this.pushCallback != null) {
body.put(JSON_KEY_PUSH_CALLBACK, this.pushCallback);
}
if (this.pushPublicKey != null) {
body.put(JSON_KEY_PUSH_PUBLICKEY, this.pushPublicKey);
}
if (this.pushAuthKey != null) {
body.put(JSON_KEY_PUSH_AUTHKEY, this.pushAuthKey);
}
return body;
}
}

View file

@ -1,282 +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;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClient;
import org.mozilla.gecko.background.fxa.FxAccountClient20;
import org.mozilla.gecko.background.fxa.FxAccountClient20.AccountStatusResponse;
import org.mozilla.gecko.background.fxa.FxAccountClient20.RequestDelegate;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
import org.mozilla.gecko.background.fxa.FxAccountRemoteError;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount.InvalidFxAState;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
import org.mozilla.gecko.util.BundleEventListener;
import org.mozilla.gecko.util.EventCallback;
import java.io.UnsupportedEncodingException;
import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.GeneralSecurityException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/* This class provides a way to register the current device against FxA
* and also stores the registration details in the Android FxAccount.
* This should be used in a state where we possess a sessionToken, most likely the Married state.
*/
public class FxAccountDeviceRegistrator implements BundleEventListener {
private static final String LOG_TAG = "FxADeviceRegistrator";
// The current version of the device registration, we use this to re-register
// devices after we update what we send on device registration.
public static final Integer DEVICE_REGISTRATION_VERSION = 2;
private static FxAccountDeviceRegistrator instance;
private final WeakReference<Context> context;
private FxAccountDeviceRegistrator(Context appContext) {
this.context = new WeakReference<Context>(appContext);
}
private static FxAccountDeviceRegistrator getInstance(Context appContext) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
if (instance == null) {
FxAccountDeviceRegistrator tempInstance = new FxAccountDeviceRegistrator(appContext);
tempInstance.setupListeners(); // Set up listener for FxAccountPush:Subscribe:Response
instance = tempInstance;
}
return instance;
}
public static void register(Context context) {
Context appContext = context.getApplicationContext();
try {
getInstance(appContext).beginRegistration(appContext);
} catch (Exception e) {
Log.e(LOG_TAG, "Could not start FxA device registration", e);
}
}
private void beginRegistration(Context context) {
// Fire up gecko and send event
// We create the Intent ourselves instead of using GeckoService.getIntentToCreateServices
// because we can't import these modules (circular dependency between browser and services)
final Intent geckoIntent = new Intent();
geckoIntent.setAction("create-services");
geckoIntent.setClassName(context, "org.mozilla.gecko.GeckoService");
geckoIntent.putExtra("category", "android-push-service");
geckoIntent.putExtra("data", "android-fxa-subscribe");
final AndroidFxAccount fxAccount = AndroidFxAccount.fromContext(context);
geckoIntent.putExtra("org.mozilla.gecko.intent.PROFILE_NAME", fxAccount.getProfile());
context.startService(geckoIntent);
// -> handleMessage()
}
@Override
public void handleMessage(String event, Bundle message, EventCallback callback) {
if ("FxAccountsPush:Subscribe:Response".equals(event)) {
try {
doFxaRegistration(message.getBundle("subscription"));
} catch (InvalidFxAState e) {
Log.d(LOG_TAG, "Invalid state when trying to register with FxA ", e);
}
} else {
Log.e(LOG_TAG, "No action defined for " + event);
}
}
private void doFxaRegistration(Bundle subscription) throws InvalidFxAState {
final Context context = this.context.get();
if (this.context == null) {
throw new IllegalStateException("Application context has been gc'ed");
}
doFxaRegistration(context, subscription, true);
}
private static void doFxaRegistration(final Context context, final Bundle subscription, final boolean allowRecursion) throws InvalidFxAState {
String pushCallback = subscription.getString("pushCallback");
String pushPublicKey = subscription.getString("pushPublicKey");
String pushAuthKey = subscription.getString("pushAuthKey");
final AndroidFxAccount fxAccount = AndroidFxAccount.fromContext(context);
if (fxAccount == null) {
Log.e(LOG_TAG, "AndroidFxAccount is null");
return;
}
final byte[] sessionToken = fxAccount.getSessionToken();
final FxAccountDevice device;
String deviceId = fxAccount.getDeviceId();
String clientName = getClientName(fxAccount, context);
if (TextUtils.isEmpty(deviceId)) {
Log.i(LOG_TAG, "Attempting registration for a new device");
device = FxAccountDevice.forRegister(clientName, "mobile", pushCallback, pushPublicKey, pushAuthKey);
} else {
Log.i(LOG_TAG, "Attempting registration for an existing device");
Logger.pii(LOG_TAG, "Device ID: " + deviceId);
device = FxAccountDevice.forUpdate(deviceId, clientName, pushCallback, pushPublicKey, pushAuthKey);
}
ExecutorService executor = Executors.newSingleThreadExecutor(); // Not called often, it's okay to spawn another thread
final FxAccountClient20 fxAccountClient =
new FxAccountClient20(fxAccount.getAccountServerURI(), executor);
fxAccountClient.registerOrUpdateDevice(sessionToken, device, new RequestDelegate<FxAccountDevice>() {
@Override
public void handleError(Exception e) {
Log.e(LOG_TAG, "Error while updating a device registration: ", e);
}
@Override
public void handleFailure(FxAccountClientRemoteException error) {
Log.e(LOG_TAG, "Error while updating a device registration: ", error);
if (error.httpStatusCode == 400) {
if (error.apiErrorNumber == FxAccountRemoteError.UNKNOWN_DEVICE) {
recoverFromUnknownDevice(fxAccount);
} else if (error.apiErrorNumber == FxAccountRemoteError.DEVICE_SESSION_CONFLICT) {
recoverFromDeviceSessionConflict(error, fxAccountClient, sessionToken, fxAccount, context,
subscription, allowRecursion);
}
} else
if (error.httpStatusCode == 401
&& error.apiErrorNumber == FxAccountRemoteError.INVALID_AUTHENTICATION_TOKEN) {
handleTokenError(error, fxAccountClient, fxAccount);
} else {
logErrorAndResetDeviceRegistrationVersion(error, fxAccount);
}
}
@Override
public void handleSuccess(FxAccountDevice result) {
Log.i(LOG_TAG, "Device registration complete");
Logger.pii(LOG_TAG, "Registered device ID: " + result.id);
fxAccount.setFxAUserData(result.id, DEVICE_REGISTRATION_VERSION);
}
});
}
private static void logErrorAndResetDeviceRegistrationVersion(
final FxAccountClientRemoteException error, final AndroidFxAccount fxAccount) {
Log.e(LOG_TAG, "Device registration failed", error);
fxAccount.resetDeviceRegistrationVersion();
}
@Nullable
private static String getClientName(final AndroidFxAccount fxAccount, final Context context) {
try {
SharedPreferencesClientsDataDelegate clientsDataDelegate =
new SharedPreferencesClientsDataDelegate(fxAccount.getSyncPrefs(), context);
return clientsDataDelegate.getClientName();
} catch (UnsupportedEncodingException | GeneralSecurityException e) {
Log.e(LOG_TAG, "Unable to get client name.", e);
return null;
}
}
private static void handleTokenError(final FxAccountClientRemoteException error,
final FxAccountClient fxAccountClient,
final AndroidFxAccount fxAccount) {
Log.i(LOG_TAG, "Recovering from invalid token error: ", error);
logErrorAndResetDeviceRegistrationVersion(error, fxAccount);
fxAccountClient.accountStatus(fxAccount.getState().uid,
new RequestDelegate<AccountStatusResponse>() {
@Override
public void handleError(Exception e) {
}
@Override
public void handleFailure(FxAccountClientRemoteException e) {
}
@Override
public void handleSuccess(AccountStatusResponse result) {
State doghouseState = fxAccount.getState().makeDoghouseState();
if (!result.exists) {
Log.i(LOG_TAG, "token invalidated because the account no longer exists");
// TODO: Should be in a "I have an Android account, but the FxA is gone." State.
// This will do for now..
fxAccount.setState(doghouseState);
return;
}
Log.e(LOG_TAG, "sessionToken invalid");
fxAccount.setState(doghouseState);
}
});
}
private static void recoverFromUnknownDevice(final AndroidFxAccount fxAccount) {
Log.i(LOG_TAG, "unknown device id, clearing the cached device id");
fxAccount.setDeviceId(null);
}
/**
* Will call delegate#complete in all cases
*/
private static void recoverFromDeviceSessionConflict(final FxAccountClientRemoteException error,
final FxAccountClient fxAccountClient,
final byte[] sessionToken,
final AndroidFxAccount fxAccount,
final Context context,
final Bundle subscription,
final boolean allowRecursion) {
Log.w(LOG_TAG, "device session conflict, attempting to ascertain the correct device id");
fxAccountClient.deviceList(sessionToken, new RequestDelegate<FxAccountDevice[]>() {
private void onError() {
Log.e(LOG_TAG, "failed to recover from device-session conflict");
logErrorAndResetDeviceRegistrationVersion(error, fxAccount);
}
@Override
public void handleError(Exception e) {
onError();
}
@Override
public void handleFailure(FxAccountClientRemoteException e) {
onError();
}
@Override
public void handleSuccess(FxAccountDevice[] devices) {
for (FxAccountDevice device : devices) {
if (device.isCurrentDevice) {
fxAccount.setFxAUserData(device.id, 0); // Reset device registration version
if (!allowRecursion) {
Log.d(LOG_TAG, "Failure to register a device on the second try");
break;
}
try {
doFxaRegistration(context, subscription, false);
return;
} catch (InvalidFxAState e) {
Log.d(LOG_TAG, "Invalid state when trying to recover from a session conflict ", e);
break;
}
}
}
onError();
}
});
}
private void setupListeners() throws ClassNotFoundException, NoSuchMethodException,
InvocationTargetException, IllegalAccessException {
// We have no choice but to use reflection here, sorry :(
Class<?> eventDispatcher = Class.forName("org.mozilla.gecko.EventDispatcher");
Method getInstance = eventDispatcher.getMethod("getInstance");
Object instance = getInstance.invoke(null);
Method registerBackgroundThreadListener = eventDispatcher.getMethod("registerBackgroundThreadListener",
BundleEventListener.class, String[].class);
registerBackgroundThreadListener.invoke(instance, this, new String[] { "FxAccountsPush:Subscribe:Response" });
}
}

View file

@ -1,95 +0,0 @@
package org.mozilla.gecko.fxa;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
public class FxAccountPushHandler {
private static final String LOG_TAG = "FxAccountPush";
private static final String COMMAND_DEVICE_DISCONNECTED = "fxaccounts:device_disconnected";
private static final String COMMAND_COLLECTION_CHANGED = "sync:collection_changed";
private static final String CLIENTS_COLLECTION = "clients";
// Forbid instantiation
private FxAccountPushHandler() {}
public static void handleFxAPushMessage(Context context, Bundle bundle) {
Log.i(LOG_TAG, "Handling FxA Push Message");
String rawMessage = bundle.getString("message");
JSONObject message = null;
if (!TextUtils.isEmpty(rawMessage)) {
try {
message = new JSONObject(rawMessage);
} catch (JSONException e) {
Log.e(LOG_TAG, "Could not parse JSON", e);
return;
}
}
if (message == null) {
// An empty body means we should check the verification state of the account (FxA sends this
// when the account email is verified for example).
// TODO: We're only registering the push endpoint when we are in the Married state, that's why we're skipping the message :(
Log.d(LOG_TAG, "Skipping empty message");
return;
}
try {
String command = message.getString("command");
JSONObject data = message.getJSONObject("data");
switch (command) {
case COMMAND_DEVICE_DISCONNECTED:
handleDeviceDisconnection(context, data);
break;
case COMMAND_COLLECTION_CHANGED:
handleCollectionChanged(context, data);
break;
default:
Log.d(LOG_TAG, "No handler defined for FxA Push command " + command);
break;
}
} catch (JSONException e) {
Log.e(LOG_TAG, "Error while handling FxA push notification", e);
}
}
private static void handleCollectionChanged(Context context, JSONObject data) throws JSONException {
JSONArray collections = data.getJSONArray("collections");
int len = collections.length();
for (int i = 0; i < len; i++) {
if (collections.getString(i).equals(CLIENTS_COLLECTION)) {
final Account account = FirefoxAccounts.getFirefoxAccount(context);
if (account == null) {
Log.e(LOG_TAG, "The account does not exist anymore");
return;
}
final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
fxAccount.requestImmediateSync(new String[] { CLIENTS_COLLECTION }, null);
return;
}
}
}
private static void handleDeviceDisconnection(Context context, JSONObject data) throws JSONException {
final Account account = FirefoxAccounts.getFirefoxAccount(context);
if (account == null) {
Log.e(LOG_TAG, "The account does not exist anymore");
return;
}
final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
if (!fxAccount.getDeviceId().equals(data.getString("id"))) {
Log.e(LOG_TAG, "The device ID to disconnect doesn't match with the local device ID.\n"
+ "Local: " + fxAccount.getDeviceId() + ", ID to disconnect: " + data.getString("id"));
return;
}
AccountManager.get(context).removeAccount(account, null, null);
}
}

View file

@ -1,31 +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;
import android.accounts.Account;
import android.content.Context;
import android.support.annotation.UiThread;
/**
* Interface definition for a callback to be invoked when an sync status change.
*/
public interface SyncStatusListener {
public Context getContext();
public Account getAccount();
/**
* Called when sync has started.
* This is always called in UiThread.
*/
@UiThread
public void onSyncStarted();
/**
* Called when sync has finished.
* This is always called in UiThread.
*/
@UiThread
public void onSyncFinished();
}

View file

@ -1,52 +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 org.mozilla.gecko.R;
import android.content.Context;
import android.content.res.TypedArray;
import android.preference.Preference;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
/**
* This preference is used to define custom colors for both title and summary texts.
* Color code #777777 (placeholder_grey) is used as the fallback color for both title and summary.
*/
public class CustomColorPreference extends Preference {
private int mTitleColor;
private int mSummaryColor;
public CustomColorPreference(Context context) {
super(context);
}
public CustomColorPreference(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public CustomColorPreference(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs);
}
public void init(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomColorPreference);
mTitleColor = a.getColor(R.styleable.CustomColorPreference_titleColor, R.color.placeholder_grey);
mSummaryColor = a.getColor(R.styleable.CustomColorPreference_summaryColor, R.color.placeholder_grey);
a.recycle();
}
@Override
protected void onBindView(View view) {
super.onBindView(view);
final TextView title = (TextView) view.findViewById(android.R.id.title);
final TextView summary = (TextView) view.findViewById(android.R.id.summary);
title.setTextColor(mTitleColor);
summary.setTextColor(mSummaryColor);
}
}

View file

@ -1,80 +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 android.accounts.Account;
import android.app.Activity;
import android.content.Intent;
import org.mozilla.gecko.Locales.LocaleAwareActivity;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.FxAccountConstants;
public abstract class FxAccountAbstractActivity extends LocaleAwareActivity {
private static final String LOG_TAG = FxAccountAbstractActivity.class.getSimpleName();
protected final boolean cannotResumeWhenAccountsExist;
protected final boolean cannotResumeWhenNoAccountsExist;
public static final int CAN_ALWAYS_RESUME = 0;
public static final int CANNOT_RESUME_WHEN_ACCOUNTS_EXIST = 1 << 0;
public static final int CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST = 1 << 1;
public FxAccountAbstractActivity(int resume) {
super();
this.cannotResumeWhenAccountsExist = 0 != (resume & CANNOT_RESUME_WHEN_ACCOUNTS_EXIST);
this.cannotResumeWhenNoAccountsExist = 0 != (resume & CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST);
}
/**
* Many Firefox Accounts activities shouldn't display if an account already
* exists. This function redirects as appropriate.
*
* @return true if redirected.
*/
protected boolean redirectIfAppropriate() {
if (cannotResumeWhenAccountsExist || cannotResumeWhenNoAccountsExist) {
final Account account = FirefoxAccounts.getFirefoxAccount(this);
if (cannotResumeWhenAccountsExist && account != null) {
redirectToAction(FxAccountConstants.ACTION_FXA_STATUS);
return true;
}
if (cannotResumeWhenNoAccountsExist && account == null) {
redirectToAction(FxAccountConstants.ACTION_FXA_GET_STARTED);
return true;
}
}
return false;
}
@Override
public void onResume() {
super.onResume();
redirectIfAppropriate();
}
@Override
public void onBackPressed() {
super.onBackPressed();
overridePendingTransition(0, 0);
}
protected void launchActivity(Class<? extends Activity> activityClass) {
Intent intent = new Intent(this, activityClass);
// Per http://stackoverflow.com/a/8992365, this triggers a known bug with
// the soft keyboard not being shown for the started activity. Why, Android, why?
intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
startActivity(intent);
}
protected void redirectToAction(final String action) {
final Intent intent = new Intent(action);
// Per http://stackoverflow.com/a/8992365, this triggers a known bug with
// the soft keyboard not being shown for the started activity. Why, Android, why?
intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
startActivity(intent);
finish();
}
}

View file

@ -1,11 +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;
public class FxAccountConfirmAccountActivityWeb extends FxAccountWebFlowActivity {
public FxAccountConfirmAccountActivityWeb() {
super(CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST, "manage");
}
}

View file

@ -1,11 +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;
public class FxAccountFinishMigratingActivityWeb extends FxAccountWebFlowActivity {
public FxAccountFinishMigratingActivityWeb() {
super(CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST, "signin", "migration=sync11");
}
}

View file

@ -1,11 +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;
public class FxAccountGetStartedActivityWeb extends FxAccountWebFlowActivity {
public FxAccountGetStartedActivityWeb() {
super(CANNOT_RESUME_WHEN_ACCOUNTS_EXIST, "signup");
}
}

View file

@ -1,228 +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 android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar;
import android.util.TypedValue;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.widget.Toast;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.Locales.LocaleAwareAppCompatActivity;
import org.mozilla.gecko.R;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.sync.Utils;
/**
* Activity which displays account status.
*/
public class FxAccountStatusActivity extends LocaleAwareAppCompatActivity {
private static final String LOG_TAG = FxAccountStatusActivity.class.getSimpleName();
protected FxAccountStatusFragment statusFragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Display the fragment as the content.
statusFragment = new FxAccountStatusFragment();
getSupportFragmentManager()
.beginTransaction()
.replace(android.R.id.content, statusFragment)
.commit();
maybeSetHomeButtonEnabled();
}
/**
* Sufficiently recent Android versions need additional code to receive taps
* on the status bar to go "up". See <a
* href="http://stackoverflow.com/a/8953148">this stackoverflow answer</a> for
* more information.
*/
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
protected void maybeSetHomeButtonEnabled() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
Logger.debug(LOG_TAG, "Not enabling home button; version too low.");
return;
}
final ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
Logger.debug(LOG_TAG, "Enabling home button.");
actionBar.setHomeButtonEnabled(true);
actionBar.setDisplayHomeAsUpEnabled(true);
return;
}
Logger.debug(LOG_TAG, "Not enabling home button.");
}
@Override
public void onResume() {
super.onResume();
final AndroidFxAccount fxAccount = getAndroidFxAccount();
if (fxAccount == null) {
Logger.warn(LOG_TAG, "Could not get Firefox Account.");
// Gracefully redirect to get started.
final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED);
// Per http://stackoverflow.com/a/8992365, this triggers a known bug with
// the soft keyboard not being shown for the started activity. Why, Android, why?
intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
startActivity(intent);
setResult(RESULT_CANCELED);
finish();
return;
}
statusFragment.refresh(fxAccount);
}
/**
* Helper to fetch (unique) Android Firefox Account if one exists, or return null.
*/
protected AndroidFxAccount getAndroidFxAccount() {
Account account = FirefoxAccounts.getFirefoxAccount(this);
if (account == null) {
return null;
}
return new AndroidFxAccount(this, account);
}
/**
* Helper function to maybe remove the given Android account.
*/
@SuppressLint("InlinedApi")
public static void maybeDeleteAndroidAccount(final Activity activity, final Account account, final Intent intent) {
if (account == null) {
Logger.warn(LOG_TAG, "Trying to delete null account; ignoring request.");
return;
}
final AccountManagerCallback<Boolean> callback = new AccountManagerCallback<Boolean>() {
@Override
public void run(AccountManagerFuture<Boolean> future) {
Logger.info(LOG_TAG, "Account " + Utils.obfuscateEmail(account.name) + " removed.");
final String text = activity.getResources().getString(R.string.fxaccount_remove_account_toast, account.name);
Toast.makeText(activity, text, Toast.LENGTH_LONG).show();
if (intent != null) {
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
activity.startActivity(intent);
}
activity.finish();
}
};
/*
* Get the best dialog icon from the theme on v11+.
* See http://stackoverflow.com/questions/14910536/android-dialog-theme-makes-icon-too-light/14910945#14910945.
*/
final int icon;
final TypedValue typedValue = new TypedValue();
activity.getTheme().resolveAttribute(android.R.attr.alertDialogIcon, typedValue, true);
icon = typedValue.resourceId;
final AlertDialog dialog = new AlertDialog.Builder(activity)
.setTitle(R.string.fxaccount_remove_account_dialog_title)
.setIcon(icon)
.setMessage(R.string.fxaccount_remove_account_dialog_message)
.setPositiveButton(android.R.string.ok, new Dialog.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
AccountManager.get(activity).removeAccount(account, callback, null);
}
})
.setNegativeButton(android.R.string.cancel, new Dialog.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
}
})
.create();
dialog.show();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int itemId = item.getItemId();
if (itemId == android.R.id.home) {
finish();
return true;
}
if (itemId == R.id.enable_debug_mode) {
FxAccountUtils.LOG_PERSONAL_INFORMATION = !FxAccountUtils.LOG_PERSONAL_INFORMATION;
Toast.makeText(this, (FxAccountUtils.LOG_PERSONAL_INFORMATION ? "Enabled" : "Disabled") +
" Firefox Account personal information!", Toast.LENGTH_LONG).show();
item.setChecked(!item.isChecked());
// Display or hide debug options.
statusFragment.hardRefresh();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
final MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.fxaccount_status_menu, menu);
// !defined(MOZILLA_OFFICIAL) || defined(NIGHTLY_BUILD) || defined(MOZ_DEBUG)
boolean enabled = !AppConstants.MOZILLA_OFFICIAL || AppConstants.NIGHTLY_BUILD || AppConstants.DEBUG_BUILD;
if (!enabled) {
menu.removeItem(R.id.enable_debug_mode);
} else {
final MenuItem debugModeItem = menu.findItem(R.id.enable_debug_mode);
if (debugModeItem != null) {
// Update checked state based on internal flag.
menu.findItem(R.id.enable_debug_mode).setChecked(FxAccountUtils.LOG_PERSONAL_INFORMATION);
}
}
return super.onCreateOptionsMenu(menu);
};
@Override
public void openOptionsMenu() {
// This is a workaround of an Android bug:
// https://code.google.com/p/android/issues/detail?id=185217
// openOptionsMenu isn't overriden by WindowDecorActionBar, which is used by AppCompatActivity,
// meaning getSupportActionbar().openOptionsMenu doesn't work.
// Based loosely on the code in:
// http://androidxref.com/6.0.1_r10/xref/frameworks/support/v7/appcompat/src/android/support/v7/internal/app/WindowDecorActionBar.java#getDecorToolbar
final Window window = getWindow();
final View decor = window.getDecorView();
final View view = decor.findViewById(R.id.action_bar);
if (view instanceof Toolbar) {
final Toolbar toolbar = (Toolbar) view;
toolbar.showOverflowMenu();
}
}
}

View file

@ -1,949 +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 android.accounts.Account;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
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.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import android.text.format.DateUtils;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.R;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.background.preferences.PreferenceFragment;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.SyncStatusListener;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
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.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
import org.mozilla.gecko.sync.SyncConfiguration;
import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
import org.mozilla.gecko.util.HardwareUtils;
import org.mozilla.gecko.util.ThreadUtils;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* A fragment that displays the status of an AndroidFxAccount.
* <p>
* The owning activity is responsible for providing an AndroidFxAccount at
* appropriate times.
*/
public class FxAccountStatusFragment
extends PreferenceFragment
implements OnPreferenceClickListener, OnPreferenceChangeListener {
private static final String LOG_TAG = FxAccountStatusFragment.class.getSimpleName();
/**
* If a device claims to have synced before this date, we will assume it has never synced.
*/
private static final Date EARLIEST_VALID_SYNCED_DATE;
static {
final Calendar c = GregorianCalendar.getInstance();
c.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
EARLIEST_VALID_SYNCED_DATE = c.getTime();
}
// When a checkbox is toggled, wait 5 seconds (for other checkbox actions)
// before trying to sync. Should we kill off the fragment before the sync
// request happens, that's okay: the runnable will run if the UI thread is
// still around to service it, and since we're not updating any UI, we'll just
// schedule the sync as usual. See also comment below about garbage
// collection.
private static final long DELAY_IN_MILLISECONDS_BEFORE_REQUESTING_SYNC = 5 * 1000;
private static final long LAST_SYNCED_TIME_UPDATE_INTERVAL_IN_MILLISECONDS = 60 * 1000;
private static final long PROFILE_FETCH_RETRY_INTERVAL_IN_MILLISECONDS = 60 * 1000;
private static final String[] STAGES_TO_SYNC_ON_DEVICE_NAME_CHANGE = new String[] { "clients" };
// By default, the auth/account server preference is only shown when the
// account is configured to use a custom server. In debug mode, this is set.
private static boolean ALWAYS_SHOW_AUTH_SERVER = false;
// By default, the Sync server preference is only shown when the account is
// configured to use a custom Sync server. In debug mode, this is set.
private static boolean ALWAYS_SHOW_SYNC_SERVER = false;
protected PreferenceCategory accountCategory;
protected Preference profilePreference;
protected Preference manageAccountPreference;
protected Preference authServerPreference;
protected Preference removeAccountPreference;
protected Preference needsPasswordPreference;
protected Preference needsUpgradePreference;
protected Preference needsVerificationPreference;
protected Preference needsMasterSyncAutomaticallyEnabledPreference;
protected Preference needsFinishMigratingPreference;
protected PreferenceCategory syncCategory;
protected CheckBoxPreference bookmarksPreference;
protected CheckBoxPreference historyPreference;
protected CheckBoxPreference tabsPreference;
protected CheckBoxPreference passwordsPreference;
protected CheckBoxPreference readingListPreference;
protected EditTextPreference deviceNamePreference;
protected Preference syncServerPreference;
protected Preference morePreference;
protected Preference syncNowPreference;
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;
// Member variable so that re-posting pushes back the already posted instance.
// This Runnable references the fxAccount above, but it is not specific to a
// single account. (That is, it does not capture a single account instance.)
protected Runnable requestSyncRunnable;
// Runnable to update last synced time.
protected Runnable lastSyncedTimeUpdateRunnable;
// Broadcast Receiver to update profile Information.
protected FxAccountProfileInformationReceiver accountProfileInformationReceiver;
protected final InnerSyncStatusDelegate syncStatusDelegate = new InnerSyncStatusDelegate();
private Target profileAvatarTarget;
protected Preference ensureFindPreference(String key) {
Preference preference = findPreference(key);
if (preference == null) {
throw new IllegalStateException("Could not find preference with key: " + key);
}
return preference;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// We need to do this before we can query the hardware menu button state.
// We're guaranteed to have an activity at this point (onAttach is called
// before onCreate). It's okay to call this multiple times (with different
// contexts).
HardwareUtils.init(getActivity());
addPreferences();
}
protected void addPreferences() {
addPreferencesFromResource(R.xml.fxaccount_status_prefscreen);
accountCategory = (PreferenceCategory) ensureFindPreference("signed_in_as_category");
profilePreference = ensureFindPreference("profile");
manageAccountPreference = ensureFindPreference("manage_account");
authServerPreference = ensureFindPreference("auth_server");
removeAccountPreference = ensureFindPreference("remove_account");
needsPasswordPreference = ensureFindPreference("needs_credentials");
needsUpgradePreference = ensureFindPreference("needs_upgrade");
needsVerificationPreference = ensureFindPreference("needs_verification");
needsMasterSyncAutomaticallyEnabledPreference = ensureFindPreference("needs_master_sync_automatically_enabled");
needsFinishMigratingPreference = ensureFindPreference("needs_finish_migrating");
syncCategory = (PreferenceCategory) ensureFindPreference("sync_category");
bookmarksPreference = (CheckBoxPreference) ensureFindPreference("bookmarks");
historyPreference = (CheckBoxPreference) ensureFindPreference("history");
tabsPreference = (CheckBoxPreference) ensureFindPreference("tabs");
passwordsPreference = (CheckBoxPreference) ensureFindPreference("passwords");
if (!FxAccountUtils.LOG_PERSONAL_INFORMATION) {
removeDebugButtons();
} else {
connectDebugButtons();
ALWAYS_SHOW_AUTH_SERVER = true;
ALWAYS_SHOW_SYNC_SERVER = true;
}
profilePreference.setOnPreferenceClickListener(this);
manageAccountPreference.setOnPreferenceClickListener(this);
removeAccountPreference.setOnPreferenceClickListener(this);
needsPasswordPreference.setOnPreferenceClickListener(this);
needsVerificationPreference.setOnPreferenceClickListener(this);
needsFinishMigratingPreference.setOnPreferenceClickListener(this);
bookmarksPreference.setOnPreferenceClickListener(this);
historyPreference.setOnPreferenceClickListener(this);
tabsPreference.setOnPreferenceClickListener(this);
passwordsPreference.setOnPreferenceClickListener(this);
deviceNamePreference = (EditTextPreference) ensureFindPreference("device_name");
deviceNamePreference.setOnPreferenceChangeListener(this);
syncServerPreference = ensureFindPreference("sync_server");
morePreference = ensureFindPreference("more");
morePreference.setOnPreferenceClickListener(this);
syncNowPreference = ensureFindPreference("sync_now");
syncNowPreference.setEnabled(true);
syncNowPreference.setOnPreferenceClickListener(this);
ensureFindPreference("linktos").setOnPreferenceClickListener(this);
ensureFindPreference("linkprivacy").setOnPreferenceClickListener(this);
}
/**
* We intentionally don't refresh here. Our owning activity is responsible for
* providing an AndroidFxAccount to our refresh method in its onResume method.
*/
@Override
public void onResume() {
super.onResume();
}
@Override
public boolean onPreferenceClick(Preference preference) {
if (preference == profilePreference) {
ActivityUtils.openURLInFennec(getActivity().getApplicationContext(), "about:accounts?action=avatar");
return true;
}
if (preference == manageAccountPreference) {
ActivityUtils.openURLInFennec(getActivity().getApplicationContext(), "about:accounts?action=manage");
return true;
}
if (preference == removeAccountPreference) {
FxAccountStatusActivity.maybeDeleteAndroidAccount(getActivity(), fxAccount.getAndroidAccount(), null);
return true;
}
if (preference == needsPasswordPreference) {
final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_UPDATE_CREDENTIALS);
intent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_PREFERENCES);
// Per http://stackoverflow.com/a/8992365, this triggers a known bug with
// the soft keyboard not being shown for the started activity. Why, Android, why?
intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
startActivity(intent);
return true;
}
if (preference == needsFinishMigratingPreference) {
final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_FINISH_MIGRATING);
intent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_PREFERENCES);
// Per http://stackoverflow.com/a/8992365, this triggers a known bug with
// the soft keyboard not being shown for the started activity. Why, Android, why?
intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
startActivity(intent);
return true;
}
if (preference == needsVerificationPreference) {
final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_CONFIRM_ACCOUNT);
// Per http://stackoverflow.com/a/8992365, this triggers a known bug with
// the soft keyboard not being shown for the started activity. Why, Android, why?
intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
intent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_PREFERENCES);
startActivity(intent);
return true;
}
if (preference == bookmarksPreference ||
preference == historyPreference ||
preference == passwordsPreference ||
preference == tabsPreference) {
saveEngineSelections();
return true;
}
if (preference == morePreference) {
getActivity().openOptionsMenu();
return true;
}
if (preference == syncNowPreference) {
if (fxAccount != null) {
fxAccount.requestImmediateSync(null, null);
}
return true;
}
if (TextUtils.equals("linktos", preference.getKey())) {
ActivityUtils.openURLInFennec(getActivity().getApplicationContext(), getResources().getString(R.string.fxaccount_link_tos));
return true;
}
if (TextUtils.equals("linkprivacy", preference.getKey())) {
ActivityUtils.openURLInFennec(getActivity().getApplicationContext(), getResources().getString(R.string.fxaccount_link_pn));
return true;
}
return false;
}
protected void setCheckboxesEnabled(boolean enabled) {
bookmarksPreference.setEnabled(enabled);
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);
syncNowPreference.setEnabled(enabled);
}
/**
* Show at most one error preference, hiding all others.
*
* @param errorPreferenceToShow
* single error preference to show; if null, hide all error preferences
*/
protected void showOnlyOneErrorPreference(Preference errorPreferenceToShow) {
final Preference[] errorPreferences = new Preference[] {
this.needsPasswordPreference,
this.needsUpgradePreference,
this.needsVerificationPreference,
this.needsMasterSyncAutomaticallyEnabledPreference,
this.needsFinishMigratingPreference,
};
for (Preference errorPreference : errorPreferences) {
final boolean currentlyShown = null != findPreference(errorPreference.getKey());
final boolean shouldBeShown = errorPreference == errorPreferenceToShow;
if (currentlyShown == shouldBeShown) {
continue;
}
if (shouldBeShown) {
syncCategory.addPreference(errorPreference);
} else {
syncCategory.removePreference(errorPreference);
}
}
}
protected void showNeedsPassword() {
syncCategory.setTitle(R.string.fxaccount_status_sync);
showOnlyOneErrorPreference(needsPasswordPreference);
setCheckboxesEnabled(false);
}
protected void showNeedsUpgrade() {
syncCategory.setTitle(R.string.fxaccount_status_sync);
showOnlyOneErrorPreference(needsUpgradePreference);
setCheckboxesEnabled(false);
}
protected void showNeedsVerification() {
syncCategory.setTitle(R.string.fxaccount_status_sync);
showOnlyOneErrorPreference(needsVerificationPreference);
setCheckboxesEnabled(false);
}
protected void showNeedsMasterSyncAutomaticallyEnabled() {
syncCategory.setTitle(R.string.fxaccount_status_sync);
needsMasterSyncAutomaticallyEnabledPreference.setTitle(AppConstants.Versions.preLollipop ?
R.string.fxaccount_status_needs_master_sync_automatically_enabled :
R.string.fxaccount_status_needs_master_sync_automatically_enabled_v21);
showOnlyOneErrorPreference(needsMasterSyncAutomaticallyEnabledPreference);
setCheckboxesEnabled(false);
}
protected void showNeedsFinishMigrating() {
syncCategory.setTitle(R.string.fxaccount_status_sync);
showOnlyOneErrorPreference(needsFinishMigratingPreference);
setCheckboxesEnabled(false);
}
protected void showConnected() {
syncCategory.setTitle(R.string.fxaccount_status_sync_enabled);
showOnlyOneErrorPreference(null);
setCheckboxesEnabled(true);
}
protected class InnerSyncStatusDelegate implements SyncStatusListener {
protected final Runnable refreshRunnable = new Runnable() {
@Override
public void run() {
refresh();
}
};
@Override
public Context getContext() {
return FxAccountStatusFragment.this.getActivity();
}
@Override
public Account getAccount() {
return fxAccount.getAndroidAccount();
}
@Override
public void onSyncStarted() {
if (fxAccount == null) {
return;
}
Logger.info(LOG_TAG, "Got sync started message; refreshing.");
getActivity().runOnUiThread(refreshRunnable);
}
@Override
public void onSyncFinished() {
if (fxAccount == null) {
return;
}
Logger.info(LOG_TAG, "Got sync finished message; refreshing.");
getActivity().runOnUiThread(refreshRunnable);
}
}
/**
* Notify the fragment that a new AndroidFxAccount instance is current.
* <p>
* <b>Important:</b> call this method on the UI thread!
* <p>
* In future, this might be a Loader.
*
* @param fxAccount new instance.
*/
public void refresh(AndroidFxAccount fxAccount) {
if (fxAccount == null) {
throw new IllegalArgumentException("fxAccount must not be null");
}
this.fxAccount = fxAccount;
try {
this.clientsDataDelegate = new SharedPreferencesClientsDataDelegate(fxAccount.getSyncPrefs(), getActivity().getApplicationContext());
} 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.
// Runnable is not specific to one Firefox Account. This runnable will keep
// a reference to this fragment alive, but we expect posted runnables to be
// serviced very quickly, so this is not an issue.
requestSyncRunnable = new RequestSyncRunnable();
lastSyncedTimeUpdateRunnable = new LastSyncTimeUpdateRunnable();
// We would very much like register these status observers in bookended
// onResume/onPause calls, but because the Fragment gets onResume during the
// Activity's super.onResume, it hasn't yet been told its Firefox Account.
// So we register the observer here (and remove it in onPause), and open
// ourselves to the possibility that we don't have properly paired
// register/unregister calls.
FxAccountSyncStatusHelper.getInstance().startObserving(syncStatusDelegate);
// Register a local broadcast receiver to get profile cached notification.
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(FxAccountConstants.ACCOUNT_PROFILE_JSON_UPDATED_ACTION);
accountProfileInformationReceiver = new FxAccountProfileInformationReceiver();
LocalBroadcastManager.getInstance(getActivity()).registerReceiver(accountProfileInformationReceiver, intentFilter);
// profilePreference is set during onCreate, so it's definitely not null here.
final float cornerRadius = getResources().getDimension(R.dimen.fxaccount_profile_image_width) / 2;
profileAvatarTarget = new PicassoPreferenceIconTarget(getResources(), profilePreference, cornerRadius);
refresh();
}
@Override
public void onPause() {
super.onPause();
FxAccountSyncStatusHelper.getInstance().stopObserving(syncStatusDelegate);
// Focus lost, remove scheduled update if any.
if (lastSyncedTimeUpdateRunnable != null) {
handler.removeCallbacks(lastSyncedTimeUpdateRunnable);
}
// Focus lost, unregister broadcast receiver.
if (accountProfileInformationReceiver != null) {
LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(accountProfileInformationReceiver);
}
if (profileAvatarTarget != null) {
Picasso.with(getActivity()).cancelRequest(profileAvatarTarget);
profileAvatarTarget = null;
}
}
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
// refresh(AndroidFxAccount) method).
if (fxAccount == null) {
throw new IllegalArgumentException("fxAccount must not be null");
}
updateProfileInformation();
updateAuthServerPreference();
updateSyncServerPreference();
try {
// There are error states determined by Android, not the login state
// machine, and we have a chance to present these states here. We handle
// them specially, since we can't surface these states as part of syncing,
// because they generally stop syncs from happening regularly. Right now
// there are no such states.
// Interrogate the Firefox Account's state.
State state = fxAccount.getState();
switch (state.getNeededAction()) {
case NeedsUpgrade:
showNeedsUpgrade();
break;
case NeedsPassword:
showNeedsPassword();
break;
case NeedsVerification:
showNeedsVerification();
break;
case NeedsFinishMigrating:
showNeedsFinishMigrating();
break;
case None:
showConnected();
break;
}
// We check for the master setting last, since it is not strictly
// necessary for the user to address this error state: it's really a
// warning state. We surface it for the user's convenience, and to prevent
// confused folks wondering why Sync is not working at all.
final boolean masterSyncAutomatically = ContentResolver.getMasterSyncAutomatically();
if (!masterSyncAutomatically) {
showNeedsMasterSyncAutomaticallyEnabled();
return;
}
} finally {
// No matter our state, we should update the checkboxes.
updateSelectedEngines();
}
final String clientName = clientsDataDelegate.getClientName();
deviceNamePreference.setSummary(clientName);
deviceNamePreference.setText(clientName);
updateSyncNowPreference();
}
// This is a helper function similar to TabsAccessor.getLastSyncedString() to calculate relative "Last synced" time span.
private String getLastSyncedString(final long startTime) {
if (new Date(startTime).before(EARLIEST_VALID_SYNCED_DATE)) {
return getActivity().getString(R.string.fxaccount_status_never_synced);
}
final CharSequence relativeTimeSpanString = DateUtils.getRelativeTimeSpanString(startTime);
return getActivity().getResources().getString(R.string.fxaccount_status_last_synced, relativeTimeSpanString);
}
protected void updateSyncNowPreference() {
final boolean currentlySyncing = fxAccount.isCurrentlySyncing();
syncNowPreference.setEnabled(!currentlySyncing);
if (currentlySyncing) {
syncNowPreference.setTitle(R.string.fxaccount_status_syncing);
} else {
syncNowPreference.setTitle(R.string.fxaccount_status_sync_now);
}
scheduleAndUpdateLastSyncedTime();
}
private void updateProfileInformation() {
final ExtendedJSONObject profileJSON = fxAccount.getProfileJSON();
if (profileJSON == null) {
// Update the profile title with email as the fallback.
// Profile icon by default use the default avatar as the fallback.
profilePreference.setTitle(fxAccount.getEmail());
return;
}
updateProfileInformation(profileJSON);
}
/**
* Update profile information from json on UI thread.
*
* @param profileJSON json fetched from server.
*/
protected void updateProfileInformation(final ExtendedJSONObject profileJSON) {
// View changes must always be done on UI thread.
ThreadUtils.assertOnUiThread();
FxAccountUtils.pii(LOG_TAG, "Profile JSON is: " + profileJSON.toJSONString());
final String userName = profileJSON.getString(FxAccountConstants.KEY_PROFILE_JSON_USERNAME);
// Update the profile username and email if available.
if (!TextUtils.isEmpty(userName)) {
profilePreference.setTitle(userName);
profilePreference.setSummary(fxAccount.getEmail());
} else {
profilePreference.setTitle(fxAccount.getEmail());
}
// Avatar URI empty, skip profile image fetch.
final String avatarURI = profileJSON.getString(FxAccountConstants.KEY_PROFILE_JSON_AVATAR);
if (TextUtils.isEmpty(avatarURI)) {
Logger.info(LOG_TAG, "AvatarURI is empty, skipping profile image fetch.");
return;
}
// Using noPlaceholder would avoid a pop of the default image, but it's not available in the version of Picasso
// we ship in the tree.
Picasso
.with(getActivity())
.load(avatarURI)
.centerInside()
.resizeDimen(R.dimen.fxaccount_profile_image_width, R.dimen.fxaccount_profile_image_height)
.placeholder(R.drawable.sync_avatar_default)
.error(R.drawable.sync_avatar_default)
.into(profileAvatarTarget);
}
private void scheduleAndUpdateLastSyncedTime() {
final String lastSynced = getLastSyncedString(fxAccount.getLastSyncedTimestamp());
syncNowPreference.setSummary(lastSynced);
handler.postDelayed(lastSyncedTimeUpdateRunnable, LAST_SYNCED_TIME_UPDATE_INTERVAL_IN_MILLISECONDS);
}
protected void updateAuthServerPreference() {
final String authServer = fxAccount.getAccountServerURI();
final boolean shouldBeShown = ALWAYS_SHOW_AUTH_SERVER || !FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT.equals(authServer);
final boolean currentlyShown = null != findPreference(authServerPreference.getKey());
if (currentlyShown != shouldBeShown) {
if (shouldBeShown) {
accountCategory.addPreference(authServerPreference);
} else {
accountCategory.removePreference(authServerPreference);
}
}
// Always set the summary, because on first run, the preference is visible,
// and the above block will be skipped if there is a custom value.
authServerPreference.setSummary(authServer);
}
protected void updateSyncServerPreference() {
final String syncServer = fxAccount.getTokenServerURI();
final boolean shouldBeShown = ALWAYS_SHOW_SYNC_SERVER || !FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT.equals(syncServer);
final boolean currentlyShown = null != findPreference(syncServerPreference.getKey());
if (currentlyShown != shouldBeShown) {
if (shouldBeShown) {
syncCategory.addPreference(syncServerPreference);
} else {
syncCategory.removePreference(syncServerPreference);
}
}
// Always set the summary, because on first run, the preference is visible,
// and the above block will be skipped if there is a custom value.
syncServerPreference.setSummary(syncServer);
}
/**
* Query shared prefs for the current engine state, and update the UI
* accordingly.
* <p>
* In future, we might want this to be on a background thread, or implemented
* as a Loader.
*/
protected void updateSelectedEngines() {
try {
SharedPreferences syncPrefs = fxAccount.getSyncPrefs();
Map<String, Boolean> engines = SyncConfiguration.getUserSelectedEngines(syncPrefs);
if (engines != null) {
bookmarksPreference.setChecked(engines.containsKey("bookmarks") && engines.get("bookmarks"));
historyPreference.setChecked(engines.containsKey("history") && engines.get("history"));
passwordsPreference.setChecked(engines.containsKey("passwords") && engines.get("passwords"));
tabsPreference.setChecked(engines.containsKey("tabs") && engines.get("tabs"));
return;
}
// We don't have user specified preferences. Perhaps we have seen a meta/global?
Set<String> enabledNames = SyncConfiguration.getEnabledEngineNames(syncPrefs);
if (enabledNames != null) {
bookmarksPreference.setChecked(enabledNames.contains("bookmarks"));
historyPreference.setChecked(enabledNames.contains("history"));
passwordsPreference.setChecked(enabledNames.contains("passwords"));
tabsPreference.setChecked(enabledNames.contains("tabs"));
return;
}
// Okay, we don't have userSelectedEngines or enabledEngines. That means
// the user hasn't specified to begin with, we haven't specified here, and
// we haven't already seen, Sync engines. We don't know our state, so
// let's check everything (the default) and disable everything.
bookmarksPreference.setChecked(true);
historyPreference.setChecked(true);
passwordsPreference.setChecked(true);
tabsPreference.setChecked(true);
setCheckboxesEnabled(false);
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception getting engines to select; ignoring.", e);
return;
}
}
/**
* Persist engine selections to local shared preferences, and request a sync
* to persist selections to remote storage.
*/
protected void saveEngineSelections() {
final Map<String, Boolean> engineSelections = new HashMap<String, Boolean>();
engineSelections.put("bookmarks", bookmarksPreference.isChecked());
engineSelections.put("history", historyPreference.isChecked());
engineSelections.put("passwords", passwordsPreference.isChecked());
engineSelections.put("tabs", tabsPreference.isChecked());
// No GlobalSession.config, so store directly to shared prefs. We do this on
// a background thread to avoid IO on the main thread and strict mode
// warnings.
new Thread(new PersistEngineSelectionsRunnable(engineSelections)).start();
}
protected void requestDelayedSync() {
Logger.info(LOG_TAG, "Posting a delayed request for a sync sometime soon.");
handler.removeCallbacks(requestSyncRunnable);
handler.postDelayed(requestSyncRunnable, DELAY_IN_MILLISECONDS_BEFORE_REQUESTING_SYNC);
}
/**
* Remove all traces of debug buttons. By default, no debug buttons are shown.
*/
protected void removeDebugButtons() {
final PreferenceScreen statusScreen = (PreferenceScreen) ensureFindPreference("status_screen");
final PreferenceCategory debugCategory = (PreferenceCategory) ensureFindPreference("debug_category");
statusScreen.removePreference(debugCategory);
}
/**
* A Runnable that persists engine selections to shared prefs, and then
* requests a delayed sync.
* <p>
* References the member <code>fxAccount</code> and is specific to the Android
* account associated to that account.
*/
protected class PersistEngineSelectionsRunnable implements Runnable {
private final Map<String, Boolean> engineSelections;
protected PersistEngineSelectionsRunnable(Map<String, Boolean> engineSelections) {
this.engineSelections = engineSelections;
}
@Override
public void run() {
try {
// Name shadowing -- do you like it, or do you love it?
AndroidFxAccount fxAccount = FxAccountStatusFragment.this.fxAccount;
if (fxAccount == null) {
return;
}
Logger.info(LOG_TAG, "Persisting engine selections: " + engineSelections.toString());
SyncConfiguration.storeSelectedEnginesToPrefs(fxAccount.getSyncPrefs(), engineSelections);
requestDelayedSync();
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception persisting selected engines; ignoring.", e);
return;
}
}
}
/**
* A Runnable that requests a sync.
* <p>
* References the member <code>fxAccount</code>, but is not specific to the
* Android account associated to that account.
*/
protected class RequestSyncRunnable implements Runnable {
@Override
public void run() {
// Name shadowing -- do you like it, or do you love it?
AndroidFxAccount fxAccount = FxAccountStatusFragment.this.fxAccount;
if (fxAccount == null) {
return;
}
Logger.info(LOG_TAG, "Requesting a sync sometime soon.");
fxAccount.requestEventualSync(null, null);
}
}
/**
* The Runnable that schedules a future update and updates the last synced time.
*/
protected class LastSyncTimeUpdateRunnable implements Runnable {
@Override
public void run() {
scheduleAndUpdateLastSyncedTime();
}
}
/**
* Broadcast receiver to receive updates for the cached profile action.
*/
public class FxAccountProfileInformationReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (!intent.getAction().equals(FxAccountConstants.ACCOUNT_PROFILE_JSON_UPDATED_ACTION)) {
return;
}
Logger.info(LOG_TAG, "Profile avatar cache update action broadcast received.");
// Update the UI from cached profile json on the main thread.
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
updateProfileInformation();
}
});
}
}
/**
* A separate listener to separate debug logic from main code paths.
*/
protected class DebugPreferenceClickListener implements OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
final String key = preference.getKey();
if ("debug_refresh".equals(key)) {
Logger.info(LOG_TAG, "Refreshing.");
refresh();
} else if ("debug_dump".equals(key)) {
fxAccount.dump();
} else if ("debug_force_sync".equals(key)) {
Logger.info(LOG_TAG, "Force syncing.");
fxAccount.requestImmediateSync(null, null);
// No sense refreshing, since the sync will complete in the future.
} else if ("debug_forget_certificate".equals(key)) {
State state = fxAccount.getState();
try {
Married married = (Married) state;
Logger.info(LOG_TAG, "Moving to Cohabiting state: Forgetting certificate.");
fxAccount.setState(married.makeCohabitingState());
refresh();
} catch (ClassCastException e) {
Logger.info(LOG_TAG, "Not in Married state; can't forget certificate.");
// Ignore.
}
} else if ("debug_invalidate_certificate".equals(key)) {
State state = fxAccount.getState();
try {
Married married = (Married) state;
Logger.info(LOG_TAG, "Invalidating certificate.");
fxAccount.setState(married.makeCohabitingState().withCertificate("INVALID CERTIFICATE"));
refresh();
} catch (ClassCastException e) {
Logger.info(LOG_TAG, "Not in Married state; can't invalidate certificate.");
// Ignore.
}
} else if ("debug_require_password".equals(key)) {
Logger.info(LOG_TAG, "Moving to Separated state: Forgetting password.");
State state = fxAccount.getState();
fxAccount.setState(state.makeSeparatedState());
refresh();
} else if ("debug_require_upgrade".equals(key)) {
Logger.info(LOG_TAG, "Moving to Doghouse state: Requiring upgrade.");
State state = fxAccount.getState();
fxAccount.setState(state.makeDoghouseState());
refresh();
} else if ("debug_migrated_from_sync11".equals(key)) {
Logger.info(LOG_TAG, "Moving to MigratedFromSync11 state: Requiring password.");
State state = fxAccount.getState();
fxAccount.setState(state.makeMigratedFromSync11State(null));
refresh();
} else if ("debug_make_account_stage".equals(key)) {
Logger.info(LOG_TAG, "Moving Account endpoints, in place, to stage. Deleting Sync and RL prefs and requiring password.");
fxAccount.unsafeTransitionToStageEndpoints();
refresh();
} else if ("debug_make_account_default".equals(key)) {
Logger.info(LOG_TAG, "Moving Account endpoints, in place, to default (production). Deleting Sync and RL prefs and requiring password.");
fxAccount.unsafeTransitionToDefaultEndpoints();
refresh();
} else {
return false;
}
return true;
}
}
/**
* Iterate through debug buttons, adding a special debug preference click
* listener to each of them.
*/
protected void connectDebugButtons() {
// Separate listener to really separate debug logic from main code paths.
final OnPreferenceClickListener listener = new DebugPreferenceClickListener();
// We don't want to use Android resource strings for debug UI, so we just
// use the keys throughout.
final PreferenceCategory debugCategory = (PreferenceCategory) ensureFindPreference("debug_category");
debugCategory.setTitle(debugCategory.getKey());
for (int i = 0; i < debugCategory.getPreferenceCount(); i++) {
final Preference button = debugCategory.getPreference(i);
button.setTitle(button.getKey()); // Not very friendly, but this is for debugging only!
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);
// Force sync the client record, we want the user to see the device name change immediately
// on the FxA Device Manager if possible ( = we are online) to avoid confusion
// ("I changed my Android's device name but I don't see it on my computer").
fxAccount.requestImmediateSync(STAGES_TO_SYNC_ON_DEVICE_NAME_CHANGE, null);
hardRefresh(); // Updates the value displayed to the user, among other things.
return true;
}
// For everything else, accept the change.
return true;
}
}

View file

@ -1,11 +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;
public class FxAccountUpdateCredentialsActivityWeb extends FxAccountWebFlowActivity {
public FxAccountUpdateCredentialsActivityWeb() {
super(CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST, "force_auth");
}
}

View file

@ -1,91 +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 android.content.Intent;
import android.os.Bundle;
import org.mozilla.gecko.Locales;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
/**
* Activity which shows the status activity or passes through to web flow.
*/
public abstract class FxAccountWebFlowActivity extends FxAccountAbstractActivity {
protected static final String LOG_TAG = FxAccountWebFlowActivity.class.getSimpleName();
protected static final String ABOUT_ACCOUNTS = "about:accounts";
public static final String EXTRA_ENDPOINT = "entrypoint";
protected static final String[] EXTRAS_TO_PASSTHROUGH = new String[] {
EXTRA_ENDPOINT,
};
private final String action;
private final String extras;
public FxAccountWebFlowActivity(int resume, String action) {
this(resume, action, null);
}
public FxAccountWebFlowActivity(int resume, String action, String extras) {
super(resume);
this.action = action;
this.extras = (extras != null) ? ("&" + extras) : "";
}
/**
* {@inheritDoc}
*/
@Override
public void onCreate(Bundle icicle) {
Logger.setThreadLogTag(FxAccountConstants.GLOBAL_LOG_TAG);
Logger.debug(LOG_TAG, "onCreate(" + icicle + ")");
Locales.initializeLocale(getApplicationContext());
super.onCreate(icicle);
}
protected boolean redirectIfAppropriate() {
final boolean redirected = super.redirectIfAppropriate();
if (redirected) {
return true;
}
final StringBuilder sb = new StringBuilder();
sb.append(ABOUT_ACCOUNTS);
sb.append("?action=");
sb.append(action);
sb.append(extras);
// Pass through a set of known string values from intent extras to about:accounts.
final Intent intent = getIntent();
if (intent != null) {
for (String key : EXTRAS_TO_PASSTHROUGH) {
final String value = intent.getStringExtra(key);
if (value != null) {
sb.append("&");
sb.append(key);
sb.append("=");
sb.append(value);
}
}
}
ActivityUtils.openURLInFennec(getApplicationContext(), sb.toString());
return true;
}
@Override
public void onResume() {
super.onResume();
// We are always redirected.
this.finish();
}
}

View file

@ -1,63 +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 android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.preference.Preference;
import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
import org.mozilla.gecko.AppConstants;
/**
* A Picasso Target that updates a preference icon.
*
* Nota bene: Android grew support for updating preference icons programatically
* only in API 11. This class silently ignores requests before API 11.
*/
public class PicassoPreferenceIconTarget implements Target {
private final Preference preference;
private final Resources resources;
private final float cornerRadius;
public PicassoPreferenceIconTarget(Resources resources, Preference preference) {
this(resources, preference, 0);
}
public PicassoPreferenceIconTarget(Resources resources, Preference preference, float cornerRadius) {
this.resources = resources;
this.preference = preference;
this.cornerRadius = cornerRadius;
}
@Override
public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
final Drawable drawable;
if (cornerRadius > 0) {
final RoundedBitmapDrawable roundedBitmapDrawable;
roundedBitmapDrawable = RoundedBitmapDrawableFactory.create(resources, bitmap);
roundedBitmapDrawable.setCornerRadius(cornerRadius);
roundedBitmapDrawable.setAntiAlias(true);
drawable = roundedBitmapDrawable;
} else {
drawable = new BitmapDrawable(resources, bitmap);
}
preference.setIcon(drawable);
}
@Override
public void onBitmapFailed(Drawable errorDrawable) {
preference.setIcon(errorDrawable);
}
@Override
public void onPrepareLoad(Drawable placeHolderDrawable) {
preference.setIcon(placeHolderDrawable);
}
}

View file

@ -1,362 +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.authenticator;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.login.State.StateLabel;
import org.mozilla.gecko.fxa.login.StateFactory;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.NonObjectJSONException;
import org.mozilla.gecko.sync.Utils;
import android.content.Context;
/**
* Android deletes Account objects when the Authenticator that owns the Account
* disappears. This happens when an App is installed to the SD card and the SD
* card is un-mounted or the device is rebooted.
* <p>
* We work around this by pickling the current Firefox account data every sync
* and unpickling when we check if Firefox accounts exist (called from Fennec).
* <p>
* Android just doesn't support installing Apps that define long-lived Services
* and/or own Account types onto the SD card. The documentation says not to do
* it. There are hordes of developers who want to do it, and have tried to
* register for almost every "package installation changed" broadcast intent
* that Android supports. They all explicitly state that the package that has
* changed does *not* receive the broadcast intent, thereby preventing an App
* from re-establishing its state.
* <p>
* <a href="http://developer.android.com/guide/topics/data/install-location.html">Reference.</a>
* <p>
* <b>Quote</b>: Your AbstractThreadedSyncAdapter and all its sync functionality
* will not work until external storage is remounted.
* <p>
* <b>Quote</b>: Your running Service will be killed and will not be restarted
* when external storage is remounted. You can, however, register for the
* ACTION_EXTERNAL_APPLICATIONS_AVAILABLE broadcast Intent, which will notify
* your application when applications installed on external storage have become
* available to the system again. At which time, you can restart your Service.
* <p>
* Problem: <a href="http://code.google.com/p/android/issues/detail?id=8485">that intent doesn't work</a>!
* <p>
* See bug 768102 for more information in the context of Sync.
*/
public class AccountPickler {
public static final String LOG_TAG = AccountPickler.class.getSimpleName();
public static final long PICKLE_VERSION = 3;
public static final String KEY_PICKLE_VERSION = "pickle_version";
public static final String KEY_PICKLE_TIMESTAMP = "pickle_timestamp";
public static final String KEY_ACCOUNT_VERSION = "account_version";
public static final String KEY_ACCOUNT_TYPE = "account_type";
public static final String KEY_EMAIL = "email";
public static final String KEY_PROFILE = "profile";
public static final String KEY_IDP_SERVER_URI = "idpServerURI";
public static final String KEY_TOKEN_SERVER_URI = "tokenServerURI";
public static final String KEY_PROFILE_SERVER_URI = "profileServerURI";
public static final String KEY_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP = "authoritiesToSyncAutomaticallyMap";
// Deprecated, but maintained for migration purposes.
public static final String KEY_IS_SYNCING_ENABLED = "isSyncingEnabled";
public static final String KEY_BUNDLE = "bundle";
/**
* Remove Firefox account persisted to disk.
* This operation is synchronized to avoid race condition while deleting the account.
*
* @param context Android context.
* @param filename name of persisted pickle file; must not contain path separators.
* @return <code>true</code> if given pickle existed and was successfully deleted.
*/
public synchronized static boolean deletePickle(final Context context, final String filename) {
return context.deleteFile(filename);
}
public static ExtendedJSONObject toJSON(final AndroidFxAccount account, final long now) {
final ExtendedJSONObject o = new ExtendedJSONObject();
o.put(KEY_PICKLE_VERSION, PICKLE_VERSION);
o.put(KEY_PICKLE_TIMESTAMP, now);
o.put(KEY_ACCOUNT_VERSION, AndroidFxAccount.CURRENT_ACCOUNT_VERSION);
o.put(KEY_ACCOUNT_TYPE, FxAccountConstants.ACCOUNT_TYPE);
o.put(KEY_EMAIL, account.getEmail());
o.put(KEY_PROFILE, account.getProfile());
o.put(KEY_IDP_SERVER_URI, account.getAccountServerURI());
o.put(KEY_TOKEN_SERVER_URI, account.getTokenServerURI());
o.put(KEY_PROFILE_SERVER_URI, account.getProfileServerURI());
final ExtendedJSONObject p = new ExtendedJSONObject();
for (Entry<String, Boolean> pair : account.getAuthoritiesToSyncAutomaticallyMap().entrySet()) {
p.put(pair.getKey(), pair.getValue());
}
o.put(KEY_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP, p);
// TODO: If prefs version changes under us, SyncPrefsPath will change, "clearing" prefs.
final ExtendedJSONObject bundle = account.unbundle();
if (bundle == null) {
Logger.warn(LOG_TAG, "Unable to obtain account bundle; aborting.");
return null;
}
o.put(KEY_BUNDLE, bundle);
return o;
}
/**
* Persist Firefox account to disk as a JSON object.
* This operation is synchronized to avoid race condition while deleting the account.
*
* @param account the AndroidFxAccount to persist to disk
* @param filename name of file to persist to; must not contain path separators.
*/
public synchronized static void pickle(final AndroidFxAccount account, final String filename) {
final ExtendedJSONObject o = toJSON(account, System.currentTimeMillis());
writeToDisk(account.context, filename, o);
}
private static void writeToDisk(final Context context, final String filename,
final ExtendedJSONObject pickle) {
try {
final FileOutputStream fos = context.openFileOutput(filename, Context.MODE_PRIVATE);
try {
final PrintStream ps = new PrintStream(fos);
try {
ps.print(pickle.toJSONString());
Logger.debug(LOG_TAG, "Persisted " + pickle.keySet().size() +
" account settings to " + filename + ".");
} finally {
ps.close();
}
} finally {
fos.close();
}
} catch (Exception e) {
Logger.warn(LOG_TAG, "Caught exception persisting account settings to " + filename +
"; ignoring.", e);
}
}
/**
* Create Android account from saved JSON object. Assumes that an account does not exist.
* This operation is synchronized to avoid race condition while deleting the account.
*
* @param context
* Android context.
* @param filename
* name of file to read from; must not contain path separators.
* @return created Android account, or null on error.
*/
public synchronized static AndroidFxAccount unpickle(final Context context, final String filename) {
final String jsonString = Utils.readFile(context, filename);
if (jsonString == null) {
Logger.info(LOG_TAG, "Pickle file '" + filename + "' not found; aborting.");
return null;
}
ExtendedJSONObject json = null;
try {
json = new ExtendedJSONObject(jsonString);
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception reading pickle file '" + filename + "'; aborting.", e);
return null;
}
final UnpickleParams params;
try {
params = UnpickleParams.fromJSON(json);
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception extracting unpickle json; aborting.", e);
return null;
}
final AndroidFxAccount account;
try {
account = AndroidFxAccount.addAndroidAccount(context, params.email, params.profile,
params.authServerURI, params.tokenServerURI, params.profileServerURI, params.state,
params.authoritiesToSyncAutomaticallyMap,
params.accountVersion,
true, params.bundle);
} catch (Exception e) {
Logger.warn(LOG_TAG, "Exception when adding Android Account; aborting.", e);
return null;
}
if (account == null) {
Logger.warn(LOG_TAG, "Failed to add Android Account; aborting.");
return null;
}
Long timestamp = json.getLong(KEY_PICKLE_TIMESTAMP);
if (timestamp == null) {
Logger.warn(LOG_TAG, "Did not find timestamp in pickle file; ignoring.");
timestamp = -1L;
}
Logger.info(LOG_TAG, "Un-pickled Android account named " + params.email + " (version " +
params.pickleVersion + ", pickled at " + timestamp + ").");
return account;
}
private static class UnpickleParams {
private Long pickleVersion;
private int accountVersion;
private String email;
private String profile;
private String authServerURI;
private String tokenServerURI;
private String profileServerURI;
private final Map<String, Boolean> authoritiesToSyncAutomaticallyMap = new HashMap<>();
private ExtendedJSONObject bundle;
private State state;
private UnpickleParams() {
}
private static UnpickleParams fromJSON(final ExtendedJSONObject json)
throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException {
final UnpickleParams params = new UnpickleParams();
params.pickleVersion = json.getLong(KEY_PICKLE_VERSION);
if (params.pickleVersion == null) {
throw new IllegalStateException("Pickle version not found.");
}
/*
* Version 1 and version 2 are identical, except version 2 throws if the
* internal Android Account type has changed. Version 1 used to throw in
* this case, but we intentionally used the pickle file to migrate across
* Account types, bumping the version simultaneously.
*
* Version 3 replaces "isSyncEnabled" with a map (String -> Boolean)
* associating Android authorities to whether or not they are configured
* to sync automatically.
*/
switch (params.pickleVersion.intValue()) {
case 3: {
// Sanity check.
final String accountType = json.getString(KEY_ACCOUNT_TYPE);
if (!FxAccountConstants.ACCOUNT_TYPE.equals(accountType)) {
throw new IllegalStateException("Account type has changed from " + accountType + " to " + FxAccountConstants.ACCOUNT_TYPE + ".");
}
params.unpickleV3(json);
}
break;
case 2: {
// Sanity check.
final String accountType = json.getString(KEY_ACCOUNT_TYPE);
if (!FxAccountConstants.ACCOUNT_TYPE.equals(accountType)) {
throw new IllegalStateException("Account type has changed from " + accountType + " to " + FxAccountConstants.ACCOUNT_TYPE + ".");
}
params.unpickleV1(json);
}
break;
case 1: {
// Warn about account type changing, but don't throw over it.
final String accountType = json.getString(KEY_ACCOUNT_TYPE);
if (!FxAccountConstants.ACCOUNT_TYPE.equals(accountType)) {
Logger.warn(LOG_TAG, "Account type has changed from " + accountType + " to " + FxAccountConstants.ACCOUNT_TYPE + "; ignoring.");
}
params.unpickleV1(json);
}
break;
default:
throw new IllegalStateException("Unknown pickle version, " + params.pickleVersion + ".");
}
return params;
}
private void unpickleV1(final ExtendedJSONObject json)
throws NonObjectJSONException, NoSuchAlgorithmException, InvalidKeySpecException {
this.accountVersion = json.getIntegerSafely(KEY_ACCOUNT_VERSION);
this.email = json.getString(KEY_EMAIL);
this.profile = json.getString(KEY_PROFILE);
this.authServerURI = json.getString(KEY_IDP_SERVER_URI);
this.tokenServerURI = json.getString(KEY_TOKEN_SERVER_URI);
this.profileServerURI = json.getString(KEY_PROFILE_SERVER_URI);
// Fallback to default value when profile server URI was not pickled.
if (this.profileServerURI == null) {
this.profileServerURI = FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT.equals(this.authServerURI)
? FxAccountConstants.DEFAULT_PROFILE_SERVER_ENDPOINT
: FxAccountConstants.STAGE_PROFILE_SERVER_ENDPOINT;
}
// We get the default value for everything except syncing browser data.
this.authoritiesToSyncAutomaticallyMap.put(BrowserContract.AUTHORITY, json.getBoolean(KEY_IS_SYNCING_ENABLED));
this.bundle = json.getObject(KEY_BUNDLE);
if (bundle == null) {
throw new IllegalStateException("Pickle bundle is null.");
}
this.state = getState(bundle);
}
private void unpickleV3(final ExtendedJSONObject json)
throws NonObjectJSONException, NoSuchAlgorithmException, InvalidKeySpecException {
// We'll overwrite the extracted sync automatically map.
unpickleV1(json);
// Extract the map of authorities to sync automatically.
authoritiesToSyncAutomaticallyMap.clear();
final ExtendedJSONObject o = json.getObject(KEY_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP);
if (o == null) {
return;
}
for (String key : o.keySet()) {
final Boolean enabled = o.getBoolean(key);
if (enabled != null) {
authoritiesToSyncAutomaticallyMap.put(key, enabled);
}
}
}
private State getState(final ExtendedJSONObject bundle) throws InvalidKeySpecException,
NonObjectJSONException, NoSuchAlgorithmException {
// TODO: Should copy-pasta BUNDLE_KEY_STATE & LABEL to this file to ensure we maintain
// old versions?
final StateLabel stateLabelString = StateLabel.valueOf(
bundle.getString(AndroidFxAccount.BUNDLE_KEY_STATE_LABEL));
final String stateString = bundle.getString(AndroidFxAccount.BUNDLE_KEY_STATE);
if (stateLabelString == null || stateString == null) {
throw new IllegalStateException("stateLabel and stateString must not be null, but: " +
"(stateLabel == null) = " + (stateLabelString == null) +
" and (stateString == null) = " + (stateString == null));
}
try {
return StateFactory.fromJSONObject(stateLabelString, new ExtendedJSONObject(stateString));
} catch (Exception e) {
throw new IllegalStateException("could not get state", e);
}
}
}
}

View file

@ -1,929 +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.authenticator;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import android.os.ResultReceiver;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import android.util.Log;
import org.mozilla.gecko.background.common.GlobalConstants;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.login.State.StateLabel;
import org.mozilla.gecko.fxa.login.StateFactory;
import org.mozilla.gecko.fxa.login.TokensAndKeysState;
import org.mozilla.gecko.fxa.sync.FxAccountProfileService;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.setup.Constants;
import org.mozilla.gecko.util.ThreadUtils;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
/**
* A Firefox Account that stores its details and state as user data attached to
* an Android Account instance.
* <p>
* Account user data is accessible only to the Android App(s) that own the
* Account type. Account user data is not removed when the App's private data is
* cleared.
*/
public class AndroidFxAccount {
protected static final String LOG_TAG = AndroidFxAccount.class.getSimpleName();
public static final int CURRENT_SYNC_PREFS_VERSION = 1;
public static final int CURRENT_RL_PREFS_VERSION = 1;
// When updating the account, do not forget to update AccountPickler.
public static final int CURRENT_ACCOUNT_VERSION = 3;
public static final String ACCOUNT_KEY_ACCOUNT_VERSION = "version";
public static final String ACCOUNT_KEY_PROFILE = "profile";
public static final String ACCOUNT_KEY_IDP_SERVER = "idpServerURI";
private static final String ACCOUNT_KEY_PROFILE_SERVER = "profileServerURI";
public static final String ACCOUNT_KEY_TOKEN_SERVER = "tokenServerURI"; // Sync-specific.
public static final String ACCOUNT_KEY_DESCRIPTOR = "descriptor";
public static final int CURRENT_BUNDLE_VERSION = 2;
public static final String BUNDLE_KEY_BUNDLE_VERSION = "version";
public static final String BUNDLE_KEY_STATE_LABEL = "stateLabel";
public static final String BUNDLE_KEY_STATE = "state";
public static final String BUNDLE_KEY_PROFILE_JSON = "profile";
public static final String ACCOUNT_KEY_DEVICE_ID = "deviceId";
public static final String ACCOUNT_KEY_DEVICE_REGISTRATION_VERSION = "deviceRegistrationVersion";
// Account authentication token type for fetching account profile.
public static final String PROFILE_OAUTH_TOKEN_TYPE = "oauth::profile";
// Services may request OAuth tokens from the Firefox Account dynamically.
// Each such token is prefixed with "oauth::" and a service-dependent scope.
// Such tokens should be destroyed when the account is removed from the device.
// This list collects all the known "oauth::" token types in order to delete them when necessary.
private static final List<String> KNOWN_OAUTH_TOKEN_TYPES;
static {
final List<String> list = new ArrayList<>();
list.add(PROFILE_OAUTH_TOKEN_TYPE);
KNOWN_OAUTH_TOKEN_TYPES = Collections.unmodifiableList(list);
}
public static final Map<String, Boolean> DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP;
static {
final HashMap<String, Boolean> m = new HashMap<String, Boolean>();
// By default, Firefox Sync is enabled.
m.put(BrowserContract.AUTHORITY, true);
DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP = Collections.unmodifiableMap(m);
}
private static final String PREF_KEY_LAST_SYNCED_TIMESTAMP = "lastSyncedTimestamp";
protected final Context context;
protected final AccountManager accountManager;
protected final Account account;
/**
* A cache associating Account name (email address) to a representation of the
* account's internal bundle.
* <p>
* The cache is invalidated entirely when <it>any</it> new Account is added,
* because there is no reliable way to know that an Account has been removed
* and then re-added.
*/
protected static final ConcurrentHashMap<String, ExtendedJSONObject> perAccountBundleCache =
new ConcurrentHashMap<>();
public static void invalidateCaches() {
perAccountBundleCache.clear();
}
/**
* Create an Android Firefox Account instance backed by an Android Account
* instance.
* <p>
* We expect a long-lived application context to avoid life-cycle issues that
* might arise if the internally cached AccountManager instance surfaces UI.
* <p>
* We take care to not install any listeners or observers that might outlive
* the AccountManager; and Android ensures the AccountManager doesn't outlive
* the associated context.
*
* @param applicationContext
* to use as long-lived ambient Android context.
* @param account
* Android account to use for storage.
*/
public AndroidFxAccount(Context applicationContext, Account account) {
this.context = applicationContext;
this.account = account;
this.accountManager = AccountManager.get(this.context);
}
public static AndroidFxAccount fromContext(Context context) {
context = context.getApplicationContext();
Account account = FirefoxAccounts.getFirefoxAccount(context);
if (account == null) {
return null;
}
return new AndroidFxAccount(context, account);
}
/**
* Persist the Firefox account to disk as a JSON object. Note that this is a wrapper around
* {@link AccountPickler#pickle}, and is identical to calling it directly.
* <p>
* Note that pickling is different from bundling, which involves operations on a
* {@link android.os.Bundle Bundle} object of miscellaneous data associated with the account.
* See {@link #persistBundle} and {@link #unbundle} for more.
*/
public void pickle(final String filename) {
AccountPickler.pickle(this, filename);
}
public Account getAndroidAccount() {
return this.account;
}
protected int getAccountVersion() {
String v = accountManager.getUserData(account, ACCOUNT_KEY_ACCOUNT_VERSION);
if (v == null) {
return 0; // Implicit.
}
try {
return Integer.parseInt(v, 10);
} catch (NumberFormatException ex) {
return 0;
}
}
/**
* Saves the given data as the internal bundle associated with this account.
* @param bundle to write to account.
*/
protected synchronized void persistBundle(ExtendedJSONObject bundle) {
perAccountBundleCache.put(account.name, bundle);
accountManager.setUserData(account, ACCOUNT_KEY_DESCRIPTOR, bundle.toJSONString());
}
protected ExtendedJSONObject unbundle() {
return unbundle(true);
}
/**
* Retrieve the internal bundle associated with this account.
* @return bundle associated with account.
*/
protected synchronized ExtendedJSONObject unbundle(boolean allowCachedBundle) {
if (allowCachedBundle) {
final ExtendedJSONObject cachedBundle = perAccountBundleCache.get(account.name);
if (cachedBundle != null) {
Logger.debug(LOG_TAG, "Returning cached account bundle.");
return cachedBundle;
}
}
final int version = getAccountVersion();
if (version < CURRENT_ACCOUNT_VERSION) {
// Needs upgrade. For now, do nothing. We'd like to just put your account
// into the Separated state here and have you update your credentials.
return null;
}
if (version > CURRENT_ACCOUNT_VERSION) {
// Oh dear.
return null;
}
String bundleString = accountManager.getUserData(account, ACCOUNT_KEY_DESCRIPTOR);
if (bundleString == null) {
return null;
}
final ExtendedJSONObject bundle = unbundleAccountV2(bundleString);
perAccountBundleCache.put(account.name, bundle);
Logger.info(LOG_TAG, "Account bundle persisted to cache.");
return bundle;
}
protected String getBundleData(String key) {
ExtendedJSONObject o = unbundle();
if (o == null) {
return null;
}
return o.getString(key);
}
protected boolean getBundleDataBoolean(String key, boolean def) {
ExtendedJSONObject o = unbundle();
if (o == null) {
return def;
}
Boolean b = o.getBoolean(key);
if (b == null) {
return def;
}
return b;
}
protected byte[] getBundleDataBytes(String key) {
ExtendedJSONObject o = unbundle();
if (o == null) {
return null;
}
return o.getByteArrayHex(key);
}
protected void updateBundleValues(String key, String value, String... more) {
if (more.length % 2 != 0) {
throw new IllegalArgumentException("more must be a list of key, value pairs");
}
ExtendedJSONObject descriptor = unbundle();
if (descriptor == null) {
return;
}
descriptor.put(key, value);
for (int i = 0; i + 1 < more.length; i += 2) {
descriptor.put(more[i], more[i+1]);
}
persistBundle(descriptor);
}
private ExtendedJSONObject unbundleAccountV1(String bundle) {
ExtendedJSONObject o;
try {
o = new ExtendedJSONObject(bundle);
} catch (Exception e) {
return null;
}
if (CURRENT_BUNDLE_VERSION == o.getIntegerSafely(BUNDLE_KEY_BUNDLE_VERSION)) {
return o;
}
return null;
}
private ExtendedJSONObject unbundleAccountV2(String bundle) {
return unbundleAccountV1(bundle);
}
/**
* Note that if the user clears data, an account will be left pointing to a
* deleted profile. Such is life.
*/
public String getProfile() {
return accountManager.getUserData(account, ACCOUNT_KEY_PROFILE);
}
public String getAccountServerURI() {
return accountManager.getUserData(account, ACCOUNT_KEY_IDP_SERVER);
}
public String getTokenServerURI() {
return accountManager.getUserData(account, ACCOUNT_KEY_TOKEN_SERVER);
}
public String getProfileServerURI() {
String profileURI = accountManager.getUserData(account, ACCOUNT_KEY_PROFILE_SERVER);
if (profileURI == null) {
if (isStaging()) {
return FxAccountConstants.STAGE_PROFILE_SERVER_ENDPOINT;
}
return FxAccountConstants.DEFAULT_PROFILE_SERVER_ENDPOINT;
}
return profileURI;
}
public String getOAuthServerURI() {
// Allow testing against stage.
if (isStaging()) {
return FxAccountConstants.STAGE_OAUTH_SERVER_ENDPOINT;
} else {
return FxAccountConstants.DEFAULT_OAUTH_SERVER_ENDPOINT;
}
}
private boolean isStaging() {
return FxAccountConstants.STAGE_AUTH_SERVER_ENDPOINT.equals(getAccountServerURI());
}
private String constructPrefsPath(String product, long version, String extra) throws GeneralSecurityException, UnsupportedEncodingException {
String profile = getProfile();
String username = account.name;
if (profile == null) {
throw new IllegalStateException("Missing profile. Cannot fetch prefs.");
}
if (username == null) {
throw new IllegalStateException("Missing username. Cannot fetch prefs.");
}
final String fxaServerURI = getAccountServerURI();
if (fxaServerURI == null) {
throw new IllegalStateException("No account server URI. Cannot fetch prefs.");
}
// This is unique for each syncing 'view' of the account.
final String serverURLThing = fxaServerURI + "!" + extra;
return Utils.getPrefsPath(product, username, serverURLThing, profile, version);
}
/**
* This needs to return a string because of the tortured prefs access in GlobalSession.
*/
public String getSyncPrefsPath() throws GeneralSecurityException, UnsupportedEncodingException {
final String tokenServerURI = getTokenServerURI();
if (tokenServerURI == null) {
throw new IllegalStateException("No token server URI. Cannot fetch prefs.");
}
final String product = GlobalConstants.BROWSER_INTENT_PACKAGE + ".fxa";
final long version = CURRENT_SYNC_PREFS_VERSION;
return constructPrefsPath(product, version, tokenServerURI);
}
public String getReadingListPrefsPath() throws GeneralSecurityException, UnsupportedEncodingException {
final String product = GlobalConstants.BROWSER_INTENT_PACKAGE + ".reading";
final long version = CURRENT_RL_PREFS_VERSION;
return constructPrefsPath(product, version, "");
}
public SharedPreferences getSyncPrefs() throws UnsupportedEncodingException, GeneralSecurityException {
return context.getSharedPreferences(getSyncPrefsPath(), Utils.SHARED_PREFERENCES_MODE);
}
public SharedPreferences getReadingListPrefs() throws UnsupportedEncodingException, GeneralSecurityException {
return context.getSharedPreferences(getReadingListPrefsPath(), Utils.SHARED_PREFERENCES_MODE);
}
/**
* Extract a JSON dictionary of the string values associated to this account.
* <p>
* <b>For debugging use only!</b> The contents of this JSON object completely
* determine the user's Firefox Account status and yield access to whatever
* user data the device has access to.
*
* @return JSON-object of Strings.
*/
public ExtendedJSONObject toJSONObject() {
ExtendedJSONObject o = unbundle();
o.put("email", account.name);
try {
o.put("emailUTF8", Utils.byte2Hex(account.name.getBytes("UTF-8")));
} catch (UnsupportedEncodingException e) {
// Ignore.
}
o.put("fxaDeviceId", getDeviceId());
o.put("fxaDeviceRegistrationVersion", getDeviceRegistrationVersion());
return o;
}
public static AndroidFxAccount addAndroidAccount(
Context context,
String email,
String profile,
String idpServerURI,
String tokenServerURI,
String profileServerURI,
State state,
final Map<String, Boolean> authoritiesToSyncAutomaticallyMap)
throws UnsupportedEncodingException, GeneralSecurityException, URISyntaxException {
return addAndroidAccount(context, email, profile, idpServerURI, tokenServerURI, profileServerURI, state,
authoritiesToSyncAutomaticallyMap,
CURRENT_ACCOUNT_VERSION, false, null);
}
public static AndroidFxAccount addAndroidAccount(
Context context,
String email,
String profile,
String idpServerURI,
String tokenServerURI,
String profileServerURI,
State state,
final Map<String, Boolean> authoritiesToSyncAutomaticallyMap,
final int accountVersion,
final boolean fromPickle,
ExtendedJSONObject bundle)
throws UnsupportedEncodingException, GeneralSecurityException, URISyntaxException {
if (email == null) {
throw new IllegalArgumentException("email must not be null");
}
if (profile == null) {
throw new IllegalArgumentException("profile must not be null");
}
if (idpServerURI == null) {
throw new IllegalArgumentException("idpServerURI must not be null");
}
if (tokenServerURI == null) {
throw new IllegalArgumentException("tokenServerURI must not be null");
}
if (profileServerURI == null) {
throw new IllegalArgumentException("profileServerURI must not be null");
}
if (state == null) {
throw new IllegalArgumentException("state must not be null");
}
// TODO: Add migration code.
if (accountVersion != CURRENT_ACCOUNT_VERSION) {
throw new IllegalStateException("Could not create account of version " + accountVersion +
". Current version is " + CURRENT_ACCOUNT_VERSION + ".");
}
// Android has internal restrictions that require all values in this
// bundle to be strings. *sigh*
Bundle userdata = new Bundle();
userdata.putString(ACCOUNT_KEY_ACCOUNT_VERSION, "" + CURRENT_ACCOUNT_VERSION);
userdata.putString(ACCOUNT_KEY_IDP_SERVER, idpServerURI);
userdata.putString(ACCOUNT_KEY_TOKEN_SERVER, tokenServerURI);
userdata.putString(ACCOUNT_KEY_PROFILE_SERVER, profileServerURI);
userdata.putString(ACCOUNT_KEY_PROFILE, profile);
if (bundle == null) {
bundle = new ExtendedJSONObject();
// TODO: How to upgrade?
bundle.put(BUNDLE_KEY_BUNDLE_VERSION, CURRENT_BUNDLE_VERSION);
}
bundle.put(BUNDLE_KEY_STATE_LABEL, state.getStateLabel().name());
bundle.put(BUNDLE_KEY_STATE, state.toJSONObject().toJSONString());
userdata.putString(ACCOUNT_KEY_DESCRIPTOR, bundle.toJSONString());
Account account = new Account(email, FxAccountConstants.ACCOUNT_TYPE);
AccountManager accountManager = AccountManager.get(context);
// We don't set an Android password, because we don't want to persist the
// password (or anything else as powerful as the password). Instead, we
// internally manage a sessionToken with a remotely owned lifecycle.
boolean added = accountManager.addAccountExplicitly(account, null, userdata);
if (!added) {
return null;
}
// Try to work around an intermittent issue described at
// http://stackoverflow.com/a/11698139. What happens is that tests that
// delete and re-create the same account frequently will find the account
// missing all or some of the userdata bundle, possibly due to an Android
// AccountManager caching bug.
for (String key : userdata.keySet()) {
accountManager.setUserData(account, key, userdata.getString(key));
}
AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
if (!fromPickle) {
fxAccount.clearSyncPrefs();
}
fxAccount.setAuthoritiesToSyncAutomaticallyMap(authoritiesToSyncAutomaticallyMap);
return fxAccount;
}
public void clearSyncPrefs() throws UnsupportedEncodingException, GeneralSecurityException {
getSyncPrefs().edit().clear().commit();
}
public void setAuthoritiesToSyncAutomaticallyMap(Map<String, Boolean> authoritiesToSyncAutomaticallyMap) {
if (authoritiesToSyncAutomaticallyMap == null) {
throw new IllegalArgumentException("authoritiesToSyncAutomaticallyMap must not be null");
}
for (String authority : DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP.keySet()) {
boolean authorityEnabled = DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP.get(authority);
final Boolean enabled = authoritiesToSyncAutomaticallyMap.get(authority);
if (enabled != null) {
authorityEnabled = enabled.booleanValue();
}
// Accounts are always capable of being synced ...
ContentResolver.setIsSyncable(account, authority, 1);
// ... but not always automatically synced.
ContentResolver.setSyncAutomatically(account, authority, authorityEnabled);
}
}
public Map<String, Boolean> getAuthoritiesToSyncAutomaticallyMap() {
final Map<String, Boolean> authoritiesToSync = new HashMap<>();
for (String authority : DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP.keySet()) {
final boolean enabled = ContentResolver.getSyncAutomatically(account, authority);
authoritiesToSync.put(authority, enabled);
}
return authoritiesToSync;
}
/**
* Is a sync currently in progress?
*
* @return true if Android is currently syncing the underlying Android Account.
*/
public boolean isCurrentlySyncing() {
boolean active = false;
for (String authority : AndroidFxAccount.DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP.keySet()) {
active |= ContentResolver.isSyncActive(account, authority);
}
return active;
}
/**
* Request an immediate sync. Use this to sync as soon as possible in response to user action.
*
* @param stagesToSync stage names to sync; can be null to sync <b>all</b> known stages.
* @param stagesToSkip stage names to skip; can be null to skip <b>no</b> known stages.
*/
public void requestImmediateSync(String[] stagesToSync, String[] stagesToSkip) {
FirefoxAccounts.requestImmediateSync(getAndroidAccount(), stagesToSync, stagesToSkip);
}
/**
* Request an eventual sync. Use this to request the system queue a sync for some time in the
* future.
*
* @param stagesToSync stage names to sync; can be null to sync <b>all</b> known stages.
* @param stagesToSkip stage names to skip; can be null to skip <b>no</b> known stages.
*/
public void requestEventualSync(String[] stagesToSync, String[] stagesToSkip) {
FirefoxAccounts.requestEventualSync(getAndroidAccount(), stagesToSync, stagesToSkip);
}
public synchronized void setState(State state) {
if (state == null) {
throw new IllegalArgumentException("state must not be null");
}
Logger.info(LOG_TAG, "Moving account named like " + getObfuscatedEmail() +
" to state " + state.getStateLabel().toString());
updateBundleValues(
BUNDLE_KEY_STATE_LABEL, state.getStateLabel().name(),
BUNDLE_KEY_STATE, state.toJSONObject().toJSONString());
broadcastAccountStateChangedIntent();
}
protected void broadcastAccountStateChangedIntent() {
final Intent intent = new Intent(FxAccountConstants.ACCOUNT_STATE_CHANGED_ACTION);
intent.putExtra(Constants.JSON_KEY_ACCOUNT, account.name);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}
public synchronized State getState() {
String stateLabelString = getBundleData(BUNDLE_KEY_STATE_LABEL);
String stateString = getBundleData(BUNDLE_KEY_STATE);
if (stateLabelString == null || stateString == null) {
throw new IllegalStateException("stateLabelString and stateString must not be null, but: " +
"(stateLabelString == null) = " + (stateLabelString == null) +
" and (stateString == null) = " + (stateString == null));
}
try {
StateLabel stateLabel = StateLabel.valueOf(stateLabelString);
Logger.debug(LOG_TAG, "Account is in state " + stateLabel);
return StateFactory.fromJSONObject(stateLabel, new ExtendedJSONObject(stateString));
} catch (Exception e) {
throw new IllegalStateException("could not get state", e);
}
}
public byte[] getSessionToken() throws InvalidFxAState {
State state = getState();
StateLabel stateLabel = state.getStateLabel();
if (stateLabel == StateLabel.Cohabiting || stateLabel == StateLabel.Married) {
TokensAndKeysState tokensAndKeysState = (TokensAndKeysState) state;
return tokensAndKeysState.getSessionToken();
}
throw new InvalidFxAState("Cannot get sessionToken: not in a TokensAndKeysState state");
}
public static class InvalidFxAState extends Exception {
private static final long serialVersionUID = -8537626959811195978L;
public InvalidFxAState(String message) {
super(message);
}
}
/**
* <b>For debugging only!</b>
*/
public void dump() {
if (!FxAccountUtils.LOG_PERSONAL_INFORMATION) {
return;
}
ExtendedJSONObject o = toJSONObject();
ArrayList<String> list = new ArrayList<String>(o.keySet());
Collections.sort(list);
for (String key : list) {
FxAccountUtils.pii(LOG_TAG, key + ": " + o.get(key));
}
}
/**
* Return the Firefox Account's local email address.
* <p>
* It is important to note that this is the local email address, and not
* necessarily the normalized remote email address that the server expects.
*
* @return local email address.
*/
public String getEmail() {
return account.name;
}
/**
* Return the Firefox Account's local email address, obfuscated.
* <p>
* Use this when logging.
*
* @return local email address, obfuscated.
*/
public String getObfuscatedEmail() {
return Utils.obfuscateEmail(account.name);
}
/**
* Populate an intent used for starting FxAccountDeletedService service.
*
* @param intent Intent to populate with necessary extras
* @return <code>Intent</code> with a deleted action and account/OAuth information extras
*/
public Intent populateDeletedAccountIntent(final Intent intent) {
final List<String> tokens = new ArrayList<>();
intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_VERSION_KEY,
Long.valueOf(FxAccountConstants.ACCOUNT_DELETED_INTENT_VERSION));
intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_KEY, account.name);
intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_PROFILE, getProfile());
// Get the tokens from AccountManager. Note: currently, only reading list service supports OAuth. The following logic will
// be extended in future to support OAuth for other services.
for (String tokenKey : KNOWN_OAUTH_TOKEN_TYPES) {
final String authToken = accountManager.peekAuthToken(account, tokenKey);
if (authToken != null) {
tokens.add(authToken);
}
}
// Update intent with tokens and service URI.
intent.putExtra(FxAccountConstants.ACCOUNT_OAUTH_SERVICE_ENDPOINT_KEY, getOAuthServerURI());
// Deleted broadcasts are package-private, so there's no security risk include the tokens in the extras
intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_AUTH_TOKENS, tokens.toArray(new String[tokens.size()]));
return intent;
}
/**
* Create an intent announcing that the profile JSON attached to this Firefox Account has been updated.
* <p>
* It is not guaranteed that the profile JSON has changed.
*
* @return <code>Intent</code> to broadcast.
*/
private Intent makeProfileJSONUpdatedIntent() {
final Intent intent = new Intent();
intent.setAction(FxAccountConstants.ACCOUNT_PROFILE_JSON_UPDATED_ACTION);
return intent;
}
public void setLastSyncedTimestamp(long now) {
try {
getSyncPrefs().edit().putLong(PREF_KEY_LAST_SYNCED_TIMESTAMP, now).commit();
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception setting last synced time; ignoring.", e);
}
}
public long getLastSyncedTimestamp() {
final long neverSynced = -1L;
try {
return getSyncPrefs().getLong(PREF_KEY_LAST_SYNCED_TIMESTAMP, neverSynced);
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception getting last synced time; ignoring.", e);
return neverSynced;
}
}
// Debug only! This is dangerous!
public void unsafeTransitionToDefaultEndpoints() {
unsafeTransitionToStageEndpoints(
FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT,
FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT,
FxAccountConstants.DEFAULT_PROFILE_SERVER_ENDPOINT);
}
// Debug only! This is dangerous!
public void unsafeTransitionToStageEndpoints() {
unsafeTransitionToStageEndpoints(
FxAccountConstants.STAGE_AUTH_SERVER_ENDPOINT,
FxAccountConstants.STAGE_TOKEN_SERVER_ENDPOINT,
FxAccountConstants.STAGE_PROFILE_SERVER_ENDPOINT);
}
protected void unsafeTransitionToStageEndpoints(String authServerEndpoint, String tokenServerEndpoint, String profileServerEndpoint) {
try {
getReadingListPrefs().edit().clear().commit();
} catch (UnsupportedEncodingException | GeneralSecurityException e) {
// Ignore.
}
try {
getSyncPrefs().edit().clear().commit();
} catch (UnsupportedEncodingException | GeneralSecurityException e) {
// Ignore.
}
State state = getState();
setState(state.makeSeparatedState());
accountManager.setUserData(account, ACCOUNT_KEY_IDP_SERVER, authServerEndpoint);
accountManager.setUserData(account, ACCOUNT_KEY_TOKEN_SERVER, tokenServerEndpoint);
accountManager.setUserData(account, ACCOUNT_KEY_PROFILE_SERVER, profileServerEndpoint);
ContentResolver.setIsSyncable(account, BrowserContract.READING_LIST_AUTHORITY, 1);
}
/**
* Returns the current profile JSON if available, or null.
*
* @return profile JSON object.
*/
public ExtendedJSONObject getProfileJSON() {
final String profileString = getBundleData(BUNDLE_KEY_PROFILE_JSON);
if (profileString == null) {
return null;
}
try {
return new ExtendedJSONObject(profileString);
} catch (Exception e) {
Logger.error(LOG_TAG, "Failed to parse profile JSON; ignoring and returning null.", e);
}
return null;
}
/**
* Fetch the profile JSON associated to the underlying Firefox Account from the server and update the local store.
* <p>
* The LocalBroadcastManager is used to notify the receivers asynchronously after a successful fetch.
*/
public void fetchProfileJSON() {
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
// Fetch profile information from server.
String authToken;
try {
authToken = accountManager.blockingGetAuthToken(account, AndroidFxAccount.PROFILE_OAUTH_TOKEN_TYPE, true);
if (authToken == null) {
throw new RuntimeException("Couldn't get oauth token! Aborting profile fetch.");
}
} catch (Exception e) {
Logger.error(LOG_TAG, "Error fetching profile information; ignoring.", e);
return;
}
Logger.info(LOG_TAG, "Intent service launched to fetch profile.");
final Intent intent = new Intent(context, FxAccountProfileService.class);
intent.putExtra(FxAccountProfileService.KEY_AUTH_TOKEN, authToken);
intent.putExtra(FxAccountProfileService.KEY_PROFILE_SERVER_URI, getProfileServerURI());
intent.putExtra(FxAccountProfileService.KEY_RESULT_RECEIVER, new ProfileResultReceiver(new Handler()));
context.startService(intent);
}
});
}
@Nullable
public synchronized String getDeviceId() {
return accountManager.getUserData(account, ACCOUNT_KEY_DEVICE_ID);
}
@NonNull
public synchronized int getDeviceRegistrationVersion() {
String versionStr = accountManager.getUserData(account, ACCOUNT_KEY_DEVICE_REGISTRATION_VERSION);
if (TextUtils.isEmpty(versionStr)) {
return 0;
} else {
try {
return Integer.parseInt(versionStr);
} catch (NumberFormatException ex) {
return 0;
}
}
}
public synchronized void setDeviceId(String id) {
accountManager.setUserData(account, ACCOUNT_KEY_DEVICE_ID, id);
}
public synchronized void setDeviceRegistrationVersion(int deviceRegistrationVersion) {
accountManager.setUserData(account, ACCOUNT_KEY_DEVICE_REGISTRATION_VERSION,
Integer.toString(deviceRegistrationVersion));
}
public synchronized void resetDeviceRegistrationVersion() {
setDeviceRegistrationVersion(0);
}
public synchronized void setFxAUserData(String id, int deviceRegistrationVersion) {
accountManager.setUserData(account, ACCOUNT_KEY_DEVICE_ID, id);
accountManager.setUserData(account, ACCOUNT_KEY_DEVICE_REGISTRATION_VERSION,
Integer.toString(deviceRegistrationVersion));
}
@SuppressLint("ParcelCreator") // The CREATOR field is defined in the super class.
private class ProfileResultReceiver extends ResultReceiver {
public ProfileResultReceiver(Handler handler) {
super(handler);
}
@Override
protected void onReceiveResult(int resultCode, Bundle bundle) {
super.onReceiveResult(resultCode, bundle);
switch (resultCode) {
case Activity.RESULT_OK:
final String resultData = bundle.getString(FxAccountProfileService.KEY_RESULT_STRING);
updateBundleValues(BUNDLE_KEY_PROFILE_JSON, resultData);
Logger.info(LOG_TAG, "Profile JSON fetch succeeeded!");
FxAccountUtils.pii(LOG_TAG, "Profile JSON fetch returned: " + resultData);
LocalBroadcastManager.getInstance(context).sendBroadcast(makeProfileJSONUpdatedIntent());
break;
case Activity.RESULT_CANCELED:
Logger.warn(LOG_TAG, "Failed to fetch profile JSON; ignoring.");
break;
default:
Logger.warn(LOG_TAG, "Invalid result code received; ignoring.");
break;
}
}
}
/**
* Take the lock to own updating any Firefox Account's internal state.
*
* We use a <code>Semaphore</code> rather than a <code>ReentrantLock</code>
* because the callback that needs to release the lock may not be invoked on
* the thread that initially acquired the lock. Be aware!
*/
protected static final Semaphore sLock = new Semaphore(1, true /* fair */);
// Which consumer took the lock?
// Synchronized by this.
protected String lockTag = null;
// Are we locked? (It's not easy to determine who took the lock dynamically,
// so we maintain this flag internally.)
// Synchronized by this.
protected boolean locked = false;
// Block until we can take the shared state lock.
public synchronized void acquireSharedAccountStateLock(final String tag) throws InterruptedException {
final long id = Thread.currentThread().getId();
this.lockTag = tag;
Log.d(Logger.DEFAULT_LOG_TAG, "Thread with tag and thread id acquiring lock: " + lockTag + ", " + id + " ...");
sLock.acquire();
locked = true;
Log.d(Logger.DEFAULT_LOG_TAG, "Thread with tag and thread id acquiring lock: " + lockTag + ", " + id + " ... ACQUIRED");
}
// If we hold the shared state lock, release it. Otherwise, ignore the request.
public synchronized void releaseSharedAccountStateLock() {
final long id = Thread.currentThread().getId();
Log.d(Logger.DEFAULT_LOG_TAG, "Thread with tag and thread id releasing lock: " + lockTag + ", " + id + " ...");
if (locked) {
sLock.release();
locked = false;
Log.d(Logger.DEFAULT_LOG_TAG, "Thread with tag and thread id releasing lock: " + lockTag + ", " + id + " ... RELEASED");
} else {
Log.d(Logger.DEFAULT_LOG_TAG, "Thread with tag and thread id releasing lock: " + lockTag + ", " + id + " ... NOT LOCKED");
}
}
@Override
protected synchronized void finalize() {
if (locked) {
// Should never happen, but...
sLock.release();
locked = false;
final long id = Thread.currentThread().getId();
Log.e(Logger.DEFAULT_LOG_TAG, "Thread with tag and thread id releasing lock: " + lockTag + ", " + id + " ... RELEASED DURING FINALIZE");
}
}
}

View file

@ -1,84 +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.authenticator;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClient;
import org.mozilla.gecko.background.fxa.FxAccountClient20;
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.LoginStateMachineDelegate;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition;
import org.mozilla.gecko.fxa.login.Married;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.login.State.StateLabel;
import org.mozilla.gecko.fxa.login.StateFactory;
import org.mozilla.gecko.fxa.sync.FxAccountNotificationManager;
import org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter;
import android.content.Context;
public abstract class FxADefaultLoginStateMachineDelegate implements LoginStateMachineDelegate {
protected final static String LOG_TAG = LoginStateMachineDelegate.class.getSimpleName();
protected final Context context;
protected final AndroidFxAccount fxAccount;
protected final Executor executor;
protected final FxAccountClient client;
public FxADefaultLoginStateMachineDelegate(Context context, AndroidFxAccount fxAccount) {
this.context = context;
this.fxAccount = fxAccount;
this.executor = Executors.newSingleThreadExecutor();
this.client = new FxAccountClient20(fxAccount.getAccountServerURI(), executor);
}
abstract public void handleNotMarried(State notMarried);
abstract public void handleMarried(Married married);
@Override
public FxAccountClient getClient() {
return client;
}
@Override
public long getCertificateDurationInMilliseconds() {
return 12 * 60 * 60 * 1000;
}
@Override
public long getAssertionDurationInMilliseconds() {
return 15 * 60 * 1000;
}
@Override
public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException {
return StateFactory.generateKeyPair();
}
@Override
public void handleTransition(Transition transition, State state) {
Logger.info(LOG_TAG, "handleTransition: " + transition + " to " + state.getStateLabel());
}
@Override
public void handleFinal(State state) {
Logger.info(LOG_TAG, "handleFinal: in " + state.getStateLabel());
fxAccount.setState(state);
// Update any notifications displayed.
final FxAccountNotificationManager notificationManager = new FxAccountNotificationManager(FxAccountSyncAdapter.NOTIFICATION_ID);
notificationManager.update(context, fxAccount);
if (state.getStateLabel() != StateLabel.Married) {
handleNotMarried(state);
return;
} else {
handleMarried((Married) state);
}
}
}

View file

@ -1,385 +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.authenticator;
import android.accounts.AbstractAccountAuthenticator;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.accounts.NetworkErrorException;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClient;
import org.mozilla.gecko.background.fxa.FxAccountClient20;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClient.RequestDelegate;
import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException.FxAccountAbstractClientRemoteException;
import org.mozilla.gecko.background.fxa.oauth.FxAccountOAuthClient10;
import org.mozilla.gecko.background.fxa.oauth.FxAccountOAuthClient10.AuthorizationResponse;
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
import org.mozilla.gecko.browserid.JSONWebTokenUtils;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.LoginStateMachineDelegate;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition;
import org.mozilla.gecko.fxa.login.Married;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.login.State.StateLabel;
import org.mozilla.gecko.fxa.login.StateFactory;
import org.mozilla.gecko.fxa.receivers.FxAccountDeletedService;
import org.mozilla.gecko.fxa.sync.FxAccountNotificationManager;
import org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter;
import org.mozilla.gecko.util.ThreadUtils;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class FxAccountAuthenticator extends AbstractAccountAuthenticator {
public static final String LOG_TAG = FxAccountAuthenticator.class.getSimpleName();
public static final int UNKNOWN_ERROR_CODE = 999;
protected final Context context;
protected final AccountManager accountManager;
public FxAccountAuthenticator(Context context) {
super(context);
this.context = context;
this.accountManager = AccountManager.get(context);
}
@Override
public Bundle addAccount(AccountAuthenticatorResponse response,
String accountType, String authTokenType, String[] requiredFeatures,
Bundle options)
throws NetworkErrorException {
Logger.debug(LOG_TAG, "addAccount");
// The data associated to each Account should be invalidated when we change
// the set of Firefox Accounts on the system.
AndroidFxAccount.invalidateCaches();
final Bundle res = new Bundle();
if (!FxAccountConstants.ACCOUNT_TYPE.equals(accountType)) {
res.putInt(AccountManager.KEY_ERROR_CODE, -1);
res.putString(AccountManager.KEY_ERROR_MESSAGE, "Not adding unknown account type.");
return res;
}
final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED);
res.putParcelable(AccountManager.KEY_INTENT, intent);
return res;
}
@Override
public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options)
throws NetworkErrorException {
Logger.debug(LOG_TAG, "confirmCredentials");
return null;
}
@Override
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
Logger.debug(LOG_TAG, "editProperties");
return null;
}
protected static class Responder {
final AccountAuthenticatorResponse response;
final AndroidFxAccount fxAccount;
public Responder(AccountAuthenticatorResponse response, AndroidFxAccount fxAccount) {
this.response = response;
this.fxAccount = fxAccount;
}
public void fail(Exception e) {
Logger.warn(LOG_TAG, "Responding with error!", e);
fxAccount.releaseSharedAccountStateLock();
final Bundle result = new Bundle();
result.putInt(AccountManager.KEY_ERROR_CODE, UNKNOWN_ERROR_CODE);
result.putString(AccountManager.KEY_ERROR_MESSAGE, e.toString());
response.onResult(result);
}
public void succeed(String authToken) {
Logger.info(LOG_TAG, "Responding with success!");
fxAccount.releaseSharedAccountStateLock();
final Bundle result = new Bundle();
result.putString(AccountManager.KEY_ACCOUNT_NAME, fxAccount.account.name);
result.putString(AccountManager.KEY_ACCOUNT_TYPE, fxAccount.account.type);
result.putString(AccountManager.KEY_AUTHTOKEN, authToken);
response.onResult(result);
}
}
public abstract static class FxADefaultLoginStateMachineDelegate implements LoginStateMachineDelegate {
protected final Context context;
protected final AndroidFxAccount fxAccount;
protected final Executor executor;
protected final FxAccountClient client;
public FxADefaultLoginStateMachineDelegate(Context context, AndroidFxAccount fxAccount) {
this.context = context;
this.fxAccount = fxAccount;
this.executor = Executors.newSingleThreadExecutor();
this.client = new FxAccountClient20(fxAccount.getAccountServerURI(), executor);
}
@Override
public FxAccountClient getClient() {
return client;
}
@Override
public long getCertificateDurationInMilliseconds() {
return 12 * 60 * 60 * 1000;
}
@Override
public long getAssertionDurationInMilliseconds() {
return 15 * 60 * 1000;
}
@Override
public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException {
return StateFactory.generateKeyPair();
}
@Override
public void handleTransition(Transition transition, State state) {
Logger.info(LOG_TAG, "handleTransition: " + transition + " to " + state.getStateLabel());
}
abstract public void handleNotMarried(State notMarried);
abstract public void handleMarried(Married married);
@Override
public void handleFinal(State state) {
Logger.info(LOG_TAG, "handleFinal: in " + state.getStateLabel());
fxAccount.setState(state);
// Update any notifications displayed.
final FxAccountNotificationManager notificationManager = new FxAccountNotificationManager(FxAccountSyncAdapter.NOTIFICATION_ID);
notificationManager.update(context, fxAccount);
if (state.getStateLabel() != StateLabel.Married) {
handleNotMarried(state);
return;
} else {
handleMarried((Married) state);
}
}
}
protected void getOAuthToken(final AccountAuthenticatorResponse response, final AndroidFxAccount fxAccount, final String scope) throws NetworkErrorException {
Logger.info(LOG_TAG, "Fetching oauth token with scope: " + scope);
final Responder responder = new Responder(response, fxAccount);
final String oauthServerUri = fxAccount.getOAuthServerURI();
final String audience;
try {
audience = FxAccountUtils.getAudienceForURL(oauthServerUri); // The assertion gets traded in for an oauth bearer token.
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception fetching oauth token.", e);
responder.fail(e);
return;
}
final FxAccountLoginStateMachine stateMachine = new FxAccountLoginStateMachine();
stateMachine.advance(fxAccount.getState(), StateLabel.Married, new FxADefaultLoginStateMachineDelegate(context, fxAccount) {
@Override
public void handleNotMarried(State state) {
final String message = "Cannot fetch oauth token from state: " + state.getStateLabel();
Logger.warn(LOG_TAG, message);
responder.fail(new RuntimeException(message));
}
@Override
public void handleMarried(final Married married) {
final String assertion;
try {
assertion = married.generateAssertion(audience, JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER);
if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
JSONWebTokenUtils.dumpAssertion(assertion);
}
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception fetching oauth token.", e);
responder.fail(e);
return;
}
final FxAccountOAuthClient10 oauthClient = new FxAccountOAuthClient10(oauthServerUri, executor);
Logger.debug(LOG_TAG, "OAuth fetch for scope: " + scope);
oauthClient.authorization(FxAccountConstants.OAUTH_CLIENT_ID_FENNEC, assertion, null, scope, new RequestDelegate<FxAccountOAuthClient10.AuthorizationResponse>() {
@Override
public void handleSuccess(AuthorizationResponse result) {
Logger.debug(LOG_TAG, "OAuth success.");
FxAccountUtils.pii(LOG_TAG, "Fetched oauth token: " + result.access_token);
responder.succeed(result.access_token);
}
@Override
public void handleFailure(FxAccountAbstractClientRemoteException e) {
Logger.error(LOG_TAG, "OAuth failure.", e);
if (e.isInvalidAuthentication()) {
// We were married, generated an assertion, and our assertion was rejected by the
// oauth client. If it's a 401, we probably have a stale certificate. If instead of
// a stale certificate we have bad credentials, the state machine will fail to sign
// our public key and drive us back to Separated.
fxAccount.setState(married.makeCohabitingState());
}
responder.fail(e);
}
@Override
public void handleError(Exception e) {
Logger.error(LOG_TAG, "OAuth error.", e);
responder.fail(e);
}
});
}
});
}
@Override
public Bundle getAuthToken(final AccountAuthenticatorResponse response,
final Account account, final String authTokenType, final Bundle options)
throws NetworkErrorException {
Logger.debug(LOG_TAG, "getAuthToken: " + authTokenType);
// If we have a cached authToken, hand it over.
final String cachedAuthToken = AccountManager.get(context).peekAuthToken(account, authTokenType);
if (cachedAuthToken != null && !cachedAuthToken.isEmpty()) {
Logger.info(LOG_TAG, "Return cached token.");
final Bundle result = new Bundle();
result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type);
result.putString(AccountManager.KEY_AUTHTOKEN, cachedAuthToken);
return result;
}
// If we're asked for an oauth::scope token, try to generate one.
final String oauthPrefix = "oauth::";
if (authTokenType != null && authTokenType.startsWith(oauthPrefix)) {
final String scope = authTokenType.substring(oauthPrefix.length());
final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
try {
fxAccount.acquireSharedAccountStateLock(LOG_TAG);
} catch (InterruptedException e) {
Logger.warn(LOG_TAG, "Could not acquire account state lock; return error bundle.");
final Bundle bundle = new Bundle();
bundle.putInt(AccountManager.KEY_ERROR_CODE, 1);
bundle.putString(AccountManager.KEY_ERROR_MESSAGE, "Could not acquire account state lock.");
return bundle;
}
getOAuthToken(response, fxAccount, scope);
return null;
}
// Otherwise, fail.
Logger.warn(LOG_TAG, "Returning error bundle for getAuthToken with unknown token type.");
final Bundle bundle = new Bundle();
bundle.putInt(AccountManager.KEY_ERROR_CODE, 2);
bundle.putString(AccountManager.KEY_ERROR_MESSAGE, "Unknown token type: " + authTokenType);
return bundle;
}
@Override
public String getAuthTokenLabel(String authTokenType) {
Logger.debug(LOG_TAG, "getAuthTokenLabel");
return null;
}
@Override
public Bundle hasFeatures(AccountAuthenticatorResponse response,
Account account, String[] features) throws NetworkErrorException {
Logger.debug(LOG_TAG, "hasFeatures");
return null;
}
@Override
public Bundle updateCredentials(AccountAuthenticatorResponse response,
Account account, String authTokenType, Bundle options)
throws NetworkErrorException {
Logger.debug(LOG_TAG, "updateCredentials");
return null;
}
/**
* If the account is going to be removed, broadcast an "account deleted"
* intent. This allows us to clean up the account.
* <p>
* It is preferable to receive Android's LOGIN_ACCOUNTS_CHANGED_ACTION broadcast
* than to create our own hacky broadcast here, but that doesn't include enough
* information about which Accounts changed to correctly identify whether a Sync
* account has been removed (when some Firefox channels are installed on the SD
* card). We can work around this by storing additional state but it's both messy
* and expensive because the broadcast is noisy.
* <p>
* Note that this is <b>not</b> called when an Android Account is blown away
* due to the SD card being unmounted.
*/
@Override
public Bundle getAccountRemovalAllowed(final AccountAuthenticatorResponse response, Account account)
throws NetworkErrorException {
Bundle result = super.getAccountRemovalAllowed(response, account);
if (result == null ||
!result.containsKey(AccountManager.KEY_BOOLEAN_RESULT) ||
result.containsKey(AccountManager.KEY_INTENT)) {
return result;
}
final boolean removalAllowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT);
if (!removalAllowed) {
return result;
}
// Broadcast a message to all Firefox channels sharing this Android
// Account type telling that this Firefox account has been deleted.
//
// Broadcast intents protected with permissions are secure, so it's okay
// to include private information such as a password.
final AndroidFxAccount androidFxAccount = new AndroidFxAccount(context, account);
// Deleting the pickle file in a blocking manner will avoid race conditions that might happen when
// an account is unpickled while an FxAccount is being deleted.
// Also we have an assumption that this method is always called from a background thread, so we delete
// the pickle file directly without being afraid from a StrictMode violation.
ThreadUtils.assertNotOnUiThread();
final Intent serviceIntent = androidFxAccount.populateDeletedAccountIntent(
new Intent(context, FxAccountDeletedService.class)
);
Logger.info(LOG_TAG, "Account named " + account.name + " being removed; " +
"starting FxAccountDeletedService with action: " + serviceIntent.getAction() + ".");
context.startService(serviceIntent);
Logger.info(LOG_TAG, "Firefox account named " + account.name + " being removed; " +
"deleting saved pickle file '" + FxAccountConstants.ACCOUNT_PICKLE_FILENAME + "'.");
deletePickle();
return result;
}
private void deletePickle() {
try {
AccountPickler.deletePickle(context, FxAccountConstants.ACCOUNT_PICKLE_FILENAME);
} catch (Exception e) {
// This should never happen, but we really don't want to die in a background thread.
Logger.warn(LOG_TAG, "Got exception deleting saved pickle file; ignoring.", e);
}
}
}

View file

@ -1,55 +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.authenticator;
import org.mozilla.gecko.background.common.log.Logger;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
public class FxAccountAuthenticatorService extends Service {
public static final String LOG_TAG = FxAccountAuthenticatorService.class.getSimpleName();
// Lazily initialized by <code>getAuthenticator</code>.
protected FxAccountAuthenticator accountAuthenticator;
protected synchronized FxAccountAuthenticator getAuthenticator() {
if (accountAuthenticator == null) {
accountAuthenticator = new FxAccountAuthenticator(this);
}
return accountAuthenticator;
}
@Override
public void onCreate() {
Logger.debug(LOG_TAG, "onCreate");
accountAuthenticator = getAuthenticator();
}
@Override
public IBinder onBind(Intent intent) {
Logger.debug(LOG_TAG, "onBind");
if (intent == null) {
// Should never happen, but can -- Bug 1025937.
return null;
}
if (!android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT.equals(intent.getAction())) {
return null;
}
final FxAccountAuthenticator authenticator = getAuthenticator();
if (authenticator == null) {
// Should never happen.
return null;
}
return authenticator.getIBinder();
}
}

View file

@ -1,26 +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.authenticator;
/**
* Abstraction around things that might need to be signalled to the user via UI,
* such as:
* <ul>
* <li>account not yet verified;</li>
* <li>account password needs to be updated;</li>
* <li>account key management required or changed;</li>
* <li>auth protocol has changed and Firefox needs to be upgraded;</li>
* </ul>
* etc.
* <p>
* Consumers of this code should differentiate error classes based on the types
* of the exceptions thrown. Exceptions that do not have special meaning are of
* type <code>FxAccountLoginException</code> with an appropriate
* <code>cause</code> inner exception.
*/
public interface FxAccountLoginDelegate {
public void handleError(FxAccountLoginException e);
public void handleSuccess(String assertion);
}

View file

@ -1,33 +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.authenticator;
public class FxAccountLoginException extends Exception {
public FxAccountLoginException(String string) {
super(string);
}
public FxAccountLoginException(Exception e) {
super(e);
}
private static final long serialVersionUID = 397685959625820798L;
public static class FxAccountLoginBadPasswordException extends FxAccountLoginException {
public FxAccountLoginBadPasswordException(String string) {
super(string);
}
private static final long serialVersionUID = 397685959625820799L;
}
public static class FxAccountLoginAccountNotVerifiedException extends FxAccountLoginException {
public FxAccountLoginAccountNotVerifiedException(String string) {
super(string);
}
private static final long serialVersionUID = 397685959625820800L;
}
}

View file

@ -1,49 +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.login;
import org.mozilla.gecko.background.fxa.FxAccountClient20;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.AccountNeedsVerification;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LocalError;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.RemoteError;
public abstract class BaseRequestDelegate<T> implements FxAccountClient20.RequestDelegate<T> {
protected final ExecuteDelegate delegate;
protected final State state;
public BaseRequestDelegate(State state, ExecuteDelegate delegate) {
this.delegate = delegate;
this.state = state;
}
@Override
public void handleFailure(FxAccountClientRemoteException e) {
// Order matters here: we don't want to ignore upgrade required responses
// even if the server tells us something else as well. We don't go directly
// to the Doghouse on upgrade required; we want the user to try to update
// their credentials, and then display UI telling them they need to upgrade.
// Then they go to the Doghouse.
if (e.isUpgradeRequired()) {
delegate.handleTransition(new RemoteError(e), new Separated(state.email, state.uid, state.verified));
return;
}
if (e.isInvalidAuthentication()) {
delegate.handleTransition(new RemoteError(e), new Separated(state.email, state.uid, state.verified));
return;
}
if (e.isUnverified()) {
delegate.handleTransition(new AccountNeedsVerification(), state);
return;
}
delegate.handleTransition(new RemoteError(e), state);
}
@Override
public void handleError(Exception e) {
delegate.handleTransition(new LocalError(e), state);
}
}

View file

@ -1,50 +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.login;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
import org.mozilla.gecko.browserid.JSONWebTokenUtils;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LogMessage;
import org.mozilla.gecko.sync.ExtendedJSONObject;
public class Cohabiting extends TokensAndKeysState {
private static final String LOG_TAG = Cohabiting.class.getSimpleName();
public Cohabiting(String email, String uid, byte[] sessionToken, byte[] kA, byte[] kB, BrowserIDKeyPair keyPair) {
super(StateLabel.Cohabiting, email, uid, sessionToken, kA, kB, keyPair);
}
public Married withCertificate(String certificate) {
return new Married(email, uid, sessionToken, kA, kB, keyPair, certificate);
}
@Override
public void execute(final ExecuteDelegate delegate) {
delegate.getClient().sign(sessionToken, keyPair.getPublic().toJSONObject(), delegate.getCertificateDurationInMilliseconds(),
new BaseRequestDelegate<String>(this, delegate) {
@Override
public void handleSuccess(String certificate) {
if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
try {
FxAccountUtils.pii(LOG_TAG, "Fetched certificate: " + certificate);
ExtendedJSONObject c = JSONWebTokenUtils.parseCertificate(certificate);
if (c != null) {
FxAccountUtils.pii(LOG_TAG, "Header : " + c.getObject("header"));
FxAccountUtils.pii(LOG_TAG, "Payload : " + c.getObject("payload"));
FxAccountUtils.pii(LOG_TAG, "Signature: " + c.getString("signature"));
} else {
FxAccountUtils.pii(LOG_TAG, "Could not parse certificate!");
}
} catch (Exception e) {
FxAccountUtils.pii(LOG_TAG, "Could not parse certificate!");
}
}
delegate.handleTransition(new LogMessage("sign succeeded"), withCertificate(certificate));
}
});
}
}

View file

@ -1,25 +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.login;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LogMessage;
public class Doghouse extends State {
public Doghouse(String email, String uid, boolean verified) {
super(StateLabel.Doghouse, email, uid, verified);
}
@Override
public void execute(final ExecuteDelegate delegate) {
delegate.handleTransition(new LogMessage("Upgraded Firefox clients might know what to do here."), this);
}
@Override
public Action getNeededAction() {
return Action.NeedsUpgrade;
}
}

View file

@ -1,91 +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.login;
import java.security.NoSuchAlgorithmException;
import org.mozilla.gecko.background.fxa.FxAccountClient20.TwoKeys;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.AccountVerified;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LocalError;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LogMessage;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.RemoteError;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.Utils;
public class Engaged extends State {
private static final String LOG_TAG = Engaged.class.getSimpleName();
protected final byte[] sessionToken;
protected final byte[] keyFetchToken;
protected final byte[] unwrapkB;
public Engaged(String email, String uid, boolean verified, byte[] unwrapkB, byte[] sessionToken, byte[] keyFetchToken) {
super(StateLabel.Engaged, email, uid, verified);
Utils.throwIfNull(unwrapkB, sessionToken, keyFetchToken);
this.unwrapkB = unwrapkB;
this.sessionToken = sessionToken;
this.keyFetchToken = keyFetchToken;
}
@Override
public ExtendedJSONObject toJSONObject() {
ExtendedJSONObject o = super.toJSONObject();
// Fields are non-null by constructor.
o.put("unwrapkB", Utils.byte2Hex(unwrapkB));
o.put("sessionToken", Utils.byte2Hex(sessionToken));
o.put("keyFetchToken", Utils.byte2Hex(keyFetchToken));
return o;
}
@Override
public void execute(final ExecuteDelegate delegate) {
BrowserIDKeyPair theKeyPair;
try {
theKeyPair = delegate.generateKeyPair();
} catch (NoSuchAlgorithmException e) {
delegate.handleTransition(new LocalError(e), new Doghouse(email, uid, verified));
return;
}
final BrowserIDKeyPair keyPair = theKeyPair;
delegate.getClient().keys(keyFetchToken, new BaseRequestDelegate<TwoKeys>(this, delegate) {
@Override
public void handleSuccess(TwoKeys result) {
byte[] kB;
try {
kB = FxAccountUtils.unwrapkB(unwrapkB, result.wrapkB);
if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
FxAccountUtils.pii(LOG_TAG, "Fetched kA: " + Utils.byte2Hex(result.kA));
FxAccountUtils.pii(LOG_TAG, "And wrapkB: " + Utils.byte2Hex(result.wrapkB));
FxAccountUtils.pii(LOG_TAG, "Giving kB : " + Utils.byte2Hex(kB));
}
} catch (Exception e) {
delegate.handleTransition(new RemoteError(e), new Separated(email, uid, verified));
return;
}
Transition transition = verified
? new LogMessage("keys succeeded")
: new AccountVerified();
delegate.handleTransition(transition, new Cohabiting(email, uid, sessionToken, result.kA, kB, keyPair));
}
});
}
@Override
public Action getNeededAction() {
if (!verified) {
return Action.NeedsVerification;
}
return Action.None;
}
public byte[] getSessionToken() {
return sessionToken;
}
}

View file

@ -1,84 +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.login;
import java.security.NoSuchAlgorithmException;
import java.util.EnumSet;
import java.util.Set;
import org.mozilla.gecko.background.fxa.FxAccountClient;
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition;
import org.mozilla.gecko.fxa.login.State.StateLabel;
public class FxAccountLoginStateMachine {
public static final String LOG_TAG = FxAccountLoginStateMachine.class.getSimpleName();
public interface LoginStateMachineDelegate {
public FxAccountClient getClient();
public long getCertificateDurationInMilliseconds();
public long getAssertionDurationInMilliseconds();
public void handleTransition(Transition transition, State state);
public void handleFinal(State state);
public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException;
}
public static class ExecuteDelegate {
protected final LoginStateMachineDelegate delegate;
protected final StateLabel desiredStateLabel;
// It's as difficult to detect arbitrary cycles as repeated states.
protected final Set<StateLabel> stateLabelsSeen = EnumSet.noneOf(StateLabel.class);
protected ExecuteDelegate(StateLabel initialStateLabel, StateLabel desiredStateLabel, LoginStateMachineDelegate delegate) {
this.delegate = delegate;
this.desiredStateLabel = desiredStateLabel;
this.stateLabelsSeen.add(initialStateLabel);
}
public FxAccountClient getClient() {
return delegate.getClient();
}
public long getCertificateDurationInMilliseconds() {
return delegate.getCertificateDurationInMilliseconds();
}
public long getAssertionDurationInMilliseconds() {
return delegate.getAssertionDurationInMilliseconds();
}
public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException {
return delegate.generateKeyPair();
}
public void handleTransition(Transition transition, State state) {
// Always trigger the transition callback.
delegate.handleTransition(transition, state);
// Possibly trigger the final callback. We trigger if we're at our desired
// state, or if we've seen this state before.
StateLabel stateLabel = state.getStateLabel();
if (stateLabel == desiredStateLabel || stateLabelsSeen.contains(stateLabel)) {
delegate.handleFinal(state);
return;
}
// If this wasn't the last state, leave a bread crumb and move on to the
// next state.
stateLabelsSeen.add(stateLabel);
state.execute(this);
}
}
public void advance(State initialState, final StateLabel desiredStateLabel, final LoginStateMachineDelegate delegate) {
if (initialState.getStateLabel() == desiredStateLabel) {
// We're already where we want to be!
delegate.handleFinal(initialState);
return;
}
ExecuteDelegate executeDelegate = new ExecuteDelegate(initialState.getStateLabel(), desiredStateLabel, delegate);
initialState.execute(executeDelegate);
}
}

View file

@ -1,68 +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.login;
public class FxAccountLoginTransition {
public interface Transition {
}
public static class LogMessage implements Transition {
public final String detailMessage;
public LogMessage(String detailMessage) {
this.detailMessage = detailMessage;
}
@Override
public String toString() {
return getClass().getSimpleName() + (this.detailMessage == null ? "" : "('" + this.detailMessage + "')");
}
}
public static class AccountNeedsVerification extends LogMessage {
public AccountNeedsVerification() {
super(null);
}
}
public static class AccountVerified extends LogMessage {
public AccountVerified() {
super(null);
}
}
public static class PasswordRequired extends LogMessage {
public PasswordRequired() {
super(null);
}
}
public static class LocalError implements Transition {
public final Exception e;
public LocalError(Exception e) {
this.e = e;
}
@Override
public String toString() {
return "Log(" + this.e + ")";
}
}
public static class RemoteError implements Transition {
public final Exception e;
public RemoteError(Exception e) {
this.e = e;
}
@Override
public String toString() {
return "Log(" + (this.e == null ? "null" : this.e) + ")";
}
}
}

View file

@ -1,117 +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.login;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
import org.mozilla.gecko.browserid.JSONWebTokenUtils;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LogMessage;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.NonObjectJSONException;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.crypto.KeyBundle;
public class Married extends TokensAndKeysState {
private static final String LOG_TAG = Married.class.getSimpleName();
protected final String certificate;
protected final String clientState;
public Married(String email, String uid, byte[] sessionToken, byte[] kA, byte[] kB, BrowserIDKeyPair keyPair, String certificate) {
super(StateLabel.Married, email, uid, sessionToken, kA, kB, keyPair);
Utils.throwIfNull(certificate);
this.certificate = certificate;
try {
this.clientState = FxAccountUtils.computeClientState(kB);
} catch (NoSuchAlgorithmException e) {
// This should never occur.
throw new IllegalStateException("Unable to compute client state from kB.");
}
}
@Override
public ExtendedJSONObject toJSONObject() {
ExtendedJSONObject o = super.toJSONObject();
// Fields are non-null by constructor.
o.put("certificate", certificate);
return o;
}
@Override
public void execute(final ExecuteDelegate delegate) {
delegate.handleTransition(new LogMessage("staying married"), this);
}
public String generateAssertion(String audience, String issuer) throws NonObjectJSONException, IOException, GeneralSecurityException {
// We generate assertions with no iat and an exp after 2050 to avoid
// invalid-timestamp errors from the token server.
final long expiresAt = JSONWebTokenUtils.DEFAULT_FUTURE_EXPIRES_AT_IN_MILLISECONDS;
String assertion = JSONWebTokenUtils.createAssertion(keyPair.getPrivate(), certificate, audience, issuer, null, expiresAt);
if (!FxAccountUtils.LOG_PERSONAL_INFORMATION) {
return assertion;
}
try {
FxAccountUtils.pii(LOG_TAG, "Generated assertion: " + assertion);
ExtendedJSONObject a = JSONWebTokenUtils.parseAssertion(assertion);
if (a != null) {
FxAccountUtils.pii(LOG_TAG, "aHeader : " + a.getObject("header"));
FxAccountUtils.pii(LOG_TAG, "aPayload : " + a.getObject("payload"));
FxAccountUtils.pii(LOG_TAG, "aSignature: " + a.getString("signature"));
String certificate = a.getString("certificate");
if (certificate != null) {
ExtendedJSONObject c = JSONWebTokenUtils.parseCertificate(certificate);
FxAccountUtils.pii(LOG_TAG, "cHeader : " + c.getObject("header"));
FxAccountUtils.pii(LOG_TAG, "cPayload : " + c.getObject("payload"));
FxAccountUtils.pii(LOG_TAG, "cSignature: " + c.getString("signature"));
// Print the relevant timestamps in sorted order with labels.
HashMap<Long, String> map = new HashMap<Long, String>();
map.put(a.getObject("payload").getLong("iat"), "aiat");
map.put(a.getObject("payload").getLong("exp"), "aexp");
map.put(c.getObject("payload").getLong("iat"), "ciat");
map.put(c.getObject("payload").getLong("exp"), "cexp");
ArrayList<Long> values = new ArrayList<Long>(map.keySet());
Collections.sort(values);
for (Long value : values) {
FxAccountUtils.pii(LOG_TAG, map.get(value) + ": " + value);
}
} else {
FxAccountUtils.pii(LOG_TAG, "Could not parse certificate!");
}
} else {
FxAccountUtils.pii(LOG_TAG, "Could not parse assertion!");
}
} catch (Exception e) {
FxAccountUtils.pii(LOG_TAG, "Got exception dumping assertion debug info.");
}
return assertion;
}
public KeyBundle getSyncKeyBundle() throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
// TODO Document this choice for deriving from kB.
return FxAccountUtils.generateSyncKeyBundle(kB);
}
public String getClientState() {
if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
FxAccountUtils.pii(LOG_TAG, "Client state: " + this.clientState);
}
return this.clientState;
}
public Cohabiting makeCohabitingState() {
return new Cohabiting(email, uid, sessionToken, kA, kB, keyPair);
}
}

View file

@ -1,28 +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.login;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.PasswordRequired;
public class MigratedFromSync11 extends State {
public final String password;
public MigratedFromSync11(String email, String uid, boolean verified, String password) {
super(StateLabel.MigratedFromSync11, email, uid, verified);
// Null password is allowed.
this.password = password;
}
@Override
public void execute(final ExecuteDelegate delegate) {
delegate.handleTransition(new PasswordRequired(), this);
}
@Override
public Action getNeededAction() {
return Action.NeedsFinishMigrating;
}
}

View file

@ -1,25 +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.login;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.PasswordRequired;
public class Separated extends State {
public Separated(String email, String uid, boolean verified) {
super(StateLabel.Separated, email, uid, verified);
}
@Override
public void execute(final ExecuteDelegate delegate) {
delegate.handleTransition(new PasswordRequired(), this);
}
@Override
public Action getNeededAction() {
return Action.NeedsPassword;
}
}

View file

@ -1,72 +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.login;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.Utils;
public abstract class State {
public static final long CURRENT_VERSION = 3L;
public enum StateLabel {
Engaged,
Cohabiting,
Married,
Separated,
Doghouse,
MigratedFromSync11,
}
public enum Action {
NeedsUpgrade,
NeedsPassword,
NeedsVerification,
NeedsFinishMigrating,
None,
}
protected final StateLabel stateLabel;
public final String email;
public final String uid;
public final boolean verified;
public State(StateLabel stateLabel, String email, String uid, boolean verified) {
Utils.throwIfNull(email, uid);
this.stateLabel = stateLabel;
this.email = email;
this.uid = uid;
this.verified = verified;
}
public StateLabel getStateLabel() {
return this.stateLabel;
}
public ExtendedJSONObject toJSONObject() {
ExtendedJSONObject o = new ExtendedJSONObject();
o.put("version", State.CURRENT_VERSION);
o.put("email", email);
o.put("uid", uid);
o.put("verified", verified);
return o;
}
public State makeSeparatedState() {
return new Separated(email, uid, verified);
}
public State makeDoghouseState() {
return new Doghouse(email, uid, verified);
}
public State makeMigratedFromSync11State(String password) {
return new MigratedFromSync11(email, uid, verified, password);
}
public abstract void execute(ExecuteDelegate delegate);
public abstract Action getNeededAction();
}

View file

@ -1,206 +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.login;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
import org.mozilla.gecko.browserid.DSACryptoImplementation;
import org.mozilla.gecko.browserid.RSACryptoImplementation;
import org.mozilla.gecko.fxa.login.State.StateLabel;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.NonObjectJSONException;
import org.mozilla.gecko.sync.Utils;
/**
* Create {@link State} instances from serialized representations.
* <p>
* Version 1 recognizes 5 state labels (Engaged, Cohabiting, Married, Separated,
* Doghouse). In the Cohabiting and Married states, the associated key pairs are
* always RSA key pairs.
* <p>
* Version 2 is identical to version 1, except that in the Cohabiting and
* Married states, the associated keypairs are always DSA key pairs.
*/
public class StateFactory {
private static final String LOG_TAG = StateFactory.class.getSimpleName();
private static final int KEY_PAIR_SIZE_IN_BITS_V1 = 1024;
public static BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException {
// New key pairs are always DSA.
return DSACryptoImplementation.generateKeyPair(KEY_PAIR_SIZE_IN_BITS_V1);
}
protected static BrowserIDKeyPair keyPairFromJSONObjectV1(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
// V1 key pairs are RSA.
return RSACryptoImplementation.fromJSONObject(o);
}
protected static BrowserIDKeyPair keyPairFromJSONObjectV2(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
// V2 key pairs are DSA.
return DSACryptoImplementation.fromJSONObject(o);
}
public static State fromJSONObject(StateLabel stateLabel, ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException {
Long version = o.getLong("version");
if (version == null) {
throw new IllegalStateException("version must not be null");
}
final int v = version.intValue();
if (v == 3) {
// The most common case is the most recent version.
return fromJSONObjectV3(stateLabel, o);
}
if (v == 2) {
return fromJSONObjectV2(stateLabel, o);
}
if (v == 1) {
final State state = fromJSONObjectV1(stateLabel, o);
return migrateV1toV2(stateLabel, state);
}
throw new IllegalStateException("version must be in {1, 2}");
}
protected static State fromJSONObjectV1(StateLabel stateLabel, ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException {
switch (stateLabel) {
case Engaged:
return new Engaged(
o.getString("email"),
o.getString("uid"),
o.getBoolean("verified"),
Utils.hex2Byte(o.getString("unwrapkB")),
Utils.hex2Byte(o.getString("sessionToken")),
Utils.hex2Byte(o.getString("keyFetchToken")));
case Cohabiting:
return new Cohabiting(
o.getString("email"),
o.getString("uid"),
Utils.hex2Byte(o.getString("sessionToken")),
Utils.hex2Byte(o.getString("kA")),
Utils.hex2Byte(o.getString("kB")),
keyPairFromJSONObjectV1(o.getObject("keyPair")));
case Married:
return new Married(
o.getString("email"),
o.getString("uid"),
Utils.hex2Byte(o.getString("sessionToken")),
Utils.hex2Byte(o.getString("kA")),
Utils.hex2Byte(o.getString("kB")),
keyPairFromJSONObjectV1(o.getObject("keyPair")),
o.getString("certificate"));
case Separated:
return new Separated(
o.getString("email"),
o.getString("uid"),
o.getBoolean("verified"));
case Doghouse:
return new Doghouse(
o.getString("email"),
o.getString("uid"),
o.getBoolean("verified"));
default:
throw new IllegalStateException("unrecognized state label: " + stateLabel);
}
}
/**
* Exactly the same as {@link fromJSONObjectV1}, except that all key pairs are DSA key pairs.
*/
protected static State fromJSONObjectV2(StateLabel stateLabel, ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException {
switch (stateLabel) {
case Cohabiting:
return new Cohabiting(
o.getString("email"),
o.getString("uid"),
Utils.hex2Byte(o.getString("sessionToken")),
Utils.hex2Byte(o.getString("kA")),
Utils.hex2Byte(o.getString("kB")),
keyPairFromJSONObjectV2(o.getObject("keyPair")));
case Married:
return new Married(
o.getString("email"),
o.getString("uid"),
Utils.hex2Byte(o.getString("sessionToken")),
Utils.hex2Byte(o.getString("kA")),
Utils.hex2Byte(o.getString("kB")),
keyPairFromJSONObjectV2(o.getObject("keyPair")),
o.getString("certificate"));
default:
return fromJSONObjectV1(stateLabel, o);
}
}
/**
* Exactly the same as {@link fromJSONObjectV2}, except that there's a new
* MigratedFromSyncV11 state.
*/
protected static State fromJSONObjectV3(StateLabel stateLabel, ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException {
switch (stateLabel) {
case MigratedFromSync11:
return new MigratedFromSync11(
o.getString("email"),
o.getString("uid"),
o.getBoolean("verified"),
o.getString("password"));
default:
return fromJSONObjectV2(stateLabel, o);
}
}
protected static void logMigration(State from, State to) {
if (!FxAccountUtils.LOG_PERSONAL_INFORMATION) {
return;
}
try {
FxAccountUtils.pii(LOG_TAG, "V1 persisted state is: " + from.toJSONObject().toJSONString());
} catch (Exception e) {
Logger.warn(LOG_TAG, "Error producing JSON representation of V1 state.", e);
}
FxAccountUtils.pii(LOG_TAG, "Generated new V2 state: " + to.toJSONObject().toJSONString());
}
protected static State migrateV1toV2(StateLabel stateLabel, State state) throws NoSuchAlgorithmException {
if (state == null) {
// This should never happen, but let's be careful.
Logger.error(LOG_TAG, "Got null state in migrateV1toV2; returning null.");
return state;
}
Logger.info(LOG_TAG, "Migrating V1 persisted State to V2; stateLabel: " + stateLabel);
// In V1, we use an RSA keyPair. In V2, we use a DSA keyPair. Only
// Cohabiting and Married states have a persisted keyPair at all; all
// other states need no conversion at all.
switch (stateLabel) {
case Cohabiting: {
// In the Cohabiting state, we can just generate a new key pair and move on.
final Cohabiting cohabiting = (Cohabiting) state;
final BrowserIDKeyPair keyPair = generateKeyPair();
final State migrated = new Cohabiting(cohabiting.email, cohabiting.uid, cohabiting.sessionToken, cohabiting.kA, cohabiting.kB, keyPair);
logMigration(cohabiting, migrated);
return migrated;
}
case Married: {
// In the Married state, we cannot only change the key pair: the stored
// certificate signs the public key of the now obsolete key pair. We
// regress to the Cohabiting state; the next time we sync, we should
// advance back to Married.
final Married married = (Married) state;
final BrowserIDKeyPair keyPair = generateKeyPair();
final State migrated = new Cohabiting(married.email, married.uid, married.sessionToken, married.kA, married.kB, keyPair);
logMigration(married, migrated);
return migrated;
}
default:
// Otherwise, V1 and V2 states are identical.
return state;
}
}
}

View file

@ -1,45 +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.login;
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.Utils;
public abstract class TokensAndKeysState extends State {
protected final byte[] sessionToken;
protected final byte[] kA;
protected final byte[] kB;
protected final BrowserIDKeyPair keyPair;
public TokensAndKeysState(StateLabel stateLabel, String email, String uid, byte[] sessionToken, byte[] kA, byte[] kB, BrowserIDKeyPair keyPair) {
super(stateLabel, email, uid, true);
Utils.throwIfNull(sessionToken, kA, kB, keyPair);
this.sessionToken = sessionToken;
this.kA = kA;
this.kB = kB;
this.keyPair = keyPair;
}
@Override
public ExtendedJSONObject toJSONObject() {
ExtendedJSONObject o = super.toJSONObject();
// Fields are non-null by constructor.
o.put("sessionToken", Utils.byte2Hex(sessionToken));
o.put("kA", Utils.byte2Hex(kA));
o.put("kB", Utils.byte2Hex(kB));
o.put("keyPair", keyPair.toJSONObject());
return o;
}
public byte[] getSessionToken() {
return sessionToken;
}
@Override
public Action getNeededAction() {
return Action.None;
}
}

View file

@ -1,154 +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.receivers;
import android.app.IntentService;
import android.content.Context;
import android.content.Intent;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClient;
import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException.FxAccountAbstractClientRemoteException;
import org.mozilla.gecko.background.fxa.oauth.FxAccountOAuthClient10;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.sync.FxAccountNotificationManager;
import org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter;
import org.mozilla.gecko.sync.repositories.android.ClientsDatabase;
import org.mozilla.gecko.sync.repositories.android.FennecTabsRepository;
import java.util.concurrent.Executor;
/**
* A background service to clean up after a Firefox Account is deleted.
* <p>
* Note that we specifically handle deleting the pickle file using a Service and a
* BroadcastReceiver, rather than a background thread, to allow channels sharing a Firefox account
* to delete their respective pickle files (since, if one remains, the account will be restored
* when that channel is used).
*/
public class FxAccountDeletedService extends IntentService {
public static final String LOG_TAG = FxAccountDeletedService.class.getSimpleName();
public FxAccountDeletedService() {
super(LOG_TAG);
}
@Override
protected void onHandleIntent(final Intent intent) {
// We have an in-memory accounts cache which we use for a variety of tasks; it needs to be cleared.
// It should be fine to invalidate it before doing anything else, as the tasks below do not rely
// on this data.
AndroidFxAccount.invalidateCaches();
// Intent can, in theory, be null. Bug 1025937.
if (intent == null) {
Logger.debug(LOG_TAG, "Short-circuiting on null intent.");
return;
}
final Context context = this;
long intentVersion = intent.getLongExtra(
FxAccountConstants.ACCOUNT_DELETED_INTENT_VERSION_KEY, 0);
long expectedVersion = FxAccountConstants.ACCOUNT_DELETED_INTENT_VERSION;
if (intentVersion != expectedVersion) {
Logger.warn(LOG_TAG, "Intent malformed: version " + intentVersion + " given but " +
"version " + expectedVersion + "expected. Not cleaning up after deleted Account.");
return;
}
// Android Account name, not Sync encoded account name.
final String accountName = intent.getStringExtra(
FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_KEY);
if (accountName == null) {
Logger.warn(LOG_TAG, "Intent malformed: no account name given. Not cleaning up after " +
"deleted Account.");
return;
}
// Fire up gecko and unsubscribe push
final Intent geckoIntent = new Intent();
geckoIntent.setAction("create-services");
geckoIntent.setClassName(context, "org.mozilla.gecko.GeckoService");
geckoIntent.putExtra("category", "android-push-service");
geckoIntent.putExtra("data", "android-fxa-unsubscribe");
final AndroidFxAccount fxAccount = AndroidFxAccount.fromContext(context);
geckoIntent.putExtra("org.mozilla.gecko.intent.PROFILE_NAME",
intent.getStringExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_PROFILE));
context.startService(geckoIntent);
// Delete client database and non-local tabs.
Logger.info(LOG_TAG, "Deleting the entire Fennec clients database and non-local tabs");
FennecTabsRepository.deleteNonLocalClientsAndTabs(context);
// Clear Firefox Sync client tables.
try {
Logger.info(LOG_TAG, "Deleting the Firefox Sync clients database.");
ClientsDatabase db = null;
try {
db = new ClientsDatabase(context);
db.wipeClientsTable();
db.wipeCommandsTable();
} finally {
if (db != null) {
db.close();
}
}
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception deleting the Firefox Sync clients database; ignoring.", e);
}
// Remove any displayed notifications.
new FxAccountNotificationManager(FxAccountSyncAdapter.NOTIFICATION_ID).clear(context);
// Bug 1147275: Delete cached oauth tokens. There's no way to query all
// oauth tokens from Android, so this is tricky to do comprehensively. We
// can query, individually, for specific oauth tokens to delete, however.
final String oauthServerURI = intent.getStringExtra(FxAccountConstants.ACCOUNT_OAUTH_SERVICE_ENDPOINT_KEY);
final String[] tokens = intent.getStringArrayExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_AUTH_TOKENS);
if (oauthServerURI != null && tokens != null) {
final Executor directExecutor = new Executor() {
@Override
public void execute(Runnable runnable) {
runnable.run();
}
};
final FxAccountOAuthClient10 oauthClient = new FxAccountOAuthClient10(oauthServerURI, directExecutor);
for (String token : tokens) {
if (token == null) {
Logger.error(LOG_TAG, "Cached OAuth token is null; should never happen. Ignoring.");
continue;
}
try {
oauthClient.deleteToken(token, new FxAccountAbstractClient.RequestDelegate<Void>() {
@Override
public void handleSuccess(Void result) {
Logger.info(LOG_TAG, "Successfully deleted cached OAuth token.");
}
@Override
public void handleError(Exception e) {
Logger.error(LOG_TAG, "Failed to delete cached OAuth token; ignoring.", e);
}
@Override
public void handleFailure(FxAccountAbstractClientRemoteException e) {
Logger.error(LOG_TAG, "Exception during cached OAuth token deletion; ignoring.", e);
}
});
} catch (Exception e) {
Logger.error(LOG_TAG, "Exception during cached OAuth token deletion; ignoring.", e);
}
}
} else {
Logger.error(LOG_TAG, "Cached OAuth server URI is null or cached OAuth tokens are null; ignoring.");
}
}
}

View file

@ -1,133 +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.receivers;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.login.State.StateLabel;
import org.mozilla.gecko.sync.Utils;
import android.accounts.Account;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
/**
* A receiver that takes action when our Android package is upgraded (replaced).
*/
public class FxAccountUpgradeReceiver extends BroadcastReceiver {
private static final String LOG_TAG = FxAccountUpgradeReceiver.class.getSimpleName();
/**
* Produce a list of Runnable instances to be executed sequentially on
* upgrade.
* <p>
* Each Runnable will be executed sequentially on a background thread. Any
* unchecked Exception thrown will be caught and ignored.
*
* @param context Android context.
* @return list of Runnable instances.
*/
protected List<Runnable> onUpgradeRunnables(Context context) {
List<Runnable> runnables = new LinkedList<Runnable>();
runnables.add(new MaybeUnpickleRunnable(context));
// Recovering accounts that are in the Doghouse should happen *after* we
// unpickle any accounts saved to disk.
runnables.add(new AdvanceFromDoghouseRunnable(context));
return runnables;
}
@Override
public void onReceive(final Context context, Intent intent) {
Logger.setThreadLogTag(FxAccountConstants.GLOBAL_LOG_TAG);
Logger.info(LOG_TAG, "Upgrade broadcast received.");
// Iterate Runnable instances one at a time.
final Executor executor = Executors.newSingleThreadExecutor();
for (final Runnable runnable : onUpgradeRunnables(context)) {
executor.execute(new Runnable() {
@Override
public void run() {
try {
runnable.run();
} catch (Exception e) {
// We really don't want to throw on a background thread, so we
// catch, log, and move on.
Logger.error(LOG_TAG, "Got exception executing background upgrade Runnable; ignoring.", e);
}
}
});
}
}
/**
* A Runnable that tries to unpickle any pickled Firefox Accounts.
*/
protected static class MaybeUnpickleRunnable implements Runnable {
protected final Context context;
public MaybeUnpickleRunnable(Context context) {
this.context = context;
}
@Override
public void run() {
// Querying the accounts will unpickle any pickled Firefox Account.
Logger.info(LOG_TAG, "Trying to unpickle any pickled Firefox Account.");
FirefoxAccounts.getFirefoxAccounts(context);
}
}
/**
* A Runnable that tries to advance existing Firefox Accounts that are in the
* Doghouse state to the Separated state.
* <p>
* This is our main deprecation-and-upgrade mechanism: in some way, the
* Account gets moved to the Doghouse state. If possible, an upgraded version
* of the package advances to Separated, prompting the user to re-connect the
* Account.
*/
protected static class AdvanceFromDoghouseRunnable implements Runnable {
protected final Context context;
public AdvanceFromDoghouseRunnable(Context context) {
this.context = context;
}
@Override
public void run() {
final Account[] accounts = FirefoxAccounts.getFirefoxAccounts(context);
Logger.info(LOG_TAG, "Trying to advance " + accounts.length + " existing Firefox Accounts from the Doghouse to Separated (if necessary).");
for (Account account : accounts) {
try {
final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
// For great debugging.
if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
fxAccount.dump();
}
State state = fxAccount.getState();
if (state == null || state.getStateLabel() != StateLabel.Doghouse) {
Logger.debug(LOG_TAG, "Account named like " + Utils.obfuscateEmail(account.name) + " is not in the Doghouse; skipping.");
continue;
}
Logger.debug(LOG_TAG, "Account named like " + Utils.obfuscateEmail(account.name) + " is in the Doghouse; advancing to Separated.");
fxAccount.setState(state.makeSeparatedState());
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception trying to advance account named like " + Utils.obfuscateEmail(account.name) +
" from Doghouse to Separated state; ignoring.", e);
}
}
}
}
}

View file

@ -1,114 +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.sync;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationCompat.Builder;
import org.mozilla.gecko.Locales;
import org.mozilla.gecko.R;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.common.telemetry.TelemetryWrapper;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.activities.FxAccountWebFlowActivity;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.login.State.Action;
import org.mozilla.gecko.sync.telemetry.TelemetryContract;
/**
* Abstraction that manages notifications shown or hidden for a Firefox Account.
* <p>
* In future, we anticipate this tracking things like:
* <ul>
* <li>new engines to offer to Sync;</li>
* <li>service interruption updates;</li>
* <li>messages from other clients.</li>
* </ul>
*/
public class FxAccountNotificationManager {
private static final String LOG_TAG = FxAccountNotificationManager.class.getSimpleName();
protected final int notificationId;
// We're lazy about updating our locale info, because most syncs don't notify.
private volatile boolean localeUpdated;
public FxAccountNotificationManager(int notificationId) {
this.notificationId = notificationId;
}
/**
* Remove all Firefox Account related notifications from the notification manager.
*
* @param context
* Android context.
*/
public void clear(Context context) {
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(notificationId);
}
/**
* Reflect new Firefox Account state to the notification manager: show or hide
* notifications reflecting the state of a Firefox Account.
*
* @param context
* Android context.
* @param fxAccount
* Firefox Account to reflect to the notification manager.
*/
public void update(Context context, AndroidFxAccount fxAccount) {
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
final State state = fxAccount.getState();
final Action action = state.getNeededAction();
if (action == Action.None) {
Logger.info(LOG_TAG, "State " + state.getStateLabel() + " needs no action; cancelling any existing notification.");
notificationManager.cancel(notificationId);
return;
}
if (!localeUpdated) {
localeUpdated = true;
Locales.getLocaleManager().getAndApplyPersistedLocale(context);
}
final String title;
final String text;
final Intent notificationIntent;
if (action == Action.NeedsFinishMigrating) {
TelemetryWrapper.addToHistogram(TelemetryContract.SYNC11_MIGRATION_NOTIFICATIONS_OFFERED, 1);
title = context.getResources().getString(R.string.fxaccount_sync_finish_migrating_notification_title);
text = context.getResources().getString(R.string.fxaccount_sync_finish_migrating_notification_text, state.email);
notificationIntent = new Intent(FxAccountConstants.ACTION_FXA_FINISH_MIGRATING);
} else {
title = context.getResources().getString(R.string.fxaccount_sync_sign_in_error_notification_title);
text = context.getResources().getString(R.string.fxaccount_sync_sign_in_error_notification_text, state.email);
notificationIntent = new Intent(FxAccountConstants.ACTION_FXA_STATUS);
}
notificationIntent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_NOTIFICATION);
Logger.info(LOG_TAG, "State " + state.getStateLabel() + " needs action; offering notification with title: " + title);
FxAccountUtils.pii(LOG_TAG, "And text: " + text);
final PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0);
final Builder builder = new NotificationCompat.Builder(context);
builder
.setContentTitle(title)
.setContentText(text)
.setSmallIcon(R.drawable.ic_status_logo)
.setAutoCancel(true)
.setContentIntent(pendingIntent);
notificationManager.notify(notificationId, builder.build());
}
}

View file

@ -1,107 +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.sync;
import android.accounts.AccountManager;
import android.app.Activity;
import android.app.IntentService;
import android.content.Intent;
import android.os.Bundle;
import android.os.ResultReceiver;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClient;
import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException;
import org.mozilla.gecko.background.fxa.profile.FxAccountProfileClient10;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class FxAccountProfileService extends IntentService {
private static final String LOG_TAG = "FxAccountProfileService";
private static final Executor EXECUTOR_SERVICE = Executors.newSingleThreadExecutor();
public static final String KEY_AUTH_TOKEN = "auth_token";
public static final String KEY_PROFILE_SERVER_URI = "profileServerURI";
public static final String KEY_RESULT_RECEIVER = "resultReceiver";
public static final String KEY_RESULT_STRING = "RESULT_STRING";
public FxAccountProfileService() {
super("FxAccountProfileService");
}
@Override
protected void onHandleIntent(Intent intent) {
final String authToken = intent.getStringExtra(KEY_AUTH_TOKEN);
final String profileServerURI = intent.getStringExtra(KEY_PROFILE_SERVER_URI);
final ResultReceiver resultReceiver = intent.getParcelableExtra(KEY_RESULT_RECEIVER);
if (resultReceiver == null) {
Logger.warn(LOG_TAG, "Result receiver must not be null; ignoring intent.");
return;
}
if (authToken == null || authToken.length() == 0) {
Logger.warn(LOG_TAG, "Invalid Auth Token");
sendResult("Invalid Auth Token", resultReceiver, Activity.RESULT_CANCELED);
return;
}
if (profileServerURI == null || profileServerURI.length() == 0) {
Logger.warn(LOG_TAG, "Invalid profile Server Endpoint");
sendResult("Invalid profile Server Endpoint", resultReceiver, Activity.RESULT_CANCELED);
return;
}
// This delegate fetches the profile avatar json.
FxAccountProfileClient10.RequestDelegate<ExtendedJSONObject> delegate = new FxAccountAbstractClient.RequestDelegate<ExtendedJSONObject>() {
@Override
public void handleError(Exception e) {
Logger.error(LOG_TAG, "Error fetching Account profile.", e);
sendResult("Error fetching Account profile.", resultReceiver, Activity.RESULT_CANCELED);
}
@Override
public void handleFailure(FxAccountAbstractClientException.FxAccountAbstractClientRemoteException e) {
Logger.warn(LOG_TAG, "Failed to fetch Account profile.", e);
if (e.isInvalidAuthentication()) {
// The profile server rejected the cached oauth token! Invalidate it.
// A new token will be generated upon next request.
Logger.info(LOG_TAG, "Invalidating oauth token after 401!");
AccountManager.get(FxAccountProfileService.this).invalidateAuthToken(FxAccountConstants.ACCOUNT_TYPE, authToken);
}
sendResult("Failed to fetch Account profile.", resultReceiver, Activity.RESULT_CANCELED);
}
@Override
public void handleSuccess(ExtendedJSONObject result) {
if (result != null){
FxAccountUtils.pii(LOG_TAG, "Profile server return profile: " + result.toJSONString());
sendResult(result.toJSONString(), resultReceiver, Activity.RESULT_OK);
}
}
};
FxAccountProfileClient10 client = new FxAccountProfileClient10(profileServerURI, EXECUTOR_SERVICE);
try {
client.profile(authToken, delegate);
} catch (Exception e) {
Logger.error(LOG_TAG, "Got exception fetching profile.", e);
delegate.handleError(e);
}
}
private void sendResult(final String result, final ResultReceiver resultReceiver, final int code) {
if (resultReceiver != null) {
final Bundle bundle = new Bundle();
bundle.putString(KEY_RESULT_STRING, result);
resultReceiver.send(code, bundle);
}
}
}

View file

@ -1,178 +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.sync;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.login.State.Action;
import org.mozilla.gecko.sync.BackoffHandler;
import android.accounts.Account;
import android.content.ContentResolver;
import android.content.Context;
import android.os.Bundle;
public class FxAccountSchedulePolicy implements SchedulePolicy {
private static final String LOG_TAG = "FxAccountSchedulePolicy";
// Our poll intervals are used to trigger automatic background syncs
// in the absence of user activity.
//
// We also receive sync requests as a result of network tickles, so
// these intervals are long, with the exception of the rapid polling
// while we wait for verification: if we're waiting for the user to
// click on a verification link, we sync very often in order to detect
// a change in state.
//
// In the case of unverified -> unverified (no transition), this should be
// very close to a single HTTP request (with the SyncAdapter overhead, of
// course, but that's not wildly different from alarm manager overhead).
//
// The /account/status endpoint is HAWK authed by sessionToken, so we still
// have to do some crypto no matter what.
// TODO: only do this for a while...
public static final long POLL_INTERVAL_PENDING_VERIFICATION = 60; // 1 minute.
// If we're in some kind of error state, there's no point trying often.
// This is not the same as a server-imposed backoff, which will be
// reflected dynamically.
public static final long POLL_INTERVAL_ERROR_STATE_SEC = 24 * 60 * 60; // 24 hours.
// If we're the only device, just sync once or twice a day in case that
// changes.
public static final long POLL_INTERVAL_SINGLE_DEVICE_SEC = 18 * 60 * 60; // 18 hours.
// And if we know there are other devices, let's sync often enough that
// we'll be more likely to be caught up (even if not completely) by the
// time you next use this device. This is also achieved via Android's
// network tickles.
public static final long POLL_INTERVAL_MULTI_DEVICE_SEC = 12 * 60 * 60; // 12 hours.
// This is used solely as an optimization for backoff handling, so it's not
// persisted.
private static volatile long POLL_INTERVAL_CURRENT_SEC = POLL_INTERVAL_SINGLE_DEVICE_SEC;
// Never sync more frequently than this, unless forced.
// This is to avoid overly-frequent syncs during active browsing.
public static final long RATE_LIMIT_FUNDAMENTAL_SEC = 90; // 90 seconds.
/**
* We are prompted to sync by several inputs:
* * Periodic syncs that we schedule at long intervals. See the POLL constants.
* * Network-tickle-based syncs that Android starts.
* * Upload-only syncs that are caused by local database writes.
*
* We rate-limit periodic and network-sourced events with this constant.
* We rate limit <b>both</b> with {@link FxAccountSchedulePolicy#RATE_LIMIT_FUNDAMENTAL_SEC}.
*/
public static final long RATE_LIMIT_BACKGROUND_SEC = 60 * 60; // 1 hour.
private final AndroidFxAccount account;
private final Context context;
public FxAccountSchedulePolicy(Context context, AndroidFxAccount account) {
this.account = account;
this.context = context;
}
/**
* Return a millisecond timestamp in the future, offset from the current
* time by the provided amount.
* @param millis the duration by which to delay
* @return a timestamp.
*/
private static long delay(long millis) {
return System.currentTimeMillis() + millis;
}
/**
* Updates the existing system periodic sync interval to the specified duration.
*
* @param intervalSeconds the requested period, which Android will vary by up to 4%.
*/
protected void requestPeriodicSync(final long intervalSeconds) {
final String authority = BrowserContract.AUTHORITY;
final Account account = this.account.getAndroidAccount();
this.context.getContentResolver();
Logger.info(LOG_TAG, "Scheduling periodic sync for " + intervalSeconds + ".");
ContentResolver.addPeriodicSync(account, authority, Bundle.EMPTY, intervalSeconds);
POLL_INTERVAL_CURRENT_SEC = intervalSeconds;
}
@Override
public void onSuccessfulSync(int otherClientsCount) {
this.account.setLastSyncedTimestamp(System.currentTimeMillis());
// This undoes the change made in observeBackoffMillis -- once we hit backoff we'll
// periodically sync at the backoff duration, but as soon as we succeed we'll switch
// into the client-count-dependent interval.
long interval = (otherClientsCount > 0) ? POLL_INTERVAL_MULTI_DEVICE_SEC : POLL_INTERVAL_SINGLE_DEVICE_SEC;
requestPeriodicSync(interval);
}
@Override
public void onHandleFinal(Action needed) {
switch (needed) {
case NeedsPassword:
case NeedsUpgrade:
case NeedsFinishMigrating:
requestPeriodicSync(POLL_INTERVAL_ERROR_STATE_SEC);
break;
case NeedsVerification:
requestPeriodicSync(POLL_INTERVAL_PENDING_VERIFICATION);
break;
case None:
// No action needed: we'll set the periodic sync interval
// when the sync finishes, via the SessionCallback.
break;
}
}
@Override
public void onUpgradeRequired() {
// TODO: this shouldn't occur in FxA, but when we upgrade we
// need to reduce the interval again.
requestPeriodicSync(POLL_INTERVAL_ERROR_STATE_SEC);
}
@Override
public void onUnauthorized() {
// TODO: this shouldn't occur in FxA, but when we fix our credentials
// we need to reduce the interval again.
requestPeriodicSync(POLL_INTERVAL_ERROR_STATE_SEC);
}
@Override
public void configureBackoffMillisOnBackoff(BackoffHandler backoffHandler, long backoffMillis, boolean onlyExtend) {
if (onlyExtend) {
backoffHandler.extendEarliestNextRequest(delay(backoffMillis));
} else {
backoffHandler.setEarliestNextRequest(delay(backoffMillis));
}
// Yes, we might be part-way through the interval, in which case the backoff
// code will do its job. But we certainly don't want to reduce the interval
// if we're given a small backoff instruction.
// We'll reset the poll interval next time we sync without a backoff instruction.
if (backoffMillis > (POLL_INTERVAL_CURRENT_SEC * 1000)) {
// Slightly inflate the backoff duration to ensure that a fuzzed
// periodic sync doesn't occur before our backoff has passed. Android
// 19+ default to a 4% fuzz factor.
requestPeriodicSync((long) Math.ceil((1.05 * backoffMillis) / 1000));
}
}
/**
* Accepts two {@link BackoffHandler} instances as input. These are used
* respectively to track fundamental rate limiting, and to separately
* rate-limit periodic and network-tickled syncs.
*/
@Override
public void configureBackoffMillisBeforeSyncing(BackoffHandler fundamentalRateHandler, BackoffHandler backgroundRateHandler) {
fundamentalRateHandler.setEarliestNextRequest(delay(RATE_LIMIT_FUNDAMENTAL_SEC * 1000));
backgroundRateHandler.setEarliestNextRequest(delay(RATE_LIMIT_BACKGROUND_SEC * 1000));
}
}

View file

@ -1,568 +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.sync;
import android.accounts.Account;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SyncResult;
import android.os.Bundle;
import android.os.SystemClock;
import android.text.TextUtils;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.common.telemetry.TelemetryWrapper;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.background.fxa.SkewHandler;
import org.mozilla.gecko.browserid.JSONWebTokenUtils;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.FxAccountDeviceRegistrator;
import org.mozilla.gecko.fxa.authenticator.AccountPickler;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.authenticator.FxADefaultLoginStateMachineDelegate;
import org.mozilla.gecko.fxa.authenticator.FxAccountAuthenticator;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine;
import org.mozilla.gecko.fxa.login.Married;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.login.State.StateLabel;
import org.mozilla.gecko.fxa.sync.FxAccountSyncDelegate.Result;
import org.mozilla.gecko.sync.BackoffHandler;
import org.mozilla.gecko.sync.GlobalSession;
import org.mozilla.gecko.sync.PrefsBackoffHandler;
import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
import org.mozilla.gecko.sync.SyncConfiguration;
import org.mozilla.gecko.sync.ThreadPool;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.crypto.KeyBundle;
import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
import org.mozilla.gecko.sync.net.AuthHeaderProvider;
import org.mozilla.gecko.sync.net.HawkAuthHeaderProvider;
import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
import org.mozilla.gecko.sync.telemetry.TelemetryContract;
import org.mozilla.gecko.tokenserver.TokenServerClient;
import org.mozilla.gecko.tokenserver.TokenServerClientDelegate;
import org.mozilla.gecko.tokenserver.TokenServerException;
import org.mozilla.gecko.tokenserver.TokenServerToken;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
private static final String LOG_TAG = FxAccountSyncAdapter.class.getSimpleName();
public static final int NOTIFICATION_ID = LOG_TAG.hashCode();
// Tracks the last seen storage hostname for backoff purposes.
private static final String PREF_BACKOFF_STORAGE_HOST = "backoffStorageHost";
// Used to do cheap in-memory rate limiting. Don't sync again if we
// successfully synced within this duration.
private static final int MINIMUM_SYNC_DELAY_MILLIS = 15 * 1000; // 15 seconds.
private volatile long lastSyncRealtimeMillis;
protected final ExecutorService executor;
protected final FxAccountNotificationManager notificationManager;
public FxAccountSyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize);
this.executor = Executors.newSingleThreadExecutor();
this.notificationManager = new FxAccountNotificationManager(NOTIFICATION_ID);
}
protected static class SyncDelegate extends FxAccountSyncDelegate {
@Override
public void handleSuccess() {
Logger.info(LOG_TAG, "Sync succeeded.");
super.handleSuccess();
TelemetryWrapper.addToHistogram(TelemetryContract.SYNC_COMPLETED, 1);
}
@Override
public void handleError(Exception e) {
Logger.error(LOG_TAG, "Got exception syncing.", e);
super.handleError(e);
TelemetryWrapper.addToHistogram(TelemetryContract.SYNC_FAILED, 1);
}
@Override
public void handleCannotSync(State finalState) {
Logger.warn(LOG_TAG, "Cannot sync from state: " + finalState.getStateLabel());
super.handleCannotSync(finalState);
}
@Override
public void postponeSync(long millis) {
if (millis <= 0) {
Logger.debug(LOG_TAG, "Asked to postpone sync, but zero delay.");
}
super.postponeSync(millis);
}
@Override
public void rejectSync() {
super.rejectSync();
}
protected final Collection<String> stageNamesToSync;
public SyncDelegate(BlockingQueue<Result> latch, SyncResult syncResult, AndroidFxAccount fxAccount, Collection<String> stageNamesToSync) {
super(latch, syncResult);
this.stageNamesToSync = Collections.unmodifiableCollection(stageNamesToSync);
}
public Collection<String> getStageNamesToSync() {
return this.stageNamesToSync;
}
}
protected static class SessionCallback implements GlobalSessionCallback {
protected final SyncDelegate syncDelegate;
protected final SchedulePolicy schedulePolicy;
protected volatile BackoffHandler storageBackoffHandler;
public SessionCallback(SyncDelegate syncDelegate, SchedulePolicy schedulePolicy) {
this.syncDelegate = syncDelegate;
this.schedulePolicy = schedulePolicy;
}
public void setBackoffHandler(BackoffHandler backoffHandler) {
this.storageBackoffHandler = backoffHandler;
}
@Override
public boolean shouldBackOffStorage() {
return storageBackoffHandler.delayMilliseconds() > 0;
}
@Override
public void requestBackoff(long backoffMillis) {
final boolean onlyExtend = true; // Because we trust what the storage server says.
schedulePolicy.configureBackoffMillisOnBackoff(storageBackoffHandler, backoffMillis, onlyExtend);
}
@Override
public void informUpgradeRequiredResponse(GlobalSession session) {
schedulePolicy.onUpgradeRequired();
}
@Override
public void informUnauthorizedResponse(GlobalSession globalSession, URI oldClusterURL) {
schedulePolicy.onUnauthorized();
}
@Override
public void informMigrated(GlobalSession globalSession) {
// It's not possible to migrate a Firefox Account to another Account type
// yet. Yell loudly but otherwise ignore.
Logger.error(LOG_TAG,
"Firefox Account informMigrated called, but it's not yet possible to migrate. " +
"Ignoring even though something is terribly wrong.");
}
@Override
public void handleStageCompleted(Stage currentState, GlobalSession globalSession) {
}
@Override
public void handleSuccess(GlobalSession globalSession) {
Logger.info(LOG_TAG, "Global session succeeded.");
// Get the number of clients, so we can schedule the sync interval accordingly.
try {
int otherClientsCount = globalSession.getClientsDelegate().getClientsCount();
Logger.debug(LOG_TAG, "" + otherClientsCount + " other client(s).");
this.schedulePolicy.onSuccessfulSync(otherClientsCount);
} finally {
// Continue with the usual success flow.
syncDelegate.handleSuccess();
}
}
@Override
public void handleError(GlobalSession globalSession, Exception e) {
Logger.warn(LOG_TAG, "Global session failed."); // Exception will be dumped by delegate below.
syncDelegate.handleError(e);
// TODO: should we reduce the periodic sync interval?
}
@Override
public void handleAborted(GlobalSession globalSession, String reason) {
Logger.warn(LOG_TAG, "Global session aborted: " + reason);
syncDelegate.handleError(null);
// TODO: should we reduce the periodic sync interval?
}
};
/**
* Return true if the provided {@link BackoffHandler} isn't reporting that we're in
* a backoff state, or the provided {@link Bundle} contains flags that indicate
* we should force a sync.
*/
private boolean shouldPerformSync(final BackoffHandler backoffHandler, final String kind, final Bundle extras) {
final long delay = backoffHandler.delayMilliseconds();
if (delay <= 0) {
return true;
}
if (extras == null) {
return false;
}
final boolean forced = extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false);
if (forced) {
Logger.info(LOG_TAG, "Forced sync (" + kind + "): overruling remaining backoff of " + delay + "ms.");
} else {
Logger.info(LOG_TAG, "Not syncing (" + kind + "): must wait another " + delay + "ms.");
}
return forced;
}
protected void syncWithAssertion(final String audience,
final String assertion,
final URI tokenServerEndpointURI,
final BackoffHandler tokenBackoffHandler,
final SharedPreferences sharedPrefs,
final KeyBundle syncKeyBundle,
final String clientState,
final SessionCallback callback,
final Bundle extras,
final AndroidFxAccount fxAccount) {
final TokenServerClientDelegate delegate = new TokenServerClientDelegate() {
private boolean didReceiveBackoff = false;
@Override
public String getUserAgent() {
return FxAccountConstants.USER_AGENT;
}
@Override
public void handleSuccess(final TokenServerToken token) {
FxAccountUtils.pii(LOG_TAG, "Got token! uid is " + token.uid + " and endpoint is " + token.endpoint + ".");
fxAccount.releaseSharedAccountStateLock();
if (!didReceiveBackoff) {
// We must be OK to touch this token server.
tokenBackoffHandler.setEarliestNextRequest(0L);
}
final URI storageServerURI;
try {
storageServerURI = new URI(token.endpoint);
} catch (URISyntaxException e) {
handleError(e);
return;
}
final String storageHostname = storageServerURI.getHost();
// We back off on a per-host basis. When we have an endpoint URI from a token, we
// can check on the backoff status for that host.
// If we're supposed to be backing off, we abort the not-yet-started session.
final BackoffHandler storageBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "sync.storage");
callback.setBackoffHandler(storageBackoffHandler);
String lastStorageHost = sharedPrefs.getString(PREF_BACKOFF_STORAGE_HOST, null);
final boolean storageHostIsUnchanged = lastStorageHost != null &&
lastStorageHost.equalsIgnoreCase(storageHostname);
if (storageHostIsUnchanged) {
Logger.debug(LOG_TAG, "Storage host is unchanged.");
if (!shouldPerformSync(storageBackoffHandler, "storage", extras)) {
Logger.info(LOG_TAG, "Not syncing: storage server requested backoff.");
callback.handleAborted(null, "Storage backoff");
return;
}
} else {
Logger.debug(LOG_TAG, "Received new storage host.");
}
// Invalidate the previous backoff, because our storage host has changed,
// or we never had one at all, or we're OK to sync.
storageBackoffHandler.setEarliestNextRequest(0L);
GlobalSession globalSession = null;
try {
final ClientsDataDelegate clientsDataDelegate = new SharedPreferencesClientsDataDelegate(sharedPrefs, getContext());
if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
FxAccountUtils.pii(LOG_TAG, "Client device name is: '" + clientsDataDelegate.getClientName() + "'.");
FxAccountUtils.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
// timestamps. Eventually we might want this to adapt within the scope of a
// global session.
final SkewHandler storageServerSkewHandler = SkewHandler.getSkewHandlerForHostname(storageHostname);
final long storageServerSkew = storageServerSkewHandler.getSkewInSeconds();
// We expect Sync to upload large sets of records. Calculating the
// payload verification hash for these record sets could be expensive,
// so we explicitly do not send payload verification hashes to the
// Sync storage endpoint.
final boolean includePayloadVerificationHash = false;
final AuthHeaderProvider authHeaderProvider = new HawkAuthHeaderProvider(token.id, token.key.getBytes("UTF-8"), includePayloadVerificationHash, storageServerSkew);
final Context context = getContext();
final SyncConfiguration syncConfig = new SyncConfiguration(token.uid, authHeaderProvider, sharedPrefs, syncKeyBundle);
Collection<String> knownStageNames = SyncConfiguration.validEngineNames();
syncConfig.stagesToSync = Utils.getStagesToSyncFromBundle(knownStageNames, extras);
syncConfig.setClusterURL(storageServerURI);
globalSession = new GlobalSession(syncConfig, callback, context, clientsDataDelegate);
globalSession.start();
} catch (Exception e) {
callback.handleError(globalSession, e);
return;
}
}
@Override
public void handleFailure(TokenServerException e) {
Logger.error(LOG_TAG, "Failed to get token.", e);
try {
// We should only get here *after* we're locked into the married state.
State state = fxAccount.getState();
if (state.getStateLabel() == StateLabel.Married) {
Married married = (Married) state;
fxAccount.setState(married.makeCohabitingState());
}
} finally {
fxAccount.releaseSharedAccountStateLock();
}
callback.handleError(null, e);
}
@Override
public void handleError(Exception e) {
Logger.error(LOG_TAG, "Failed to get token.", e);
fxAccount.releaseSharedAccountStateLock();
callback.handleError(null, e);
}
@Override
public void handleBackoff(int backoffSeconds) {
// This is the token server telling us to back off.
Logger.info(LOG_TAG, "Token server requesting backoff of " + backoffSeconds + "s. Backoff handler: " + tokenBackoffHandler);
didReceiveBackoff = true;
// If we've already stored a backoff, overrule it: we only use the server
// value for token server scheduling.
tokenBackoffHandler.setEarliestNextRequest(delay(backoffSeconds * 1000));
}
private long delay(long delay) {
return System.currentTimeMillis() + delay;
}
};
TokenServerClient tokenServerclient = new TokenServerClient(tokenServerEndpointURI, executor);
tokenServerclient.getTokenFromBrowserIDAssertion(assertion, true, clientState, delegate);
}
/**
* A trivial Sync implementation that does not cache client keys,
* certificates, or tokens.
*
* This should be replaced with a full {@link FxAccountAuthenticator}-based
* token implementation.
*/
@Override
public void onPerformSync(final Account account, final Bundle extras, final String authority, ContentProviderClient provider, final SyncResult syncResult) {
Logger.setThreadLogTag(FxAccountConstants.GLOBAL_LOG_TAG);
Logger.resetLogging();
final Context context = getContext();
final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
Logger.info(LOG_TAG, "Syncing FxAccount" +
" account named like " + Utils.obfuscateEmail(account.name) +
" for authority " + authority +
" with instance " + this + ".");
Logger.info(LOG_TAG, "Account last synced at: " + fxAccount.getLastSyncedTimestamp());
if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
fxAccount.dump();
}
FirefoxAccounts.logSyncOptions(extras);
if (this.lastSyncRealtimeMillis > 0L &&
(this.lastSyncRealtimeMillis + MINIMUM_SYNC_DELAY_MILLIS) > SystemClock.elapsedRealtime() &&
!extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false)) {
Logger.info(LOG_TAG, "Not syncing FxAccount " + Utils.obfuscateEmail(account.name) +
": minimum interval not met.");
TelemetryWrapper.addToHistogram(TelemetryContract.SYNC_FAILED_BACKOFF, 1);
return;
}
// Pickle in a background thread to avoid strict mode warnings.
ThreadPool.run(new Runnable() {
@Override
public void run() {
try {
AccountPickler.pickle(fxAccount, FxAccountConstants.ACCOUNT_PICKLE_FILENAME);
} catch (Exception e) {
// Should never happen, but we really don't want to die in a background thread.
Logger.warn(LOG_TAG, "Got exception pickling current account details; ignoring.", e);
}
}
});
final BlockingQueue<Result> latch = new LinkedBlockingQueue<>(1);
Collection<String> knownStageNames = SyncConfiguration.validEngineNames();
Collection<String> stageNamesToSync = Utils.getStagesToSyncFromBundle(knownStageNames, extras);
final SyncDelegate syncDelegate = new SyncDelegate(latch, syncResult, fxAccount, stageNamesToSync);
try {
// This will be the same chunk of SharedPreferences that we pass through to GlobalSession/SyncConfiguration.
final SharedPreferences sharedPrefs = fxAccount.getSyncPrefs();
final BackoffHandler backgroundBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "background");
final BackoffHandler rateLimitBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "rate");
// If this sync was triggered by user action, this will be true.
final boolean isImmediate = (extras != null) &&
(extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false) ||
extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false));
// If it's not an immediate sync, it must be either periodic or tickled.
// Check our background rate limiter.
if (!isImmediate) {
if (!shouldPerformSync(backgroundBackoffHandler, "background", extras)) {
syncDelegate.rejectSync();
return;
}
}
// Regardless, let's make sure we're not syncing too often.
if (!shouldPerformSync(rateLimitBackoffHandler, "rate", extras)) {
syncDelegate.postponeSync(rateLimitBackoffHandler.delayMilliseconds());
return;
}
final SchedulePolicy schedulePolicy = new FxAccountSchedulePolicy(context, fxAccount);
// Set a small scheduled 'backoff' to rate-limit the next sync,
// and extend the background delay even further into the future.
schedulePolicy.configureBackoffMillisBeforeSyncing(rateLimitBackoffHandler, backgroundBackoffHandler);
final String tokenServerEndpoint = fxAccount.getTokenServerURI();
final URI tokenServerEndpointURI = new URI(tokenServerEndpoint);
final String audience = FxAccountUtils.getAudienceForURL(tokenServerEndpoint);
try {
// The clock starts... now!
fxAccount.acquireSharedAccountStateLock(FxAccountSyncAdapter.LOG_TAG);
} catch (InterruptedException e) {
// OK, skip this sync.
syncDelegate.handleError(e);
return;
}
final State state;
try {
state = fxAccount.getState();
} catch (Exception e) {
fxAccount.releaseSharedAccountStateLock();
syncDelegate.handleError(e);
return;
}
TelemetryWrapper.addToHistogram(TelemetryContract.SYNC_STARTED, 1);
final FxAccountLoginStateMachine stateMachine = new FxAccountLoginStateMachine();
stateMachine.advance(state, StateLabel.Married, new FxADefaultLoginStateMachineDelegate(context, fxAccount) {
@Override
public void handleNotMarried(State notMarried) {
Logger.info(LOG_TAG, "handleNotMarried: in " + notMarried.getStateLabel());
schedulePolicy.onHandleFinal(notMarried.getNeededAction());
syncDelegate.handleCannotSync(notMarried);
}
private boolean shouldRequestToken(final BackoffHandler tokenBackoffHandler, final Bundle extras) {
return shouldPerformSync(tokenBackoffHandler, "token", extras);
}
@Override
public void handleMarried(Married married) {
schedulePolicy.onHandleFinal(married.getNeededAction());
Logger.info(LOG_TAG, "handleMarried: in " + married.getStateLabel());
try {
final String assertion = married.generateAssertion(audience, JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER);
/*
* At this point we're in the correct state to sync, and we're ready to fetch
* a token and do some work.
*
* But first we need to do two things:
* 1. Check to see whether we're in a backoff situation for the token server.
* If we are, but we're not forcing a sync, then we go no further.
* 2. Clear an existing backoff (if we're syncing it doesn't matter, and if
* we're forcing we'll get a new backoff if things are still bad).
*
* Note that we don't check the storage backoff before the token dance: the token
* server tells us which server we're syncing to!
*
* That logic lives in the TokenServerClientDelegate elsewhere in this file.
*/
// Strictly speaking this backoff check could be done prior to walking through
// the login state machine, allowing us to short-circuit sooner.
// We don't expect many token server backoffs, and most users will be sitting
// in the Married state, so instead we simply do this here, once.
final BackoffHandler tokenBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "token");
if (!shouldRequestToken(tokenBackoffHandler, extras)) {
Logger.info(LOG_TAG, "Not syncing (token server).");
syncDelegate.postponeSync(tokenBackoffHandler.delayMilliseconds());
return;
}
final SessionCallback sessionCallback = new SessionCallback(syncDelegate, schedulePolicy);
final KeyBundle syncKeyBundle = married.getSyncKeyBundle();
final String clientState = married.getClientState();
syncWithAssertion(audience, assertion, tokenServerEndpointURI, tokenBackoffHandler, sharedPrefs, syncKeyBundle, clientState, sessionCallback, extras, fxAccount);
// Register the device if necessary (asynchronous, in another thread)
if (fxAccount.getDeviceRegistrationVersion() != FxAccountDeviceRegistrator.DEVICE_REGISTRATION_VERSION
|| TextUtils.isEmpty(fxAccount.getDeviceId())) {
FxAccountDeviceRegistrator.register(context);
}
// Force fetch the profile avatar information. (asynchronous, in another thread)
Logger.info(LOG_TAG, "Fetching profile avatar information.");
fxAccount.fetchProfileJSON();
} catch (Exception e) {
syncDelegate.handleError(e);
return;
}
}
});
latch.take();
} catch (Exception e) {
Logger.error(LOG_TAG, "Got error syncing.", e);
syncDelegate.handleError(e);
} finally {
fxAccount.releaseSharedAccountStateLock();
}
Logger.info(LOG_TAG, "Syncing done.");
lastSyncRealtimeMillis = SystemClock.elapsedRealtime();
}
}

View file

@ -1,110 +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.sync;
import java.util.concurrent.BlockingQueue;
import org.mozilla.gecko.fxa.login.State;
import android.content.SyncResult;
public class FxAccountSyncDelegate {
public enum Result {
Success,
Error,
Postponed,
Rejected,
}
protected final BlockingQueue<Result> latch;
protected final SyncResult syncResult;
public FxAccountSyncDelegate(BlockingQueue<Result> latch, SyncResult syncResult) {
if (latch == null) {
throw new IllegalArgumentException("latch must not be null");
}
if (syncResult == null) {
throw new IllegalArgumentException("syncResult must not be null");
}
this.latch = latch;
this.syncResult = syncResult;
}
/**
* No error! Say that we made progress.
*/
protected void setSyncResultSuccess() {
syncResult.stats.numUpdates += 1;
}
/**
* Soft error. Say that we made progress, so that Android will sync us again
* after exponential backoff.
*/
protected void setSyncResultSoftError() {
syncResult.stats.numUpdates += 1;
syncResult.stats.numIoExceptions += 1;
}
/**
* Hard error. We don't want Android to sync us again, even if we make
* progress, until the user intervenes.
*/
protected void setSyncResultHardError() {
syncResult.stats.numAuthExceptions += 1;
}
public void handleSuccess() {
setSyncResultSuccess();
latch.offer(Result.Success);
}
public void handleError(Exception e) {
setSyncResultSoftError();
latch.offer(Result.Error);
}
/**
* When the login machine terminates, we might not be in the
* <code>Married</code> state, and therefore we can't sync. This method
* messages as much to the user.
* <p>
* To avoid stopping us syncing altogether, we set a soft error rather than
* a hard error. In future, we would like to set a hard error if we are in,
* for example, the <code>Separated</code> state, and then have some user
* initiated activity mark the Android account as ready to sync again. This
* is tricky, though, so we play it safe for now.
*
* @param finalState
* that login machine ended in.
*/
public void handleCannotSync(State finalState) {
setSyncResultSoftError();
latch.offer(Result.Error);
}
public void postponeSync(long millis) {
if (millis > 0) {
// delayUntil is broken: https://code.google.com/p/android/issues/detail?id=65669
// So we don't bother doing this. Instead, we rely on the periodic sync
// we schedule, and the backoff handler for the rest.
/*
Logger.warn(LOG_TAG, "Postponing sync by " + millis + "ms.");
syncResult.delayUntil = millis / 1000;
*/
}
setSyncResultSoftError();
latch.offer(Result.Postponed);
}
/**
* Simply don't sync, without setting any error flags.
* This is the appropriate behavior when a routine backoff has not yet
* been met.
*/
public void rejectSync() {
latch.offer(Result.Rejected);
}
}

View file

@ -1,28 +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.sync;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
public class FxAccountSyncService extends Service {
private static final Object syncAdapterLock = new Object();
private static FxAccountSyncAdapter syncAdapter;
@Override
public void onCreate() {
synchronized (syncAdapterLock) {
if (syncAdapter == null) {
syncAdapter = new FxAccountSyncAdapter(getApplicationContext(), true);
}
}
}
@Override
public IBinder onBind(Intent intent) {
return syncAdapter.getSyncAdapterBinder();
}
}

View file

@ -1,113 +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.sync;
import java.util.Map;
import java.util.Map.Entry;
import java.util.WeakHashMap;
import org.mozilla.gecko.fxa.SyncStatusListener;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.util.ThreadUtils;
import android.content.ContentResolver;
import android.content.SyncStatusObserver;
/**
* Abstract away some details of Android's SyncStatusObserver.
* <p>
* Provides a simplified sync started/sync finished delegate.
*/
public class FxAccountSyncStatusHelper implements SyncStatusObserver {
@SuppressWarnings("unused")
private static final String LOG_TAG = FxAccountSyncStatusHelper.class.getSimpleName();
protected static FxAccountSyncStatusHelper sInstance;
public synchronized static FxAccountSyncStatusHelper getInstance() {
if (sInstance == null) {
sInstance = new FxAccountSyncStatusHelper();
}
return sInstance;
}
// Used to unregister this as a listener.
protected Object handle;
// Maps delegates to whether their underlying Android account was syncing the
// last time we observed a status change.
protected Map<SyncStatusListener, Boolean> delegates = new WeakHashMap<SyncStatusListener, Boolean>();
@Override
public synchronized void onStatusChanged(int which) {
for (Entry<SyncStatusListener, Boolean> entry : delegates.entrySet()) {
final SyncStatusListener delegate = entry.getKey();
final AndroidFxAccount fxAccount = new AndroidFxAccount(delegate.getContext(), delegate.getAccount());
final boolean active = fxAccount.isCurrentlySyncing();
// Remember for later.
boolean wasActiveLastTime = entry.getValue();
// It's okay to update the value of an entry while iterating the entrySet.
entry.setValue(active);
if (active && !wasActiveLastTime) {
// We've started a sync.
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
delegate.onSyncStarted();
}
});
}
if (!active && wasActiveLastTime) {
// We've finished a sync.
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
delegate.onSyncFinished();
}
});
}
}
}
protected void addListener() {
final int mask = ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE;
if (this.handle != null) {
throw new IllegalStateException("Already registered this as an observer?");
}
this.handle = ContentResolver.addStatusChangeListener(mask, this);
}
protected void removeListener() {
Object handle = this.handle;
this.handle = null;
if (handle != null) {
ContentResolver.removeStatusChangeListener(handle);
}
}
public synchronized void startObserving(SyncStatusListener delegate) {
if (delegate == null) {
throw new IllegalArgumentException("delegate must not be null");
}
if (delegates.containsKey(delegate)) {
return;
}
// If we are the first delegate to the party, start listening.
if (delegates.isEmpty()) {
addListener();
}
delegates.put(delegate, Boolean.FALSE);
}
public synchronized void stopObserving(SyncStatusListener delegate) {
delegates.remove(delegate);
// If we are the last delegate leaving the party, stop listening.
if (delegates.isEmpty()) {
removeListener();
}
}
}

View file

@ -1,43 +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.sync;
import org.mozilla.gecko.fxa.login.State.Action;
import org.mozilla.gecko.sync.BackoffHandler;
public interface SchedulePolicy {
/**
* Call this with the number of other clients syncing to the account.
*/
public abstract void onSuccessfulSync(int otherClientsCount);
public abstract void onHandleFinal(Action needed);
public abstract void onUpgradeRequired();
public abstract void onUnauthorized();
/**
* Before a sync we typically wish to adjust our backoff policy. This cleans
* the slate prior to encountering a new backoff, and also functions as a rate
* limiter.
*
* The {@link SchedulePolicy} acts as a controller for the {@link BackoffHandler}.
* As a result of calling these two methods, the {@link BackoffHandler} will be
* mutated, and additional side-effects (such as scheduling periodic syncs) can
* occur.
*
* @param rateHandler the backoff handler to configure for basic rate limiting.
* @param backgroundHandler the backoff handler to configure for background operations.
*/
public abstract void configureBackoffMillisBeforeSyncing(BackoffHandler rateHandler, BackoffHandler backgroundHandler);
/**
* We received an explicit backoff instruction, typically from a server.
*
* @param onlyExtend
* if <code>true</code>, the backoff handler will be asked to update
* its backoff only if the provided value is greater than the current
* backoff.
*/
public abstract void configureBackoffMillisOnBackoff(BackoffHandler backoffHandler, long backoffMillis, boolean onlyExtend);
}

View file

@ -1,19 +0,0 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* 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.push;
/**
* Thin container for a register User-Agent response.
*/
public class RegisterUserAgentResponse {
public final String uaid;
public final String secret;
public RegisterUserAgentResponse(String uaid, String secret) {
this.uaid = uaid;
this.secret = secret;
}
}

View file

@ -1,19 +0,0 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* 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.push;
/**
* Thin container for a subscribe channel response.
*/
public class SubscribeChannelResponse {
public final String channelID;
public final String endpoint;
public SubscribeChannelResponse(String channelID, String endpoint) {
this.channelID = channelID;
this.endpoint = endpoint;
}
}

View file

@ -1,410 +0,0 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* 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.push.autopush;
import android.text.TextUtils;
import org.mozilla.gecko.Locales;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.push.RegisterUserAgentResponse;
import org.mozilla.gecko.push.SubscribeChannelResponse;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.net.AuthHeaderProvider;
import org.mozilla.gecko.sync.net.BaseResource;
import org.mozilla.gecko.sync.net.BaseResourceDelegate;
import org.mozilla.gecko.sync.net.BearerAuthHeaderProvider;
import org.mozilla.gecko.sync.net.Resource;
import org.mozilla.gecko.sync.net.SyncResponse;
import org.mozilla.gecko.sync.net.SyncStorageResponse;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import java.util.Locale;
import java.util.concurrent.Executor;
import ch.boye.httpclientandroidlib.HttpEntity;
import ch.boye.httpclientandroidlib.HttpHeaders;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.client.ClientProtocolException;
import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
/**
* Interact with the autopush endpoint HTTP API.
* <p/>
* The API is a Mozilla-proprietary interface, and not even specified to Mozilla's usual ad-hoc standards.
* This client is written against a work-in-progress, un-deployed upstream commit.
*/
public class AutopushClient {
protected static final String LOG_TAG = AutopushClient.class.getSimpleName();
protected static final String ACCEPT_HEADER = "application/json;charset=utf-8";
protected static final String TYPE = "gcm";
protected static final String JSON_KEY_UAID = "uaid";
protected static final String JSON_KEY_SECRET = "secret";
protected static final String JSON_KEY_CHANNEL_ID = "channelID";
protected static final String JSON_KEY_ENDPOINT = "endpoint";
protected static final String[] REGISTER_USER_AGENT_RESPONSE_REQUIRED_STRING_FIELDS = new String[] { JSON_KEY_UAID, JSON_KEY_SECRET, JSON_KEY_CHANNEL_ID, JSON_KEY_ENDPOINT };
protected static final String[] REGISTER_CHANNEL_RESPONSE_REQUIRED_STRING_FIELDS = new String[] { JSON_KEY_CHANNEL_ID, JSON_KEY_ENDPOINT };
public static final String JSON_KEY_CODE = "code";
public static final String JSON_KEY_ERRNO = "errno";
public static final String JSON_KEY_ERROR = "error";
public static final String JSON_KEY_MESSAGE = "message";
protected static final String[] requiredErrorStringFields = { JSON_KEY_ERROR, JSON_KEY_MESSAGE };
protected static final String[] requiredErrorLongFields = { JSON_KEY_CODE, JSON_KEY_ERRNO };
/**
* The server's URI.
* <p>
* We assume throughout that this ends with a trailing slash (and guarantee as
* much in the constructor).
*/
public final String serverURI;
protected final Executor executor;
public AutopushClient(String serverURI, Executor executor) {
if (serverURI == null) {
throw new IllegalArgumentException("Must provide a server URI.");
}
if (executor == null) {
throw new IllegalArgumentException("Must provide a non-null executor.");
}
this.serverURI = serverURI.endsWith("/") ? serverURI : serverURI + "/";
if (!this.serverURI.endsWith("/")) {
throw new IllegalArgumentException("Constructed serverURI must end with a trailing slash: " + this.serverURI);
}
this.executor = executor;
}
/**
* A legal autopush server URL includes a sender ID embedded into it. Extract it.
*
* @return a non-null non-empty sender ID.
* @throws AutopushClientException on failure.
*/
public String getSenderIDFromServerURI() throws AutopushClientException {
// Turn "https://updates-autopush-dev.stage.mozaws.net/v1/gcm/829133274407/" into "829133274407".
final String[] parts = serverURI.split("/", -1); // The -1 keeps the trailing empty part.
if (parts.length < 3) {
throw new AutopushClientException("Could not get sender ID from autopush server URI: " + serverURI);
}
if (!TextUtils.isEmpty(parts[parts.length - 1])) {
// We guarantee a trailing slash, so we should always have an empty part at the tail.
throw new AutopushClientException("Could not get sender ID from autopush server URI: " + serverURI);
}
if (!TextUtils.equals("gcm", parts[parts.length - 3])) {
// We should always have /gcm/senderID/.
throw new AutopushClientException("Could not get sender ID from autopush server URI: " + serverURI);
}
final String senderID = parts[parts.length - 2];
if (TextUtils.isEmpty(senderID)) {
// Something is horribly wrong -- we have /gcm//. Abort.
throw new AutopushClientException("Could not get sender ID from autopush server URI: " + serverURI);
}
return senderID;
}
/**
* Process a typed value extracted from a successful response (in an
* endpoint-dependent way).
*/
public interface RequestDelegate<T> {
void handleError(Exception e);
void handleFailure(AutopushClientException e);
void handleSuccess(T result);
}
/**
* Intepret a response from the autopush server.
* <p>
* Throw an appropriate exception on errors; otherwise, return the response's
* status code.
*
* @return response's HTTP status code.
* @throws AutopushClientException
*/
public static int validateResponse(HttpResponse response) throws AutopushClientException {
final int status = response.getStatusLine().getStatusCode();
if (200 <= status && status <= 299) {
return status;
}
long code;
long errno;
String error;
String message;
String info;
ExtendedJSONObject body;
try {
body = new SyncStorageResponse(response).jsonObjectBody();
// TODO: The service doesn't do the right thing yet :(
// body.throwIfFieldsMissingOrMisTyped(requiredErrorStringFields, String.class);
body.throwIfFieldsMissingOrMisTyped(requiredErrorLongFields, Long.class);
// Would throw above if missing; the -1 defaults quiet NPE warnings.
code = body.getLong(JSON_KEY_CODE, -1);
errno = body.getLong(JSON_KEY_ERRNO, -1);
error = body.getString(JSON_KEY_ERROR);
message = body.getString(JSON_KEY_MESSAGE);
} catch (Exception e) {
throw new AutopushClientException.AutopushClientMalformedResponseException(response);
}
throw new AutopushClientException.AutopushClientRemoteException(response, code, errno, error, message, body);
}
protected <T> void invokeHandleError(final RequestDelegate<T> delegate, final Exception e) {
executor.execute(new Runnable() {
@Override
public void run() {
delegate.handleError(e);
}
});
}
protected <T> void post(BaseResource resource, final ExtendedJSONObject requestBody, final RequestDelegate<T> delegate) {
try {
if (requestBody == null) {
resource.post((HttpEntity) null);
} else {
resource.post(requestBody);
}
} catch (Exception e) {
invokeHandleError(delegate, e);
return;
}
}
/**
* Translate resource callbacks into request callbacks invoked on the provided
* executor.
* <p>
* Override <code>handleSuccess</code> to parse the body of the resource
* request and call the request callback. <code>handleSuccess</code> is
* invoked via the executor, so you don't need to delegate further.
*/
protected abstract class ResourceDelegate<T> extends BaseResourceDelegate {
protected abstract void handleSuccess(final int status, HttpResponse response, final ExtendedJSONObject body);
protected final String secret;
protected final RequestDelegate<T> delegate;
/**
* Create a delegate for an un-authenticated resource.
*/
public ResourceDelegate(final Resource resource, final String secret, final RequestDelegate<T> delegate) {
super(resource);
this.delegate = delegate;
this.secret = secret;
}
@Override
public AuthHeaderProvider getAuthHeaderProvider() {
if (secret != null) {
return new BearerAuthHeaderProvider(secret);
}
return null;
}
@Override
public String getUserAgent() {
return FxAccountConstants.USER_AGENT;
}
@Override
public void handleHttpResponse(HttpResponse response) {
try {
final int status = validateResponse(response);
invokeHandleSuccess(status, response);
} catch (AutopushClientException e) {
invokeHandleFailure(e);
}
}
protected void invokeHandleFailure(final AutopushClientException e) {
executor.execute(new Runnable() {
@Override
public void run() {
delegate.handleFailure(e);
}
});
}
protected void invokeHandleSuccess(final int status, final HttpResponse response) {
executor.execute(new Runnable() {
@Override
public void run() {
try {
ExtendedJSONObject body = new SyncResponse(response).jsonObjectBody();
ResourceDelegate.this.handleSuccess(status, response, body);
} catch (Exception e) {
delegate.handleError(e);
}
}
});
}
@Override
public void handleHttpProtocolException(final ClientProtocolException e) {
invokeHandleError(delegate, e);
}
@Override
public void handleHttpIOException(IOException e) {
invokeHandleError(delegate, e);
}
@Override
public void handleTransportException(GeneralSecurityException e) {
invokeHandleError(delegate, e);
}
@Override
public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
super.addHeaders(request, client);
// The basics.
final Locale locale = Locale.getDefault();
request.addHeader(HttpHeaders.ACCEPT_LANGUAGE, Locales.getLanguageTag(locale));
request.addHeader(HttpHeaders.ACCEPT, ACCEPT_HEADER);
}
}
public void registerUserAgent(final String token, RequestDelegate<RegisterUserAgentResponse> delegate) {
BaseResource resource;
try {
resource = new BaseResource(new URI(serverURI + "registration"));
} catch (URISyntaxException e) {
invokeHandleError(delegate, e);
return;
}
resource.delegate = new ResourceDelegate<RegisterUserAgentResponse>(resource, null, delegate) {
@Override
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
try {
body.throwIfFieldsMissingOrMisTyped(REGISTER_USER_AGENT_RESPONSE_REQUIRED_STRING_FIELDS, String.class);
final String uaid = body.getString(JSON_KEY_UAID);
final String secret = body.getString(JSON_KEY_SECRET);
delegate.handleSuccess(new RegisterUserAgentResponse(uaid, secret));
return;
} catch (Exception e) {
delegate.handleError(e);
return;
}
}
};
final ExtendedJSONObject body = new ExtendedJSONObject();
body.put("type", TYPE);
body.put("token", token);
resource.post(body);
}
public void reregisterUserAgent(final String uaid, final String secret, final String token, RequestDelegate<Void> delegate) {
final BaseResource resource;
try {
resource = new BaseResource(new URI(serverURI + "registration/" + uaid));
} catch (Exception e) {
invokeHandleError(delegate, e);
return;
}
resource.delegate = new ResourceDelegate<Void>(resource, secret, delegate) {
@Override
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
try {
delegate.handleSuccess(null);
return;
} catch (Exception e) {
delegate.handleError(e);
return;
}
}
};
final ExtendedJSONObject body = new ExtendedJSONObject();
body.put("type", TYPE);
body.put("token", token);
resource.put(body);
}
public void subscribeChannel(final String uaid, final String secret, final String appServerKey, RequestDelegate<SubscribeChannelResponse> delegate) {
final BaseResource resource;
try {
resource = new BaseResource(new URI(serverURI + "registration/" + uaid + "/subscription"));
} catch (Exception e) {
invokeHandleError(delegate, e);
return;
}
resource.delegate = new ResourceDelegate<SubscribeChannelResponse>(resource, secret, delegate) {
@Override
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
try {
body.throwIfFieldsMissingOrMisTyped(REGISTER_CHANNEL_RESPONSE_REQUIRED_STRING_FIELDS, String.class);
final String channelID = body.getString(JSON_KEY_CHANNEL_ID);
final String endpoint = body.getString(JSON_KEY_ENDPOINT);
delegate.handleSuccess(new SubscribeChannelResponse(channelID, endpoint));
return;
} catch (Exception e) {
delegate.handleError(e);
return;
}
}
};
final ExtendedJSONObject body = new ExtendedJSONObject();
body.put("key", appServerKey);
resource.post(body);
}
public void unsubscribeChannel(final String uaid, final String secret, final String channelID, RequestDelegate<Void> delegate) {
final BaseResource resource;
try {
resource = new BaseResource(new URI(serverURI + "registration/" + uaid + "/subscription/" + channelID));
} catch (Exception e) {
invokeHandleError(delegate, e);
return;
}
resource.delegate = new ResourceDelegate<Void>(resource, secret, delegate) {
@Override
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
delegate.handleSuccess(null);
}
};
resource.delete();
}
public void unregisterUserAgent(final String uaid, final String secret, RequestDelegate<Void> delegate) {
final BaseResource resource;
try {
resource = new BaseResource(new URI(serverURI + "registration/" + uaid));
} catch (Exception e) {
invokeHandleError(delegate, e);
return;
}
resource.delegate = new ResourceDelegate<Void>(resource, secret, delegate) {
@Override
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
delegate.handleSuccess(null);
}
};
resource.delete();
}
}

View file

@ -1,81 +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.push.autopush;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.HttpStatus;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.HTTPFailureException;
import org.mozilla.gecko.sync.net.SyncStorageResponse;
public class AutopushClientException extends Exception {
private static final long serialVersionUID = 7953459541558266500L;
public AutopushClientException(String detailMessage) {
super(detailMessage);
}
public AutopushClientException(Exception e) {
super(e);
}
public boolean isTransientError() {
return false;
}
public static class AutopushClientRemoteException extends AutopushClientException {
private static final long serialVersionUID = 2209313149952001000L;
public final HttpResponse response;
public final long httpStatusCode;
public final long apiErrorNumber;
public final String error;
public final String message;
public final ExtendedJSONObject body;
public AutopushClientRemoteException(HttpResponse response, long httpStatusCode, long apiErrorNumber, String error, String message, ExtendedJSONObject body) {
super(new HTTPFailureException(new SyncStorageResponse(response)));
if (body == null) {
throw new IllegalArgumentException("body must not be null");
}
this.response = response;
this.httpStatusCode = httpStatusCode;
this.apiErrorNumber = apiErrorNumber;
this.error = error;
this.message = message;
this.body = body;
}
@Override
public String toString() {
return "<AutopushClientRemoteException " + this.httpStatusCode + " [" + this.apiErrorNumber + "]: " + this.message + ">";
}
public boolean isInvalidAuthentication() {
return httpStatusCode == HttpStatus.SC_UNAUTHORIZED;
}
public boolean isNotFound() {
return httpStatusCode == HttpStatus.SC_NOT_FOUND;
}
public boolean isGone() {
return httpStatusCode == HttpStatus.SC_GONE;
}
@Override
public boolean isTransientError() {
return httpStatusCode >= 500;
}
}
public static class AutopushClientMalformedResponseException extends AutopushClientRemoteException {
private static final long serialVersionUID = 2209313149952001909L;
public AutopushClientMalformedResponseException(HttpResponse response) {
super(response, 0, 999, "Response malformed", "Response malformed", new ExtendedJSONObject());
}
}
}

View file

@ -1,22 +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.sync;
import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
import android.content.SyncResult;
public class AlreadySyncingException extends SyncException {
Stage inState;
public AlreadySyncingException(Stage currentState) {
inState = currentState;
}
private static final long serialVersionUID = -5647548462539009893L;
@Override
public void updateStats(GlobalSession globalSession, SyncResult syncResult) {
}
}

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