|
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/. */ |
|
4 |
|
5 package org.mozilla.gecko.fxa.activities; |
|
6 |
|
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; |
|
13 |
|
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; |
|
25 |
|
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; |
|
45 |
|
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(); |
|
51 |
|
52 private static final int CHILD_REQUEST_CODE = 2; |
|
53 |
|
54 protected String[] yearItems; |
|
55 protected EditText yearEdit; |
|
56 protected CheckBox chooseCheckBox; |
|
57 |
|
58 protected Map<String, Boolean> selectedEngines; |
|
59 |
|
60 /** |
|
61 * {@inheritDoc} |
|
62 */ |
|
63 @Override |
|
64 public void onCreate(Bundle icicle) { |
|
65 Logger.debug(LOG_TAG, "onCreate(" + icicle + ")"); |
|
66 |
|
67 super.onCreate(icicle); |
|
68 setContentView(R.layout.fxaccount_create_account); |
|
69 |
|
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>(); |
|
79 |
|
80 createCreateAccountButton(); |
|
81 createYearEdit(); |
|
82 addListeners(); |
|
83 updateButtonState(); |
|
84 createShowPasswordButton(); |
|
85 linkifyPolicy(); |
|
86 createChooseCheckBox(); |
|
87 |
|
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 }); |
|
97 |
|
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 } |
|
105 |
|
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 } |
|
119 |
|
120 @Override |
|
121 protected void showClientRemoteException(final FxAccountClientRemoteException e) { |
|
122 if (!e.isAccountAlreadyExists()) { |
|
123 super.showClientRemoteException(e); |
|
124 return; |
|
125 } |
|
126 |
|
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(); |
|
134 |
|
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 } |
|
151 |
|
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 } |
|
166 |
|
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 } |
|
182 |
|
183 protected void createYearEdit() { |
|
184 yearItems = getYearItems(); |
|
185 |
|
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(); |
|
201 |
|
202 dialog.show(); |
|
203 } |
|
204 }); |
|
205 } |
|
206 |
|
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 } |
|
217 |
|
218 @Override |
|
219 public void handleFailure(FxAccountClientRemoteException e) { |
|
220 showRemoteError(e, R.string.fxaccount_create_account_unknown_error); |
|
221 } |
|
222 }; |
|
223 |
|
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 } |
|
233 |
|
234 @Override |
|
235 protected boolean shouldButtonBeEnabled() { |
|
236 return super.shouldButtonBeEnabled() && |
|
237 (yearEdit.length() > 0); |
|
238 } |
|
239 |
|
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 } |
|
265 |
|
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; |
|
276 |
|
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 } |
|
287 |
|
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 }; |
|
311 |
|
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 }; |
|
320 |
|
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(); |
|
328 |
|
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 }); |
|
336 |
|
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 } |