1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/fxa/activities/FxAccountCreateAccountActivity.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,352 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +package org.mozilla.gecko.fxa.activities; 1.9 + 1.10 +import java.util.Calendar; 1.11 +import java.util.HashMap; 1.12 +import java.util.LinkedList; 1.13 +import java.util.Map; 1.14 +import java.util.concurrent.Executor; 1.15 +import java.util.concurrent.Executors; 1.16 + 1.17 +import org.mozilla.gecko.R; 1.18 +import org.mozilla.gecko.background.common.log.Logger; 1.19 +import org.mozilla.gecko.background.fxa.FxAccountAgeLockoutHelper; 1.20 +import org.mozilla.gecko.background.fxa.FxAccountClient; 1.21 +import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate; 1.22 +import org.mozilla.gecko.background.fxa.FxAccountClient20; 1.23 +import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse; 1.24 +import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException; 1.25 +import org.mozilla.gecko.background.fxa.PasswordStretcher; 1.26 +import org.mozilla.gecko.fxa.FxAccountConstants; 1.27 +import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.FxAccountCreateAccountTask; 1.28 + 1.29 +import android.app.AlertDialog; 1.30 +import android.app.Dialog; 1.31 +import android.content.DialogInterface; 1.32 +import android.content.Intent; 1.33 +import android.os.Bundle; 1.34 +import android.os.SystemClock; 1.35 +import android.text.Spannable; 1.36 +import android.text.Spanned; 1.37 +import android.text.method.LinkMovementMethod; 1.38 +import android.text.style.ClickableSpan; 1.39 +import android.view.View; 1.40 +import android.view.View.OnClickListener; 1.41 +import android.widget.AutoCompleteTextView; 1.42 +import android.widget.Button; 1.43 +import android.widget.CheckBox; 1.44 +import android.widget.EditText; 1.45 +import android.widget.ListView; 1.46 +import android.widget.ProgressBar; 1.47 +import android.widget.TextView; 1.48 + 1.49 +/** 1.50 + * Activity which displays create account screen to the user. 1.51 + */ 1.52 +public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivity { 1.53 + protected static final String LOG_TAG = FxAccountCreateAccountActivity.class.getSimpleName(); 1.54 + 1.55 + private static final int CHILD_REQUEST_CODE = 2; 1.56 + 1.57 + protected String[] yearItems; 1.58 + protected EditText yearEdit; 1.59 + protected CheckBox chooseCheckBox; 1.60 + 1.61 + protected Map<String, Boolean> selectedEngines; 1.62 + 1.63 + /** 1.64 + * {@inheritDoc} 1.65 + */ 1.66 + @Override 1.67 + public void onCreate(Bundle icicle) { 1.68 + Logger.debug(LOG_TAG, "onCreate(" + icicle + ")"); 1.69 + 1.70 + super.onCreate(icicle); 1.71 + setContentView(R.layout.fxaccount_create_account); 1.72 + 1.73 + emailEdit = (AutoCompleteTextView) ensureFindViewById(null, R.id.email, "email edit"); 1.74 + passwordEdit = (EditText) ensureFindViewById(null, R.id.password, "password edit"); 1.75 + showPasswordButton = (Button) ensureFindViewById(null, R.id.show_password, "show password button"); 1.76 + yearEdit = (EditText) ensureFindViewById(null, R.id.year_edit, "year edit"); 1.77 + remoteErrorTextView = (TextView) ensureFindViewById(null, R.id.remote_error, "remote error text view"); 1.78 + button = (Button) ensureFindViewById(null, R.id.button, "create account button"); 1.79 + progressBar = (ProgressBar) ensureFindViewById(null, R.id.progress, "progress bar"); 1.80 + chooseCheckBox = (CheckBox) ensureFindViewById(null, R.id.choose_what_to_sync_checkbox, "choose what to sync check box"); 1.81 + selectedEngines = new HashMap<String, Boolean>(); 1.82 + 1.83 + createCreateAccountButton(); 1.84 + createYearEdit(); 1.85 + addListeners(); 1.86 + updateButtonState(); 1.87 + createShowPasswordButton(); 1.88 + linkifyPolicy(); 1.89 + createChooseCheckBox(); 1.90 + 1.91 + View signInInsteadLink = ensureFindViewById(null, R.id.sign_in_instead_link, "sign in instead link"); 1.92 + signInInsteadLink.setOnClickListener(new OnClickListener() { 1.93 + @Override 1.94 + public void onClick(View v) { 1.95 + final String email = emailEdit.getText().toString(); 1.96 + final String password = passwordEdit.getText().toString(); 1.97 + doSigninInstead(email, password); 1.98 + } 1.99 + }); 1.100 + 1.101 + // Only set email/password in onCreate; we don't want to overwrite edited values onResume. 1.102 + if (getIntent() != null && getIntent().getExtras() != null) { 1.103 + Bundle bundle = getIntent().getExtras(); 1.104 + emailEdit.setText(bundle.getString("email")); 1.105 + passwordEdit.setText(bundle.getString("password")); 1.106 + } 1.107 + } 1.108 + 1.109 + protected void doSigninInstead(final String email, final String password) { 1.110 + Intent intent = new Intent(this, FxAccountSignInActivity.class); 1.111 + if (email != null) { 1.112 + intent.putExtra("email", email); 1.113 + } 1.114 + if (password != null) { 1.115 + intent.putExtra("password", password); 1.116 + } 1.117 + // Per http://stackoverflow.com/a/8992365, this triggers a known bug with 1.118 + // the soft keyboard not being shown for the started activity. Why, Android, why? 1.119 + intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); 1.120 + startActivityForResult(intent, CHILD_REQUEST_CODE); 1.121 + } 1.122 + 1.123 + @Override 1.124 + protected void showClientRemoteException(final FxAccountClientRemoteException e) { 1.125 + if (!e.isAccountAlreadyExists()) { 1.126 + super.showClientRemoteException(e); 1.127 + return; 1.128 + } 1.129 + 1.130 + // This horrible bit of special-casing is because we want this error message to 1.131 + // contain a clickable, extra chunk of text, but we don't want to pollute 1.132 + // the exception class with Android specifics. 1.133 + final String clickablePart = getString(R.string.fxaccount_sign_in_button_label); 1.134 + final String message = getString(e.getErrorMessageStringResource(), clickablePart); 1.135 + final int clickableStart = message.lastIndexOf(clickablePart); 1.136 + final int clickableEnd = clickableStart + clickablePart.length(); 1.137 + 1.138 + final Spannable span = Spannable.Factory.getInstance().newSpannable(message); 1.139 + span.setSpan(new ClickableSpan() { 1.140 + @Override 1.141 + public void onClick(View widget) { 1.142 + // Pass through the email address that already existed. 1.143 + String email = e.body.getString("email"); 1.144 + if (email == null) { 1.145 + email = emailEdit.getText().toString(); 1.146 + } 1.147 + final String password = passwordEdit.getText().toString(); 1.148 + doSigninInstead(email, password); 1.149 + } 1.150 + }, clickableStart, clickableEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 1.151 + remoteErrorTextView.setMovementMethod(LinkMovementMethod.getInstance()); 1.152 + remoteErrorTextView.setText(span); 1.153 + } 1.154 + 1.155 + /** 1.156 + * We might have switched to the SignIn activity; if that activity 1.157 + * succeeds, feed its result back to the authenticator. 1.158 + */ 1.159 + @Override 1.160 + public void onActivityResult(int requestCode, int resultCode, Intent data) { 1.161 + Logger.debug(LOG_TAG, "onActivityResult: " + requestCode); 1.162 + if (requestCode != CHILD_REQUEST_CODE || resultCode != RESULT_OK) { 1.163 + super.onActivityResult(requestCode, resultCode, data); 1.164 + return; 1.165 + } 1.166 + this.setResult(resultCode, data); 1.167 + this.finish(); 1.168 + } 1.169 + 1.170 + /** 1.171 + * Return years to display in picker. 1.172 + * 1.173 + * @return 1990 or earlier, 1991, 1992, up to five years before current year. 1.174 + * (So, if it is currently 2014, up to 2009.) 1.175 + */ 1.176 + protected String[] getYearItems() { 1.177 + int year = Calendar.getInstance().get(Calendar.YEAR); 1.178 + LinkedList<String> years = new LinkedList<String>(); 1.179 + years.add(getResources().getString(R.string.fxaccount_create_account_1990_or_earlier)); 1.180 + for (int i = 1991; i <= year - 5; i++) { 1.181 + years.add(Integer.toString(i)); 1.182 + } 1.183 + return years.toArray(new String[0]); 1.184 + } 1.185 + 1.186 + protected void createYearEdit() { 1.187 + yearItems = getYearItems(); 1.188 + 1.189 + yearEdit.setOnClickListener(new OnClickListener() { 1.190 + @Override 1.191 + public void onClick(View v) { 1.192 + android.content.DialogInterface.OnClickListener listener = new Dialog.OnClickListener() { 1.193 + @Override 1.194 + public void onClick(DialogInterface dialog, int which) { 1.195 + yearEdit.setText(yearItems[which]); 1.196 + updateButtonState(); 1.197 + } 1.198 + }; 1.199 + final AlertDialog dialog = new AlertDialog.Builder(FxAccountCreateAccountActivity.this) 1.200 + .setTitle(R.string.fxaccount_create_account_year_of_birth) 1.201 + .setItems(yearItems, listener) 1.202 + .setIcon(R.drawable.icon) 1.203 + .create(); 1.204 + 1.205 + dialog.show(); 1.206 + } 1.207 + }); 1.208 + } 1.209 + 1.210 + public void createAccount(String email, String password, Map<String, Boolean> engines) { 1.211 + String serverURI = FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT; 1.212 + PasswordStretcher passwordStretcher = makePasswordStretcher(password); 1.213 + // This delegate creates a new Android account on success, opens the 1.214 + // appropriate "success!" activity, and finishes this activity. 1.215 + RequestDelegate<LoginResponse> delegate = new AddAccountDelegate(email, passwordStretcher, serverURI, engines) { 1.216 + @Override 1.217 + public void handleError(Exception e) { 1.218 + showRemoteError(e, R.string.fxaccount_create_account_unknown_error); 1.219 + } 1.220 + 1.221 + @Override 1.222 + public void handleFailure(FxAccountClientRemoteException e) { 1.223 + showRemoteError(e, R.string.fxaccount_create_account_unknown_error); 1.224 + } 1.225 + }; 1.226 + 1.227 + Executor executor = Executors.newSingleThreadExecutor(); 1.228 + FxAccountClient client = new FxAccountClient20(serverURI, executor); 1.229 + try { 1.230 + hideRemoteError(); 1.231 + new FxAccountCreateAccountTask(this, this, email, passwordStretcher, client, delegate).execute(); 1.232 + } catch (Exception e) { 1.233 + showRemoteError(e, R.string.fxaccount_create_account_unknown_error); 1.234 + } 1.235 + } 1.236 + 1.237 + @Override 1.238 + protected boolean shouldButtonBeEnabled() { 1.239 + return super.shouldButtonBeEnabled() && 1.240 + (yearEdit.length() > 0); 1.241 + } 1.242 + 1.243 + protected void createCreateAccountButton() { 1.244 + button.setOnClickListener(new OnClickListener() { 1.245 + @Override 1.246 + public void onClick(View v) { 1.247 + if (!updateButtonState()) { 1.248 + return; 1.249 + } 1.250 + final String email = emailEdit.getText().toString(); 1.251 + final String password = passwordEdit.getText().toString(); 1.252 + // Only include selected engines if the user currently has the option checked. 1.253 + final Map<String, Boolean> engines = chooseCheckBox.isChecked() 1.254 + ? selectedEngines 1.255 + : null; 1.256 + if (FxAccountAgeLockoutHelper.passesAgeCheck(yearEdit.getText().toString(), yearItems)) { 1.257 + FxAccountConstants.pii(LOG_TAG, "Passed age check."); 1.258 + createAccount(email, password, engines); 1.259 + } else { 1.260 + FxAccountConstants.pii(LOG_TAG, "Failed age check!"); 1.261 + FxAccountAgeLockoutHelper.lockOut(SystemClock.elapsedRealtime()); 1.262 + setResult(RESULT_CANCELED); 1.263 + redirectToActivity(FxAccountCreateAccountNotAllowedActivity.class); 1.264 + } 1.265 + } 1.266 + }); 1.267 + } 1.268 + 1.269 + /** 1.270 + * The "Choose what to sync" checkbox pops up a multi-choice dialog when it is 1.271 + * unchecked. It toggles to unchecked from checked. 1.272 + */ 1.273 + protected void createChooseCheckBox() { 1.274 + final int INDEX_BOOKMARKS = 0; 1.275 + final int INDEX_HISTORY = 1; 1.276 + final int INDEX_TABS = 2; 1.277 + final int INDEX_PASSWORDS = 3; 1.278 + final int NUMBER_OF_ENGINES = 4; 1.279 + 1.280 + final String items[] = new String[NUMBER_OF_ENGINES]; 1.281 + final boolean checkedItems[] = new boolean[NUMBER_OF_ENGINES]; 1.282 + items[INDEX_BOOKMARKS] = getResources().getString(R.string.fxaccount_status_bookmarks); 1.283 + items[INDEX_HISTORY] = getResources().getString(R.string.fxaccount_status_history); 1.284 + items[INDEX_TABS] = getResources().getString(R.string.fxaccount_status_tabs); 1.285 + items[INDEX_PASSWORDS] = getResources().getString(R.string.fxaccount_status_passwords); 1.286 + // Default to everything checked. 1.287 + for (int i = 0; i < NUMBER_OF_ENGINES; i++) { 1.288 + checkedItems[i] = true; 1.289 + } 1.290 + 1.291 + final DialogInterface.OnClickListener clickListener = new DialogInterface.OnClickListener() { 1.292 + @Override 1.293 + public void onClick(DialogInterface dialog, int which) { 1.294 + if (which != DialogInterface.BUTTON_POSITIVE) { 1.295 + Logger.debug(LOG_TAG, "onClick: not button positive, unchecking."); 1.296 + chooseCheckBox.setChecked(false); 1.297 + return; 1.298 + } 1.299 + // We only check the box on success. 1.300 + Logger.debug(LOG_TAG, "onClick: button positive, checking."); 1.301 + chooseCheckBox.setChecked(true); 1.302 + // And then remember for future use. 1.303 + ListView selectionsList = ((AlertDialog) dialog).getListView(); 1.304 + for (int i = 0; i < NUMBER_OF_ENGINES; i++) { 1.305 + checkedItems[i] = selectionsList.isItemChecked(i); 1.306 + } 1.307 + selectedEngines.put("bookmarks", checkedItems[INDEX_BOOKMARKS]); 1.308 + selectedEngines.put("history", checkedItems[INDEX_HISTORY]); 1.309 + selectedEngines.put("tabs", checkedItems[INDEX_TABS]); 1.310 + selectedEngines.put("passwords", checkedItems[INDEX_PASSWORDS]); 1.311 + FxAccountConstants.pii(LOG_TAG, "Updating selectedEngines: " + selectedEngines.toString()); 1.312 + } 1.313 + }; 1.314 + 1.315 + final DialogInterface.OnMultiChoiceClickListener multiChoiceClickListener = new DialogInterface.OnMultiChoiceClickListener() { 1.316 + @Override 1.317 + public void onClick(DialogInterface dialog, int which, boolean isChecked) { 1.318 + // Display multi-selection clicks in UI. 1.319 + ListView selectionsList = ((AlertDialog) dialog).getListView(); 1.320 + selectionsList.setItemChecked(which, isChecked); 1.321 + } 1.322 + }; 1.323 + 1.324 + final AlertDialog dialog = new AlertDialog.Builder(this) 1.325 + .setTitle(R.string.fxaccount_create_account_choose_what_to_sync) 1.326 + .setIcon(R.drawable.icon) 1.327 + .setMultiChoiceItems(items, checkedItems, multiChoiceClickListener) 1.328 + .setPositiveButton(android.R.string.ok, clickListener) 1.329 + .setNegativeButton(android.R.string.cancel, clickListener) 1.330 + .create(); 1.331 + 1.332 + dialog.setOnCancelListener(new DialogInterface.OnCancelListener() { 1.333 + @Override 1.334 + public void onCancel(DialogInterface dialog) { 1.335 + Logger.debug(LOG_TAG, "onCancel: unchecking."); 1.336 + chooseCheckBox.setChecked(false); 1.337 + } 1.338 + }); 1.339 + 1.340 + chooseCheckBox.setOnClickListener(new OnClickListener() { 1.341 + @Override 1.342 + public void onClick(View v) { 1.343 + // There appears to be no way to stop Android interpreting the click 1.344 + // first. So, if the user clicked on an unchecked box, it's checked by 1.345 + // the time we get here. 1.346 + if (!chooseCheckBox.isChecked()) { 1.347 + Logger.debug(LOG_TAG, "onClick: was checked, not showing dialog."); 1.348 + return; 1.349 + } 1.350 + Logger.debug(LOG_TAG, "onClick: was unchecked, showing dialog."); 1.351 + dialog.show(); 1.352 + } 1.353 + }); 1.354 + } 1.355 +}