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

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

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.sync.setup.activities;
     7 import java.io.UnsupportedEncodingException;
     8 import java.util.HashMap;
    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;
    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;
    43 public class SetupSyncActivity extends AccountAuthenticatorActivity {
    44   private final static String LOG_TAG = "SetupSync";
    46   private boolean pairWithPin = false;
    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;
    55   // UI elements for pairing through PIN generation.
    56   private TextView            pinTextView1;
    57   private TextView            pinTextView2;
    58   private TextView            pinTextView3;
    59   private JPakeClient         jClient;
    61   // Android context.
    62   private AccountManager      mAccountManager;
    63   private Context             mContext;
    65   public SetupSyncActivity() {
    66     super();
    67   }
    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);
    76     // Set Activity variables.
    77     mContext = getApplicationContext();
    78     Logger.debug(LOG_TAG, "AccountManager.get(" + mContext + ")");
    79     mAccountManager = AccountManager.get(mContext);
    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   }
    89   @Override
    90   public void onResume() {
    91     ActivityUtils.prepareLogging();
    92     Logger.info(LOG_TAG, "Called SetupSyncActivity.onResume.");
    93     super.onResume();
    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     }
   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   }
   117   public void finishResume(Account[] accts) {
   118     Logger.debug(LOG_TAG, "Finishing Resume after fetching accounts.");
   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     }
   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     }
   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();
   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   }
   163   @Override
   164   public void onPause() {
   165     super.onPause();
   167     if (jClient != null) {
   168       jClient.abort(Constants.JPAKE_ERROR_USERABORT);
   169     }
   170     if (pairWithPin) {
   171       finish();
   172     }
   173   }
   175   @Override
   176   public void onNewIntent(Intent intent) {
   177     Logger.debug(LOG_TAG, "Started SetupSyncActivity with new intent.");
   178     setIntent(intent);
   179   }
   181   @Override
   182   public void onDestroy() {
   183     Logger.debug(LOG_TAG, "onDestroy() called.");
   184     super.onDestroy();
   185   }
   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   }
   195   public void cancelClickHandler(View target) {
   196     finish();
   197   }
   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);
   207     // Extract PIN.
   208     String pin = row1.getText().toString();
   209     pin += row2.getText().toString() + row3.getText().toString();
   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   }
   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   }
   238   /* Controller methods */
   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());
   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", ", "));
   269         view2.setText(pin2);
   270         view2.setContentDescription(pin2.replaceAll("\\B", ", "));
   272         view3.setText(pin3);
   273         view3.setContentDescription(pin3.replaceAll("\\B", ", "));
   274       }
   275     });
   276   }
   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();
   303           // Display error.
   304           pinError.setVisibility(View.VISIBLE);
   305         }
   306       });
   307       return;
   308     }
   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   }
   324   @SuppressWarnings({ "unchecked", "static-method" })
   325   protected JSONObject makeAccountJSON(String username, String password,
   326                                        String syncKey, String serverURL) {
   328     JSONObject jAccount = new JSONObject();
   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;
   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);
   339     if (Logger.LOG_PERSONAL_INFORMATION) {
   340       Logger.pii(LOG_TAG, "Extracted account data: " + jAccount.toJSONString());
   341     }
   342     return jAccount;
   343   }
   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     }
   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);
   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   }
   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   }
   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);
   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       }
   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   }
   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   }
   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   }
   451   /*
   452    * Helper functions
   453    */
   454   private void activateButton(Button button, boolean toActivate) {
   455     button.setEnabled(toActivate);
   456     button.setClickable(toActivate);
   457   }
   459   private void enablePinEntry(boolean toEnable) {
   460     row1.setEnabled(toEnable);
   461     row2.setEnabled(toEnable);
   462     row3.setEnabled(toEnable);
   463   }
   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   }
   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   }
   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();
   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   }
   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() {
   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);
   531         row1 = (EditText) findViewById(R.id.pair_row1);
   532         row2 = (EditText) findViewById(R.id.pair_row2);
   533         row3 = (EditText) findViewById(R.id.pair_row3);
   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           }
   544           @Override
   545           public void beforeTextChanged(CharSequence s, int start, int count,
   546               int after) {
   547           }
   549           @Override
   550           public void onTextChanged(CharSequence s, int start, int before, int count) {
   551           }
   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           }
   563           @Override
   564           public void beforeTextChanged(CharSequence s, int start, int count,
   565               int after) {
   566           }
   568           @Override
   569           public void onTextChanged(CharSequence s, int start, int before, int count) {
   570           }
   572         });
   574         row3.addTextChangedListener(new TextWatcher() {
   575           @Override
   576           public void afterTextChanged(Editable s) {
   577             activateButton(connectButton, pinEntryCompleted());
   578           }
   580           @Override
   581           public void beforeTextChanged(CharSequence s, int start, int count,
   582               int after) {
   583           }
   585           @Override
   586           public void onTextChanged(CharSequence s, int start, int before, int count) {
   587           }
   588         });
   590         row1.requestFocus();
   591       }
   592     });
   593   }
   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(){
   603       @Override
   604       public void run() {
   605         setContentView(R.layout.sync_setup);
   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   }
   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