Bug 1058806 - Implement magic year month/day boxes in Android Firefox Account create account flow. r=nalexander,vivek

========

21c63a8d1a
Author: vivek <vivekb.balakrishnan@gmail.com>
    Bug 1058806 - Part 4: Maintain magic year state across orientation changes.

========

da074438fe
Author: vivek <vivekb.balakrishnan@gmail.com>
Date:   Fri Jan 16 16:46:47 2015 +0200

    Bug 1058806 - Part 3: Maintain magic year across create -> sign in -> create loop.

========

982d692575
Author: vivek <vivekb.balakrishnan@gmail.com>
Date:   Sat Dec 20 20:07:45 2014 +0200

    Bug 1058806 - Part 2: Add date and month to UI.

========

15594d36c9
Author: vivek <vivekb.balakrishnan@gmail.com>
Date:   Sat Dec 20 04:36:49 2014 +0200

    Bug 1058806 - Part 1: Make age pass check consider date and month.

--HG--
extra : rebase_source : 11ab11c519461fa1be8d6dda34777620db05468d
This commit is contained in:
Vivek Balakrishnan 2015-01-18 04:43:19 +02:00
parent 3ea68f39ad
commit a1f24342ea
7 changed files with 262 additions and 20 deletions

View File

@ -4,8 +4,10 @@
package org.mozilla.gecko.background.fxa;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Locale;
import org.mozilla.gecko.fxa.FxAccountConstants;
@ -46,31 +48,69 @@ public class FxAccountAgeLockoutHelper {
}
/**
* Return true if the age of somebody born in <code>yearOfBirth</code> is
* definitely old enough to create an account.
* Return true if the given year is the magic year.
* <p>
* This errs towards locking out users who might be old enough, but are not
* definitely old enough.
* The <i>magic year</i> is the calendar year when the user is the minimum age
* older. That is, for part of the magic year the user is younger than the age
* limit and for part of the magic year the user is older than the age limit.
*
* @param yearOfBirth
* @return true if somebody born in <code>yearOfBirth</code> is definitely old
* enough.
* @return true if <code>yearOfBirth</code> is the magic year.
*/
public static boolean passesAgeCheck(int yearOfBirth) {
int thisYear = Calendar.getInstance().get(Calendar.YEAR);
int approximateAge = thisYear - yearOfBirth;
boolean oldEnough = approximateAge >= FxAccountConstants.MINIMUM_AGE_TO_CREATE_AN_ACCOUNT;
public static boolean isMagicYear(int yearOfBirth) {
final Calendar cal = Calendar.getInstance();
final int thisYear = cal.get(Calendar.YEAR);
return (thisYear - yearOfBirth) == FxAccountConstants.MINIMUM_AGE_TO_CREATE_AN_ACCOUNT;
}
/**
* Return true if the age of somebody born in
* <code>dayOfBirth/zeroBasedMonthOfBirth/yearOfBirth</code> is old enough to
* create an account.
*
* @param dayOfBirth
* @param zeroBasedMonthOfBirth
* @param yearOfBirth
* @return true if somebody born in
* <code>dayOfBirth/zeroBasedMonthOfBirth/yearOfBirth</code> is old enough.
*/
public static boolean passesAgeCheck(final int dayOfBirth, final int zeroBasedMonthOfBirth, final int yearOfBirth) {
final Calendar latestBirthday = Calendar.getInstance();
final int y = latestBirthday.get(Calendar.YEAR);
final int m = latestBirthday.get(Calendar.MONTH);
final int d = latestBirthday.get(Calendar.DAY_OF_MONTH);
latestBirthday.clear();
latestBirthday.set(y - FxAccountConstants.MINIMUM_AGE_TO_CREATE_AN_ACCOUNT, m, d);
// Go back one second, so that the exact same birthday and latestBirthday satisfy birthday <= latestBirthday.
latestBirthday.add(Calendar.SECOND, 1);
final Calendar birthday = Calendar.getInstance();
birthday.clear();
birthday.set(yearOfBirth, zeroBasedMonthOfBirth, dayOfBirth);
boolean oldEnough = birthday.before(latestBirthday);
if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
FxAccountUtils.pii(LOG_TAG, "Age check " + (oldEnough ? "passes" : "fails") +
": age is " + approximateAge + " = " + thisYear + " - " + yearOfBirth);
final StringBuilder message = new StringBuilder();
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd", Locale.getDefault());
message.append("Age check ");
message.append(oldEnough ? "passes" : "fails");
message.append(": birthday is ");
message.append(sdf.format(birthday.getTime()));
message.append("; latest birthday is ");
message.append(sdf.format(latestBirthday.getTime()));
message.append(" (Y/M/D).");
FxAccountUtils.pii(LOG_TAG, message.toString());
}
return oldEnough;
}
/**
* Custom function for UI use only.
*/
public static boolean passesAgeCheck(String yearText, String[] yearItems) {
public static boolean passesAgeCheck(int dayOfBirth, int zeroBaseMonthOfBirth, String yearText, String[] yearItems) {
if (yearText == null) {
throw new IllegalArgumentException("yearText must not be null");
}
@ -91,6 +131,7 @@ public class FxAccountAgeLockoutHelper {
FxAccountUtils.pii(LOG_TAG, "Passed age check: year text was found in item list but was not a number.");
return true;
}
return passesAgeCheck(yearOfBirth);
return passesAgeCheck(dayOfBirth, zeroBaseMonthOfBirth, yearOfBirth);
}
}

View File

@ -16,8 +16,8 @@ public class FxAccountConstants {
public static final String STAGE_AUTH_SERVER_ENDPOINT = "https://api-accounts.stage.mozaws.net/v1";
public static final String STAGE_TOKEN_SERVER_ENDPOINT = "https://token.stage.mozaws.net/1.0/sync/1.5";
// You must be at least 14 years old to create a Firefox Account.
public static final int MINIMUM_AGE_TO_CREATE_AN_ACCOUNT = 14;
// You must be at least 13 years old, on the day of creation, to create a Firefox Account.
public static final int MINIMUM_AGE_TO_CREATE_AN_ACCOUNT = 13;
// You must wait 15 minutes after failing an age check before trying to create a different account.
public static final long MINIMUM_TIME_TO_WAIT_AFTER_AGE_CHECK_FAILED_IN_MILLISECONDS = 15 * 60 * 1000;

View File

@ -63,6 +63,8 @@ abstract public class FxAccountAbstractSetupActivity extends FxAccountAbstractAc
public static final String EXTRA_PASSWORD = "password";
public static final String EXTRA_PASSWORD_SHOWN = "password_shown";
public static final String EXTRA_YEAR = "year";
public static final String EXTRA_MONTH = "month";
public static final String EXTRA_DAY = "day";
public static final String EXTRA_EXTRAS = "extras";
public static final String JSON_KEY_AUTH = "auth";

View File

@ -7,6 +7,7 @@ package org.mozilla.gecko.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;
@ -52,8 +53,13 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi
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<String, Boolean> selectedEngines;
@ -71,6 +77,9 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi
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");
@ -84,6 +93,7 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi
createShowPasswordButton();
linkifyPolicy();
createChooseCheckBox();
initializeMonthAndDayValues();
View signInInsteadLink = ensureFindViewById(null, R.id.sign_in_instead_link, "sign in instead link");
signInInsteadLink.setOnClickListener(new OnClickListener() {
@ -97,11 +107,23 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi
updateFromIntentExtras();
}
@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);
final String year = yearEdit.getText().toString();
extras.putString(EXTRA_YEAR, year);
extras.putString(EXTRA_YEAR, yearEdit.getText().toString());
updateBundleWithMonthAndDay(extras);
return extras;
}
@ -111,9 +133,40 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi
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()) {
@ -186,6 +239,7 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi
@Override
public void onClick(DialogInterface dialog, int which) {
yearEdit.setText(yearItems[which]);
maybeEnableMonthAndDayButtons();
updateButtonState();
}
};
@ -200,6 +254,114 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi
});
}
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<String, Integer> monthNamesMap = calendar.getDisplayNames(Calendar.MONTH, Calendar.LONG, Locale.getDefault());
monthItems = new String[monthNamesMap.size()];
for (Map.Entry<String, Integer> 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<String> days = new LinkedList<String>();
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<String, Boolean> engines) {
String serverURI = getAuthServerEndpoint();
PasswordStretcher passwordStretcher = makePasswordStretcher(password);
@ -230,7 +392,9 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi
@Override
protected boolean shouldButtonBeEnabled() {
return super.shouldButtonBeEnabled() &&
(yearEdit.length() > 0);
(yearEdit.length() > 0) &&
(monthEdit.getTag() != null) &&
(dayEdit.getTag() != null);
}
protected void createCreateAccountButton() {
@ -242,11 +406,13 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi
}
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<String, Boolean> engines = chooseCheckBox.isChecked()
? selectedEngines
: null;
if (FxAccountAgeLockoutHelper.passesAgeCheck(yearEdit.getText().toString(), yearItems)) {
if (FxAccountAgeLockoutHelper.passesAgeCheck(dayOfBirth, zeroBasedMonthOfBirth, yearEdit.getText().toString(), yearItems)) {
FxAccountUtils.pii(LOG_TAG, "Passed age check.");
createAccount(email, password, engines);
} else {

View File

@ -150,6 +150,8 @@
etc). The account remains a "Firefox Account". -->
<!ENTITY fxaccount_create_account_header2 'Create a Firefox Account'>
<!ENTITY fxaccount_create_account_password_length_restriction 'Must be at least 8 characters'>
<!ENTITY fxaccount_create_account_day_of_birth 'Day'>
<!ENTITY fxaccount_create_account_month_of_birth 'Month'>
<!ENTITY fxaccount_create_account_year_of_birth 'Year of birth'>
<!-- Localization note: &formatS1; is fxaccount_policy_linktos, &formatS2; is fxaccount_policy_linkprivacy, both hyperlinked. -->
<!ENTITY fxaccount_create_account_policy_text2 'By proceeding, I agree to the &formatS1; and &formatS2; of Firefox cloud services.'>

View File

@ -40,6 +40,35 @@
android:hint="@string/fxaccount_create_account_year_of_birth"
android:inputType="none" />
<LinearLayout
android:id="@+id/month_day_combo"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:weightSum="1" >
<EditText
android:id="@+id/month_edit"
style="@style/FxAccountEditItem"
android:layout_marginRight="10dp"
android:drawableRight="@drawable/fxaccount_ddarrow_inactive"
android:focusable="false"
android:layout_weight="0.5"
android:maxLength="3"
android:hint="@string/fxaccount_create_account_month_of_birth"
android:inputType="none" />
<EditText
android:id="@+id/day_edit"
style="@style/FxAccountEditItem"
android:layout_marginLeft="10dp"
android:drawableRight="@drawable/fxaccount_ddarrow_inactive"
android:focusable="false"
android:layout_weight="0.5"
android:hint="@string/fxaccount_create_account_day_of_birth"
android:inputType="none" />
</LinearLayout>
<TextView
android:id="@+id/policy"
style="@style/FxAccountLinkifiedItem"

View File

@ -138,6 +138,8 @@
<string name="fxaccount_create_account_header">&fxaccount_create_account_header2;</string>
<string name="fxaccount_create_account_password_length_restriction">&fxaccount_create_account_password_length_restriction;</string>
<string name="fxaccount_create_account_day_of_birth">&fxaccount_create_account_day_of_birth;</string>
<string name="fxaccount_create_account_month_of_birth">&fxaccount_create_account_month_of_birth;</string>
<string name="fxaccount_create_account_year_of_birth">&fxaccount_create_account_year_of_birth;</string>
<string name="fxaccount_create_account_policy_text">&fxaccount_create_account_policy_text2;</string>