mobile/android/base/sync/setup/activities/SetupSyncActivity.java

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:afdd85a2cebe
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.sync.setup.activities;
6
7 import java.io.UnsupportedEncodingException;
8 import java.util.HashMap;
9
10 import org.json.simple.JSONObject;
11 import org.mozilla.gecko.R;
12 import org.mozilla.gecko.background.common.log.Logger;
13 import org.mozilla.gecko.sync.SyncConstants;
14 import org.mozilla.gecko.sync.ThreadPool;
15 import org.mozilla.gecko.sync.Utils;
16 import org.mozilla.gecko.sync.jpake.JPakeClient;
17 import org.mozilla.gecko.sync.jpake.JPakeNoActivePairingException;
18 import org.mozilla.gecko.sync.setup.Constants;
19 import org.mozilla.gecko.sync.setup.SyncAccounts;
20 import org.mozilla.gecko.sync.setup.SyncAccounts.SyncAccountParameters;
21
22 import android.accounts.Account;
23 import android.accounts.AccountAuthenticatorActivity;
24 import android.accounts.AccountManager;
25 import android.app.Activity;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.net.ConnectivityManager;
29 import android.net.NetworkInfo;
30 import android.net.Uri;
31 import android.os.Bundle;
32 import android.text.Editable;
33 import android.text.TextWatcher;
34 import android.view.View;
35 import android.view.Window;
36 import android.view.WindowManager;
37 import android.widget.Button;
38 import android.widget.EditText;
39 import android.widget.LinearLayout;
40 import android.widget.TextView;
41 import android.widget.Toast;
42
43 public class SetupSyncActivity extends AccountAuthenticatorActivity {
44 private final static String LOG_TAG = "SetupSync";
45
46 private boolean pairWithPin = false;
47
48 // UI elements for pairing through PIN entry.
49 private EditText row1;
50 private EditText row2;
51 private EditText row3;
52 private Button connectButton;
53 private LinearLayout pinError;
54
55 // UI elements for pairing through PIN generation.
56 private TextView pinTextView1;
57 private TextView pinTextView2;
58 private TextView pinTextView3;
59 private JPakeClient jClient;
60
61 // Android context.
62 private AccountManager mAccountManager;
63 private Context mContext;
64
65 public SetupSyncActivity() {
66 super();
67 }
68
69 /** Called when the activity is first created. */
70 @Override
71 public void onCreate(Bundle savedInstanceState) {
72 ActivityUtils.prepareLogging();
73 Logger.info(LOG_TAG, "Called SetupSyncActivity.onCreate.");
74 super.onCreate(savedInstanceState);
75
76 // Set Activity variables.
77 mContext = getApplicationContext();
78 Logger.debug(LOG_TAG, "AccountManager.get(" + mContext + ")");
79 mAccountManager = AccountManager.get(mContext);
80
81 // Set "screen on" flag for this activity. Screen will not automatically dim as long as this
82 // activity is at the top of the stack.
83 // Attempting to set this flag more than once causes hanging, so we set it here, not in onResume().
84 Window w = getWindow();
85 w.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
86 Logger.debug(LOG_TAG, "Successfully set screen-on flag.");
87 }
88
89 @Override
90 public void onResume() {
91 ActivityUtils.prepareLogging();
92 Logger.info(LOG_TAG, "Called SetupSyncActivity.onResume.");
93 super.onResume();
94
95 if (!hasInternet()) {
96 runOnUiThread(new Runnable() {
97 @Override
98 public void run() {
99 setContentView(R.layout.sync_setup_nointernet);
100 }
101 });
102 return;
103 }
104
105 // Check whether Sync accounts exist; if not, display J-PAKE PIN.
106 // Run this on a separate thread to comply with Strict Mode thread policies.
107 ThreadPool.run(new Runnable() {
108 @Override
109 public void run() {
110 ActivityUtils.prepareLogging();
111 Account[] accts = mAccountManager.getAccountsByType(SyncConstants.ACCOUNTTYPE_SYNC);
112 finishResume(accts);
113 }
114 });
115 }
116
117 public void finishResume(Account[] accts) {
118 Logger.debug(LOG_TAG, "Finishing Resume after fetching accounts.");
119
120 if (accts.length == 0) { // Start J-PAKE for pairing if no accounts present.
121 Logger.debug(LOG_TAG, "No accounts; starting J-PAKE receiver.");
122 displayReceiveNoPin();
123 if (jClient != null) {
124 // Mark previous J-PAKE as finished. Don't bother propagating back up to this Activity.
125 jClient.finished = true;
126 }
127 jClient = new JPakeClient(this);
128 jClient.receiveNoPin();
129 return;
130 }
131
132 // Set layout based on starting Intent.
133 Bundle extras = this.getIntent().getExtras();
134 if (extras != null) {
135 Logger.debug(LOG_TAG, "SetupSync with extras.");
136 boolean isSetup = extras.getBoolean(Constants.INTENT_EXTRA_IS_SETUP);
137 if (!isSetup) {
138 Logger.debug(LOG_TAG, "Account exists; Pair a Device started.");
139 pairWithPin = true;
140 displayPairWithPin();
141 return;
142 }
143 }
144
145 runOnUiThread(new Runnable() {
146 @Override
147 public void run() {
148 Logger.debug(LOG_TAG, "Only one account supported. Redirecting.");
149 // Display toast for "Only one account supported."
150 // Redirect to account management.
151 Toast toast = Toast.makeText(mContext,
152 R.string.sync_notification_oneaccount, Toast.LENGTH_LONG);
153 toast.show();
154
155 // Setting up Sync when an existing account exists only happens from Settings,
156 // so we can safely finish() the activity to return to Settings.
157 finish();
158 }
159 });
160 }
161
162
163 @Override
164 public void onPause() {
165 super.onPause();
166
167 if (jClient != null) {
168 jClient.abort(Constants.JPAKE_ERROR_USERABORT);
169 }
170 if (pairWithPin) {
171 finish();
172 }
173 }
174
175 @Override
176 public void onNewIntent(Intent intent) {
177 Logger.debug(LOG_TAG, "Started SetupSyncActivity with new intent.");
178 setIntent(intent);
179 }
180
181 @Override
182 public void onDestroy() {
183 Logger.debug(LOG_TAG, "onDestroy() called.");
184 super.onDestroy();
185 }
186
187 /* Click Handlers */
188 public void manualClickHandler(View target) {
189 Intent accountIntent = new Intent(this, AccountActivity.class);
190 accountIntent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
191 startActivityForResult(accountIntent, 0);
192 overridePendingTransition(0, 0);
193 }
194
195 public void cancelClickHandler(View target) {
196 finish();
197 }
198
199 public void connectClickHandler(View target) {
200 Logger.debug(LOG_TAG, "Connect clicked.");
201 // Set UI feedback.
202 pinError.setVisibility(View.INVISIBLE);
203 enablePinEntry(false);
204 connectButton.requestFocus();
205 activateButton(connectButton, false);
206
207 // Extract PIN.
208 String pin = row1.getText().toString();
209 pin += row2.getText().toString() + row3.getText().toString();
210
211 // Start J-PAKE.
212 if (jClient != null) {
213 // Cancel previous J-PAKE exchange.
214 jClient.finished = true;
215 }
216 jClient = new JPakeClient(this);
217 jClient.pairWithPin(pin);
218 }
219
220 /**
221 * Handler when "Show me how" link is clicked.
222 * @param target
223 * View that received the click.
224 */
225 public void showClickHandler(View target) {
226 Uri uri = null;
227 // TODO: fetch these from fennec
228 if (pairWithPin) {
229 uri = Uri.parse(Constants.LINK_FIND_CODE);
230 } else {
231 uri = Uri.parse(Constants.LINK_FIND_ADD_DEVICE);
232 }
233 Intent intent = new Intent(this, WebViewActivity.class);
234 intent.setData(uri);
235 startActivity(intent);
236 }
237
238 /* Controller methods */
239
240 /**
241 * Display generated PIN to user.
242 * @param pin
243 * 12-character string generated for J-PAKE.
244 */
245 public void displayPin(String pin) {
246 if (pin == null) {
247 Logger.warn(LOG_TAG, "Asked to display null pin.");
248 return;
249 }
250 // Format PIN for display.
251 int charPerLine = pin.length() / 3;
252 final String pin1 = pin.substring(0, charPerLine);
253 final String pin2 = pin.substring(charPerLine, 2 * charPerLine);
254 final String pin3 = pin.substring(2 * charPerLine, pin.length());
255
256 runOnUiThread(new Runnable() {
257 @Override
258 public void run() {
259 TextView view1 = pinTextView1;
260 TextView view2 = pinTextView2;
261 TextView view3 = pinTextView3;
262 if (view1 == null || view2 == null || view3 == null) {
263 Logger.warn(LOG_TAG, "Couldn't find view to display PIN.");
264 return;
265 }
266 view1.setText(pin1);
267 view1.setContentDescription(pin1.replaceAll("\\B", ", "));
268
269 view2.setText(pin2);
270 view2.setContentDescription(pin2.replaceAll("\\B", ", "));
271
272 view3.setText(pin3);
273 view3.setContentDescription(pin3.replaceAll("\\B", ", "));
274 }
275 });
276 }
277
278 /**
279 * Abort current J-PAKE pairing. Clear forms/restart pairing.
280 * @param error
281 */
282 public void displayAbort(String error) {
283 if (!Constants.JPAKE_ERROR_USERABORT.equals(error) && !hasInternet()) {
284 runOnUiThread(new Runnable() {
285 @Override
286 public void run() {
287 setContentView(R.layout.sync_setup_nointernet);
288 }
289 });
290 return;
291 }
292 if (pairWithPin) {
293 // Clear PIN entries and display error.
294 runOnUiThread(new Runnable() {
295 @Override
296 public void run() {
297 enablePinEntry(true);
298 row1.setText("");
299 row2.setText("");
300 row3.setText("");
301 row1.requestFocus();
302
303 // Display error.
304 pinError.setVisibility(View.VISIBLE);
305 }
306 });
307 return;
308 }
309
310 // Start new JPakeClient for restarting J-PAKE.
311 Logger.debug(LOG_TAG, "abort reason: " + error);
312 if (!Constants.JPAKE_ERROR_USERABORT.equals(error)) {
313 jClient = new JPakeClient(this);
314 runOnUiThread(new Runnable() {
315 @Override
316 public void run() {
317 displayReceiveNoPin();
318 jClient.receiveNoPin();
319 }
320 });
321 }
322 }
323
324 @SuppressWarnings({ "unchecked", "static-method" })
325 protected JSONObject makeAccountJSON(String username, String password,
326 String syncKey, String serverURL) {
327
328 JSONObject jAccount = new JSONObject();
329
330 // Hack to try to keep Java 1.7 from complaining about unchecked types,
331 // despite the presence of SuppressWarnings.
332 HashMap<String, String> fields = (HashMap<String, String>) jAccount;
333
334 fields.put(Constants.JSON_KEY_SYNCKEY, syncKey);
335 fields.put(Constants.JSON_KEY_ACCOUNT, username);
336 fields.put(Constants.JSON_KEY_PASSWORD, password);
337 fields.put(Constants.JSON_KEY_SERVER, serverURL);
338
339 if (Logger.LOG_PERSONAL_INFORMATION) {
340 Logger.pii(LOG_TAG, "Extracted account data: " + jAccount.toJSONString());
341 }
342 return jAccount;
343 }
344
345 /**
346 * Device has finished key exchange, waiting for remote device to set up or
347 * link to a Sync account. Display "waiting for other device" dialog.
348 */
349 public void onPaired() {
350 // Extract Sync account data.
351 Account[] accts = mAccountManager.getAccountsByType(SyncConstants.ACCOUNTTYPE_SYNC);
352 if (accts.length == 0) {
353 // Error, no account present.
354 Logger.error(LOG_TAG, "No accounts present.");
355 displayAbort(Constants.JPAKE_ERROR_INVALID);
356 return;
357 }
358
359 // TODO: Single account supported. Create account selection if spec changes.
360 Account account = accts[0];
361 String username = account.name;
362 String password = mAccountManager.getPassword(account);
363 String syncKey = mAccountManager.getUserData(account, Constants.OPTION_SYNCKEY);
364 String serverURL = mAccountManager.getUserData(account, Constants.OPTION_SERVER);
365
366 JSONObject jAccount = makeAccountJSON(username, password, syncKey, serverURL);
367 try {
368 jClient.sendAndComplete(jAccount);
369 } catch (JPakeNoActivePairingException e) {
370 Logger.error(LOG_TAG, "No active J-PAKE pairing.", e);
371 displayAbort(Constants.JPAKE_ERROR_INVALID);
372 }
373 }
374
375 /**
376 * J-PAKE pairing has started, but when this device has generated the PIN for
377 * pairing, does not require UI feedback to user.
378 */
379 public void onPairingStart() {
380 if (!pairWithPin) {
381 runOnUiThread(new Runnable() {
382 @Override
383 public void run() {
384 setContentView(R.layout.sync_setup_jpake_waiting);
385 }
386 });
387 return;
388 }
389 }
390
391 /**
392 * On J-PAKE completion, store the Sync Account credentials sent by other
393 * device. Display progress to user.
394 *
395 * @param jCreds
396 */
397 public void onComplete(JSONObject jCreds) {
398 if (!pairWithPin) {
399 // Create account from received credentials.
400 String accountName = (String) jCreds.get(Constants.JSON_KEY_ACCOUNT);
401 String password = (String) jCreds.get(Constants.JSON_KEY_PASSWORD);
402 String syncKey = (String) jCreds.get(Constants.JSON_KEY_SYNCKEY);
403 String serverURL = (String) jCreds.get(Constants.JSON_KEY_SERVER);
404
405 // The password we get is double-encoded.
406 try {
407 password = Utils.decodeUTF8(password);
408 } catch (UnsupportedEncodingException e) {
409 Logger.warn(LOG_TAG, "Unsupported encoding when decoding UTF-8 ASCII J-PAKE message. Ignoring.");
410 }
411
412 final SyncAccountParameters syncAccount = new SyncAccountParameters(mContext, mAccountManager, accountName,
413 syncKey, password, serverURL);
414 createAccountOnThread(syncAccount);
415 } else {
416 // No need to create an account; just clean up.
417 displayResultAndFinish(true);
418 }
419 }
420
421 private void displayResultAndFinish(final boolean isSuccess) {
422 jClient = null;
423 runOnUiThread(new Runnable() {
424 @Override
425 public void run() {
426 int result = isSuccess ? RESULT_OK : RESULT_CANCELED;
427 setResult(result);
428 displayResult(isSuccess);
429 }
430 });
431 }
432
433 private void createAccountOnThread(final SyncAccountParameters syncAccount) {
434 ThreadPool.run(new Runnable() {
435 @Override
436 public void run() {
437 Account account = SyncAccounts.createSyncAccount(syncAccount);
438 boolean isSuccess = (account != null);
439 if (isSuccess) {
440 Bundle resultBundle = new Bundle();
441 resultBundle.putString(AccountManager.KEY_ACCOUNT_NAME, syncAccount.username);
442 resultBundle.putString(AccountManager.KEY_ACCOUNT_TYPE, SyncConstants.ACCOUNTTYPE_SYNC);
443 resultBundle.putString(AccountManager.KEY_AUTHTOKEN, SyncConstants.ACCOUNTTYPE_SYNC);
444 setAccountAuthenticatorResult(resultBundle);
445 }
446 displayResultAndFinish(isSuccess);
447 }
448 });
449 }
450
451 /*
452 * Helper functions
453 */
454 private void activateButton(Button button, boolean toActivate) {
455 button.setEnabled(toActivate);
456 button.setClickable(toActivate);
457 }
458
459 private void enablePinEntry(boolean toEnable) {
460 row1.setEnabled(toEnable);
461 row2.setEnabled(toEnable);
462 row3.setEnabled(toEnable);
463 }
464
465 /**
466 * Displays Sync account setup result to user.
467 *
468 * @param isSetup
469 * true if account was set up successfully, false otherwise.
470 */
471 private void displayResult(boolean isSuccess) {
472 Intent intent = null;
473 if (isSuccess) {
474 intent = new Intent(mContext, SetupSuccessActivity.class);
475 intent.setFlags(Constants.FLAG_ACTIVITY_REORDER_TO_FRONT_NO_ANIMATION);
476 intent.putExtra(Constants.INTENT_EXTRA_IS_SETUP, !pairWithPin);
477 startActivity(intent);
478 finish();
479 } else {
480 intent = new Intent(mContext, SetupFailureActivity.class);
481 intent.putExtra(Constants.INTENT_EXTRA_IS_ACCOUNTERROR, true);
482 intent.setFlags(Constants.FLAG_ACTIVITY_REORDER_TO_FRONT_NO_ANIMATION);
483 intent.putExtra(Constants.INTENT_EXTRA_IS_SETUP, !pairWithPin);
484 startActivity(intent);
485 // Do not finish, so user can retry setup by hitting "back."
486 }
487 }
488
489 /**
490 * Validate PIN entry fields to check if the three PIN entry fields are all
491 * filled in.
492 *
493 * @return true, if all PIN fields have 4 characters, false otherwise
494 */
495 private boolean pinEntryCompleted() {
496 if (row1.length() == 4 &&
497 row2.length() == 4 &&
498 row3.length() == 4) {
499 return true;
500 }
501 return false;
502 }
503
504 private boolean hasInternet() {
505 Logger.debug(LOG_TAG, "Checking internet connectivity.");
506 ConnectivityManager connManager = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
507 NetworkInfo network = connManager.getActiveNetworkInfo();
508
509 if (network != null && network.isConnected()) {
510 Logger.debug(LOG_TAG, network + " is connected.");
511 return true;
512 }
513 Logger.debug(LOG_TAG, "No connected networks.");
514 return false;
515 }
516
517 /**
518 * Displays layout for entering a PIN from another device.
519 * A Sync Account has already been set up.
520 */
521 private void displayPairWithPin() {
522 Logger.debug(LOG_TAG, "PairWithPin initiated.");
523 runOnUiThread(new Runnable() {
524
525 @Override
526 public void run() {
527 setContentView(R.layout.sync_setup_pair);
528 connectButton = (Button) findViewById(R.id.pair_button_connect);
529 pinError = (LinearLayout) findViewById(R.id.pair_error);
530
531 row1 = (EditText) findViewById(R.id.pair_row1);
532 row2 = (EditText) findViewById(R.id.pair_row2);
533 row3 = (EditText) findViewById(R.id.pair_row3);
534
535 row1.addTextChangedListener(new TextWatcher() {
536 @Override
537 public void afterTextChanged(Editable s) {
538 activateButton(connectButton, pinEntryCompleted());
539 if (s.length() == 4) {
540 row2.requestFocus();
541 }
542 }
543
544 @Override
545 public void beforeTextChanged(CharSequence s, int start, int count,
546 int after) {
547 }
548
549 @Override
550 public void onTextChanged(CharSequence s, int start, int before, int count) {
551 }
552
553 });
554 row2.addTextChangedListener(new TextWatcher() {
555 @Override
556 public void afterTextChanged(Editable s) {
557 activateButton(connectButton, pinEntryCompleted());
558 if (s.length() == 4) {
559 row3.requestFocus();
560 }
561 }
562
563 @Override
564 public void beforeTextChanged(CharSequence s, int start, int count,
565 int after) {
566 }
567
568 @Override
569 public void onTextChanged(CharSequence s, int start, int before, int count) {
570 }
571
572 });
573
574 row3.addTextChangedListener(new TextWatcher() {
575 @Override
576 public void afterTextChanged(Editable s) {
577 activateButton(connectButton, pinEntryCompleted());
578 }
579
580 @Override
581 public void beforeTextChanged(CharSequence s, int start, int count,
582 int after) {
583 }
584
585 @Override
586 public void onTextChanged(CharSequence s, int start, int before, int count) {
587 }
588 });
589
590 row1.requestFocus();
591 }
592 });
593 }
594
595 /**
596 * Displays layout with PIN for pairing with another device.
597 * No Sync Account has been set up yet.
598 */
599 private void displayReceiveNoPin() {
600 Logger.debug(LOG_TAG, "ReceiveNoPin initiated");
601 runOnUiThread(new Runnable(){
602
603 @Override
604 public void run() {
605 setContentView(R.layout.sync_setup);
606
607 // Set up UI.
608 pinTextView1 = ((TextView) findViewById(R.id.text_pin1));
609 pinTextView2 = ((TextView) findViewById(R.id.text_pin2));
610 pinTextView3 = ((TextView) findViewById(R.id.text_pin3));
611 }
612 });
613 }
614
615 @Override
616 public void onActivityResult(int requestCode, int resultCode, Intent data) {
617 switch (resultCode) {
618 case Activity.RESULT_OK:
619 // Setup completed in manual setup.
620 finish();
621 }
622 }
623 }

mercurial