diff --git a/mobile/android/base/background/fxa/FxAccountAgeLockoutHelper.java b/mobile/android/base/background/fxa/FxAccountAgeLockoutHelper.java
index 6f726c0a2238..4b4ac22f3c33 100644
--- a/mobile/android/base/background/fxa/FxAccountAgeLockoutHelper.java
+++ b/mobile/android/base/background/fxa/FxAccountAgeLockoutHelper.java
@@ -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 yearOfBirth
is
- * definitely old enough to create an account.
+ * Return true if the given year is the magic year.
*
- * This errs towards locking out users who might be old enough, but are not
- * definitely old enough.
+ * The magic year 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 yearOfBirth
is definitely old
- * enough.
+ * @return true if yearOfBirth
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
+ * dayOfBirth/zeroBasedMonthOfBirth/yearOfBirth
is old enough to
+ * create an account.
+ *
+ * @param dayOfBirth
+ * @param zeroBasedMonthOfBirth
+ * @param yearOfBirth
+ * @return true if somebody born in
+ * dayOfBirth/zeroBasedMonthOfBirth/yearOfBirth
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);
}
}
diff --git a/mobile/android/base/fxa/FxAccountConstants.java b/mobile/android/base/fxa/FxAccountConstants.java
index 5ced80f7227a..a13c18110883 100644
--- a/mobile/android/base/fxa/FxAccountConstants.java
+++ b/mobile/android/base/fxa/FxAccountConstants.java
@@ -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;
diff --git a/mobile/android/base/fxa/activities/FxAccountAbstractSetupActivity.java b/mobile/android/base/fxa/activities/FxAccountAbstractSetupActivity.java
index b878b4673e7a..f13a3e3512d1 100644
--- a/mobile/android/base/fxa/activities/FxAccountAbstractSetupActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountAbstractSetupActivity.java
@@ -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";
diff --git a/mobile/android/base/fxa/activities/FxAccountCreateAccountActivity.java b/mobile/android/base/fxa/activities/FxAccountCreateAccountActivity.java
index 10416368043e..4a9f1a2e2200 100644
--- a/mobile/android/base/fxa/activities/FxAccountCreateAccountActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountCreateAccountActivity.java
@@ -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 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 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) {
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 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 {
diff --git a/mobile/android/base/locales/en-US/sync_strings.dtd b/mobile/android/base/locales/en-US/sync_strings.dtd
index cca0b7ece3a4..429968a79a90 100644
--- a/mobile/android/base/locales/en-US/sync_strings.dtd
+++ b/mobile/android/base/locales/en-US/sync_strings.dtd
@@ -150,6 +150,8 @@
etc). The account remains a "Firefox Account". -->
+
+
diff --git a/mobile/android/base/resources/layout/fxaccount_create_account.xml b/mobile/android/base/resources/layout/fxaccount_create_account.xml
index 77530e13b5e5..4718640012ea 100644
--- a/mobile/android/base/resources/layout/fxaccount_create_account.xml
+++ b/mobile/android/base/resources/layout/fxaccount_create_account.xml
@@ -40,6 +40,35 @@
android:hint="@string/fxaccount_create_account_year_of_birth"
android:inputType="none" />
+
+
+
+
+
+
+
&fxaccount_create_account_header2;
&fxaccount_create_account_password_length_restriction;
+&fxaccount_create_account_day_of_birth;
+&fxaccount_create_account_month_of_birth;
&fxaccount_create_account_year_of_birth;
&fxaccount_create_account_policy_text2;