mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-09 09:18:42 +09:00
Remove Firefox Accounts service and tie-ins.
See previous commit for removal of browser identity module.
This commit is contained in:
parent
ccf32a6b79
commit
c64dc935f9
420 changed files with 9 additions and 51958 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 Loader’s 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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" });
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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) + ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue