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

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 package org.mozilla.gecko.fxa.activities;
michael@0 6
michael@0 7 import java.io.IOException;
michael@0 8 import java.util.Arrays;
michael@0 9 import java.util.HashSet;
michael@0 10 import java.util.Map;
michael@0 11 import java.util.Set;
michael@0 12
michael@0 13 import org.mozilla.gecko.R;
michael@0 14 import org.mozilla.gecko.background.common.log.Logger;
michael@0 15 import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
michael@0 16 import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
michael@0 17 import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
michael@0 18 import org.mozilla.gecko.background.fxa.FxAccountUtils;
michael@0 19 import org.mozilla.gecko.background.fxa.PasswordStretcher;
michael@0 20 import org.mozilla.gecko.background.fxa.QuickPasswordStretcher;
michael@0 21 import org.mozilla.gecko.fxa.FxAccountConstants;
michael@0 22 import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.ProgressDisplay;
michael@0 23 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
michael@0 24 import org.mozilla.gecko.fxa.login.Engaged;
michael@0 25 import org.mozilla.gecko.fxa.login.State;
michael@0 26 import org.mozilla.gecko.sync.SyncConfiguration;
michael@0 27 import org.mozilla.gecko.sync.setup.Constants;
michael@0 28 import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
michael@0 29
michael@0 30 import android.accounts.Account;
michael@0 31 import android.accounts.AccountManager;
michael@0 32 import android.content.Context;
michael@0 33 import android.content.Intent;
michael@0 34 import android.os.AsyncTask;
michael@0 35 import android.text.Editable;
michael@0 36 import android.text.TextWatcher;
michael@0 37 import android.text.method.PasswordTransformationMethod;
michael@0 38 import android.text.method.SingleLineTransformationMethod;
michael@0 39 import android.util.Patterns;
michael@0 40 import android.view.KeyEvent;
michael@0 41 import android.view.View;
michael@0 42 import android.view.View.OnClickListener;
michael@0 43 import android.view.View.OnFocusChangeListener;
michael@0 44 import android.widget.ArrayAdapter;
michael@0 45 import android.widget.AutoCompleteTextView;
michael@0 46 import android.widget.Button;
michael@0 47 import android.widget.EditText;
michael@0 48 import android.widget.ProgressBar;
michael@0 49 import android.widget.TextView;
michael@0 50 import android.widget.TextView.OnEditorActionListener;
michael@0 51
michael@0 52 abstract public class FxAccountAbstractSetupActivity extends FxAccountAbstractActivity implements ProgressDisplay {
michael@0 53 public FxAccountAbstractSetupActivity() {
michael@0 54 super(CANNOT_RESUME_WHEN_ACCOUNTS_EXIST | CANNOT_RESUME_WHEN_LOCKED_OUT);
michael@0 55 }
michael@0 56
michael@0 57 protected FxAccountAbstractSetupActivity(int resume) {
michael@0 58 super(resume);
michael@0 59 }
michael@0 60
michael@0 61 private static final String LOG_TAG = FxAccountAbstractSetupActivity.class.getSimpleName();
michael@0 62
michael@0 63 protected int minimumPasswordLength = 8;
michael@0 64
michael@0 65 protected AutoCompleteTextView emailEdit;
michael@0 66 protected EditText passwordEdit;
michael@0 67 protected Button showPasswordButton;
michael@0 68 protected TextView remoteErrorTextView;
michael@0 69 protected Button button;
michael@0 70 protected ProgressBar progressBar;
michael@0 71
michael@0 72 protected void createShowPasswordButton() {
michael@0 73 showPasswordButton.setOnClickListener(new OnClickListener() {
michael@0 74 @SuppressWarnings("deprecation")
michael@0 75 @Override
michael@0 76 public void onClick(View v) {
michael@0 77 boolean isShown = passwordEdit.getTransformationMethod() instanceof SingleLineTransformationMethod;
michael@0 78
michael@0 79 // Changing input type loses position in edit text; let's try to maintain it.
michael@0 80 int start = passwordEdit.getSelectionStart();
michael@0 81 int stop = passwordEdit.getSelectionEnd();
michael@0 82
michael@0 83 if (isShown) {
michael@0 84 passwordEdit.setTransformationMethod(PasswordTransformationMethod.getInstance());
michael@0 85 showPasswordButton.setText(R.string.fxaccount_password_show);
michael@0 86 showPasswordButton.setBackgroundDrawable(getResources().getDrawable(R.drawable.fxaccount_password_button_show_background));
michael@0 87 showPasswordButton.setTextColor(getResources().getColor(R.color.fxaccount_password_show_textcolor));
michael@0 88 } else {
michael@0 89 passwordEdit.setTransformationMethod(SingleLineTransformationMethod.getInstance());
michael@0 90 showPasswordButton.setText(R.string.fxaccount_password_hide);
michael@0 91 showPasswordButton.setBackgroundDrawable(getResources().getDrawable(R.drawable.fxaccount_password_button_hide_background));
michael@0 92 showPasswordButton.setTextColor(getResources().getColor(R.color.fxaccount_password_hide_textcolor));
michael@0 93 }
michael@0 94 passwordEdit.setSelection(start, stop);
michael@0 95 }
michael@0 96 });
michael@0 97 }
michael@0 98
michael@0 99 protected void linkifyPolicy() {
michael@0 100 TextView policyView = (TextView) ensureFindViewById(null, R.id.policy, "policy links");
michael@0 101 final String linkTerms = getString(R.string.fxaccount_link_tos);
michael@0 102 final String linkPrivacy = getString(R.string.fxaccount_link_pn);
michael@0 103 final String linkedTOS = "<a href=\"" + linkTerms + "\">" + getString(R.string.fxaccount_policy_linktos) + "</a>";
michael@0 104 final String linkedPN = "<a href=\"" + linkPrivacy + "\">" + getString(R.string.fxaccount_policy_linkprivacy) + "</a>";
michael@0 105 policyView.setText(getString(R.string.fxaccount_create_account_policy_text, linkedTOS, linkedPN));
michael@0 106 final boolean underlineLinks = true;
michael@0 107 ActivityUtils.linkifyTextView(policyView, underlineLinks);
michael@0 108 }
michael@0 109
michael@0 110 protected void hideRemoteError() {
michael@0 111 remoteErrorTextView.setVisibility(View.INVISIBLE);
michael@0 112 }
michael@0 113
michael@0 114 protected void showRemoteError(Exception e, int defaultResourceId) {
michael@0 115 if (e instanceof IOException) {
michael@0 116 remoteErrorTextView.setText(R.string.fxaccount_remote_error_COULD_NOT_CONNECT);
michael@0 117 } else if (e instanceof FxAccountClientRemoteException) {
michael@0 118 showClientRemoteException((FxAccountClientRemoteException) e);
michael@0 119 } else {
michael@0 120 remoteErrorTextView.setText(defaultResourceId);
michael@0 121 }
michael@0 122 Logger.warn(LOG_TAG, "Got exception; showing error message: " + remoteErrorTextView.getText().toString(), e);
michael@0 123 remoteErrorTextView.setVisibility(View.VISIBLE);
michael@0 124 }
michael@0 125
michael@0 126 protected void showClientRemoteException(final FxAccountClientRemoteException e) {
michael@0 127 remoteErrorTextView.setText(e.getErrorMessageStringResource());
michael@0 128 }
michael@0 129
michael@0 130 protected void addListeners() {
michael@0 131 TextChangedListener textChangedListener = new TextChangedListener();
michael@0 132 EditorActionListener editorActionListener = new EditorActionListener();
michael@0 133 FocusChangeListener focusChangeListener = new FocusChangeListener();
michael@0 134
michael@0 135 emailEdit.addTextChangedListener(textChangedListener);
michael@0 136 emailEdit.setOnEditorActionListener(editorActionListener);
michael@0 137 emailEdit.setOnFocusChangeListener(focusChangeListener);
michael@0 138 passwordEdit.addTextChangedListener(textChangedListener);
michael@0 139 passwordEdit.setOnEditorActionListener(editorActionListener);
michael@0 140 passwordEdit.setOnFocusChangeListener(focusChangeListener);
michael@0 141 }
michael@0 142
michael@0 143 protected class FocusChangeListener implements OnFocusChangeListener {
michael@0 144 @Override
michael@0 145 public void onFocusChange(View v, boolean hasFocus) {
michael@0 146 if (hasFocus) {
michael@0 147 return;
michael@0 148 }
michael@0 149 updateButtonState();
michael@0 150 }
michael@0 151 }
michael@0 152
michael@0 153 protected class EditorActionListener implements OnEditorActionListener {
michael@0 154 @Override
michael@0 155 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
michael@0 156 updateButtonState();
michael@0 157 return false;
michael@0 158 }
michael@0 159 }
michael@0 160
michael@0 161 protected class TextChangedListener implements TextWatcher {
michael@0 162 @Override
michael@0 163 public void afterTextChanged(Editable s) {
michael@0 164 updateButtonState();
michael@0 165 }
michael@0 166
michael@0 167 @Override
michael@0 168 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
michael@0 169 // Do nothing.
michael@0 170 }
michael@0 171
michael@0 172 @Override
michael@0 173 public void onTextChanged(CharSequence s, int start, int before, int count) {
michael@0 174 // Do nothing.
michael@0 175 }
michael@0 176 }
michael@0 177
michael@0 178 protected boolean shouldButtonBeEnabled() {
michael@0 179 final String email = emailEdit.getText().toString();
michael@0 180 final String password = passwordEdit.getText().toString();
michael@0 181
michael@0 182 boolean enabled =
michael@0 183 (email.length() > 0) &&
michael@0 184 Patterns.EMAIL_ADDRESS.matcher(email).matches() &&
michael@0 185 (password.length() >= minimumPasswordLength);
michael@0 186 return enabled;
michael@0 187 }
michael@0 188
michael@0 189 protected boolean updateButtonState() {
michael@0 190 boolean enabled = shouldButtonBeEnabled();
michael@0 191 if (!enabled) {
michael@0 192 // The user needs to do something before you can interact with the button;
michael@0 193 // presumably that interaction will fix whatever error is shown.
michael@0 194 hideRemoteError();
michael@0 195 }
michael@0 196 if (enabled != button.isEnabled()) {
michael@0 197 Logger.debug(LOG_TAG, (enabled ? "En" : "Dis") + "abling button.");
michael@0 198 button.setEnabled(enabled);
michael@0 199 }
michael@0 200 return enabled;
michael@0 201 }
michael@0 202
michael@0 203 @Override
michael@0 204 public void showProgress() {
michael@0 205 progressBar.setVisibility(View.VISIBLE);
michael@0 206 button.setVisibility(View.INVISIBLE);
michael@0 207 }
michael@0 208
michael@0 209 @Override
michael@0 210 public void dismissProgress() {
michael@0 211 progressBar.setVisibility(View.INVISIBLE);
michael@0 212 button.setVisibility(View.VISIBLE);
michael@0 213 }
michael@0 214
michael@0 215 public Intent makeSuccessIntent(String email, LoginResponse result) {
michael@0 216 Intent successIntent;
michael@0 217 if (result.verified) {
michael@0 218 successIntent = new Intent(this, FxAccountVerifiedAccountActivity.class);
michael@0 219 } else {
michael@0 220 successIntent = new Intent(this, FxAccountConfirmAccountActivity.class);
michael@0 221 }
michael@0 222 // Per http://stackoverflow.com/a/8992365, this triggers a known bug with
michael@0 223 // the soft keyboard not being shown for the started activity. Why, Android, why?
michael@0 224 successIntent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
michael@0 225 return successIntent;
michael@0 226 }
michael@0 227
michael@0 228 protected abstract class AddAccountDelegate implements RequestDelegate<LoginResponse> {
michael@0 229 public final String email;
michael@0 230 public final PasswordStretcher passwordStretcher;
michael@0 231 public final String serverURI;
michael@0 232 public final Map<String, Boolean> selectedEngines;
michael@0 233
michael@0 234 public AddAccountDelegate(String email, PasswordStretcher passwordStretcher, String serverURI) {
michael@0 235 this(email, passwordStretcher, serverURI, null);
michael@0 236 }
michael@0 237
michael@0 238 public AddAccountDelegate(String email, PasswordStretcher passwordStretcher, String serverURI, Map<String, Boolean> selectedEngines) {
michael@0 239 if (email == null) {
michael@0 240 throw new IllegalArgumentException("email must not be null");
michael@0 241 }
michael@0 242 if (passwordStretcher == null) {
michael@0 243 throw new IllegalArgumentException("passwordStretcher must not be null");
michael@0 244 }
michael@0 245 if (serverURI == null) {
michael@0 246 throw new IllegalArgumentException("serverURI must not be null");
michael@0 247 }
michael@0 248 this.email = email;
michael@0 249 this.passwordStretcher = passwordStretcher;
michael@0 250 this.serverURI = serverURI;
michael@0 251 // selectedEngines can be null, which means don't write
michael@0 252 // userSelectedEngines to prefs. This makes any created meta/global record
michael@0 253 // have the default set of engines to sync.
michael@0 254 this.selectedEngines = selectedEngines;
michael@0 255 }
michael@0 256
michael@0 257 @Override
michael@0 258 public void handleSuccess(LoginResponse result) {
michael@0 259 Logger.info(LOG_TAG, "Got success response; adding Android account.");
michael@0 260
michael@0 261 // We're on the UI thread, but it's okay to create the account here.
michael@0 262 AndroidFxAccount fxAccount;
michael@0 263 try {
michael@0 264 final String profile = Constants.DEFAULT_PROFILE;
michael@0 265 final String tokenServerURI = FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT;
michael@0 266 // It is crucial that we use the email address provided by the server
michael@0 267 // (rather than whatever the user entered), because the user's keys are
michael@0 268 // wrapped and salted with the initial email they provided to
michael@0 269 // /create/account. Of course, we want to pass through what the user
michael@0 270 // entered locally as much as possible, so we create the Android account
michael@0 271 // with their entered email address, etc.
michael@0 272 // The passwordStretcher should have seen this email address before, so
michael@0 273 // we shouldn't be calculating the expensive stretch twice.
michael@0 274 byte[] quickStretchedPW = passwordStretcher.getQuickStretchedPW(result.remoteEmail.getBytes("UTF-8"));
michael@0 275 byte[] unwrapkB = FxAccountUtils.generateUnwrapBKey(quickStretchedPW);
michael@0 276 State state = new Engaged(email, result.uid, result.verified, unwrapkB, result.sessionToken, result.keyFetchToken);
michael@0 277 fxAccount = AndroidFxAccount.addAndroidAccount(getApplicationContext(),
michael@0 278 email,
michael@0 279 profile,
michael@0 280 serverURI,
michael@0 281 tokenServerURI,
michael@0 282 state);
michael@0 283 if (fxAccount == null) {
michael@0 284 throw new RuntimeException("Could not add Android account.");
michael@0 285 }
michael@0 286
michael@0 287 if (selectedEngines != null) {
michael@0 288 Logger.info(LOG_TAG, "User has selected engines; storing to prefs.");
michael@0 289 SyncConfiguration.storeSelectedEnginesToPrefs(fxAccount.getSyncPrefs(), selectedEngines);
michael@0 290 }
michael@0 291 } catch (Exception e) {
michael@0 292 handleError(e);
michael@0 293 return;
michael@0 294 }
michael@0 295
michael@0 296 // For great debugging.
michael@0 297 if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
michael@0 298 fxAccount.dump();
michael@0 299 }
michael@0 300
michael@0 301 // The GetStarted activity has called us and needs to return a result to the authenticator.
michael@0 302 final Intent intent = new Intent();
michael@0 303 intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, email);
michael@0 304 intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, FxAccountConstants.ACCOUNT_TYPE);
michael@0 305 // intent.putExtra(AccountManager.KEY_AUTHTOKEN, accountType);
michael@0 306 setResult(RESULT_OK, intent);
michael@0 307
michael@0 308 // Show success activity depending on verification status.
michael@0 309 Intent successIntent = makeSuccessIntent(email, result);
michael@0 310 startActivity(successIntent);
michael@0 311 finish();
michael@0 312 }
michael@0 313 }
michael@0 314
michael@0 315 /**
michael@0 316 * Factory function that produces a new PasswordStretcher instance.
michael@0 317 *
michael@0 318 * @return PasswordStretcher instance.
michael@0 319 */
michael@0 320 protected PasswordStretcher makePasswordStretcher(String password) {
michael@0 321 return new QuickPasswordStretcher(password);
michael@0 322 }
michael@0 323
michael@0 324 protected abstract static class GetAccountsAsyncTask extends AsyncTask<Void, Void, Account[]> {
michael@0 325 protected final Context context;
michael@0 326
michael@0 327 public GetAccountsAsyncTask(Context context) {
michael@0 328 super();
michael@0 329 this.context = context;
michael@0 330 }
michael@0 331
michael@0 332 @Override
michael@0 333 protected Account[] doInBackground(Void... params) {
michael@0 334 return AccountManager.get(context).getAccounts();
michael@0 335 }
michael@0 336 }
michael@0 337
michael@0 338 /**
michael@0 339 * This updates UI, so needs to be done on the foreground thread.
michael@0 340 */
michael@0 341 protected void populateEmailAddressAutocomplete(Account[] accounts) {
michael@0 342 // First a set, since we don't want repeats.
michael@0 343 final Set<String> emails = new HashSet<String>();
michael@0 344 for (Account account : accounts) {
michael@0 345 if (!Patterns.EMAIL_ADDRESS.matcher(account.name).matches()) {
michael@0 346 continue;
michael@0 347 }
michael@0 348 emails.add(account.name);
michael@0 349 }
michael@0 350
michael@0 351 // And then sorted in alphabetical order.
michael@0 352 final String[] sortedEmails = emails.toArray(new String[0]);
michael@0 353 Arrays.sort(sortedEmails);
michael@0 354
michael@0 355 final ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_dropdown_item_1line, sortedEmails);
michael@0 356 emailEdit.setAdapter(adapter);
michael@0 357 }
michael@0 358
michael@0 359 @Override
michael@0 360 public void onResume() {
michael@0 361 super.onResume();
michael@0 362
michael@0 363 // Getting Accounts accesses databases on disk, so needs to be done on a
michael@0 364 // background thread.
michael@0 365 final GetAccountsAsyncTask task = new GetAccountsAsyncTask(this) {
michael@0 366 @Override
michael@0 367 public void onPostExecute(Account[] accounts) {
michael@0 368 populateEmailAddressAutocomplete(accounts);
michael@0 369 }
michael@0 370 };
michael@0 371 task.execute();
michael@0 372 }
michael@0 373 }

mercurial