mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-06 07:48:38 +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
|
|
@ -901,18 +901,6 @@ pref("dom.push.maxRecentMessageIDsPerSubscription", 0);
|
|||
pref("dom.push.enabled", false);
|
||||
#endif
|
||||
|
||||
// The remote content URL where FxAccountsWebChannel messages originate. Must use HTTPS.
|
||||
pref("identity.fxaccounts.remote.webchannel.uri", "https://accounts.firefox.com");
|
||||
|
||||
// The remote URL of the Firefox Account profile server.
|
||||
pref("identity.fxaccounts.remote.profile.uri", "https://profile.accounts.firefox.com/v1");
|
||||
|
||||
// The remote URL of the Firefox Account oauth server.
|
||||
pref("identity.fxaccounts.remote.oauth.uri", "https://oauth.accounts.firefox.com/v1");
|
||||
|
||||
// Token server used by Firefox Account-authenticated Sync.
|
||||
pref("identity.sync.tokenserver.uri", "https://token.services.mozilla.com/1.0/sync/1.5");
|
||||
|
||||
// Enable Presentation API
|
||||
pref("dom.presentation.enabled", false);
|
||||
pref("dom.presentation.discovery.enabled", true);
|
||||
|
|
|
|||
|
|
@ -780,21 +780,6 @@ sync_java_files = [TOPSRCDIR + '/mobile/android/services/src/main/java/org/mozil
|
|||
'background/common/telemetry/TelemetryWrapper.java',
|
||||
'background/db/CursorDumper.java',
|
||||
'background/db/Tab.java',
|
||||
'background/fxa/FxAccount20CreateDelegate.java',
|
||||
'background/fxa/FxAccount20LoginDelegate.java',
|
||||
'background/fxa/FxAccountClient.java',
|
||||
'background/fxa/FxAccountClient20.java',
|
||||
'background/fxa/FxAccountClientException.java',
|
||||
'background/fxa/FxAccountRemoteError.java',
|
||||
'background/fxa/FxAccountUtils.java',
|
||||
'background/fxa/oauth/FxAccountAbstractClient.java',
|
||||
'background/fxa/oauth/FxAccountAbstractClientException.java',
|
||||
'background/fxa/oauth/FxAccountOAuthClient10.java',
|
||||
'background/fxa/oauth/FxAccountOAuthRemoteError.java',
|
||||
'background/fxa/PasswordStretcher.java',
|
||||
'background/fxa/profile/FxAccountProfileClient10.java',
|
||||
'background/fxa/QuickPasswordStretcher.java',
|
||||
'background/fxa/SkewHandler.java',
|
||||
'background/nativecode/NativeCrypto.java',
|
||||
'background/preferences/PreferenceFragment.java',
|
||||
'background/preferences/PreferenceManagerCompat.java',
|
||||
|
|
@ -813,52 +798,6 @@ sync_java_files = [TOPSRCDIR + '/mobile/android/services/src/main/java/org/mozil
|
|||
'browserid/verifier/BrowserIDVerifierDelegate.java',
|
||||
'browserid/verifier/BrowserIDVerifierException.java',
|
||||
'browserid/VerifyingPublicKey.java',
|
||||
'fxa/AccountLoader.java',
|
||||
'fxa/activities/CustomColorPreference.java',
|
||||
'fxa/activities/FxAccountAbstractActivity.java',
|
||||
'fxa/activities/FxAccountConfirmAccountActivityWeb.java',
|
||||
'fxa/activities/FxAccountFinishMigratingActivityWeb.java',
|
||||
'fxa/activities/FxAccountGetStartedActivityWeb.java',
|
||||
'fxa/activities/FxAccountStatusActivity.java',
|
||||
'fxa/activities/FxAccountStatusFragment.java',
|
||||
'fxa/activities/FxAccountUpdateCredentialsActivityWeb.java',
|
||||
'fxa/activities/FxAccountWebFlowActivity.java',
|
||||
'fxa/activities/PicassoPreferenceIconTarget.java',
|
||||
'fxa/authenticator/AccountPickler.java',
|
||||
'fxa/authenticator/AndroidFxAccount.java',
|
||||
'fxa/authenticator/FxAccountAuthenticator.java',
|
||||
'fxa/authenticator/FxAccountAuthenticatorService.java',
|
||||
'fxa/authenticator/FxAccountLoginDelegate.java',
|
||||
'fxa/authenticator/FxAccountLoginException.java',
|
||||
'fxa/authenticator/FxADefaultLoginStateMachineDelegate.java',
|
||||
'fxa/FirefoxAccounts.java',
|
||||
'fxa/FxAccountConstants.java',
|
||||
'fxa/FxAccountDevice.java',
|
||||
'fxa/FxAccountDeviceRegistrator.java',
|
||||
'fxa/FxAccountPushHandler.java',
|
||||
'fxa/login/BaseRequestDelegate.java',
|
||||
'fxa/login/Cohabiting.java',
|
||||
'fxa/login/Doghouse.java',
|
||||
'fxa/login/Engaged.java',
|
||||
'fxa/login/FxAccountLoginStateMachine.java',
|
||||
'fxa/login/FxAccountLoginTransition.java',
|
||||
'fxa/login/Married.java',
|
||||
'fxa/login/MigratedFromSync11.java',
|
||||
'fxa/login/Separated.java',
|
||||
'fxa/login/State.java',
|
||||
'fxa/login/StateFactory.java',
|
||||
'fxa/login/TokensAndKeysState.java',
|
||||
'fxa/receivers/FxAccountDeletedService.java',
|
||||
'fxa/receivers/FxAccountUpgradeReceiver.java',
|
||||
'fxa/sync/FxAccountNotificationManager.java',
|
||||
'fxa/sync/FxAccountProfileService.java',
|
||||
'fxa/sync/FxAccountSchedulePolicy.java',
|
||||
'fxa/sync/FxAccountSyncAdapter.java',
|
||||
'fxa/sync/FxAccountSyncDelegate.java',
|
||||
'fxa/sync/FxAccountSyncService.java',
|
||||
'fxa/sync/FxAccountSyncStatusHelper.java',
|
||||
'fxa/sync/SchedulePolicy.java',
|
||||
'fxa/SyncStatusListener.java',
|
||||
'push/autopush/AutopushClient.java',
|
||||
'push/autopush/AutopushClientException.java',
|
||||
'push/RegisterUserAgentResponse.java',
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import org.mozilla.gecko.Telemetry;
|
|||
import org.mozilla.gecko.TelemetryContract;
|
||||
import org.mozilla.gecko.annotation.ReflectionTarget;
|
||||
import org.mozilla.gecko.db.BrowserDB;
|
||||
import org.mozilla.gecko.fxa.FxAccountPushHandler;
|
||||
import org.mozilla.gecko.gcm.GcmTokenClient;
|
||||
import org.mozilla.gecko.push.autopush.AutopushClientException;
|
||||
import org.mozilla.gecko.util.BundleEventListener;
|
||||
|
|
@ -66,13 +65,10 @@ public class PushService implements BundleEventListener {
|
|||
"PushServiceAndroidGCM:UnregisterUserAgent",
|
||||
"PushServiceAndroidGCM:SubscribeChannel",
|
||||
"PushServiceAndroidGCM:UnsubscribeChannel",
|
||||
"FxAccountsPush:Initialized",
|
||||
"FxAccountsPush:ReceivedPushMessageToDecode:Response",
|
||||
"History:GetPrePathLastVisitedTimeMilliseconds",
|
||||
};
|
||||
|
||||
private enum GeckoComponent {
|
||||
FxAccountsPush,
|
||||
PushServiceAndroidGCM
|
||||
}
|
||||
|
||||
|
|
@ -98,7 +94,6 @@ public class PushService implements BundleEventListener {
|
|||
|
||||
// NB: These are not thread-safe, we're depending on these being access from the same background thread.
|
||||
private boolean isReadyPushServiceAndroidGCM = false;
|
||||
private boolean isReadyFxAccountsPush = false;
|
||||
private final List<JSONObject> pendingPushMessages;
|
||||
|
||||
public PushService(Context context) {
|
||||
|
|
@ -238,9 +233,6 @@ public class PushService implements BundleEventListener {
|
|||
|
||||
protected static void sendMessageToDecodeToGeckoService(final @NonNull JSONObject message) {
|
||||
Log.i(LOG_TAG, "Delivering dom/push message to decode to Gecko!");
|
||||
GeckoAppShell.notifyObservers("FxAccountsPush:ReceivedPushMessageToDecode",
|
||||
message.toString(),
|
||||
GeckoThread.State.PROFILE_READY);
|
||||
}
|
||||
|
||||
protected void registerGeckoEventListener() {
|
||||
|
|
@ -279,11 +271,6 @@ public class PushService implements BundleEventListener {
|
|||
callback.sendSuccess(null);
|
||||
return;
|
||||
}
|
||||
if ("FxAccountsPush:Initialized".equals(event)) {
|
||||
processComponentState(GeckoComponent.FxAccountsPush, true);
|
||||
callback.sendSuccess(null);
|
||||
return;
|
||||
}
|
||||
if ("PushServiceAndroidGCM:Configure".equals(event)) {
|
||||
final String endpoint = message.getString("endpoint");
|
||||
if (endpoint == null) {
|
||||
|
|
@ -390,10 +377,6 @@ public class PushService implements BundleEventListener {
|
|||
callback.sendError("Could not unsubscribe from channel: " + channelID);
|
||||
return;
|
||||
}
|
||||
if ("FxAccountsPush:ReceivedPushMessageToDecode:Response".equals(event)) {
|
||||
FxAccountPushHandler.handleFxAPushMessage(context, message);
|
||||
return;
|
||||
}
|
||||
if ("History:GetPrePathLastVisitedTimeMilliseconds".equals(event)) {
|
||||
if (callback == null) {
|
||||
Log.e(LOG_TAG, "callback must not be null in " + event);
|
||||
|
|
@ -420,10 +403,7 @@ public class PushService implements BundleEventListener {
|
|||
}
|
||||
|
||||
private void processComponentState(@NonNull GeckoComponent component, boolean isReady) {
|
||||
if (component == GeckoComponent.FxAccountsPush) {
|
||||
isReadyFxAccountsPush = isReady;
|
||||
|
||||
} else if (component == GeckoComponent.PushServiceAndroidGCM) {
|
||||
if (component == GeckoComponent.PushServiceAndroidGCM) {
|
||||
isReadyPushServiceAndroidGCM = isReady;
|
||||
}
|
||||
|
||||
|
|
@ -435,7 +415,7 @@ public class PushService implements BundleEventListener {
|
|||
}
|
||||
|
||||
private boolean canSendPushMessagesToGecko() {
|
||||
return isReadyFxAccountsPush && isReadyPushServiceAndroidGCM;
|
||||
return isReadyPushServiceAndroidGCM;
|
||||
}
|
||||
|
||||
private static void sendPushMessagesToGecko(@NonNull List<JSONObject> messages) {
|
||||
|
|
|
|||
|
|
@ -21,11 +21,6 @@
|
|||
<item name="android:colorAccent">@color/fennec_ui_orange</item>
|
||||
</style>
|
||||
|
||||
<style name="ActionBar.FxAccountStatusActivity" parent="@android:style/Widget.Material.ActionBar.Solid">
|
||||
<item name="android:displayOptions">homeAsUp|showTitle</item>
|
||||
<item name="android:titleTextStyle">@style/ActionBarTitleTextStyle</item>
|
||||
</style>
|
||||
|
||||
<style name="GeckoAppBase" parent="Gecko">
|
||||
<item name="android:actionButtonStyle">@style/GeckoActionBar.Button</item>
|
||||
<item name="android:listViewStyle">@style/Widget.ListView</item>
|
||||
|
|
|
|||
|
|
@ -1,351 +0,0 @@
|
|||
// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
|
||||
/* 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/. */
|
||||
|
||||
/**
|
||||
* Wrap a remote fxa-content-server.
|
||||
*
|
||||
* An about:accounts tab loads and displays an fxa-content-server page,
|
||||
* depending on the current Android Account status and an optional 'action'
|
||||
* parameter.
|
||||
*
|
||||
* We show a spinner while the remote iframe is loading. We expect the
|
||||
* WebChannel message listening to the fxa-content-server to send this tab's
|
||||
* <browser>'s messageManager a LOADED message when the remote iframe provides
|
||||
* the WebChannel LOADED message. See the messageManager registration and the
|
||||
* |loadedDeferred| promise. This loosely couples the WebChannel implementation
|
||||
* and about:accounts! (We need this coupling in order to distinguish
|
||||
* WebChannel LOADED messages produced by multiple about:accounts tabs.)
|
||||
*
|
||||
* We capture error conditions by accessing the inner nsIWebNavigation of the
|
||||
* iframe directly.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var {classes: Cc, interfaces: Ci, utils: Cu} = Components; /*global Components */
|
||||
|
||||
Cu.import("resource://gre/modules/Accounts.jsm"); /*global Accounts */
|
||||
Cu.import("resource://gre/modules/PromiseUtils.jsm"); /*global PromiseUtils */
|
||||
Cu.import("resource://gre/modules/Services.jsm"); /*global Services */
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); /*global XPCOMUtils */
|
||||
|
||||
const ACTION_URL_PARAM = "action";
|
||||
|
||||
const COMMAND_LOADED = "fxaccounts:loaded";
|
||||
|
||||
const log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bind("FxAccounts");
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "ParentalControls",
|
||||
"@mozilla.org/parental-controls-service;1", "nsIParentalControlsService");
|
||||
|
||||
// Shows the toplevel element with |id| to be shown - all other top-level
|
||||
// elements are hidden.
|
||||
// If |id| is 'spinner', then 'remote' is also shown, with opacity 0.
|
||||
function show(id) {
|
||||
let allTop = document.querySelectorAll(".toplevel");
|
||||
for (let elt of allTop) {
|
||||
if (elt.getAttribute("id") == id) {
|
||||
elt.style.display = 'block';
|
||||
} else {
|
||||
elt.style.display = 'none';
|
||||
}
|
||||
}
|
||||
if (id == 'spinner') {
|
||||
document.getElementById('remote').style.display = 'block';
|
||||
document.getElementById('remote').style.opacity = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Each time we try to load the remote <iframe>, loadedDeferred is replaced. It
|
||||
// is resolved by a LOADED message, and rejected by a failure to load.
|
||||
var loadedDeferred = null;
|
||||
|
||||
// We have a new load starting. Replace the existing promise with a new one,
|
||||
// and queue up the transition to remote content.
|
||||
function deferTransitionToRemoteAfterLoaded() {
|
||||
log.d('Waiting for LOADED message.');
|
||||
|
||||
loadedDeferred = PromiseUtils.defer();
|
||||
loadedDeferred.promise.then(() => {
|
||||
log.d('Got LOADED message!');
|
||||
document.getElementById("remote").style.opacity = 0;
|
||||
show("remote");
|
||||
document.getElementById("remote").style.opacity = 1;
|
||||
})
|
||||
.catch((e) => {
|
||||
log.w('Did not get LOADED message: ' + e.toString());
|
||||
});
|
||||
}
|
||||
|
||||
function handleLoadedMessage(message) {
|
||||
loadedDeferred.resolve();
|
||||
};
|
||||
|
||||
var wrapper = {
|
||||
iframe: null,
|
||||
|
||||
url: null,
|
||||
|
||||
init: function (url) {
|
||||
this.url = url;
|
||||
deferTransitionToRemoteAfterLoaded();
|
||||
|
||||
let iframe = document.getElementById("remote");
|
||||
this.iframe = iframe;
|
||||
this.iframe.QueryInterface(Ci.nsIFrameLoaderOwner);
|
||||
let docShell = this.iframe.frameLoader.docShell;
|
||||
docShell.QueryInterface(Ci.nsIWebProgress);
|
||||
docShell.addProgressListener(this.iframeListener, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
|
||||
|
||||
// Set the iframe's location with loadURI/LOAD_FLAGS_BYPASS_HISTORY to
|
||||
// avoid having a new history entry being added.
|
||||
let webNav = iframe.frameLoader.docShell.QueryInterface(Ci.nsIWebNavigation);
|
||||
webNav.loadURI(url, Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY, null, null, null);
|
||||
},
|
||||
|
||||
retry: function () {
|
||||
deferTransitionToRemoteAfterLoaded();
|
||||
|
||||
let webNav = this.iframe.frameLoader.docShell.QueryInterface(Ci.nsIWebNavigation);
|
||||
webNav.loadURI(this.url, Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY, null, null, null);
|
||||
},
|
||||
|
||||
iframeListener: {
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
|
||||
Ci.nsISupportsWeakReference,
|
||||
Ci.nsISupports]),
|
||||
|
||||
onStateChange: function(aWebProgress, aRequest, aState, aStatus) {
|
||||
let failure = false;
|
||||
|
||||
// Captive portals sometimes redirect users
|
||||
if ((aState & Ci.nsIWebProgressListener.STATE_REDIRECTING)) {
|
||||
failure = true;
|
||||
} else if ((aState & Ci.nsIWebProgressListener.STATE_STOP)) {
|
||||
if (aRequest instanceof Ci.nsIHttpChannel) {
|
||||
try {
|
||||
failure = aRequest.responseStatus != 200;
|
||||
} catch (e) {
|
||||
failure = aStatus != Components.results.NS_OK;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calling cancel() will raise some OnStateChange notifications by itself,
|
||||
// so avoid doing that more than once
|
||||
if (failure && aStatus != Components.results.NS_BINDING_ABORTED) {
|
||||
aRequest.cancel(Components.results.NS_BINDING_ABORTED);
|
||||
// Since after a promise is fulfilled, subsequent fulfillments are
|
||||
// treated as no-ops, we don't care that we might see multiple failures
|
||||
// due to multiple listener callbacks. (It's not easy to extract this
|
||||
// from the Promises spec, but it is widely quoted. Start with
|
||||
// http://stackoverflow.com/a/18218542.)
|
||||
loadedDeferred.reject(new Error("Failed in onStateChange!"));
|
||||
show("networkError");
|
||||
}
|
||||
},
|
||||
|
||||
onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) {
|
||||
if (aRequest && aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
|
||||
aRequest.cancel(Components.results.NS_BINDING_ABORTED);
|
||||
// As above, we're not concerned by multiple listener callbacks.
|
||||
loadedDeferred.reject(new Error("Failed in onLocationChange!"));
|
||||
show("networkError");
|
||||
}
|
||||
},
|
||||
|
||||
onProgressChange: function() {},
|
||||
onStatusChange: function() {},
|
||||
onSecurityChange: function() {},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
function retry() {
|
||||
log.i("Retrying.");
|
||||
show("spinner");
|
||||
wrapper.retry();
|
||||
}
|
||||
|
||||
function openPrefs() {
|
||||
log.i("Opening Sync preferences.");
|
||||
// If an Android Account exists, this will open the Status Activity.
|
||||
// Otherwise, it will begin the Get Started flow. This should only be shown
|
||||
// when an Account actually exists.
|
||||
Accounts.launchSetup();
|
||||
}
|
||||
|
||||
function getURLForAction(action, urlParams) {
|
||||
let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.webchannel.uri");
|
||||
url = url + (url.endsWith("/") ? "" : "/") + action;
|
||||
const CONTEXT = "fx_fennec_v1";
|
||||
// The only service managed by Fennec, to date, is Firefox Sync.
|
||||
const SERVICE = "sync";
|
||||
urlParams = urlParams || new URLSearchParams("");
|
||||
urlParams.set('service', SERVICE);
|
||||
urlParams.set('context', CONTEXT);
|
||||
// Ideally we'd just merge urlParams with new URL(url).searchParams, but our
|
||||
// URLSearchParams implementation doesn't support iteration (bug 1085284).
|
||||
let urlParamStr = urlParams.toString();
|
||||
if (urlParamStr) {
|
||||
url += (url.includes("?") ? "&" : "?") + urlParamStr;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function updateDisplayedEmail(user) {
|
||||
let emailDiv = document.getElementById("email");
|
||||
if (emailDiv && user) {
|
||||
emailDiv.textContent = user.email;
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
// Test for restrictions before getFirefoxAccount(), since that will fail if
|
||||
// we are restricted.
|
||||
if (!ParentalControls.isAllowed(ParentalControls.MODIFY_ACCOUNTS)) {
|
||||
// It's better to log and show an error message than to invite user
|
||||
// confusion by removing about:accounts entirely. That is, if the user is
|
||||
// restricted, this way they'll discover as much and may be able to get
|
||||
// out of their restricted profile. If we remove about:accounts entirely,
|
||||
// it will look like Fennec is buggy, and the user will be very confused.
|
||||
log.e("This profile cannot connect to Firefox Accounts: showing restricted error.");
|
||||
show("restrictedError");
|
||||
return;
|
||||
}
|
||||
|
||||
Accounts.getFirefoxAccount().then(user => {
|
||||
// It's possible for the window to start closing before getting the user
|
||||
// completes. Tests in particular can cause this.
|
||||
if (window.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateDisplayedEmail(user);
|
||||
|
||||
// Ideally we'd use new URL(document.URL).searchParams, but for about: URIs,
|
||||
// searchParams is empty.
|
||||
let urlParams = new URLSearchParams(document.URL.split("?")[1] || "");
|
||||
let action = urlParams.get(ACTION_URL_PARAM);
|
||||
urlParams.delete(ACTION_URL_PARAM);
|
||||
|
||||
switch (action) {
|
||||
case "signup":
|
||||
if (user) {
|
||||
// Asking to sign-up when already signed in just shows prefs.
|
||||
show("prefs");
|
||||
} else {
|
||||
show("spinner");
|
||||
wrapper.init(getURLForAction("signup", urlParams));
|
||||
}
|
||||
break;
|
||||
case "signin":
|
||||
if (user) {
|
||||
// Asking to sign-in when already signed in just shows prefs.
|
||||
show("prefs");
|
||||
} else {
|
||||
show("spinner");
|
||||
wrapper.init(getURLForAction("signin", urlParams));
|
||||
}
|
||||
break;
|
||||
case "force_auth":
|
||||
if (user) {
|
||||
show("spinner");
|
||||
urlParams.set("email", user.email); // In future, pin using the UID.
|
||||
wrapper.init(getURLForAction("force_auth", urlParams));
|
||||
} else {
|
||||
show("spinner");
|
||||
wrapper.init(getURLForAction("signup", urlParams));
|
||||
}
|
||||
break;
|
||||
case "manage":
|
||||
if (user) {
|
||||
show("spinner");
|
||||
urlParams.set("email", user.email); // In future, pin using the UID.
|
||||
wrapper.init(getURLForAction("settings", urlParams));
|
||||
} else {
|
||||
show("spinner");
|
||||
wrapper.init(getURLForAction("signup", urlParams));
|
||||
}
|
||||
break;
|
||||
case "avatar":
|
||||
if (user) {
|
||||
show("spinner");
|
||||
urlParams.set("email", user.email); // In future, pin using the UID.
|
||||
wrapper.init(getURLForAction("settings/avatar/change", urlParams));
|
||||
} else {
|
||||
show("spinner");
|
||||
wrapper.init(getURLForAction("signup", urlParams));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Unrecognized or no action specified.
|
||||
if (action) {
|
||||
log.w("Ignoring unrecognized action: " + action);
|
||||
}
|
||||
if (user) {
|
||||
show("prefs");
|
||||
} else {
|
||||
show("spinner");
|
||||
wrapper.init(getURLForAction("signup", urlParams));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}).catch(e => {
|
||||
log.e("Failed to get the signed in user: " + e.toString());
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function onload() {
|
||||
document.removeEventListener("DOMContentLoaded", onload, true);
|
||||
init();
|
||||
var buttonRetry = document.getElementById('buttonRetry');
|
||||
buttonRetry.addEventListener('click', retry);
|
||||
|
||||
var buttonOpenPrefs = document.getElementById('buttonOpenPrefs');
|
||||
buttonOpenPrefs.addEventListener('click', openPrefs);
|
||||
}, true);
|
||||
|
||||
// This window is contained in a XUL <browser> element. Return the
|
||||
// messageManager of that <browser> element, or null.
|
||||
function getBrowserMessageManager() {
|
||||
let browser = window
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIWebNavigation)
|
||||
.QueryInterface(Ci.nsIDocShellTreeItem)
|
||||
.rootTreeItem
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindow)
|
||||
.QueryInterface(Ci.nsIDOMChromeWindow)
|
||||
.BrowserApp
|
||||
.getBrowserForDocument(document);
|
||||
if (browser) {
|
||||
return browser.messageManager;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Add a single listener for 'loaded' messages from the iframe in this
|
||||
// <browser>. These 'loaded' messages are ferried from the WebChannel to just
|
||||
// this <browser>.
|
||||
var mm = getBrowserMessageManager();
|
||||
if (mm) {
|
||||
mm.addMessageListener(COMMAND_LOADED, handleLoadedMessage);
|
||||
} else {
|
||||
log.e('No messageManager, not listening for LOADED message!');
|
||||
}
|
||||
|
||||
window.addEventListener("unload", function(event) {
|
||||
try {
|
||||
let mm = getBrowserMessageManager();
|
||||
if (mm) {
|
||||
mm.removeMessageListener(COMMAND_LOADED, handleLoadedMessage);
|
||||
}
|
||||
} catch (e) {
|
||||
// This could fail if the page is being torn down, the tab is being
|
||||
// destroyed, etc.
|
||||
log.w('Not removing listener for LOADED message: ' + e.toString());
|
||||
}
|
||||
});
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
|
||||
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd" [
|
||||
<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
|
||||
%brandDTD;
|
||||
<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd" >
|
||||
%globalDTD;
|
||||
<!ENTITY % aboutDTD SYSTEM "chrome://browser/locale/aboutAccounts.dtd">
|
||||
%aboutDTD;
|
||||
]>
|
||||
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" dir="&locale.dir;">
|
||||
<head>
|
||||
<title>Firefox Sync</title>
|
||||
<meta name="viewport" content="width=device-width; user-scalable=0" />
|
||||
<link rel="icon" type="image/png" sizes="64x64" href="chrome://branding/content/favicon64.png" />
|
||||
<link rel="stylesheet" href="chrome://browser/skin/spinner.css" type="text/css"/>
|
||||
<link rel="stylesheet" href="chrome://browser/skin/aboutBase.css" type="text/css"/>
|
||||
<link rel="stylesheet" href="chrome://browser/skin/aboutAccounts.css" type="text/css"/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="spinner" class="toplevel">
|
||||
<div class="container flex-column">
|
||||
<!-- Empty text-container for spacing. -->
|
||||
<div class="text-container flex-column" />
|
||||
|
||||
<div class="mui-refresh-main">
|
||||
<div class="mui-refresh-wrapper">
|
||||
<div class="mui-spinner-wrapper">
|
||||
<div class="mui-spinner-main">
|
||||
<div class="mui-spinner-left">
|
||||
<div class="mui-half-circle-left" />
|
||||
</div>
|
||||
<div class="mui-spinner-right">
|
||||
<div class="mui-half-circle-right" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<iframe mozframetype="content" id="remote" class="toplevel" />
|
||||
|
||||
<div id="prefs" class="toplevel">
|
||||
<div class="container flex-column">
|
||||
<div class="text-container flex-column">
|
||||
<div class="text">&aboutAccounts.connected.title;</div>
|
||||
<div class="hint">&aboutAccounts.connected.description;</div>
|
||||
<div id="email" class="hint"></div>
|
||||
</div>
|
||||
<a id="buttonOpenPrefs" tabindex="0" href="#">&aboutAccounts.syncPreferences.label;</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="networkError" class="toplevel">
|
||||
<div class="container flex-column">
|
||||
<div class="text-container flex-column">
|
||||
<div class="text">&aboutAccounts.noConnection.title;</div>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button id="buttonRetry" class="button" tabindex="1">&aboutAccounts.retry.label;</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="restrictedError" class="toplevel">
|
||||
<div class="container flex-column">
|
||||
<div class="text-container flex-column">
|
||||
<div class="text">&aboutAccounts.restrictedError.title;</div>
|
||||
<div class="hint">&aboutAccounts.restrictedError.description;</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="application/javascript;version=1.8" src="chrome://browser/content/aboutAccounts.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -489,20 +489,6 @@ var BrowserApp = {
|
|||
let mm = window.getGroupMessageManager("browsers");
|
||||
mm.loadFrameScript("chrome://browser/content/content.js", true);
|
||||
|
||||
// We can't delay registering WebChannel listeners: if the first page is
|
||||
// about:accounts, which can happen when starting the Firefox Account flow
|
||||
// from the first run experience, or via the Firefox Account Status
|
||||
// Activity, we can and do miss messages from the fxa-content-server.
|
||||
// However, we never allow suitably restricted profiles from listening to
|
||||
// fxa-content-server messages.
|
||||
if (ParentalControls.isAllowed(ParentalControls.MODIFY_ACCOUNTS)) {
|
||||
console.log("browser.js: loading Firefox Accounts WebChannel");
|
||||
Cu.import("resource://gre/modules/FxAccountsWebChannel.jsm");
|
||||
EnsureFxAccountsWebChannel();
|
||||
} else {
|
||||
console.log("browser.js: not loading Firefox Accounts WebChannel; this profile cannot connect to Firefox Accounts.");
|
||||
}
|
||||
|
||||
// Notify Java that Gecko has loaded.
|
||||
Messaging.sendRequest({ type: "Gecko:Ready" });
|
||||
|
||||
|
|
|
|||
|
|
@ -56,8 +56,6 @@ chrome.jar:
|
|||
content/aboutHealthReport.xhtml (content/aboutHealthReport.xhtml)
|
||||
content/aboutHealthReport.js (content/aboutHealthReport.js)
|
||||
#endif
|
||||
content/aboutAccounts.xhtml (content/aboutAccounts.xhtml)
|
||||
content/aboutAccounts.js (content/aboutAccounts.js)
|
||||
content/aboutLogins.xhtml (content/aboutLogins.xhtml)
|
||||
content/aboutLogins.js (content/aboutLogins.js)
|
||||
#ifndef RELEASE_OR_BETA
|
||||
|
|
|
|||
|
|
@ -1,164 +0,0 @@
|
|||
/* jshint moz: true, esnext: true */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cu = Components.utils;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Messaging.jsm");
|
||||
const {
|
||||
PushCrypto,
|
||||
getCryptoParams,
|
||||
} = Cu.import("resource://gre/modules/PushCrypto.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "PushService",
|
||||
"@mozilla.org/push/Service;1", "nsIPushService");
|
||||
XPCOMUtils.defineLazyGetter(this, "_decoder", () => new TextDecoder());
|
||||
|
||||
const FXA_PUSH_SCOPE = "chrome://fxa-push";
|
||||
const Log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bind("FxAccountsPush");
|
||||
|
||||
function FxAccountsPush() {
|
||||
Services.obs.addObserver(this, "FxAccountsPush:ReceivedPushMessageToDecode", false);
|
||||
|
||||
Messaging.sendRequestForResult({
|
||||
type: "FxAccountsPush:Initialized"
|
||||
});
|
||||
}
|
||||
|
||||
FxAccountsPush.prototype = {
|
||||
observe: function (subject, topic, data) {
|
||||
switch (topic) {
|
||||
case "android-push-service":
|
||||
if (data === "android-fxa-subscribe") {
|
||||
this._subscribe();
|
||||
} else if (data === "android-fxa-unsubscribe") {
|
||||
this._unsubscribe();
|
||||
}
|
||||
break;
|
||||
case "FxAccountsPush:ReceivedPushMessageToDecode":
|
||||
this._decodePushMessage(data);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
_subscribe() {
|
||||
Log.i("FxAccountsPush _subscribe");
|
||||
return new Promise((resolve, reject) => {
|
||||
PushService.subscribe(FXA_PUSH_SCOPE,
|
||||
Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
(result, subscription) => {
|
||||
if (Components.isSuccessCode(result)) {
|
||||
Log.d("FxAccountsPush got subscription");
|
||||
resolve(subscription);
|
||||
} else {
|
||||
Log.w("FxAccountsPush failed to subscribe", result);
|
||||
reject(new Error("FxAccountsPush failed to subscribe"));
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(subscription => {
|
||||
Messaging.sendRequest({
|
||||
type: "FxAccountsPush:Subscribe:Response",
|
||||
subscription: {
|
||||
pushCallback: subscription.endpoint,
|
||||
pushPublicKey: urlsafeBase64Encode(subscription.getKey('p256dh')),
|
||||
pushAuthKey: urlsafeBase64Encode(subscription.getKey('auth'))
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
Log.i("Error when registering FxA push endpoint " + err);
|
||||
});
|
||||
},
|
||||
|
||||
_unsubscribe() {
|
||||
Log.i("FxAccountsPush _unsubscribe");
|
||||
return new Promise((resolve) => {
|
||||
PushService.unsubscribe(FXA_PUSH_SCOPE,
|
||||
Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
(result, ok) => {
|
||||
if (Components.isSuccessCode(result)) {
|
||||
if (ok === true) {
|
||||
Log.d("FxAccountsPush unsubscribed");
|
||||
} else {
|
||||
Log.d("FxAccountsPush had no subscription to unsubscribe");
|
||||
}
|
||||
} else {
|
||||
Log.w("FxAccountsPush failed to unsubscribe", result);
|
||||
}
|
||||
return resolve(ok);
|
||||
});
|
||||
}).catch(err => {
|
||||
Log.e("Error during unsubscribe", err);
|
||||
});
|
||||
},
|
||||
|
||||
_decodePushMessage(data) {
|
||||
Log.i("FxAccountsPush _decodePushMessage");
|
||||
data = JSON.parse(data);
|
||||
let { headers, message } = this._messageAndHeaders(data);
|
||||
return new Promise((resolve, reject) => {
|
||||
PushService.getSubscription(FXA_PUSH_SCOPE,
|
||||
Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
(result, subscription) => {
|
||||
if (!subscription) {
|
||||
return reject(new Error("No subscription found"));
|
||||
}
|
||||
return resolve(subscription);
|
||||
});
|
||||
}).then(subscription => {
|
||||
return PushCrypto.decrypt(subscription.p256dhPrivateKey,
|
||||
new Uint8Array(subscription.getKey("p256dh")),
|
||||
new Uint8Array(subscription.getKey("auth")),
|
||||
headers, message);
|
||||
})
|
||||
.then(plaintext => {
|
||||
let decryptedMessage = plaintext ? _decoder.decode(plaintext) : "";
|
||||
Messaging.sendRequestForResult({
|
||||
type: "FxAccountsPush:ReceivedPushMessageToDecode:Response",
|
||||
message: decryptedMessage
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
Log.d("Error while decoding incoming message : " + err);
|
||||
});
|
||||
},
|
||||
|
||||
// Copied from PushServiceAndroidGCM
|
||||
_messageAndHeaders(data) {
|
||||
// Default is no data (and no encryption).
|
||||
let message = null;
|
||||
let headers = null;
|
||||
|
||||
if (data.message && data.enc && (data.enckey || data.cryptokey)) {
|
||||
headers = {
|
||||
encryption_key: data.enckey,
|
||||
crypto_key: data.cryptokey,
|
||||
encryption: data.enc,
|
||||
encoding: data.con,
|
||||
};
|
||||
// Ciphertext is (urlsafe) Base 64 encoded.
|
||||
message = ChromeUtils.base64URLDecode(data.message, {
|
||||
// The Push server may append padding.
|
||||
padding: "ignore",
|
||||
});
|
||||
}
|
||||
return { headers, message };
|
||||
},
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
|
||||
|
||||
classID: Components.ID("{d1bbb0fd-1d47-4134-9c12-d7b1be20b721}")
|
||||
};
|
||||
|
||||
function urlsafeBase64Encode(key) {
|
||||
return ChromeUtils.base64URLEncode(new Uint8Array(key), { pad: false });
|
||||
}
|
||||
|
||||
var components = [ FxAccountsPush ];
|
||||
this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
|
||||
|
|
@ -100,11 +100,6 @@ contract @mozilla.org/dom/site-specific-user-agent;1 {d5234c9d-0ee2-4b3c-9da3-18
|
|||
component {18a4e042-7c7c-424b-a583-354e68553a7f} FilePicker.js
|
||||
contract @mozilla.org/filepicker;1 {18a4e042-7c7c-424b-a583-354e68553a7f}
|
||||
|
||||
# FxAccountsPush.js
|
||||
component {d1bbb0fd-1d47-4134-9c12-d7b1be20b721} FxAccountsPush.js
|
||||
contract @mozilla.org/fxa-push;1 {d1bbb0fd-1d47-4134-9c12-d7b1be20b721}
|
||||
category android-push-service FxAccountsPush @mozilla.org/fxa-push;1
|
||||
|
||||
#ifndef RELEASE_OR_BETA
|
||||
# TabSource.js
|
||||
component {5850c76e-b916-4218-b99a-31f004e0a7e7} TabSource.js
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ EXTRA_COMPONENTS += [
|
|||
'ContentPermissionPrompt.js',
|
||||
'DirectoryProvider.js',
|
||||
'FilePicker.js',
|
||||
'FxAccountsPush.js',
|
||||
'HelperAppDialog.js',
|
||||
'ImageBlockingPolicy.js',
|
||||
'LoginManagerPrompter.js',
|
||||
|
|
|
|||
|
|
@ -16,8 +16,6 @@
|
|||
-keep public class * extends android.content.BroadcastReceiver
|
||||
-keep public class * extends android.content.ContentProvider
|
||||
-keep public class * extends android.preference.Preference
|
||||
-keep public class * extends org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter
|
||||
-keep class org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter
|
||||
|
||||
-keep public class * extends android.support.v4.app.Fragment
|
||||
|
||||
|
|
|
|||
|
|
@ -57,4 +57,4 @@ MOZ_WEBGL_CONFORMANT=1
|
|||
export JS_GC_SMALL_CHUNK_SIZE=1
|
||||
|
||||
# Enable checking that add-ons are signed by the trusted root
|
||||
MOZ_ADDON_SIGNING=1
|
||||
MOZ_ADDON_SIGNING=
|
||||
|
|
|
|||
|
|
@ -503,7 +503,6 @@
|
|||
@BINPATH@/components/ImageBlockingPolicy.js
|
||||
@BINPATH@/components/DirectoryProvider.js
|
||||
@BINPATH@/components/FilePicker.js
|
||||
@BINPATH@/components/FxAccountsPush.js
|
||||
@BINPATH@/components/HelperAppDialog.js
|
||||
@BINPATH@/components/LoginManagerPrompter.js
|
||||
@BINPATH@/components/MobileComponents.manifest
|
||||
|
|
|
|||
|
|
@ -40,14 +40,8 @@ var Accounts = Object.freeze({
|
|||
}).then(data => data.exists);
|
||||
},
|
||||
|
||||
firefoxAccountsExist: function () {
|
||||
return this._accountsExist("fxa");
|
||||
},
|
||||
|
||||
syncAccountsExist: function () {
|
||||
Deprecated.warning("The legacy Sync account type has been removed from Firefox for Android. " +
|
||||
"Please use `firefoxAccountsExist` instead.",
|
||||
"https://developer.mozilla.org/en-US/Add-ons/Firefox_for_Android/API/Accounts.jsm");
|
||||
Deprecated.warning("The legacy Sync account type has been removed from Firefox for Android.");
|
||||
return Promise.resolve(false);
|
||||
},
|
||||
|
||||
|
|
@ -73,106 +67,13 @@ var Accounts = Object.freeze({
|
|||
},
|
||||
|
||||
_addDefaultEndpoints: function (json) {
|
||||
// Empty without FxA
|
||||
let newData = Cu.cloneInto(json, {}, { cloneFunctions: false });
|
||||
let associations = {
|
||||
authServerEndpoint: 'identity.fxaccounts.auth.uri',
|
||||
profileServerEndpoint: 'identity.fxaccounts.remote.profile.uri',
|
||||
tokenServerEndpoint: 'identity.sync.tokenserver.uri'
|
||||
};
|
||||
for (let key in associations) {
|
||||
newData[key] = newData[key] || Services.urlFormatter.formatURLPref(associations[key]);
|
||||
}
|
||||
return newData;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new Android Account corresponding to the given
|
||||
* fxa-content-server "login" JSON datum. The new account will be
|
||||
* in the "Engaged" state, and will start syncing immediately.
|
||||
*
|
||||
* It is an error if an Android Account already exists.
|
||||
*
|
||||
* Returns a Promise that resolves to a boolean indicating success.
|
||||
*/
|
||||
createFirefoxAccountFromJSON: function (json) {
|
||||
return Messaging.sendRequestForResult({
|
||||
type: "Accounts:CreateFirefoxAccountFromJSON",
|
||||
json: this._addDefaultEndpoints(json)
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Move an existing Android Account to the "Engaged" state with the given
|
||||
* fxa-content-server "login" JSON datum. The account will (re)start
|
||||
* syncing immediately, unless the user has manually configured the account
|
||||
* to not Sync.
|
||||
*
|
||||
* It is an error if no Android Account exists.
|
||||
*
|
||||
* Returns a Promise that resolves to a boolean indicating success.
|
||||
*/
|
||||
updateFirefoxAccountFromJSON: function (json) {
|
||||
return Messaging.sendRequestForResult({
|
||||
type: "Accounts:UpdateFirefoxAccountFromJSON",
|
||||
json: this._addDefaultEndpoints(json)
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Notify that profile for Android Account has updated.
|
||||
* The account will re-fetch the profile image.
|
||||
*
|
||||
* It is an error if no Android Account exists.
|
||||
*
|
||||
* There is no return value from this method.
|
||||
*/
|
||||
notifyFirefoxAccountProfileChanged: function () {
|
||||
Messaging.sendRequest({
|
||||
type: "Accounts:ProfileUpdated",
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch information about an existing Android Firefox Account.
|
||||
*
|
||||
* Returns a Promise that resolves to null if no Android Firefox Account
|
||||
* exists, or an object including at least a string-valued 'email' key.
|
||||
*/
|
||||
getFirefoxAccount: function () {
|
||||
return Messaging.sendRequestForResult({
|
||||
type: "Accounts:Exist",
|
||||
kind: "fxa",
|
||||
}).then(data => {
|
||||
if (!data || !data.exists) {
|
||||
return null;
|
||||
}
|
||||
delete data.exists;
|
||||
return data;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete an existing Android Firefox Account.
|
||||
*
|
||||
* It is an error if no Android Account exists.
|
||||
*
|
||||
* Returns a Promise that resolves to a boolean indicating success.
|
||||
*/
|
||||
deleteFirefoxAccount: function () {
|
||||
return Messaging.sendRequestForResult({
|
||||
type: "Accounts:DeleteFirefoxAccount",
|
||||
});
|
||||
},
|
||||
|
||||
showSyncPreferences: function () {
|
||||
// Only show Sync preferences of an existing Android Account.
|
||||
return Accounts.getFirefoxAccount().then(account => {
|
||||
if (!account) {
|
||||
throw new Error("Can't show Sync preferences of non-existent Firefox Account!");
|
||||
}
|
||||
return Messaging.sendRequestForResult({
|
||||
type: "Accounts:ShowSyncPreferences"
|
||||
});
|
||||
});
|
||||
throw new Error("Can't show Sync preferences without accounts!");
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,394 +0,0 @@
|
|||
// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
|
||||
/* 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/. */
|
||||
|
||||
/**
|
||||
* Firefox Accounts Web Channel.
|
||||
*
|
||||
* Use the WebChannel component to receive messages about account
|
||||
* state changes.
|
||||
*/
|
||||
this.EXPORTED_SYMBOLS = ["EnsureFxAccountsWebChannel"];
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; /*global Components */
|
||||
|
||||
Cu.import("resource://gre/modules/Accounts.jsm"); /*global Accounts */
|
||||
Cu.import("resource://gre/modules/Services.jsm"); /*global Services */
|
||||
Cu.import("resource://gre/modules/WebChannel.jsm"); /*global WebChannel */
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); /*global XPCOMUtils */
|
||||
|
||||
const log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bind("FxAccounts");
|
||||
|
||||
const WEBCHANNEL_ID = "account_updates";
|
||||
|
||||
const COMMAND_LOADED = "fxaccounts:loaded";
|
||||
const COMMAND_CAN_LINK_ACCOUNT = "fxaccounts:can_link_account";
|
||||
const COMMAND_LOGIN = "fxaccounts:login";
|
||||
const COMMAND_CHANGE_PASSWORD = "fxaccounts:change_password";
|
||||
const COMMAND_DELETE_ACCOUNT = "fxaccounts:delete_account";
|
||||
const COMMAND_PROFILE_CHANGE = "profile:change";
|
||||
const COMMAND_SYNC_PREFERENCES = "fxaccounts:sync_preferences";
|
||||
|
||||
const PREF_LAST_FXA_USER = "identity.fxaccounts.lastSignedInUserHash";
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "strings",
|
||||
() => Services.strings.createBundle("chrome://browser/locale/aboutAccounts.properties")); /*global strings */
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Prompt", "resource://gre/modules/Prompt.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", "resource://gre/modules/UITelemetry.jsm");
|
||||
|
||||
this.FxAccountsWebChannelHelpers = function() {
|
||||
};
|
||||
|
||||
this.FxAccountsWebChannelHelpers.prototype = {
|
||||
/**
|
||||
* Get the hash of account name of the previously signed in account.
|
||||
*/
|
||||
getPreviousAccountNameHashPref() {
|
||||
try {
|
||||
return Services.prefs.getComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString).data;
|
||||
} catch (_) {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Given an account name, set the hash of the previously signed in account.
|
||||
*
|
||||
* @param acctName the account name of the user's account.
|
||||
*/
|
||||
setPreviousAccountNameHashPref(acctName) {
|
||||
let string = Cc["@mozilla.org/supports-string;1"]
|
||||
.createInstance(Ci.nsISupportsString);
|
||||
string.data = this.sha256(acctName);
|
||||
Services.prefs.setComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString, string);
|
||||
},
|
||||
|
||||
/**
|
||||
* Given a string, returns the SHA265 hash in base64.
|
||||
*/
|
||||
sha256(str) {
|
||||
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
|
||||
.createInstance(Ci.nsIScriptableUnicodeConverter);
|
||||
converter.charset = "UTF-8";
|
||||
// Data is an array of bytes.
|
||||
let data = converter.convertToByteArray(str, {});
|
||||
let hasher = Cc["@mozilla.org/security/hash;1"]
|
||||
.createInstance(Ci.nsICryptoHash);
|
||||
hasher.init(hasher.SHA256);
|
||||
hasher.update(data, data.length);
|
||||
|
||||
return hasher.finish(true);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new FxAccountsWebChannel to listen for account updates.
|
||||
*
|
||||
* @param {Object} options Options
|
||||
* @param {Object} options
|
||||
* @param {String} options.content_uri
|
||||
* The FxA Content server uri
|
||||
* @param {String} options.channel_id
|
||||
* The ID of the WebChannel
|
||||
* @param {String} options.helpers
|
||||
* Helpers functions. Should only be passed in for testing.
|
||||
* @constructor
|
||||
*/
|
||||
this.FxAccountsWebChannel = function(options) {
|
||||
if (!options) {
|
||||
throw new Error("Missing configuration options");
|
||||
}
|
||||
if (!options["content_uri"]) {
|
||||
throw new Error("Missing 'content_uri' option");
|
||||
}
|
||||
this._contentUri = options.content_uri;
|
||||
|
||||
if (!options["channel_id"]) {
|
||||
throw new Error("Missing 'channel_id' option");
|
||||
}
|
||||
this._webChannelId = options.channel_id;
|
||||
|
||||
// options.helpers is only specified by tests.
|
||||
this._helpers = options.helpers || new FxAccountsWebChannelHelpers(options);
|
||||
|
||||
this._setupChannel();
|
||||
};
|
||||
|
||||
this.FxAccountsWebChannel.prototype = {
|
||||
/**
|
||||
* WebChannel that is used to communicate with content page
|
||||
*/
|
||||
_channel: null,
|
||||
|
||||
/**
|
||||
* WebChannel ID.
|
||||
*/
|
||||
_webChannelId: null,
|
||||
/**
|
||||
* WebChannel origin, used to validate origin of messages
|
||||
*/
|
||||
_webChannelOrigin: null,
|
||||
|
||||
/**
|
||||
* Release all resources that are in use.
|
||||
*/
|
||||
tearDown() {
|
||||
this._channel.stopListening();
|
||||
this._channel = null;
|
||||
this._channelCallback = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Configures and registers a new WebChannel
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_setupChannel() {
|
||||
// if this.contentUri is present but not a valid URI, then this will throw an error.
|
||||
try {
|
||||
this._webChannelOrigin = Services.io.newURI(this._contentUri, null, null);
|
||||
this._registerChannel();
|
||||
} catch (e) {
|
||||
log.e(e.toString());
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new channel with the WebChannelBroker, setup a callback listener
|
||||
* @private
|
||||
*/
|
||||
_registerChannel() {
|
||||
/**
|
||||
* Processes messages that are called back from the FxAccountsChannel
|
||||
*
|
||||
* @param webChannelId {String}
|
||||
* Command webChannelId
|
||||
* @param message {Object}
|
||||
* Command message
|
||||
* @param sendingContext {Object}
|
||||
* Message sending context.
|
||||
* @param sendingContext.browser {browser}
|
||||
* The <browser> object that captured the
|
||||
* WebChannelMessageToChrome.
|
||||
* @param sendingContext.eventTarget {EventTarget}
|
||||
* The <EventTarget> where the message was sent.
|
||||
* @param sendingContext.principal {Principal}
|
||||
* The <Principal> of the EventTarget where the message was sent.
|
||||
* @private
|
||||
*
|
||||
*/
|
||||
let listener = (webChannelId, message, sendingContext) => {
|
||||
if (message) {
|
||||
let command = message.command;
|
||||
let data = message.data;
|
||||
log.d("FxAccountsWebChannel message received, command: " + command);
|
||||
|
||||
// Respond to the message with true or false.
|
||||
let respond = (data) => {
|
||||
let response = {
|
||||
command: command,
|
||||
messageId: message.messageId,
|
||||
data: data
|
||||
};
|
||||
log.d("Sending response to command: " + command);
|
||||
this._channel.send(response, sendingContext);
|
||||
};
|
||||
|
||||
switch (command) {
|
||||
case COMMAND_LOADED:
|
||||
let mm = sendingContext.browser.docShell
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIContentFrameMessageManager);
|
||||
mm.sendAsyncMessage(COMMAND_LOADED);
|
||||
break;
|
||||
|
||||
case COMMAND_CAN_LINK_ACCOUNT:
|
||||
Accounts.getFirefoxAccount().then(account => {
|
||||
if (account) {
|
||||
// If we /have/ an Android Account, we never allow the user to
|
||||
// login to a different account. They need to manually delete
|
||||
// the first Android Account and then create a new one.
|
||||
if (account.email == data.email) {
|
||||
// In future, we should use a UID for this comparison.
|
||||
log.d("Relinking existing Android Account: email addresses agree.");
|
||||
respond({ok: true});
|
||||
} else {
|
||||
log.w("Not relinking existing Android Account: email addresses disagree!");
|
||||
let message = strings.GetStringFromName("relinkDenied.message");
|
||||
let buttonLabel = strings.GetStringFromName("relinkDenied.openPrefs");
|
||||
Snackbars.show(message, Snackbars.LENGTH_LONG, {
|
||||
action: {
|
||||
label: buttonLabel,
|
||||
callback: () => {
|
||||
// We have an account, so this opens Sync native preferences.
|
||||
Accounts.launchSetup();
|
||||
},
|
||||
}
|
||||
});
|
||||
respond({ok: false});
|
||||
}
|
||||
} else {
|
||||
// If we /don't have/ an Android Account, we warn if we're
|
||||
// connecting to a new Account. This is to minimize surprise;
|
||||
// we never did this when changing accounts via the native UI.
|
||||
let prevAcctHash = this._helpers.getPreviousAccountNameHashPref();
|
||||
let shouldShowWarning = prevAcctHash && (prevAcctHash != this._helpers.sha256(data.email));
|
||||
|
||||
if (shouldShowWarning) {
|
||||
log.w("Warning about creating a new Android Account: previously linked to different email address!");
|
||||
let message = strings.formatStringFromName("relinkVerify.message", [data.email], 1);
|
||||
new Prompt({
|
||||
title: strings.GetStringFromName("relinkVerify.title"),
|
||||
message: message,
|
||||
buttons: [
|
||||
// This puts Cancel on the right.
|
||||
strings.GetStringFromName("relinkVerify.cancel"),
|
||||
strings.GetStringFromName("relinkVerify.continue"),
|
||||
],
|
||||
}).show(result => respond({ok: result && result.button == 1}));
|
||||
} else {
|
||||
log.d("Not warning about creating a new Android Account: no previously linked email address.");
|
||||
respond({ok: true});
|
||||
}
|
||||
}
|
||||
}).catch(e => {
|
||||
log.e(e.toString());
|
||||
respond({ok: false});
|
||||
});
|
||||
break;
|
||||
|
||||
case COMMAND_LOGIN:
|
||||
// Either create a new Android Account or re-connect an existing
|
||||
// Android Account here. There's not much to be done if we don't
|
||||
// succeed or get an error.
|
||||
Accounts.getFirefoxAccount().then(account => {
|
||||
if (!account) {
|
||||
return Accounts.createFirefoxAccountFromJSON(data).then(success => {
|
||||
if (!success) {
|
||||
throw new Error("Could not create Firefox Account!");
|
||||
}
|
||||
UITelemetry.addEvent("action.1", "content", null, "fxaccount-create");
|
||||
return success;
|
||||
});
|
||||
} else {
|
||||
return Accounts.updateFirefoxAccountFromJSON(data).then(success => {
|
||||
if (!success) {
|
||||
throw new Error("Could not update Firefox Account!");
|
||||
}
|
||||
UITelemetry.addEvent("action.1", "content", null, "fxaccount-login");
|
||||
return success;
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(success => {
|
||||
if (!success) {
|
||||
throw new Error("Could not create or update Firefox Account!");
|
||||
}
|
||||
|
||||
// Remember who it is so we can show a relink warning when appropriate.
|
||||
this._helpers.setPreviousAccountNameHashPref(data.email);
|
||||
|
||||
log.i("Created or updated Firefox Account.");
|
||||
})
|
||||
.catch(e => {
|
||||
log.e(e.toString());
|
||||
});
|
||||
break;
|
||||
|
||||
case COMMAND_CHANGE_PASSWORD:
|
||||
// Only update an existing Android Account.
|
||||
Accounts.getFirefoxAccount().then(account => {
|
||||
if (!account) {
|
||||
throw new Error("Can't change password of non-existent Firefox Account!");
|
||||
}
|
||||
return Accounts.updateFirefoxAccountFromJSON(data);
|
||||
})
|
||||
.then(success => {
|
||||
if (!success) {
|
||||
throw new Error("Could not change Firefox Account password!");
|
||||
}
|
||||
UITelemetry.addEvent("action.1", "content", null, "fxaccount-changepassword");
|
||||
log.i("Changed Firefox Account password.");
|
||||
})
|
||||
.catch(e => {
|
||||
log.e(e.toString());
|
||||
});
|
||||
break;
|
||||
|
||||
case COMMAND_DELETE_ACCOUNT:
|
||||
// The fxa-content-server has already confirmed the user's intent.
|
||||
// Bombs away. There's no recovery from failure, and not even a
|
||||
// real need to check an account exists (although we do, for error
|
||||
// messaging only).
|
||||
Accounts.getFirefoxAccount().then(account => {
|
||||
if (!account) {
|
||||
throw new Error("Can't delete non-existent Firefox Account!");
|
||||
}
|
||||
return Accounts.deleteFirefoxAccount().then(success => {
|
||||
if (!success) {
|
||||
throw new Error("Could not delete Firefox Account!");
|
||||
}
|
||||
UITelemetry.addEvent("action.1", "content", null, "fxaccount-delete");
|
||||
log.i("Firefox Account deleted.");
|
||||
});
|
||||
}).catch(e => {
|
||||
log.e(e.toString());
|
||||
});
|
||||
break;
|
||||
|
||||
case COMMAND_PROFILE_CHANGE:
|
||||
// Only update an existing Android Account.
|
||||
Accounts.getFirefoxAccount().then(account => {
|
||||
if (!account) {
|
||||
throw new Error("Can't change profile of non-existent Firefox Account!");
|
||||
}
|
||||
UITelemetry.addEvent("action.1", "content", null, "fxaccount-changeprofile");
|
||||
return Accounts.notifyFirefoxAccountProfileChanged();
|
||||
})
|
||||
.catch(e => {
|
||||
log.e(e.toString());
|
||||
});
|
||||
break;
|
||||
|
||||
case COMMAND_SYNC_PREFERENCES:
|
||||
UITelemetry.addEvent("action.1", "content", null, "fxaccount-syncprefs");
|
||||
Accounts.showSyncPreferences()
|
||||
.catch(e => {
|
||||
log.e(e.toString());
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
log.w("Ignoring unrecognized FxAccountsWebChannel command: " + JSON.stringify(command));
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this._channelCallback = listener;
|
||||
this._channel = new WebChannel(this._webChannelId, this._webChannelOrigin);
|
||||
this._channel.listen(listener);
|
||||
|
||||
log.d("FxAccountsWebChannel registered: " + this._webChannelId + " with origin " + this._webChannelOrigin.prePath);
|
||||
}
|
||||
};
|
||||
|
||||
var singleton;
|
||||
// The entry-point for this module, which ensures only one of our channels is
|
||||
// ever created - we require this because the WebChannel is global in scope and
|
||||
// allowing multiple channels would cause such notifications to be sent multiple
|
||||
// times.
|
||||
this.EnsureFxAccountsWebChannel = function() {
|
||||
if (!singleton) {
|
||||
let contentUri = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.webchannel.uri");
|
||||
// The FxAccountsWebChannel listens for events and updates the Java layer.
|
||||
singleton = new this.FxAccountsWebChannel({
|
||||
content_uri: contentUri,
|
||||
channel_id: WEBCHANNEL_ID,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -10,7 +10,6 @@ EXTRA_JS_MODULES += [
|
|||
'dbg-browser-actors.js',
|
||||
'DelayedInit.jsm',
|
||||
'DownloadNotifications.jsm',
|
||||
'FxAccountsWebChannel.jsm',
|
||||
'HelperApps.jsm',
|
||||
'Home.jsm',
|
||||
'HomeProvider.jsm',
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
These files are managed in the android-sync repo. Do not modify directly, or your changes will be lost.
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
<activity
|
||||
android:theme="@style/FxAccountTheme.FxAccountStatusActivity"
|
||||
android:label="@string/fxaccount_status_activity_label"
|
||||
android:clearTaskOnLaunch="true"
|
||||
android:taskAffinity="@ANDROID_PACKAGE_NAME@.FXA"
|
||||
android:name="org.mozilla.gecko.fxa.activities.FxAccountStatusActivity"
|
||||
android:configChanges="locale|layoutDirection"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Adding a launcher will make this activity appear on the
|
||||
Apps screen, which we only want when testing. -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<!-- <category android:name="android.intent.category.LAUNCHER" /> -->
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="@ANDROID_PACKAGE_NAME@.ACTION_FXA_STATUS"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<receiver
|
||||
android:name="org.mozilla.gecko.fxa.receivers.FxAccountUpgradeReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.PACKAGE_REPLACED" />
|
||||
<data android:scheme="package"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name="org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivityWeb">
|
||||
<intent-filter>
|
||||
<action android:name="@ANDROID_PACKAGE_NAME@.ACTION_FXA_GET_STARTED"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name="org.mozilla.gecko.fxa.activities.FxAccountUpdateCredentialsActivityWeb">
|
||||
<intent-filter>
|
||||
<action android:name="@ANDROID_PACKAGE_NAME@.ACTION_FXA_UPDATE_CREDENTIALS"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name="org.mozilla.gecko.fxa.activities.FxAccountFinishMigratingActivityWeb">
|
||||
<intent-filter>
|
||||
<action android:name="@ANDROID_PACKAGE_NAME@.ACTION_FXA_FINISH_MIGRATING"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name="org.mozilla.gecko.fxa.activities.FxAccountConfirmAccountActivityWeb">
|
||||
<intent-filter>
|
||||
<action android:name="@ANDROID_PACKAGE_NAME@.ACTION_FXA_CONFIRM_ACCOUNT"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
|
||||
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.READ_SYNC_STATS" />
|
||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
||||
|
||||
<!-- A signature level permission granted only to the Firefox
|
||||
channels sharing an Android Account type. -->
|
||||
<permission
|
||||
android:name="@ANDROID_PACKAGE_NAME@_fxaccount.permission.PER_ACCOUNT_TYPE"
|
||||
android:protectionLevel="signature">
|
||||
</permission>
|
||||
|
||||
<uses-permission android:name="@ANDROID_PACKAGE_NAME@_fxaccount.permission.PER_ACCOUNT_TYPE" />
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
<service
|
||||
android:exported="true"
|
||||
android:name="org.mozilla.gecko.fxa.authenticator.FxAccountAuthenticatorService" >
|
||||
<intent-filter >
|
||||
<action android:name="android.accounts.AccountAuthenticator" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.accounts.AccountAuthenticator"
|
||||
android:resource="@xml/fxaccount_authenticator" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:exported="false"
|
||||
android:name="org.mozilla.gecko.fxa.receivers.FxAccountDeletedService" >
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:exported="false"
|
||||
android:name="org.mozilla.gecko.fxa.sync.FxAccountProfileService" >
|
||||
</service>
|
||||
|
||||
<!-- Firefox Sync. -->
|
||||
<service
|
||||
android:exported="false"
|
||||
android:name="org.mozilla.gecko.fxa.sync.FxAccountSyncService" >
|
||||
<intent-filter >
|
||||
<action android:name="android.content.SyncAdapter" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/fxaccount_syncadapter" />
|
||||
</service>
|
||||
|
|
@ -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) + ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
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