michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0:
michael@0: package org.mozilla.gecko.fxa.activities;
michael@0:
michael@0: import java.io.IOException;
michael@0: import java.util.Arrays;
michael@0: import java.util.HashSet;
michael@0: import java.util.Map;
michael@0: import java.util.Set;
michael@0:
michael@0: import org.mozilla.gecko.R;
michael@0: import org.mozilla.gecko.background.common.log.Logger;
michael@0: import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
michael@0: import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
michael@0: import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
michael@0: import org.mozilla.gecko.background.fxa.FxAccountUtils;
michael@0: import org.mozilla.gecko.background.fxa.PasswordStretcher;
michael@0: import org.mozilla.gecko.background.fxa.QuickPasswordStretcher;
michael@0: import org.mozilla.gecko.fxa.FxAccountConstants;
michael@0: import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.ProgressDisplay;
michael@0: import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
michael@0: import org.mozilla.gecko.fxa.login.Engaged;
michael@0: import org.mozilla.gecko.fxa.login.State;
michael@0: import org.mozilla.gecko.sync.SyncConfiguration;
michael@0: import org.mozilla.gecko.sync.setup.Constants;
michael@0: import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
michael@0:
michael@0: import android.accounts.Account;
michael@0: import android.accounts.AccountManager;
michael@0: import android.content.Context;
michael@0: import android.content.Intent;
michael@0: import android.os.AsyncTask;
michael@0: import android.text.Editable;
michael@0: import android.text.TextWatcher;
michael@0: import android.text.method.PasswordTransformationMethod;
michael@0: import android.text.method.SingleLineTransformationMethod;
michael@0: import android.util.Patterns;
michael@0: import android.view.KeyEvent;
michael@0: import android.view.View;
michael@0: import android.view.View.OnClickListener;
michael@0: import android.view.View.OnFocusChangeListener;
michael@0: import android.widget.ArrayAdapter;
michael@0: import android.widget.AutoCompleteTextView;
michael@0: import android.widget.Button;
michael@0: import android.widget.EditText;
michael@0: import android.widget.ProgressBar;
michael@0: import android.widget.TextView;
michael@0: import android.widget.TextView.OnEditorActionListener;
michael@0:
michael@0: abstract public class FxAccountAbstractSetupActivity extends FxAccountAbstractActivity implements ProgressDisplay {
michael@0: public FxAccountAbstractSetupActivity() {
michael@0: super(CANNOT_RESUME_WHEN_ACCOUNTS_EXIST | CANNOT_RESUME_WHEN_LOCKED_OUT);
michael@0: }
michael@0:
michael@0: protected FxAccountAbstractSetupActivity(int resume) {
michael@0: super(resume);
michael@0: }
michael@0:
michael@0: private static final String LOG_TAG = FxAccountAbstractSetupActivity.class.getSimpleName();
michael@0:
michael@0: protected int minimumPasswordLength = 8;
michael@0:
michael@0: protected AutoCompleteTextView emailEdit;
michael@0: protected EditText passwordEdit;
michael@0: protected Button showPasswordButton;
michael@0: protected TextView remoteErrorTextView;
michael@0: protected Button button;
michael@0: protected ProgressBar progressBar;
michael@0:
michael@0: protected void createShowPasswordButton() {
michael@0: showPasswordButton.setOnClickListener(new OnClickListener() {
michael@0: @SuppressWarnings("deprecation")
michael@0: @Override
michael@0: public void onClick(View v) {
michael@0: boolean isShown = passwordEdit.getTransformationMethod() instanceof SingleLineTransformationMethod;
michael@0:
michael@0: // Changing input type loses position in edit text; let's try to maintain it.
michael@0: int start = passwordEdit.getSelectionStart();
michael@0: int stop = passwordEdit.getSelectionEnd();
michael@0:
michael@0: if (isShown) {
michael@0: passwordEdit.setTransformationMethod(PasswordTransformationMethod.getInstance());
michael@0: showPasswordButton.setText(R.string.fxaccount_password_show);
michael@0: showPasswordButton.setBackgroundDrawable(getResources().getDrawable(R.drawable.fxaccount_password_button_show_background));
michael@0: showPasswordButton.setTextColor(getResources().getColor(R.color.fxaccount_password_show_textcolor));
michael@0: } else {
michael@0: passwordEdit.setTransformationMethod(SingleLineTransformationMethod.getInstance());
michael@0: showPasswordButton.setText(R.string.fxaccount_password_hide);
michael@0: showPasswordButton.setBackgroundDrawable(getResources().getDrawable(R.drawable.fxaccount_password_button_hide_background));
michael@0: showPasswordButton.setTextColor(getResources().getColor(R.color.fxaccount_password_hide_textcolor));
michael@0: }
michael@0: passwordEdit.setSelection(start, stop);
michael@0: }
michael@0: });
michael@0: }
michael@0:
michael@0: protected void linkifyPolicy() {
michael@0: TextView policyView = (TextView) ensureFindViewById(null, R.id.policy, "policy links");
michael@0: final String linkTerms = getString(R.string.fxaccount_link_tos);
michael@0: final String linkPrivacy = getString(R.string.fxaccount_link_pn);
michael@0: final String linkedTOS = "" + getString(R.string.fxaccount_policy_linktos) + "";
michael@0: final String linkedPN = "" + getString(R.string.fxaccount_policy_linkprivacy) + "";
michael@0: policyView.setText(getString(R.string.fxaccount_create_account_policy_text, linkedTOS, linkedPN));
michael@0: final boolean underlineLinks = true;
michael@0: ActivityUtils.linkifyTextView(policyView, underlineLinks);
michael@0: }
michael@0:
michael@0: protected void hideRemoteError() {
michael@0: remoteErrorTextView.setVisibility(View.INVISIBLE);
michael@0: }
michael@0:
michael@0: protected void showRemoteError(Exception e, int defaultResourceId) {
michael@0: if (e instanceof IOException) {
michael@0: remoteErrorTextView.setText(R.string.fxaccount_remote_error_COULD_NOT_CONNECT);
michael@0: } else if (e instanceof FxAccountClientRemoteException) {
michael@0: showClientRemoteException((FxAccountClientRemoteException) e);
michael@0: } else {
michael@0: remoteErrorTextView.setText(defaultResourceId);
michael@0: }
michael@0: Logger.warn(LOG_TAG, "Got exception; showing error message: " + remoteErrorTextView.getText().toString(), e);
michael@0: remoteErrorTextView.setVisibility(View.VISIBLE);
michael@0: }
michael@0:
michael@0: protected void showClientRemoteException(final FxAccountClientRemoteException e) {
michael@0: remoteErrorTextView.setText(e.getErrorMessageStringResource());
michael@0: }
michael@0:
michael@0: protected void addListeners() {
michael@0: TextChangedListener textChangedListener = new TextChangedListener();
michael@0: EditorActionListener editorActionListener = new EditorActionListener();
michael@0: FocusChangeListener focusChangeListener = new FocusChangeListener();
michael@0:
michael@0: emailEdit.addTextChangedListener(textChangedListener);
michael@0: emailEdit.setOnEditorActionListener(editorActionListener);
michael@0: emailEdit.setOnFocusChangeListener(focusChangeListener);
michael@0: passwordEdit.addTextChangedListener(textChangedListener);
michael@0: passwordEdit.setOnEditorActionListener(editorActionListener);
michael@0: passwordEdit.setOnFocusChangeListener(focusChangeListener);
michael@0: }
michael@0:
michael@0: protected class FocusChangeListener implements OnFocusChangeListener {
michael@0: @Override
michael@0: public void onFocusChange(View v, boolean hasFocus) {
michael@0: if (hasFocus) {
michael@0: return;
michael@0: }
michael@0: updateButtonState();
michael@0: }
michael@0: }
michael@0:
michael@0: protected class EditorActionListener implements OnEditorActionListener {
michael@0: @Override
michael@0: public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
michael@0: updateButtonState();
michael@0: return false;
michael@0: }
michael@0: }
michael@0:
michael@0: protected class TextChangedListener implements TextWatcher {
michael@0: @Override
michael@0: public void afterTextChanged(Editable s) {
michael@0: updateButtonState();
michael@0: }
michael@0:
michael@0: @Override
michael@0: public void beforeTextChanged(CharSequence s, int start, int count, int after) {
michael@0: // Do nothing.
michael@0: }
michael@0:
michael@0: @Override
michael@0: public void onTextChanged(CharSequence s, int start, int before, int count) {
michael@0: // Do nothing.
michael@0: }
michael@0: }
michael@0:
michael@0: protected boolean shouldButtonBeEnabled() {
michael@0: final String email = emailEdit.getText().toString();
michael@0: final String password = passwordEdit.getText().toString();
michael@0:
michael@0: boolean enabled =
michael@0: (email.length() > 0) &&
michael@0: Patterns.EMAIL_ADDRESS.matcher(email).matches() &&
michael@0: (password.length() >= minimumPasswordLength);
michael@0: return enabled;
michael@0: }
michael@0:
michael@0: protected boolean updateButtonState() {
michael@0: boolean enabled = shouldButtonBeEnabled();
michael@0: if (!enabled) {
michael@0: // The user needs to do something before you can interact with the button;
michael@0: // presumably that interaction will fix whatever error is shown.
michael@0: hideRemoteError();
michael@0: }
michael@0: if (enabled != button.isEnabled()) {
michael@0: Logger.debug(LOG_TAG, (enabled ? "En" : "Dis") + "abling button.");
michael@0: button.setEnabled(enabled);
michael@0: }
michael@0: return enabled;
michael@0: }
michael@0:
michael@0: @Override
michael@0: public void showProgress() {
michael@0: progressBar.setVisibility(View.VISIBLE);
michael@0: button.setVisibility(View.INVISIBLE);
michael@0: }
michael@0:
michael@0: @Override
michael@0: public void dismissProgress() {
michael@0: progressBar.setVisibility(View.INVISIBLE);
michael@0: button.setVisibility(View.VISIBLE);
michael@0: }
michael@0:
michael@0: public Intent makeSuccessIntent(String email, LoginResponse result) {
michael@0: Intent successIntent;
michael@0: if (result.verified) {
michael@0: successIntent = new Intent(this, FxAccountVerifiedAccountActivity.class);
michael@0: } else {
michael@0: successIntent = new Intent(this, FxAccountConfirmAccountActivity.class);
michael@0: }
michael@0: // Per http://stackoverflow.com/a/8992365, this triggers a known bug with
michael@0: // the soft keyboard not being shown for the started activity. Why, Android, why?
michael@0: successIntent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
michael@0: return successIntent;
michael@0: }
michael@0:
michael@0: protected abstract class AddAccountDelegate implements RequestDelegate {
michael@0: public final String email;
michael@0: public final PasswordStretcher passwordStretcher;
michael@0: public final String serverURI;
michael@0: public final Map selectedEngines;
michael@0:
michael@0: public AddAccountDelegate(String email, PasswordStretcher passwordStretcher, String serverURI) {
michael@0: this(email, passwordStretcher, serverURI, null);
michael@0: }
michael@0:
michael@0: public AddAccountDelegate(String email, PasswordStretcher passwordStretcher, String serverURI, Map selectedEngines) {
michael@0: if (email == null) {
michael@0: throw new IllegalArgumentException("email must not be null");
michael@0: }
michael@0: if (passwordStretcher == null) {
michael@0: throw new IllegalArgumentException("passwordStretcher must not be null");
michael@0: }
michael@0: if (serverURI == null) {
michael@0: throw new IllegalArgumentException("serverURI must not be null");
michael@0: }
michael@0: this.email = email;
michael@0: this.passwordStretcher = passwordStretcher;
michael@0: this.serverURI = serverURI;
michael@0: // selectedEngines can be null, which means don't write
michael@0: // userSelectedEngines to prefs. This makes any created meta/global record
michael@0: // have the default set of engines to sync.
michael@0: this.selectedEngines = selectedEngines;
michael@0: }
michael@0:
michael@0: @Override
michael@0: public void handleSuccess(LoginResponse result) {
michael@0: Logger.info(LOG_TAG, "Got success response; adding Android account.");
michael@0:
michael@0: // We're on the UI thread, but it's okay to create the account here.
michael@0: AndroidFxAccount fxAccount;
michael@0: try {
michael@0: final String profile = Constants.DEFAULT_PROFILE;
michael@0: final String tokenServerURI = FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT;
michael@0: // It is crucial that we use the email address provided by the server
michael@0: // (rather than whatever the user entered), because the user's keys are
michael@0: // wrapped and salted with the initial email they provided to
michael@0: // /create/account. Of course, we want to pass through what the user
michael@0: // entered locally as much as possible, so we create the Android account
michael@0: // with their entered email address, etc.
michael@0: // The passwordStretcher should have seen this email address before, so
michael@0: // we shouldn't be calculating the expensive stretch twice.
michael@0: byte[] quickStretchedPW = passwordStretcher.getQuickStretchedPW(result.remoteEmail.getBytes("UTF-8"));
michael@0: byte[] unwrapkB = FxAccountUtils.generateUnwrapBKey(quickStretchedPW);
michael@0: State state = new Engaged(email, result.uid, result.verified, unwrapkB, result.sessionToken, result.keyFetchToken);
michael@0: fxAccount = AndroidFxAccount.addAndroidAccount(getApplicationContext(),
michael@0: email,
michael@0: profile,
michael@0: serverURI,
michael@0: tokenServerURI,
michael@0: state);
michael@0: if (fxAccount == null) {
michael@0: throw new RuntimeException("Could not add Android account.");
michael@0: }
michael@0:
michael@0: if (selectedEngines != null) {
michael@0: Logger.info(LOG_TAG, "User has selected engines; storing to prefs.");
michael@0: SyncConfiguration.storeSelectedEnginesToPrefs(fxAccount.getSyncPrefs(), selectedEngines);
michael@0: }
michael@0: } catch (Exception e) {
michael@0: handleError(e);
michael@0: return;
michael@0: }
michael@0:
michael@0: // For great debugging.
michael@0: if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
michael@0: fxAccount.dump();
michael@0: }
michael@0:
michael@0: // The GetStarted activity has called us and needs to return a result to the authenticator.
michael@0: final Intent intent = new Intent();
michael@0: intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, email);
michael@0: intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, FxAccountConstants.ACCOUNT_TYPE);
michael@0: // intent.putExtra(AccountManager.KEY_AUTHTOKEN, accountType);
michael@0: setResult(RESULT_OK, intent);
michael@0:
michael@0: // Show success activity depending on verification status.
michael@0: Intent successIntent = makeSuccessIntent(email, result);
michael@0: startActivity(successIntent);
michael@0: finish();
michael@0: }
michael@0: }
michael@0:
michael@0: /**
michael@0: * Factory function that produces a new PasswordStretcher instance.
michael@0: *
michael@0: * @return PasswordStretcher instance.
michael@0: */
michael@0: protected PasswordStretcher makePasswordStretcher(String password) {
michael@0: return new QuickPasswordStretcher(password);
michael@0: }
michael@0:
michael@0: protected abstract static class GetAccountsAsyncTask extends AsyncTask {
michael@0: protected final Context context;
michael@0:
michael@0: public GetAccountsAsyncTask(Context context) {
michael@0: super();
michael@0: this.context = context;
michael@0: }
michael@0:
michael@0: @Override
michael@0: protected Account[] doInBackground(Void... params) {
michael@0: return AccountManager.get(context).getAccounts();
michael@0: }
michael@0: }
michael@0:
michael@0: /**
michael@0: * This updates UI, so needs to be done on the foreground thread.
michael@0: */
michael@0: protected void populateEmailAddressAutocomplete(Account[] accounts) {
michael@0: // First a set, since we don't want repeats.
michael@0: final Set emails = new HashSet();
michael@0: for (Account account : accounts) {
michael@0: if (!Patterns.EMAIL_ADDRESS.matcher(account.name).matches()) {
michael@0: continue;
michael@0: }
michael@0: emails.add(account.name);
michael@0: }
michael@0:
michael@0: // And then sorted in alphabetical order.
michael@0: final String[] sortedEmails = emails.toArray(new String[0]);
michael@0: Arrays.sort(sortedEmails);
michael@0:
michael@0: final ArrayAdapter adapter = new ArrayAdapter(this, android.R.layout.simple_dropdown_item_1line, sortedEmails);
michael@0: emailEdit.setAdapter(adapter);
michael@0: }
michael@0:
michael@0: @Override
michael@0: public void onResume() {
michael@0: super.onResume();
michael@0:
michael@0: // Getting Accounts accesses databases on disk, so needs to be done on a
michael@0: // background thread.
michael@0: final GetAccountsAsyncTask task = new GetAccountsAsyncTask(this) {
michael@0: @Override
michael@0: public void onPostExecute(Account[] accounts) {
michael@0: populateEmailAddressAutocomplete(accounts);
michael@0: }
michael@0: };
michael@0: task.execute();
michael@0: }
michael@0: }