/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.gecko; import android.accounts.Account; import android.accounts.AccountManager; import android.accounts.AccountManagerCallback; import android.accounts.AccountManagerFuture; import android.accounts.AuthenticatorException; import android.accounts.OperationCanceledException; import android.content.Context; import android.content.Intent; import android.util.Log; import org.json.JSONException; import org.mozilla.gecko.background.fxa.FxAccountUtils; import org.mozilla.gecko.fxa.FirefoxAccounts; import org.mozilla.gecko.fxa.FxAccountConstants; import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; import org.mozilla.gecko.fxa.login.Engaged; import org.mozilla.gecko.fxa.login.State; import org.mozilla.gecko.restrictions.Restrictable; import org.mozilla.gecko.restrictions.Restrictions; import org.mozilla.gecko.sync.SyncConfiguration; import org.mozilla.gecko.sync.Utils; import org.mozilla.gecko.util.BundleEventListener; import org.mozilla.gecko.util.EventCallback; import org.mozilla.gecko.util.GeckoBundle; import org.mozilla.gecko.util.ThreadUtils; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URISyntaxException; import java.security.GeneralSecurityException; import java.util.HashMap; import java.util.Map; /** * Helper class to manage Android Accounts corresponding to Firefox Accounts. */ public class AccountsHelper implements BundleEventListener { private static final String LOGTAG = "GeckoAccounts"; protected final Context mContext; protected final GeckoProfile mProfile; public AccountsHelper(Context context, GeckoProfile profile) { mContext = context; mProfile = profile; EventDispatcher.getInstance().registerGeckoThreadListener(this, "Accounts:CreateFirefoxAccountFromJSON", "Accounts:UpdateFirefoxAccountFromJSON", "Accounts:DeleteFirefoxAccount", "Accounts:Exist", "Accounts:ProfileUpdated"); EventDispatcher.getInstance().registerUiThreadListener(this, "Accounts:Create", "Accounts:ShowSyncPreferences"); } public synchronized void uninit() { EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "Accounts:CreateFirefoxAccountFromJSON", "Accounts:UpdateFirefoxAccountFromJSON", "Accounts:DeleteFirefoxAccount", "Accounts:Exist", "Accounts:ProfileUpdated"); EventDispatcher.getInstance().unregisterUiThreadListener(this, "Accounts:Create", "Accounts:ShowSyncPreferences"); } @Override // BundleEventListener public void handleMessage(final String event, final GeckoBundle message, final EventCallback callback) { if (!Restrictions.isAllowed(mContext, Restrictable.MODIFY_ACCOUNTS)) { // We register for messages in all contexts; we drop, with a log and an error to JavaScript, // when the profile is restricted. It's better to return errors than silently ignore messages. Log.e(LOGTAG, "Profile is not allowed to modify accounts! Ignoring event: " + event); if (callback != null) { callback.sendError("Profile is not allowed to modify accounts!"); } return; } if ("Accounts:CreateFirefoxAccountFromJSON".equals(event)) { AndroidFxAccount fxAccount = null; try { final GeckoBundle json = message.getBundle("json"); final String email = json.getString("email"); final String uid = json.getString("uid"); final boolean verified = json.getBoolean("verified", false); final byte[] unwrapkB = Utils.hex2Byte(json.getString("unwrapBKey")); final byte[] sessionToken = Utils.hex2Byte(json.getString("sessionToken")); final byte[] keyFetchToken = Utils.hex2Byte(json.getString("keyFetchToken")); final String authServerEndpoint = json.getString("authServerEndpoint", FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT); final String tokenServerEndpoint = json.getString("tokenServerEndpoint", FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT); final String profileServerEndpoint = json.getString("profileServerEndpoint", FxAccountConstants.DEFAULT_PROFILE_SERVER_ENDPOINT); // TODO: handle choose what to Sync. State state = new Engaged(email, uid, verified, unwrapkB, sessionToken, keyFetchToken); fxAccount = AndroidFxAccount.addAndroidAccount(mContext, uid, email, mProfile.getName(), authServerEndpoint, tokenServerEndpoint, profileServerEndpoint, state, AndroidFxAccount.DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP); final String[] declinedSyncEngines = json.getStringArray("declinedSyncEngines"); if (declinedSyncEngines != null) { Log.i(LOGTAG, "User has selected engines; storing to prefs."); final Map selectedEngines = new HashMap(); for (String enabledSyncEngine : SyncConfiguration.validEngineNames()) { selectedEngines.put(enabledSyncEngine, true); } for (String declinedSyncEngine : declinedSyncEngines) { selectedEngines.put(declinedSyncEngine, false); } // The "forms" engine has the same state as the "history" engine. selectedEngines.put("forms", selectedEngines.get("history")); FxAccountUtils.pii(LOGTAG, "User selected engines: " + selectedEngines.toString()); try { SyncConfiguration.storeSelectedEnginesToPrefs( fxAccount.getSyncPrefs(), selectedEngines); } catch (UnsupportedEncodingException | GeneralSecurityException e) { Log.e(LOGTAG, "Got exception storing selected engines; ignoring.", e); } } } catch (URISyntaxException | GeneralSecurityException | UnsupportedEncodingException e) { Log.w(LOGTAG, "Got exception creating Firefox Account from JSON; ignoring.", e); if (callback != null) { callback.sendError("Could not create Firefox Account from JSON: " + e.toString()); return; } } if (callback != null) { callback.sendSuccess(fxAccount != null); } } else if ("Accounts:UpdateFirefoxAccountFromJSON".equals(event)) { final Account account = FirefoxAccounts.getFirefoxAccount(mContext); if (account == null) { if (callback != null) { callback.sendError("Could not update Firefox Account since none exists"); } return; } final GeckoBundle json = message.getBundle("json"); final String email = json.getString("email"); final String uid = json.getString("uid"); // Protect against cross-connecting accounts. if (account.name == null || !account.name.equals(email)) { final String errorMessage = "Cannot update Firefox Account from JSON: " + "datum has different email address!"; Log.e(LOGTAG, errorMessage); if (callback != null) { callback.sendError(errorMessage); } return; } final boolean verified = json.getBoolean("verified", false); final byte[] unwrapkB = Utils.hex2Byte(json.getString("unwrapBKey", "")); final byte[] sessionToken = Utils.hex2Byte(json.getString("sessionToken", "")); final byte[] keyFetchToken = Utils.hex2Byte(json.getString("keyFetchToken", "")); if (unwrapkB.length == 0 || sessionToken.length == 0 || keyFetchToken.length == 0) { final String error = "Cannot update Firefox Account from JSON: invalid key/tokens"; Log.e(LOGTAG, error); if (callback != null) { callback.sendError(error); } return; } final State state = new Engaged(email, uid, verified, unwrapkB, sessionToken, keyFetchToken); final AndroidFxAccount fxAccount = new AndroidFxAccount(mContext, account); fxAccount.setState(state); fxAccount.updateFirstRunScope(mContext); if (callback != null) { callback.sendSuccess(true); } } else if ("Accounts:Create".equals(event)) { // Do exactly the same thing as if you tapped 'Sync' in Settings. final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); final GeckoBundle extras = message.getBundle("extras"); if (extras != null) { try { intent.putExtra("extras", extras.toJSONObject().toString()); } catch (final JSONException e) { Log.e(LOGTAG, "Cannot convert extras", e); } } mContext.startActivity(intent); } else if ("Accounts:DeleteFirefoxAccount".equals(event)) { try { final Account account = FirefoxAccounts.getFirefoxAccount(mContext); if (account == null) { Log.w(LOGTAG, "Could not delete Firefox Account since none exists!"); if (callback != null) { callback.sendError("Could not delete Firefox Account since none exists"); } return; } final AccountManagerCallback accountManagerCallback = new AccountManagerCallback() { @Override public void run(AccountManagerFuture future) { try { final boolean result = future.getResult(); Log.i(LOGTAG, "Account named like " + Utils.obfuscateEmail(account.name) + " removed: " + result); if (callback != null) { callback.sendSuccess(result); } } catch (OperationCanceledException | IOException | AuthenticatorException e) { if (callback != null) { callback.sendError("Could not delete Firefox Account: " + e.toString()); } } } }; AccountManager.get(mContext).removeAccount( account, accountManagerCallback, ThreadUtils.getBackgroundHandler()); } catch (Exception e) { Log.w(LOGTAG, "Got exception updating Firefox Account from JSON; ignoring.", e); if (callback != null) { callback.sendError("Could not update Firefox Account from JSON: " + e.toString()); return; } } } else if ("Accounts:Exist".equals(event)) { if (callback == null) { Log.w(LOGTAG, "Accounts:Exist requires a callback"); return; } final String kind = message.getString("kind", null); final GeckoBundle response = new GeckoBundle(); if ("any".equals(kind)) { response.putBoolean("exists", FirefoxAccounts.firefoxAccountsExist(mContext)); callback.sendSuccess(response); } else if ("fxa".equals(kind)) { final Account account = FirefoxAccounts.getFirefoxAccount(mContext); response.putBoolean("exists", account != null); if (account != null) { response.putString("email", account.name); // We should always be able to extract the server endpoints. final AndroidFxAccount fxAccount = new AndroidFxAccount(mContext, account); response.putString("authServerEndpoint", fxAccount.getAccountServerURI()); response.putString("profileServerEndpoint", fxAccount.getProfileServerURI()); response.putString("tokenServerEndpoint", fxAccount.getTokenServerURI()); try { // It is possible for the state fetch to fail and us to not be // able to provide a UID. Long term, the UID (and verification // flag) will be attached to the Android account user data and not // the internal state representation. final State state = fxAccount.getState(); response.putString("uid", state.uid); } catch (Exception e) { Log.w(LOGTAG, "Got exception extracting account UID; ignoring.", e); } } callback.sendSuccess(response); } else { callback.sendError("Could not query account existence: unknown kind."); } } else if ("Accounts:ProfileUpdated".equals(event)) { final Account account = FirefoxAccounts.getFirefoxAccount(mContext); if (account == null) { Log.w(LOGTAG, "Can't change profile of non-existent Firefox Account!; ignored"); return; } final AndroidFxAccount androidFxAccount = new AndroidFxAccount(mContext, account); androidFxAccount.fetchProfileJSON(); } else if ("Accounts:ShowSyncPreferences".equals(event)) { final Account account = FirefoxAccounts.getFirefoxAccount(mContext); if (account == null) { Log.w(LOGTAG, "Can't change show Sync preferences of " + "non-existent Firefox Account!; ignored"); return; } // We don't necessarily have an Activity context here, so we always start in a new task. final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_STATUS); intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); mContext.startActivity(intent); } } }