mobile/android/base/fxa/activities/FxAccountAbstractSetupActivity.java

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/mobile/android/base/fxa/activities/FxAccountAbstractSetupActivity.java	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,373 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +package org.mozilla.gecko.fxa.activities;
     1.9 +
    1.10 +import java.io.IOException;
    1.11 +import java.util.Arrays;
    1.12 +import java.util.HashSet;
    1.13 +import java.util.Map;
    1.14 +import java.util.Set;
    1.15 +
    1.16 +import org.mozilla.gecko.R;
    1.17 +import org.mozilla.gecko.background.common.log.Logger;
    1.18 +import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
    1.19 +import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
    1.20 +import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
    1.21 +import org.mozilla.gecko.background.fxa.FxAccountUtils;
    1.22 +import org.mozilla.gecko.background.fxa.PasswordStretcher;
    1.23 +import org.mozilla.gecko.background.fxa.QuickPasswordStretcher;
    1.24 +import org.mozilla.gecko.fxa.FxAccountConstants;
    1.25 +import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.ProgressDisplay;
    1.26 +import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
    1.27 +import org.mozilla.gecko.fxa.login.Engaged;
    1.28 +import org.mozilla.gecko.fxa.login.State;
    1.29 +import org.mozilla.gecko.sync.SyncConfiguration;
    1.30 +import org.mozilla.gecko.sync.setup.Constants;
    1.31 +import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
    1.32 +
    1.33 +import android.accounts.Account;
    1.34 +import android.accounts.AccountManager;
    1.35 +import android.content.Context;
    1.36 +import android.content.Intent;
    1.37 +import android.os.AsyncTask;
    1.38 +import android.text.Editable;
    1.39 +import android.text.TextWatcher;
    1.40 +import android.text.method.PasswordTransformationMethod;
    1.41 +import android.text.method.SingleLineTransformationMethod;
    1.42 +import android.util.Patterns;
    1.43 +import android.view.KeyEvent;
    1.44 +import android.view.View;
    1.45 +import android.view.View.OnClickListener;
    1.46 +import android.view.View.OnFocusChangeListener;
    1.47 +import android.widget.ArrayAdapter;
    1.48 +import android.widget.AutoCompleteTextView;
    1.49 +import android.widget.Button;
    1.50 +import android.widget.EditText;
    1.51 +import android.widget.ProgressBar;
    1.52 +import android.widget.TextView;
    1.53 +import android.widget.TextView.OnEditorActionListener;
    1.54 +
    1.55 +abstract public class FxAccountAbstractSetupActivity extends FxAccountAbstractActivity implements ProgressDisplay {
    1.56 +  public FxAccountAbstractSetupActivity() {
    1.57 +    super(CANNOT_RESUME_WHEN_ACCOUNTS_EXIST | CANNOT_RESUME_WHEN_LOCKED_OUT);
    1.58 +  }
    1.59 +
    1.60 +  protected FxAccountAbstractSetupActivity(int resume) {
    1.61 +    super(resume);
    1.62 +  }
    1.63 +
    1.64 +  private static final String LOG_TAG = FxAccountAbstractSetupActivity.class.getSimpleName();
    1.65 +
    1.66 +  protected int minimumPasswordLength = 8;
    1.67 +
    1.68 +  protected AutoCompleteTextView emailEdit;
    1.69 +  protected EditText passwordEdit;
    1.70 +  protected Button showPasswordButton;
    1.71 +  protected TextView remoteErrorTextView;
    1.72 +  protected Button button;
    1.73 +  protected ProgressBar progressBar;
    1.74 +
    1.75 +  protected void createShowPasswordButton() {
    1.76 +    showPasswordButton.setOnClickListener(new OnClickListener() {
    1.77 +      @SuppressWarnings("deprecation")
    1.78 +      @Override
    1.79 +      public void onClick(View v) {
    1.80 +        boolean isShown = passwordEdit.getTransformationMethod() instanceof SingleLineTransformationMethod;
    1.81 +
    1.82 +        // Changing input type loses position in edit text; let's try to maintain it.
    1.83 +        int start = passwordEdit.getSelectionStart();
    1.84 +        int stop = passwordEdit.getSelectionEnd();
    1.85 +
    1.86 +        if (isShown) {
    1.87 +          passwordEdit.setTransformationMethod(PasswordTransformationMethod.getInstance());
    1.88 +          showPasswordButton.setText(R.string.fxaccount_password_show);
    1.89 +          showPasswordButton.setBackgroundDrawable(getResources().getDrawable(R.drawable.fxaccount_password_button_show_background));
    1.90 +          showPasswordButton.setTextColor(getResources().getColor(R.color.fxaccount_password_show_textcolor));
    1.91 +        } else {
    1.92 +          passwordEdit.setTransformationMethod(SingleLineTransformationMethod.getInstance());
    1.93 +          showPasswordButton.setText(R.string.fxaccount_password_hide);
    1.94 +          showPasswordButton.setBackgroundDrawable(getResources().getDrawable(R.drawable.fxaccount_password_button_hide_background));
    1.95 +          showPasswordButton.setTextColor(getResources().getColor(R.color.fxaccount_password_hide_textcolor));
    1.96 +        }
    1.97 +        passwordEdit.setSelection(start, stop);
    1.98 +      }
    1.99 +    });
   1.100 +  }
   1.101 +
   1.102 +  protected void linkifyPolicy() {
   1.103 +    TextView policyView = (TextView) ensureFindViewById(null, R.id.policy, "policy links");
   1.104 +    final String linkTerms = getString(R.string.fxaccount_link_tos);
   1.105 +    final String linkPrivacy = getString(R.string.fxaccount_link_pn);
   1.106 +    final String linkedTOS = "<a href=\"" + linkTerms + "\">" + getString(R.string.fxaccount_policy_linktos) + "</a>";
   1.107 +    final String linkedPN = "<a href=\"" + linkPrivacy + "\">" + getString(R.string.fxaccount_policy_linkprivacy) + "</a>";
   1.108 +    policyView.setText(getString(R.string.fxaccount_create_account_policy_text, linkedTOS, linkedPN));
   1.109 +    final boolean underlineLinks = true;
   1.110 +    ActivityUtils.linkifyTextView(policyView, underlineLinks);
   1.111 +  }
   1.112 +
   1.113 +  protected void hideRemoteError() {
   1.114 +    remoteErrorTextView.setVisibility(View.INVISIBLE);
   1.115 +  }
   1.116 +
   1.117 +  protected void showRemoteError(Exception e, int defaultResourceId) {
   1.118 +    if (e instanceof IOException) {
   1.119 +      remoteErrorTextView.setText(R.string.fxaccount_remote_error_COULD_NOT_CONNECT);
   1.120 +    } else if (e instanceof FxAccountClientRemoteException) {
   1.121 +      showClientRemoteException((FxAccountClientRemoteException) e);
   1.122 +    } else {
   1.123 +      remoteErrorTextView.setText(defaultResourceId);
   1.124 +    }
   1.125 +    Logger.warn(LOG_TAG, "Got exception; showing error message: " + remoteErrorTextView.getText().toString(), e);
   1.126 +    remoteErrorTextView.setVisibility(View.VISIBLE);
   1.127 +  }
   1.128 +
   1.129 +  protected void showClientRemoteException(final FxAccountClientRemoteException e) {
   1.130 +    remoteErrorTextView.setText(e.getErrorMessageStringResource());
   1.131 +  }
   1.132 +
   1.133 +  protected void addListeners() {
   1.134 +    TextChangedListener textChangedListener = new TextChangedListener();
   1.135 +    EditorActionListener editorActionListener = new EditorActionListener();
   1.136 +    FocusChangeListener focusChangeListener = new FocusChangeListener();
   1.137 +
   1.138 +    emailEdit.addTextChangedListener(textChangedListener);
   1.139 +    emailEdit.setOnEditorActionListener(editorActionListener);
   1.140 +    emailEdit.setOnFocusChangeListener(focusChangeListener);
   1.141 +    passwordEdit.addTextChangedListener(textChangedListener);
   1.142 +    passwordEdit.setOnEditorActionListener(editorActionListener);
   1.143 +    passwordEdit.setOnFocusChangeListener(focusChangeListener);
   1.144 +  }
   1.145 +
   1.146 +  protected class FocusChangeListener implements OnFocusChangeListener {
   1.147 +    @Override
   1.148 +    public void onFocusChange(View v, boolean hasFocus) {
   1.149 +      if (hasFocus) {
   1.150 +        return;
   1.151 +      }
   1.152 +      updateButtonState();
   1.153 +    }
   1.154 +  }
   1.155 +
   1.156 +  protected class EditorActionListener implements OnEditorActionListener {
   1.157 +    @Override
   1.158 +    public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
   1.159 +      updateButtonState();
   1.160 +      return false;
   1.161 +    }
   1.162 +  }
   1.163 +
   1.164 +  protected class TextChangedListener implements TextWatcher {
   1.165 +    @Override
   1.166 +    public void afterTextChanged(Editable s) {
   1.167 +      updateButtonState();
   1.168 +    }
   1.169 +
   1.170 +    @Override
   1.171 +    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
   1.172 +      // Do nothing.
   1.173 +    }
   1.174 +
   1.175 +    @Override
   1.176 +    public void onTextChanged(CharSequence s, int start, int before, int count) {
   1.177 +      // Do nothing.
   1.178 +    }
   1.179 +  }
   1.180 +
   1.181 +  protected boolean shouldButtonBeEnabled() {
   1.182 +    final String email = emailEdit.getText().toString();
   1.183 +    final String password = passwordEdit.getText().toString();
   1.184 +
   1.185 +    boolean enabled =
   1.186 +        (email.length() > 0) &&
   1.187 +        Patterns.EMAIL_ADDRESS.matcher(email).matches() &&
   1.188 +        (password.length() >= minimumPasswordLength);
   1.189 +    return enabled;
   1.190 +  }
   1.191 +
   1.192 +  protected boolean updateButtonState() {
   1.193 +    boolean enabled = shouldButtonBeEnabled();
   1.194 +    if (!enabled) {
   1.195 +      // The user needs to do something before you can interact with the button;
   1.196 +      // presumably that interaction will fix whatever error is shown.
   1.197 +      hideRemoteError();
   1.198 +    }
   1.199 +    if (enabled != button.isEnabled()) {
   1.200 +      Logger.debug(LOG_TAG, (enabled ? "En" : "Dis") + "abling button.");
   1.201 +      button.setEnabled(enabled);
   1.202 +    }
   1.203 +    return enabled;
   1.204 +  }
   1.205 +
   1.206 +  @Override
   1.207 +  public void showProgress() {
   1.208 +    progressBar.setVisibility(View.VISIBLE);
   1.209 +    button.setVisibility(View.INVISIBLE);
   1.210 +  }
   1.211 +
   1.212 +  @Override
   1.213 +  public void dismissProgress() {
   1.214 +    progressBar.setVisibility(View.INVISIBLE);
   1.215 +    button.setVisibility(View.VISIBLE);
   1.216 +  }
   1.217 +
   1.218 +  public Intent makeSuccessIntent(String email, LoginResponse result) {
   1.219 +    Intent successIntent;
   1.220 +    if (result.verified) {
   1.221 +      successIntent = new Intent(this, FxAccountVerifiedAccountActivity.class);
   1.222 +    } else {
   1.223 +      successIntent = new Intent(this, FxAccountConfirmAccountActivity.class);
   1.224 +    }
   1.225 +    // Per http://stackoverflow.com/a/8992365, this triggers a known bug with
   1.226 +    // the soft keyboard not being shown for the started activity. Why, Android, why?
   1.227 +    successIntent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
   1.228 +    return successIntent;
   1.229 +  }
   1.230 +
   1.231 +  protected abstract class AddAccountDelegate implements RequestDelegate<LoginResponse> {
   1.232 +    public final String email;
   1.233 +    public final PasswordStretcher passwordStretcher;
   1.234 +    public final String serverURI;
   1.235 +    public final Map<String, Boolean> selectedEngines;
   1.236 +
   1.237 +    public AddAccountDelegate(String email, PasswordStretcher passwordStretcher, String serverURI) {
   1.238 +      this(email, passwordStretcher, serverURI, null);
   1.239 +    }
   1.240 +
   1.241 +    public AddAccountDelegate(String email, PasswordStretcher passwordStretcher, String serverURI, Map<String, Boolean> selectedEngines) {
   1.242 +      if (email == null) {
   1.243 +        throw new IllegalArgumentException("email must not be null");
   1.244 +      }
   1.245 +      if (passwordStretcher == null) {
   1.246 +        throw new IllegalArgumentException("passwordStretcher must not be null");
   1.247 +      }
   1.248 +      if (serverURI == null) {
   1.249 +        throw new IllegalArgumentException("serverURI must not be null");
   1.250 +      }
   1.251 +      this.email = email;
   1.252 +      this.passwordStretcher = passwordStretcher;
   1.253 +      this.serverURI = serverURI;
   1.254 +      // selectedEngines can be null, which means don't write
   1.255 +      // userSelectedEngines to prefs. This makes any created meta/global record
   1.256 +      // have the default set of engines to sync.
   1.257 +      this.selectedEngines = selectedEngines;
   1.258 +    }
   1.259 +
   1.260 +    @Override
   1.261 +    public void handleSuccess(LoginResponse result) {
   1.262 +      Logger.info(LOG_TAG, "Got success response; adding Android account.");
   1.263 +
   1.264 +      // We're on the UI thread, but it's okay to create the account here.
   1.265 +      AndroidFxAccount fxAccount;
   1.266 +      try {
   1.267 +        final String profile = Constants.DEFAULT_PROFILE;
   1.268 +        final String tokenServerURI = FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT;
   1.269 +        // It is crucial that we use the email address provided by the server
   1.270 +        // (rather than whatever the user entered), because the user's keys are
   1.271 +        // wrapped and salted with the initial email they provided to
   1.272 +        // /create/account. Of course, we want to pass through what the user
   1.273 +        // entered locally as much as possible, so we create the Android account
   1.274 +        // with their entered email address, etc.
   1.275 +        // The passwordStretcher should have seen this email address before, so
   1.276 +        // we shouldn't be calculating the expensive stretch twice.
   1.277 +        byte[] quickStretchedPW = passwordStretcher.getQuickStretchedPW(result.remoteEmail.getBytes("UTF-8"));
   1.278 +        byte[] unwrapkB = FxAccountUtils.generateUnwrapBKey(quickStretchedPW);
   1.279 +        State state = new Engaged(email, result.uid, result.verified, unwrapkB, result.sessionToken, result.keyFetchToken);
   1.280 +        fxAccount = AndroidFxAccount.addAndroidAccount(getApplicationContext(),
   1.281 +            email,
   1.282 +            profile,
   1.283 +            serverURI,
   1.284 +            tokenServerURI,
   1.285 +            state);
   1.286 +        if (fxAccount == null) {
   1.287 +          throw new RuntimeException("Could not add Android account.");
   1.288 +        }
   1.289 +
   1.290 +        if (selectedEngines != null) {
   1.291 +          Logger.info(LOG_TAG, "User has selected engines; storing to prefs.");
   1.292 +          SyncConfiguration.storeSelectedEnginesToPrefs(fxAccount.getSyncPrefs(), selectedEngines);
   1.293 +        }
   1.294 +      } catch (Exception e) {
   1.295 +        handleError(e);
   1.296 +        return;
   1.297 +      }
   1.298 +
   1.299 +      // For great debugging.
   1.300 +      if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
   1.301 +        fxAccount.dump();
   1.302 +      }
   1.303 +
   1.304 +      // The GetStarted activity has called us and needs to return a result to the authenticator.
   1.305 +      final Intent intent = new Intent();
   1.306 +      intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, email);
   1.307 +      intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, FxAccountConstants.ACCOUNT_TYPE);
   1.308 +      // intent.putExtra(AccountManager.KEY_AUTHTOKEN, accountType);
   1.309 +      setResult(RESULT_OK, intent);
   1.310 +
   1.311 +      // Show success activity depending on verification status.
   1.312 +      Intent successIntent = makeSuccessIntent(email, result);
   1.313 +      startActivity(successIntent);
   1.314 +      finish();
   1.315 +    }
   1.316 +  }
   1.317 +
   1.318 +  /**
   1.319 +   * Factory function that produces a new PasswordStretcher instance.
   1.320 +   *
   1.321 +   * @return PasswordStretcher instance.
   1.322 +   */
   1.323 +  protected PasswordStretcher makePasswordStretcher(String password) {
   1.324 +    return new QuickPasswordStretcher(password);
   1.325 +  }
   1.326 +
   1.327 +  protected abstract static class GetAccountsAsyncTask extends AsyncTask<Void, Void, Account[]> {
   1.328 +    protected final Context context;
   1.329 +
   1.330 +    public GetAccountsAsyncTask(Context context) {
   1.331 +      super();
   1.332 +      this.context = context;
   1.333 +    }
   1.334 +
   1.335 +    @Override
   1.336 +    protected Account[] doInBackground(Void... params) {
   1.337 +      return AccountManager.get(context).getAccounts();
   1.338 +    }
   1.339 +  }
   1.340 +
   1.341 +  /**
   1.342 +   * This updates UI, so needs to be done on the foreground thread.
   1.343 +   */
   1.344 +  protected void populateEmailAddressAutocomplete(Account[] accounts) {
   1.345 +    // First a set, since we don't want repeats.
   1.346 +    final Set<String> emails = new HashSet<String>();
   1.347 +    for (Account account : accounts) {
   1.348 +      if (!Patterns.EMAIL_ADDRESS.matcher(account.name).matches()) {
   1.349 +        continue;
   1.350 +      }
   1.351 +      emails.add(account.name);
   1.352 +    }
   1.353 +
   1.354 +    // And then sorted in alphabetical order.
   1.355 +    final String[] sortedEmails = emails.toArray(new String[0]);
   1.356 +    Arrays.sort(sortedEmails);
   1.357 +
   1.358 +    final ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_dropdown_item_1line, sortedEmails);
   1.359 +    emailEdit.setAdapter(adapter);
   1.360 +  }
   1.361 +
   1.362 +  @Override
   1.363 +  public void onResume() {
   1.364 +    super.onResume();
   1.365 +
   1.366 +    // Getting Accounts accesses databases on disk, so needs to be done on a
   1.367 +    // background thread.
   1.368 +    final GetAccountsAsyncTask task = new GetAccountsAsyncTask(this) {
   1.369 +      @Override
   1.370 +      public void onPostExecute(Account[] accounts) {
   1.371 +        populateEmailAddressAutocomplete(accounts);
   1.372 +      }
   1.373 +    };
   1.374 +    task.execute();
   1.375 +  }
   1.376 +}

mercurial