|
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.io.IOException; |
|
8 import java.util.Arrays; |
|
9 import java.util.HashSet; |
|
10 import java.util.Map; |
|
11 import java.util.Set; |
|
12 |
|
13 import org.mozilla.gecko.R; |
|
14 import org.mozilla.gecko.background.common.log.Logger; |
|
15 import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate; |
|
16 import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse; |
|
17 import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException; |
|
18 import org.mozilla.gecko.background.fxa.FxAccountUtils; |
|
19 import org.mozilla.gecko.background.fxa.PasswordStretcher; |
|
20 import org.mozilla.gecko.background.fxa.QuickPasswordStretcher; |
|
21 import org.mozilla.gecko.fxa.FxAccountConstants; |
|
22 import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.ProgressDisplay; |
|
23 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; |
|
24 import org.mozilla.gecko.fxa.login.Engaged; |
|
25 import org.mozilla.gecko.fxa.login.State; |
|
26 import org.mozilla.gecko.sync.SyncConfiguration; |
|
27 import org.mozilla.gecko.sync.setup.Constants; |
|
28 import org.mozilla.gecko.sync.setup.activities.ActivityUtils; |
|
29 |
|
30 import android.accounts.Account; |
|
31 import android.accounts.AccountManager; |
|
32 import android.content.Context; |
|
33 import android.content.Intent; |
|
34 import android.os.AsyncTask; |
|
35 import android.text.Editable; |
|
36 import android.text.TextWatcher; |
|
37 import android.text.method.PasswordTransformationMethod; |
|
38 import android.text.method.SingleLineTransformationMethod; |
|
39 import android.util.Patterns; |
|
40 import android.view.KeyEvent; |
|
41 import android.view.View; |
|
42 import android.view.View.OnClickListener; |
|
43 import android.view.View.OnFocusChangeListener; |
|
44 import android.widget.ArrayAdapter; |
|
45 import android.widget.AutoCompleteTextView; |
|
46 import android.widget.Button; |
|
47 import android.widget.EditText; |
|
48 import android.widget.ProgressBar; |
|
49 import android.widget.TextView; |
|
50 import android.widget.TextView.OnEditorActionListener; |
|
51 |
|
52 abstract public class FxAccountAbstractSetupActivity extends FxAccountAbstractActivity implements ProgressDisplay { |
|
53 public FxAccountAbstractSetupActivity() { |
|
54 super(CANNOT_RESUME_WHEN_ACCOUNTS_EXIST | CANNOT_RESUME_WHEN_LOCKED_OUT); |
|
55 } |
|
56 |
|
57 protected FxAccountAbstractSetupActivity(int resume) { |
|
58 super(resume); |
|
59 } |
|
60 |
|
61 private static final String LOG_TAG = FxAccountAbstractSetupActivity.class.getSimpleName(); |
|
62 |
|
63 protected int minimumPasswordLength = 8; |
|
64 |
|
65 protected AutoCompleteTextView emailEdit; |
|
66 protected EditText passwordEdit; |
|
67 protected Button showPasswordButton; |
|
68 protected TextView remoteErrorTextView; |
|
69 protected Button button; |
|
70 protected ProgressBar progressBar; |
|
71 |
|
72 protected void createShowPasswordButton() { |
|
73 showPasswordButton.setOnClickListener(new OnClickListener() { |
|
74 @SuppressWarnings("deprecation") |
|
75 @Override |
|
76 public void onClick(View v) { |
|
77 boolean isShown = passwordEdit.getTransformationMethod() instanceof SingleLineTransformationMethod; |
|
78 |
|
79 // Changing input type loses position in edit text; let's try to maintain it. |
|
80 int start = passwordEdit.getSelectionStart(); |
|
81 int stop = passwordEdit.getSelectionEnd(); |
|
82 |
|
83 if (isShown) { |
|
84 passwordEdit.setTransformationMethod(PasswordTransformationMethod.getInstance()); |
|
85 showPasswordButton.setText(R.string.fxaccount_password_show); |
|
86 showPasswordButton.setBackgroundDrawable(getResources().getDrawable(R.drawable.fxaccount_password_button_show_background)); |
|
87 showPasswordButton.setTextColor(getResources().getColor(R.color.fxaccount_password_show_textcolor)); |
|
88 } else { |
|
89 passwordEdit.setTransformationMethod(SingleLineTransformationMethod.getInstance()); |
|
90 showPasswordButton.setText(R.string.fxaccount_password_hide); |
|
91 showPasswordButton.setBackgroundDrawable(getResources().getDrawable(R.drawable.fxaccount_password_button_hide_background)); |
|
92 showPasswordButton.setTextColor(getResources().getColor(R.color.fxaccount_password_hide_textcolor)); |
|
93 } |
|
94 passwordEdit.setSelection(start, stop); |
|
95 } |
|
96 }); |
|
97 } |
|
98 |
|
99 protected void linkifyPolicy() { |
|
100 TextView policyView = (TextView) ensureFindViewById(null, R.id.policy, "policy links"); |
|
101 final String linkTerms = getString(R.string.fxaccount_link_tos); |
|
102 final String linkPrivacy = getString(R.string.fxaccount_link_pn); |
|
103 final String linkedTOS = "<a href=\"" + linkTerms + "\">" + getString(R.string.fxaccount_policy_linktos) + "</a>"; |
|
104 final String linkedPN = "<a href=\"" + linkPrivacy + "\">" + getString(R.string.fxaccount_policy_linkprivacy) + "</a>"; |
|
105 policyView.setText(getString(R.string.fxaccount_create_account_policy_text, linkedTOS, linkedPN)); |
|
106 final boolean underlineLinks = true; |
|
107 ActivityUtils.linkifyTextView(policyView, underlineLinks); |
|
108 } |
|
109 |
|
110 protected void hideRemoteError() { |
|
111 remoteErrorTextView.setVisibility(View.INVISIBLE); |
|
112 } |
|
113 |
|
114 protected void showRemoteError(Exception e, int defaultResourceId) { |
|
115 if (e instanceof IOException) { |
|
116 remoteErrorTextView.setText(R.string.fxaccount_remote_error_COULD_NOT_CONNECT); |
|
117 } else if (e instanceof FxAccountClientRemoteException) { |
|
118 showClientRemoteException((FxAccountClientRemoteException) e); |
|
119 } else { |
|
120 remoteErrorTextView.setText(defaultResourceId); |
|
121 } |
|
122 Logger.warn(LOG_TAG, "Got exception; showing error message: " + remoteErrorTextView.getText().toString(), e); |
|
123 remoteErrorTextView.setVisibility(View.VISIBLE); |
|
124 } |
|
125 |
|
126 protected void showClientRemoteException(final FxAccountClientRemoteException e) { |
|
127 remoteErrorTextView.setText(e.getErrorMessageStringResource()); |
|
128 } |
|
129 |
|
130 protected void addListeners() { |
|
131 TextChangedListener textChangedListener = new TextChangedListener(); |
|
132 EditorActionListener editorActionListener = new EditorActionListener(); |
|
133 FocusChangeListener focusChangeListener = new FocusChangeListener(); |
|
134 |
|
135 emailEdit.addTextChangedListener(textChangedListener); |
|
136 emailEdit.setOnEditorActionListener(editorActionListener); |
|
137 emailEdit.setOnFocusChangeListener(focusChangeListener); |
|
138 passwordEdit.addTextChangedListener(textChangedListener); |
|
139 passwordEdit.setOnEditorActionListener(editorActionListener); |
|
140 passwordEdit.setOnFocusChangeListener(focusChangeListener); |
|
141 } |
|
142 |
|
143 protected class FocusChangeListener implements OnFocusChangeListener { |
|
144 @Override |
|
145 public void onFocusChange(View v, boolean hasFocus) { |
|
146 if (hasFocus) { |
|
147 return; |
|
148 } |
|
149 updateButtonState(); |
|
150 } |
|
151 } |
|
152 |
|
153 protected class EditorActionListener implements OnEditorActionListener { |
|
154 @Override |
|
155 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { |
|
156 updateButtonState(); |
|
157 return false; |
|
158 } |
|
159 } |
|
160 |
|
161 protected class TextChangedListener implements TextWatcher { |
|
162 @Override |
|
163 public void afterTextChanged(Editable s) { |
|
164 updateButtonState(); |
|
165 } |
|
166 |
|
167 @Override |
|
168 public void beforeTextChanged(CharSequence s, int start, int count, int after) { |
|
169 // Do nothing. |
|
170 } |
|
171 |
|
172 @Override |
|
173 public void onTextChanged(CharSequence s, int start, int before, int count) { |
|
174 // Do nothing. |
|
175 } |
|
176 } |
|
177 |
|
178 protected boolean shouldButtonBeEnabled() { |
|
179 final String email = emailEdit.getText().toString(); |
|
180 final String password = passwordEdit.getText().toString(); |
|
181 |
|
182 boolean enabled = |
|
183 (email.length() > 0) && |
|
184 Patterns.EMAIL_ADDRESS.matcher(email).matches() && |
|
185 (password.length() >= minimumPasswordLength); |
|
186 return enabled; |
|
187 } |
|
188 |
|
189 protected boolean updateButtonState() { |
|
190 boolean enabled = shouldButtonBeEnabled(); |
|
191 if (!enabled) { |
|
192 // The user needs to do something before you can interact with the button; |
|
193 // presumably that interaction will fix whatever error is shown. |
|
194 hideRemoteError(); |
|
195 } |
|
196 if (enabled != button.isEnabled()) { |
|
197 Logger.debug(LOG_TAG, (enabled ? "En" : "Dis") + "abling button."); |
|
198 button.setEnabled(enabled); |
|
199 } |
|
200 return enabled; |
|
201 } |
|
202 |
|
203 @Override |
|
204 public void showProgress() { |
|
205 progressBar.setVisibility(View.VISIBLE); |
|
206 button.setVisibility(View.INVISIBLE); |
|
207 } |
|
208 |
|
209 @Override |
|
210 public void dismissProgress() { |
|
211 progressBar.setVisibility(View.INVISIBLE); |
|
212 button.setVisibility(View.VISIBLE); |
|
213 } |
|
214 |
|
215 public Intent makeSuccessIntent(String email, LoginResponse result) { |
|
216 Intent successIntent; |
|
217 if (result.verified) { |
|
218 successIntent = new Intent(this, FxAccountVerifiedAccountActivity.class); |
|
219 } else { |
|
220 successIntent = new Intent(this, FxAccountConfirmAccountActivity.class); |
|
221 } |
|
222 // Per http://stackoverflow.com/a/8992365, this triggers a known bug with |
|
223 // the soft keyboard not being shown for the started activity. Why, Android, why? |
|
224 successIntent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); |
|
225 return successIntent; |
|
226 } |
|
227 |
|
228 protected abstract class AddAccountDelegate implements RequestDelegate<LoginResponse> { |
|
229 public final String email; |
|
230 public final PasswordStretcher passwordStretcher; |
|
231 public final String serverURI; |
|
232 public final Map<String, Boolean> selectedEngines; |
|
233 |
|
234 public AddAccountDelegate(String email, PasswordStretcher passwordStretcher, String serverURI) { |
|
235 this(email, passwordStretcher, serverURI, null); |
|
236 } |
|
237 |
|
238 public AddAccountDelegate(String email, PasswordStretcher passwordStretcher, String serverURI, Map<String, Boolean> selectedEngines) { |
|
239 if (email == null) { |
|
240 throw new IllegalArgumentException("email must not be null"); |
|
241 } |
|
242 if (passwordStretcher == null) { |
|
243 throw new IllegalArgumentException("passwordStretcher must not be null"); |
|
244 } |
|
245 if (serverURI == null) { |
|
246 throw new IllegalArgumentException("serverURI must not be null"); |
|
247 } |
|
248 this.email = email; |
|
249 this.passwordStretcher = passwordStretcher; |
|
250 this.serverURI = serverURI; |
|
251 // selectedEngines can be null, which means don't write |
|
252 // userSelectedEngines to prefs. This makes any created meta/global record |
|
253 // have the default set of engines to sync. |
|
254 this.selectedEngines = selectedEngines; |
|
255 } |
|
256 |
|
257 @Override |
|
258 public void handleSuccess(LoginResponse result) { |
|
259 Logger.info(LOG_TAG, "Got success response; adding Android account."); |
|
260 |
|
261 // We're on the UI thread, but it's okay to create the account here. |
|
262 AndroidFxAccount fxAccount; |
|
263 try { |
|
264 final String profile = Constants.DEFAULT_PROFILE; |
|
265 final String tokenServerURI = FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT; |
|
266 // It is crucial that we use the email address provided by the server |
|
267 // (rather than whatever the user entered), because the user's keys are |
|
268 // wrapped and salted with the initial email they provided to |
|
269 // /create/account. Of course, we want to pass through what the user |
|
270 // entered locally as much as possible, so we create the Android account |
|
271 // with their entered email address, etc. |
|
272 // The passwordStretcher should have seen this email address before, so |
|
273 // we shouldn't be calculating the expensive stretch twice. |
|
274 byte[] quickStretchedPW = passwordStretcher.getQuickStretchedPW(result.remoteEmail.getBytes("UTF-8")); |
|
275 byte[] unwrapkB = FxAccountUtils.generateUnwrapBKey(quickStretchedPW); |
|
276 State state = new Engaged(email, result.uid, result.verified, unwrapkB, result.sessionToken, result.keyFetchToken); |
|
277 fxAccount = AndroidFxAccount.addAndroidAccount(getApplicationContext(), |
|
278 email, |
|
279 profile, |
|
280 serverURI, |
|
281 tokenServerURI, |
|
282 state); |
|
283 if (fxAccount == null) { |
|
284 throw new RuntimeException("Could not add Android account."); |
|
285 } |
|
286 |
|
287 if (selectedEngines != null) { |
|
288 Logger.info(LOG_TAG, "User has selected engines; storing to prefs."); |
|
289 SyncConfiguration.storeSelectedEnginesToPrefs(fxAccount.getSyncPrefs(), selectedEngines); |
|
290 } |
|
291 } catch (Exception e) { |
|
292 handleError(e); |
|
293 return; |
|
294 } |
|
295 |
|
296 // For great debugging. |
|
297 if (FxAccountConstants.LOG_PERSONAL_INFORMATION) { |
|
298 fxAccount.dump(); |
|
299 } |
|
300 |
|
301 // The GetStarted activity has called us and needs to return a result to the authenticator. |
|
302 final Intent intent = new Intent(); |
|
303 intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, email); |
|
304 intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, FxAccountConstants.ACCOUNT_TYPE); |
|
305 // intent.putExtra(AccountManager.KEY_AUTHTOKEN, accountType); |
|
306 setResult(RESULT_OK, intent); |
|
307 |
|
308 // Show success activity depending on verification status. |
|
309 Intent successIntent = makeSuccessIntent(email, result); |
|
310 startActivity(successIntent); |
|
311 finish(); |
|
312 } |
|
313 } |
|
314 |
|
315 /** |
|
316 * Factory function that produces a new PasswordStretcher instance. |
|
317 * |
|
318 * @return PasswordStretcher instance. |
|
319 */ |
|
320 protected PasswordStretcher makePasswordStretcher(String password) { |
|
321 return new QuickPasswordStretcher(password); |
|
322 } |
|
323 |
|
324 protected abstract static class GetAccountsAsyncTask extends AsyncTask<Void, Void, Account[]> { |
|
325 protected final Context context; |
|
326 |
|
327 public GetAccountsAsyncTask(Context context) { |
|
328 super(); |
|
329 this.context = context; |
|
330 } |
|
331 |
|
332 @Override |
|
333 protected Account[] doInBackground(Void... params) { |
|
334 return AccountManager.get(context).getAccounts(); |
|
335 } |
|
336 } |
|
337 |
|
338 /** |
|
339 * This updates UI, so needs to be done on the foreground thread. |
|
340 */ |
|
341 protected void populateEmailAddressAutocomplete(Account[] accounts) { |
|
342 // First a set, since we don't want repeats. |
|
343 final Set<String> emails = new HashSet<String>(); |
|
344 for (Account account : accounts) { |
|
345 if (!Patterns.EMAIL_ADDRESS.matcher(account.name).matches()) { |
|
346 continue; |
|
347 } |
|
348 emails.add(account.name); |
|
349 } |
|
350 |
|
351 // And then sorted in alphabetical order. |
|
352 final String[] sortedEmails = emails.toArray(new String[0]); |
|
353 Arrays.sort(sortedEmails); |
|
354 |
|
355 final ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_dropdown_item_1line, sortedEmails); |
|
356 emailEdit.setAdapter(adapter); |
|
357 } |
|
358 |
|
359 @Override |
|
360 public void onResume() { |
|
361 super.onResume(); |
|
362 |
|
363 // Getting Accounts accesses databases on disk, so needs to be done on a |
|
364 // background thread. |
|
365 final GetAccountsAsyncTask task = new GetAccountsAsyncTask(this) { |
|
366 @Override |
|
367 public void onPostExecute(Account[] accounts) { |
|
368 populateEmailAddressAutocomplete(accounts); |
|
369 } |
|
370 }; |
|
371 task.execute(); |
|
372 } |
|
373 } |