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