diff -r 000000000000 -r 6474c204b198 mobile/android/base/fxa/activities/FxAccountCreateAccountActivity.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mobile/android/base/fxa/activities/FxAccountCreateAccountActivity.java Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,352 @@ +/* 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.util.Calendar; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +import org.mozilla.gecko.R; +import org.mozilla.gecko.background.common.log.Logger; +import org.mozilla.gecko.background.fxa.FxAccountAgeLockoutHelper; +import org.mozilla.gecko.background.fxa.FxAccountClient; +import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate; +import org.mozilla.gecko.background.fxa.FxAccountClient20; +import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse; +import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException; +import org.mozilla.gecko.background.fxa.PasswordStretcher; +import org.mozilla.gecko.fxa.FxAccountConstants; +import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.FxAccountCreateAccountTask; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.os.SystemClock; +import android.text.Spannable; +import android.text.Spanned; +import android.text.method.LinkMovementMethod; +import android.text.style.ClickableSpan; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.AutoCompleteTextView; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.ProgressBar; +import android.widget.TextView; + +/** + * Activity which displays create account screen to the user. + */ +public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivity { + protected static final String LOG_TAG = FxAccountCreateAccountActivity.class.getSimpleName(); + + private static final int CHILD_REQUEST_CODE = 2; + + protected String[] yearItems; + protected EditText yearEdit; + protected CheckBox chooseCheckBox; + + protected Map selectedEngines; + + /** + * {@inheritDoc} + */ + @Override + public void onCreate(Bundle icicle) { + Logger.debug(LOG_TAG, "onCreate(" + icicle + ")"); + + super.onCreate(icicle); + setContentView(R.layout.fxaccount_create_account); + + emailEdit = (AutoCompleteTextView) ensureFindViewById(null, R.id.email, "email edit"); + passwordEdit = (EditText) ensureFindViewById(null, R.id.password, "password edit"); + showPasswordButton = (Button) ensureFindViewById(null, R.id.show_password, "show password button"); + yearEdit = (EditText) ensureFindViewById(null, R.id.year_edit, "year edit"); + remoteErrorTextView = (TextView) ensureFindViewById(null, R.id.remote_error, "remote error text view"); + button = (Button) ensureFindViewById(null, R.id.button, "create account button"); + progressBar = (ProgressBar) ensureFindViewById(null, R.id.progress, "progress bar"); + chooseCheckBox = (CheckBox) ensureFindViewById(null, R.id.choose_what_to_sync_checkbox, "choose what to sync check box"); + selectedEngines = new HashMap(); + + createCreateAccountButton(); + createYearEdit(); + addListeners(); + updateButtonState(); + createShowPasswordButton(); + linkifyPolicy(); + createChooseCheckBox(); + + View signInInsteadLink = ensureFindViewById(null, R.id.sign_in_instead_link, "sign in instead link"); + signInInsteadLink.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + final String email = emailEdit.getText().toString(); + final String password = passwordEdit.getText().toString(); + doSigninInstead(email, password); + } + }); + + // Only set email/password in onCreate; we don't want to overwrite edited values onResume. + if (getIntent() != null && getIntent().getExtras() != null) { + Bundle bundle = getIntent().getExtras(); + emailEdit.setText(bundle.getString("email")); + passwordEdit.setText(bundle.getString("password")); + } + } + + protected void doSigninInstead(final String email, final String password) { + Intent intent = new Intent(this, FxAccountSignInActivity.class); + if (email != null) { + intent.putExtra("email", email); + } + if (password != null) { + intent.putExtra("password", password); + } + // 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? + intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); + startActivityForResult(intent, CHILD_REQUEST_CODE); + } + + @Override + protected void showClientRemoteException(final FxAccountClientRemoteException e) { + if (!e.isAccountAlreadyExists()) { + super.showClientRemoteException(e); + return; + } + + // This horrible bit of special-casing is because we want this error message to + // contain a clickable, extra chunk of text, but we don't want to pollute + // the exception class with Android specifics. + final String clickablePart = getString(R.string.fxaccount_sign_in_button_label); + final String message = getString(e.getErrorMessageStringResource(), clickablePart); + final int clickableStart = message.lastIndexOf(clickablePart); + final int clickableEnd = clickableStart + clickablePart.length(); + + final Spannable span = Spannable.Factory.getInstance().newSpannable(message); + span.setSpan(new ClickableSpan() { + @Override + public void onClick(View widget) { + // Pass through the email address that already existed. + String email = e.body.getString("email"); + if (email == null) { + email = emailEdit.getText().toString(); + } + final String password = passwordEdit.getText().toString(); + doSigninInstead(email, password); + } + }, clickableStart, clickableEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + remoteErrorTextView.setMovementMethod(LinkMovementMethod.getInstance()); + remoteErrorTextView.setText(span); + } + + /** + * We might have switched to the SignIn activity; if that activity + * succeeds, feed its result back to the authenticator. + */ + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + Logger.debug(LOG_TAG, "onActivityResult: " + requestCode); + if (requestCode != CHILD_REQUEST_CODE || resultCode != RESULT_OK) { + super.onActivityResult(requestCode, resultCode, data); + return; + } + this.setResult(resultCode, data); + this.finish(); + } + + /** + * Return years to display in picker. + * + * @return 1990 or earlier, 1991, 1992, up to five years before current year. + * (So, if it is currently 2014, up to 2009.) + */ + protected String[] getYearItems() { + int year = Calendar.getInstance().get(Calendar.YEAR); + LinkedList years = new LinkedList(); + years.add(getResources().getString(R.string.fxaccount_create_account_1990_or_earlier)); + for (int i = 1991; i <= year - 5; i++) { + years.add(Integer.toString(i)); + } + return years.toArray(new String[0]); + } + + protected void createYearEdit() { + yearItems = getYearItems(); + + yearEdit.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + android.content.DialogInterface.OnClickListener listener = new Dialog.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + yearEdit.setText(yearItems[which]); + updateButtonState(); + } + }; + final AlertDialog dialog = new AlertDialog.Builder(FxAccountCreateAccountActivity.this) + .setTitle(R.string.fxaccount_create_account_year_of_birth) + .setItems(yearItems, listener) + .setIcon(R.drawable.icon) + .create(); + + dialog.show(); + } + }); + } + + public void createAccount(String email, String password, Map engines) { + String serverURI = FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT; + PasswordStretcher passwordStretcher = makePasswordStretcher(password); + // This delegate creates a new Android account on success, opens the + // appropriate "success!" activity, and finishes this activity. + RequestDelegate delegate = new AddAccountDelegate(email, passwordStretcher, serverURI, engines) { + @Override + public void handleError(Exception e) { + showRemoteError(e, R.string.fxaccount_create_account_unknown_error); + } + + @Override + public void handleFailure(FxAccountClientRemoteException e) { + showRemoteError(e, R.string.fxaccount_create_account_unknown_error); + } + }; + + Executor executor = Executors.newSingleThreadExecutor(); + FxAccountClient client = new FxAccountClient20(serverURI, executor); + try { + hideRemoteError(); + new FxAccountCreateAccountTask(this, this, email, passwordStretcher, client, delegate).execute(); + } catch (Exception e) { + showRemoteError(e, R.string.fxaccount_create_account_unknown_error); + } + } + + @Override + protected boolean shouldButtonBeEnabled() { + return super.shouldButtonBeEnabled() && + (yearEdit.length() > 0); + } + + protected void createCreateAccountButton() { + button.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (!updateButtonState()) { + return; + } + final String email = emailEdit.getText().toString(); + final String password = passwordEdit.getText().toString(); + // Only include selected engines if the user currently has the option checked. + final Map engines = chooseCheckBox.isChecked() + ? selectedEngines + : null; + if (FxAccountAgeLockoutHelper.passesAgeCheck(yearEdit.getText().toString(), yearItems)) { + FxAccountConstants.pii(LOG_TAG, "Passed age check."); + createAccount(email, password, engines); + } else { + FxAccountConstants.pii(LOG_TAG, "Failed age check!"); + FxAccountAgeLockoutHelper.lockOut(SystemClock.elapsedRealtime()); + setResult(RESULT_CANCELED); + redirectToActivity(FxAccountCreateAccountNotAllowedActivity.class); + } + } + }); + } + + /** + * The "Choose what to sync" checkbox pops up a multi-choice dialog when it is + * unchecked. It toggles to unchecked from checked. + */ + protected void createChooseCheckBox() { + final int INDEX_BOOKMARKS = 0; + final int INDEX_HISTORY = 1; + final int INDEX_TABS = 2; + final int INDEX_PASSWORDS = 3; + final int NUMBER_OF_ENGINES = 4; + + final String items[] = new String[NUMBER_OF_ENGINES]; + final boolean checkedItems[] = new boolean[NUMBER_OF_ENGINES]; + items[INDEX_BOOKMARKS] = getResources().getString(R.string.fxaccount_status_bookmarks); + items[INDEX_HISTORY] = getResources().getString(R.string.fxaccount_status_history); + items[INDEX_TABS] = getResources().getString(R.string.fxaccount_status_tabs); + items[INDEX_PASSWORDS] = getResources().getString(R.string.fxaccount_status_passwords); + // Default to everything checked. + for (int i = 0; i < NUMBER_OF_ENGINES; i++) { + checkedItems[i] = true; + } + + final DialogInterface.OnClickListener clickListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (which != DialogInterface.BUTTON_POSITIVE) { + Logger.debug(LOG_TAG, "onClick: not button positive, unchecking."); + chooseCheckBox.setChecked(false); + return; + } + // We only check the box on success. + Logger.debug(LOG_TAG, "onClick: button positive, checking."); + chooseCheckBox.setChecked(true); + // And then remember for future use. + ListView selectionsList = ((AlertDialog) dialog).getListView(); + for (int i = 0; i < NUMBER_OF_ENGINES; i++) { + checkedItems[i] = selectionsList.isItemChecked(i); + } + selectedEngines.put("bookmarks", checkedItems[INDEX_BOOKMARKS]); + selectedEngines.put("history", checkedItems[INDEX_HISTORY]); + selectedEngines.put("tabs", checkedItems[INDEX_TABS]); + selectedEngines.put("passwords", checkedItems[INDEX_PASSWORDS]); + FxAccountConstants.pii(LOG_TAG, "Updating selectedEngines: " + selectedEngines.toString()); + } + }; + + final DialogInterface.OnMultiChoiceClickListener multiChoiceClickListener = new DialogInterface.OnMultiChoiceClickListener() { + @Override + public void onClick(DialogInterface dialog, int which, boolean isChecked) { + // Display multi-selection clicks in UI. + ListView selectionsList = ((AlertDialog) dialog).getListView(); + selectionsList.setItemChecked(which, isChecked); + } + }; + + final AlertDialog dialog = new AlertDialog.Builder(this) + .setTitle(R.string.fxaccount_create_account_choose_what_to_sync) + .setIcon(R.drawable.icon) + .setMultiChoiceItems(items, checkedItems, multiChoiceClickListener) + .setPositiveButton(android.R.string.ok, clickListener) + .setNegativeButton(android.R.string.cancel, clickListener) + .create(); + + dialog.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + Logger.debug(LOG_TAG, "onCancel: unchecking."); + chooseCheckBox.setChecked(false); + } + }); + + chooseCheckBox.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + // There appears to be no way to stop Android interpreting the click + // first. So, if the user clicked on an unchecked box, it's checked by + // the time we get here. + if (!chooseCheckBox.isChecked()) { + Logger.debug(LOG_TAG, "onClick: was checked, not showing dialog."); + return; + } + Logger.debug(LOG_TAG, "onClick: was unchecked, showing dialog."); + dialog.show(); + } + }); + } +}