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