/* 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.goanna.fxa.activities; import java.util.Calendar; import java.util.HashMap; import java.util.LinkedList; import java.util.Locale; import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import org.mozilla.goanna.AppConstants; import org.mozilla.goanna.R; import org.mozilla.goanna.background.common.log.Logger; import org.mozilla.goanna.background.fxa.FxAccountAgeLockoutHelper; import org.mozilla.goanna.background.fxa.FxAccountClient; import org.mozilla.goanna.background.fxa.FxAccountClient10.RequestDelegate; import org.mozilla.goanna.background.fxa.FxAccountClient20; import org.mozilla.goanna.background.fxa.FxAccountClient20.LoginResponse; import org.mozilla.goanna.background.fxa.FxAccountClientException.FxAccountClientRemoteException; import org.mozilla.goanna.background.fxa.FxAccountUtils; import org.mozilla.goanna.background.fxa.PasswordStretcher; import org.mozilla.goanna.db.BrowserContract; import org.mozilla.goanna.fxa.authenticator.AndroidFxAccount; import org.mozilla.goanna.fxa.tasks.FxAccountCreateAccountTask; import org.mozilla.goanna.sync.Utils; 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.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 String[] monthItems; protected String[] dayItems; protected EditText yearEdit; protected EditText monthEdit; protected EditText dayEdit; protected CheckBox chooseCheckBox; protected View monthDaycombo; protected Map selectedEngines; protected final Map authoritiesToSyncAutomaticallyMap = new HashMap(AndroidFxAccount.DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP); /** * {@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"); monthEdit = (EditText) ensureFindViewById(null, R.id.month_edit, "month edit"); dayEdit = (EditText) ensureFindViewById(null, R.id.day_edit, "day edit"); monthDaycombo = ensureFindViewById(null, R.id.month_day_combo, "month day combo"); 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(); initializeMonthAndDayValues(); 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 Bundle extras = makeExtrasBundle(null, null); startActivityInstead(FxAccountSignInActivity.class, CHILD_REQUEST_CODE, extras); } }); updateFromIntentExtras(); maybeEnableAnimations(); } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); updateMonthAndDayFromBundle(savedInstanceState); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); updateBundleWithMonthAndDay(outState); } @Override protected Bundle makeExtrasBundle(String email, String password) { final Bundle extras = super.makeExtrasBundle(email, password); extras.putString(EXTRA_YEAR, yearEdit.getText().toString()); updateBundleWithMonthAndDay(extras); return extras; } @Override protected void updateFromIntentExtras() { super.updateFromIntentExtras(); if (getIntent() != null) { yearEdit.setText(getIntent().getStringExtra(EXTRA_YEAR)); updateMonthAndDayFromBundle(getIntent().getExtras() != null ? getIntent().getExtras() : new Bundle()); } } private void updateBundleWithMonthAndDay(final Bundle bundle) { if (monthEdit.getTag() != null) { bundle.putInt(EXTRA_MONTH, (Integer) monthEdit.getTag()); } if (dayEdit.getTag() != null) { bundle.putInt(EXTRA_DAY, (Integer) dayEdit.getTag()); } } private void updateMonthAndDayFromBundle(final Bundle extras) { final Integer zeroBasedMonthIndex = (Integer) extras.get(EXTRA_MONTH); final Integer oneBasedDayIndex = (Integer) extras.get(EXTRA_DAY); maybeEnableMonthAndDayButtons(); if (zeroBasedMonthIndex != null) { monthEdit.setText(monthItems[zeroBasedMonthIndex]); monthEdit.setTag(Integer.valueOf(zeroBasedMonthIndex)); createDayEdit(zeroBasedMonthIndex); if (oneBasedDayIndex != null && dayItems != null) { dayEdit.setText(dayItems[oneBasedDayIndex - 1]); dayEdit.setTag(Integer.valueOf(oneBasedDayIndex)); } } else { monthEdit.setText(""); dayEdit.setText(""); } updateButtonState(); } @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 int messageId = e.getErrorMessageStringResource(); final int clickableId = R.string.fxaccount_sign_in_button_label; final Spannable span = Utils.interpolateClickableSpan(this, messageId, clickableId, 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(); final Bundle extras = makeExtrasBundle(email, password); startActivityInstead(FxAccountSignInActivity.class, CHILD_REQUEST_CODE, extras); } }); 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[years.size()]); } 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]); maybeEnableMonthAndDayButtons(); 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(); } }); } private void initializeMonthAndDayValues() { // Hide Month and day pickers monthDaycombo.setVisibility(View.GONE); dayEdit.setEnabled(false); // Populate month names. final Calendar calendar = Calendar.getInstance(); final Map monthNamesMap = calendar.getDisplayNames(Calendar.MONTH, Calendar.LONG, Locale.getDefault()); monthItems = new String[monthNamesMap.size()]; for (Map.Entry entry : monthNamesMap.entrySet()) { monthItems[entry.getValue()] = entry.getKey(); } createMonthEdit(); } protected void createMonthEdit() { monthEdit.setText(""); monthEdit.setTag(null); monthEdit.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) { monthEdit.setText(monthItems[which]); monthEdit.setTag(Integer.valueOf(which)); createDayEdit(which); updateButtonState(); } }; final AlertDialog dialog = new AlertDialog.Builder(FxAccountCreateAccountActivity.this) .setTitle(R.string.fxaccount_create_account_month_of_birth) .setItems(monthItems, listener) .setIcon(R.drawable.icon) .create(); dialog.show(); } }); } protected void createDayEdit(final int monthIndex) { dayEdit.setText(""); dayEdit.setTag(null); dayEdit.setEnabled(true); String yearText = yearEdit.getText().toString(); Integer birthYear; try { birthYear = Integer.parseInt(yearText); } catch (NumberFormatException e) { // Ideal this should never happen. Logger.debug(LOG_TAG, "Exception while parsing year value" + e); return; } Calendar c = Calendar.getInstance(); c.set(birthYear, monthIndex, 1); LinkedList days = new LinkedList(); for (int i = c.getActualMinimum(Calendar.DAY_OF_MONTH); i <= c.getActualMaximum(Calendar.DAY_OF_MONTH); i++) { days.add(Integer.toString(i)); } dayItems = days.toArray(new String[days.size()]); dayEdit.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) { dayEdit.setText(dayItems[which]); dayEdit.setTag(Integer.valueOf(which + 1)); // Days are 1-based. updateButtonState(); } }; final AlertDialog dialog = new AlertDialog.Builder(FxAccountCreateAccountActivity.this) .setTitle(R.string.fxaccount_create_account_day_of_birth) .setItems(dayItems, listener) .setIcon(R.drawable.icon) .create(); dialog.show(); } }); } private void maybeEnableMonthAndDayButtons() { Integer yearOfBirth = null; try { yearOfBirth = Integer.valueOf(yearEdit.getText().toString(), 10); } catch (NumberFormatException e) { Logger.debug(LOG_TAG, "Year text is not a number; assuming year is a range and that user is old enough."); } // Check if the selected year is the magic year. if (yearOfBirth == null || !FxAccountAgeLockoutHelper.isMagicYear(yearOfBirth)) { // Year/Dec/31 is the latest birthday in the selected year, corresponding // to the youngest person. monthEdit.setTag(Integer.valueOf(11)); dayEdit.setTag(Integer.valueOf(31)); return; } // Show month and date field. yearEdit.setVisibility(View.GONE); monthDaycombo.setVisibility(View.VISIBLE); monthEdit.setTag(null); dayEdit.setTag(null); } public void createAccount(String email, String password, Map engines, Map authoritiesToSyncAutomaticallyMap) { String serverURI = getAuthServerEndpoint(); 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, authoritiesToSyncAutomaticallyMap) { @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, getQueryParameters(), delegate).execute(); } catch (Exception e) { showRemoteError(e, R.string.fxaccount_create_account_unknown_error); } } @Override protected boolean shouldButtonBeEnabled() { return super.shouldButtonBeEnabled() && (yearEdit.length() > 0) && (monthEdit.getTag() != null) && (dayEdit.getTag() != null); } 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(); final int dayOfBirth = (Integer) dayEdit.getTag(); final int zeroBasedMonthOfBirth = (Integer) monthEdit.getTag(); // Only include selected engines if the user currently has the option checked. final Map engines = chooseCheckBox.isChecked() ? selectedEngines : null; // Only include authorities if the user currently has the option checked. final Map authoritiesMap = chooseCheckBox.isChecked() ? authoritiesToSyncAutomaticallyMap : AndroidFxAccount.DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP; if (FxAccountAgeLockoutHelper.passesAgeCheck(dayOfBirth, zeroBasedMonthOfBirth, yearEdit.getText().toString(), yearItems)) { FxAccountUtils.pii(LOG_TAG, "Passed age check."); createAccount(email, password, engines, authoritiesMap); } else { FxAccountUtils.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 INDEX_READING_LIST = 4; // Only valid if reading list is enabled. final int NUMBER_OF_ENGINES; if (AppConstants.MOZ_ANDROID_READING_LIST_SERVICE) { NUMBER_OF_ENGINES = 5; } else { 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); if (AppConstants.MOZ_ANDROID_READING_LIST_SERVICE) { items[INDEX_READING_LIST] = getResources().getString(R.string.fxaccount_status_reading_list); } // 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]); if (AppConstants.MOZ_ANDROID_READING_LIST_SERVICE) { authoritiesToSyncAutomaticallyMap.put(BrowserContract.READING_LIST_AUTHORITY, checkedItems[INDEX_READING_LIST]); } FxAccountUtils.pii(LOG_TAG, "Updating selectedEngines: " + selectedEngines.toString()); FxAccountUtils.pii(LOG_TAG, "Updating authorities: " + authoritiesToSyncAutomaticallyMap.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(); } }); } }