Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 package org.mozilla.gecko.fxa.activities;
7 import java.util.Calendar;
8 import java.util.HashMap;
9 import java.util.LinkedList;
10 import java.util.Map;
11 import java.util.concurrent.Executor;
12 import java.util.concurrent.Executors;
14 import org.mozilla.gecko.R;
15 import org.mozilla.gecko.background.common.log.Logger;
16 import org.mozilla.gecko.background.fxa.FxAccountAgeLockoutHelper;
17 import org.mozilla.gecko.background.fxa.FxAccountClient;
18 import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
19 import org.mozilla.gecko.background.fxa.FxAccountClient20;
20 import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
21 import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
22 import org.mozilla.gecko.background.fxa.PasswordStretcher;
23 import org.mozilla.gecko.fxa.FxAccountConstants;
24 import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.FxAccountCreateAccountTask;
26 import android.app.AlertDialog;
27 import android.app.Dialog;
28 import android.content.DialogInterface;
29 import android.content.Intent;
30 import android.os.Bundle;
31 import android.os.SystemClock;
32 import android.text.Spannable;
33 import android.text.Spanned;
34 import android.text.method.LinkMovementMethod;
35 import android.text.style.ClickableSpan;
36 import android.view.View;
37 import android.view.View.OnClickListener;
38 import android.widget.AutoCompleteTextView;
39 import android.widget.Button;
40 import android.widget.CheckBox;
41 import android.widget.EditText;
42 import android.widget.ListView;
43 import android.widget.ProgressBar;
44 import android.widget.TextView;
46 /**
47 * Activity which displays create account screen to the user.
48 */
49 public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivity {
50 protected static final String LOG_TAG = FxAccountCreateAccountActivity.class.getSimpleName();
52 private static final int CHILD_REQUEST_CODE = 2;
54 protected String[] yearItems;
55 protected EditText yearEdit;
56 protected CheckBox chooseCheckBox;
58 protected Map<String, Boolean> selectedEngines;
60 /**
61 * {@inheritDoc}
62 */
63 @Override
64 public void onCreate(Bundle icicle) {
65 Logger.debug(LOG_TAG, "onCreate(" + icicle + ")");
67 super.onCreate(icicle);
68 setContentView(R.layout.fxaccount_create_account);
70 emailEdit = (AutoCompleteTextView) ensureFindViewById(null, R.id.email, "email edit");
71 passwordEdit = (EditText) ensureFindViewById(null, R.id.password, "password edit");
72 showPasswordButton = (Button) ensureFindViewById(null, R.id.show_password, "show password button");
73 yearEdit = (EditText) ensureFindViewById(null, R.id.year_edit, "year edit");
74 remoteErrorTextView = (TextView) ensureFindViewById(null, R.id.remote_error, "remote error text view");
75 button = (Button) ensureFindViewById(null, R.id.button, "create account button");
76 progressBar = (ProgressBar) ensureFindViewById(null, R.id.progress, "progress bar");
77 chooseCheckBox = (CheckBox) ensureFindViewById(null, R.id.choose_what_to_sync_checkbox, "choose what to sync check box");
78 selectedEngines = new HashMap<String, Boolean>();
80 createCreateAccountButton();
81 createYearEdit();
82 addListeners();
83 updateButtonState();
84 createShowPasswordButton();
85 linkifyPolicy();
86 createChooseCheckBox();
88 View signInInsteadLink = ensureFindViewById(null, R.id.sign_in_instead_link, "sign in instead link");
89 signInInsteadLink.setOnClickListener(new OnClickListener() {
90 @Override
91 public void onClick(View v) {
92 final String email = emailEdit.getText().toString();
93 final String password = passwordEdit.getText().toString();
94 doSigninInstead(email, password);
95 }
96 });
98 // Only set email/password in onCreate; we don't want to overwrite edited values onResume.
99 if (getIntent() != null && getIntent().getExtras() != null) {
100 Bundle bundle = getIntent().getExtras();
101 emailEdit.setText(bundle.getString("email"));
102 passwordEdit.setText(bundle.getString("password"));
103 }
104 }
106 protected void doSigninInstead(final String email, final String password) {
107 Intent intent = new Intent(this, FxAccountSignInActivity.class);
108 if (email != null) {
109 intent.putExtra("email", email);
110 }
111 if (password != null) {
112 intent.putExtra("password", password);
113 }
114 // Per http://stackoverflow.com/a/8992365, this triggers a known bug with
115 // the soft keyboard not being shown for the started activity. Why, Android, why?
116 intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
117 startActivityForResult(intent, CHILD_REQUEST_CODE);
118 }
120 @Override
121 protected void showClientRemoteException(final FxAccountClientRemoteException e) {
122 if (!e.isAccountAlreadyExists()) {
123 super.showClientRemoteException(e);
124 return;
125 }
127 // This horrible bit of special-casing is because we want this error message to
128 // contain a clickable, extra chunk of text, but we don't want to pollute
129 // the exception class with Android specifics.
130 final String clickablePart = getString(R.string.fxaccount_sign_in_button_label);
131 final String message = getString(e.getErrorMessageStringResource(), clickablePart);
132 final int clickableStart = message.lastIndexOf(clickablePart);
133 final int clickableEnd = clickableStart + clickablePart.length();
135 final Spannable span = Spannable.Factory.getInstance().newSpannable(message);
136 span.setSpan(new ClickableSpan() {
137 @Override
138 public void onClick(View widget) {
139 // Pass through the email address that already existed.
140 String email = e.body.getString("email");
141 if (email == null) {
142 email = emailEdit.getText().toString();
143 }
144 final String password = passwordEdit.getText().toString();
145 doSigninInstead(email, password);
146 }
147 }, clickableStart, clickableEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
148 remoteErrorTextView.setMovementMethod(LinkMovementMethod.getInstance());
149 remoteErrorTextView.setText(span);
150 }
152 /**
153 * We might have switched to the SignIn activity; if that activity
154 * succeeds, feed its result back to the authenticator.
155 */
156 @Override
157 public void onActivityResult(int requestCode, int resultCode, Intent data) {
158 Logger.debug(LOG_TAG, "onActivityResult: " + requestCode);
159 if (requestCode != CHILD_REQUEST_CODE || resultCode != RESULT_OK) {
160 super.onActivityResult(requestCode, resultCode, data);
161 return;
162 }
163 this.setResult(resultCode, data);
164 this.finish();
165 }
167 /**
168 * Return years to display in picker.
169 *
170 * @return 1990 or earlier, 1991, 1992, up to five years before current year.
171 * (So, if it is currently 2014, up to 2009.)
172 */
173 protected String[] getYearItems() {
174 int year = Calendar.getInstance().get(Calendar.YEAR);
175 LinkedList<String> years = new LinkedList<String>();
176 years.add(getResources().getString(R.string.fxaccount_create_account_1990_or_earlier));
177 for (int i = 1991; i <= year - 5; i++) {
178 years.add(Integer.toString(i));
179 }
180 return years.toArray(new String[0]);
181 }
183 protected void createYearEdit() {
184 yearItems = getYearItems();
186 yearEdit.setOnClickListener(new OnClickListener() {
187 @Override
188 public void onClick(View v) {
189 android.content.DialogInterface.OnClickListener listener = new Dialog.OnClickListener() {
190 @Override
191 public void onClick(DialogInterface dialog, int which) {
192 yearEdit.setText(yearItems[which]);
193 updateButtonState();
194 }
195 };
196 final AlertDialog dialog = new AlertDialog.Builder(FxAccountCreateAccountActivity.this)
197 .setTitle(R.string.fxaccount_create_account_year_of_birth)
198 .setItems(yearItems, listener)
199 .setIcon(R.drawable.icon)
200 .create();
202 dialog.show();
203 }
204 });
205 }
207 public void createAccount(String email, String password, Map<String, Boolean> engines) {
208 String serverURI = FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT;
209 PasswordStretcher passwordStretcher = makePasswordStretcher(password);
210 // This delegate creates a new Android account on success, opens the
211 // appropriate "success!" activity, and finishes this activity.
212 RequestDelegate<LoginResponse> delegate = new AddAccountDelegate(email, passwordStretcher, serverURI, engines) {
213 @Override
214 public void handleError(Exception e) {
215 showRemoteError(e, R.string.fxaccount_create_account_unknown_error);
216 }
218 @Override
219 public void handleFailure(FxAccountClientRemoteException e) {
220 showRemoteError(e, R.string.fxaccount_create_account_unknown_error);
221 }
222 };
224 Executor executor = Executors.newSingleThreadExecutor();
225 FxAccountClient client = new FxAccountClient20(serverURI, executor);
226 try {
227 hideRemoteError();
228 new FxAccountCreateAccountTask(this, this, email, passwordStretcher, client, delegate).execute();
229 } catch (Exception e) {
230 showRemoteError(e, R.string.fxaccount_create_account_unknown_error);
231 }
232 }
234 @Override
235 protected boolean shouldButtonBeEnabled() {
236 return super.shouldButtonBeEnabled() &&
237 (yearEdit.length() > 0);
238 }
240 protected void createCreateAccountButton() {
241 button.setOnClickListener(new OnClickListener() {
242 @Override
243 public void onClick(View v) {
244 if (!updateButtonState()) {
245 return;
246 }
247 final String email = emailEdit.getText().toString();
248 final String password = passwordEdit.getText().toString();
249 // Only include selected engines if the user currently has the option checked.
250 final Map<String, Boolean> engines = chooseCheckBox.isChecked()
251 ? selectedEngines
252 : null;
253 if (FxAccountAgeLockoutHelper.passesAgeCheck(yearEdit.getText().toString(), yearItems)) {
254 FxAccountConstants.pii(LOG_TAG, "Passed age check.");
255 createAccount(email, password, engines);
256 } else {
257 FxAccountConstants.pii(LOG_TAG, "Failed age check!");
258 FxAccountAgeLockoutHelper.lockOut(SystemClock.elapsedRealtime());
259 setResult(RESULT_CANCELED);
260 redirectToActivity(FxAccountCreateAccountNotAllowedActivity.class);
261 }
262 }
263 });
264 }
266 /**
267 * The "Choose what to sync" checkbox pops up a multi-choice dialog when it is
268 * unchecked. It toggles to unchecked from checked.
269 */
270 protected void createChooseCheckBox() {
271 final int INDEX_BOOKMARKS = 0;
272 final int INDEX_HISTORY = 1;
273 final int INDEX_TABS = 2;
274 final int INDEX_PASSWORDS = 3;
275 final int NUMBER_OF_ENGINES = 4;
277 final String items[] = new String[NUMBER_OF_ENGINES];
278 final boolean checkedItems[] = new boolean[NUMBER_OF_ENGINES];
279 items[INDEX_BOOKMARKS] = getResources().getString(R.string.fxaccount_status_bookmarks);
280 items[INDEX_HISTORY] = getResources().getString(R.string.fxaccount_status_history);
281 items[INDEX_TABS] = getResources().getString(R.string.fxaccount_status_tabs);
282 items[INDEX_PASSWORDS] = getResources().getString(R.string.fxaccount_status_passwords);
283 // Default to everything checked.
284 for (int i = 0; i < NUMBER_OF_ENGINES; i++) {
285 checkedItems[i] = true;
286 }
288 final DialogInterface.OnClickListener clickListener = new DialogInterface.OnClickListener() {
289 @Override
290 public void onClick(DialogInterface dialog, int which) {
291 if (which != DialogInterface.BUTTON_POSITIVE) {
292 Logger.debug(LOG_TAG, "onClick: not button positive, unchecking.");
293 chooseCheckBox.setChecked(false);
294 return;
295 }
296 // We only check the box on success.
297 Logger.debug(LOG_TAG, "onClick: button positive, checking.");
298 chooseCheckBox.setChecked(true);
299 // And then remember for future use.
300 ListView selectionsList = ((AlertDialog) dialog).getListView();
301 for (int i = 0; i < NUMBER_OF_ENGINES; i++) {
302 checkedItems[i] = selectionsList.isItemChecked(i);
303 }
304 selectedEngines.put("bookmarks", checkedItems[INDEX_BOOKMARKS]);
305 selectedEngines.put("history", checkedItems[INDEX_HISTORY]);
306 selectedEngines.put("tabs", checkedItems[INDEX_TABS]);
307 selectedEngines.put("passwords", checkedItems[INDEX_PASSWORDS]);
308 FxAccountConstants.pii(LOG_TAG, "Updating selectedEngines: " + selectedEngines.toString());
309 }
310 };
312 final DialogInterface.OnMultiChoiceClickListener multiChoiceClickListener = new DialogInterface.OnMultiChoiceClickListener() {
313 @Override
314 public void onClick(DialogInterface dialog, int which, boolean isChecked) {
315 // Display multi-selection clicks in UI.
316 ListView selectionsList = ((AlertDialog) dialog).getListView();
317 selectionsList.setItemChecked(which, isChecked);
318 }
319 };
321 final AlertDialog dialog = new AlertDialog.Builder(this)
322 .setTitle(R.string.fxaccount_create_account_choose_what_to_sync)
323 .setIcon(R.drawable.icon)
324 .setMultiChoiceItems(items, checkedItems, multiChoiceClickListener)
325 .setPositiveButton(android.R.string.ok, clickListener)
326 .setNegativeButton(android.R.string.cancel, clickListener)
327 .create();
329 dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
330 @Override
331 public void onCancel(DialogInterface dialog) {
332 Logger.debug(LOG_TAG, "onCancel: unchecking.");
333 chooseCheckBox.setChecked(false);
334 }
335 });
337 chooseCheckBox.setOnClickListener(new OnClickListener() {
338 @Override
339 public void onClick(View v) {
340 // There appears to be no way to stop Android interpreting the click
341 // first. So, if the user clicked on an unchecked box, it's checked by
342 // the time we get here.
343 if (!chooseCheckBox.isChecked()) {
344 Logger.debug(LOG_TAG, "onClick: was checked, not showing dialog.");
345 return;
346 }
347 Logger.debug(LOG_TAG, "onClick: was unchecked, showing dialog.");
348 dialog.show();
349 }
350 });
351 }
352 }