diff -r 000000000000 -r 6474c204b198 mobile/android/base/sync/setup/activities/SetupSyncActivity.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mobile/android/base/sync/setup/activities/SetupSyncActivity.java Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,623 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.sync.setup.activities; + +import java.io.UnsupportedEncodingException; +import java.util.HashMap; + +import org.json.simple.JSONObject; +import org.mozilla.gecko.R; +import org.mozilla.gecko.background.common.log.Logger; +import org.mozilla.gecko.sync.SyncConstants; +import org.mozilla.gecko.sync.ThreadPool; +import org.mozilla.gecko.sync.Utils; +import org.mozilla.gecko.sync.jpake.JPakeClient; +import org.mozilla.gecko.sync.jpake.JPakeNoActivePairingException; +import org.mozilla.gecko.sync.setup.Constants; +import org.mozilla.gecko.sync.setup.SyncAccounts; +import org.mozilla.gecko.sync.setup.SyncAccounts.SyncAccountParameters; + +import android.accounts.Account; +import android.accounts.AccountAuthenticatorActivity; +import android.accounts.AccountManager; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.Uri; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +public class SetupSyncActivity extends AccountAuthenticatorActivity { + private final static String LOG_TAG = "SetupSync"; + + private boolean pairWithPin = false; + + // UI elements for pairing through PIN entry. + private EditText row1; + private EditText row2; + private EditText row3; + private Button connectButton; + private LinearLayout pinError; + + // UI elements for pairing through PIN generation. + private TextView pinTextView1; + private TextView pinTextView2; + private TextView pinTextView3; + private JPakeClient jClient; + + // Android context. + private AccountManager mAccountManager; + private Context mContext; + + public SetupSyncActivity() { + super(); + } + + /** Called when the activity is first created. */ + @Override + public void onCreate(Bundle savedInstanceState) { + ActivityUtils.prepareLogging(); + Logger.info(LOG_TAG, "Called SetupSyncActivity.onCreate."); + super.onCreate(savedInstanceState); + + // Set Activity variables. + mContext = getApplicationContext(); + Logger.debug(LOG_TAG, "AccountManager.get(" + mContext + ")"); + mAccountManager = AccountManager.get(mContext); + + // Set "screen on" flag for this activity. Screen will not automatically dim as long as this + // activity is at the top of the stack. + // Attempting to set this flag more than once causes hanging, so we set it here, not in onResume(). + Window w = getWindow(); + w.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + Logger.debug(LOG_TAG, "Successfully set screen-on flag."); + } + + @Override + public void onResume() { + ActivityUtils.prepareLogging(); + Logger.info(LOG_TAG, "Called SetupSyncActivity.onResume."); + super.onResume(); + + if (!hasInternet()) { + runOnUiThread(new Runnable() { + @Override + public void run() { + setContentView(R.layout.sync_setup_nointernet); + } + }); + return; + } + + // Check whether Sync accounts exist; if not, display J-PAKE PIN. + // Run this on a separate thread to comply with Strict Mode thread policies. + ThreadPool.run(new Runnable() { + @Override + public void run() { + ActivityUtils.prepareLogging(); + Account[] accts = mAccountManager.getAccountsByType(SyncConstants.ACCOUNTTYPE_SYNC); + finishResume(accts); + } + }); + } + + public void finishResume(Account[] accts) { + Logger.debug(LOG_TAG, "Finishing Resume after fetching accounts."); + + if (accts.length == 0) { // Start J-PAKE for pairing if no accounts present. + Logger.debug(LOG_TAG, "No accounts; starting J-PAKE receiver."); + displayReceiveNoPin(); + if (jClient != null) { + // Mark previous J-PAKE as finished. Don't bother propagating back up to this Activity. + jClient.finished = true; + } + jClient = new JPakeClient(this); + jClient.receiveNoPin(); + return; + } + + // Set layout based on starting Intent. + Bundle extras = this.getIntent().getExtras(); + if (extras != null) { + Logger.debug(LOG_TAG, "SetupSync with extras."); + boolean isSetup = extras.getBoolean(Constants.INTENT_EXTRA_IS_SETUP); + if (!isSetup) { + Logger.debug(LOG_TAG, "Account exists; Pair a Device started."); + pairWithPin = true; + displayPairWithPin(); + return; + } + } + + runOnUiThread(new Runnable() { + @Override + public void run() { + Logger.debug(LOG_TAG, "Only one account supported. Redirecting."); + // Display toast for "Only one account supported." + // Redirect to account management. + Toast toast = Toast.makeText(mContext, + R.string.sync_notification_oneaccount, Toast.LENGTH_LONG); + toast.show(); + + // Setting up Sync when an existing account exists only happens from Settings, + // so we can safely finish() the activity to return to Settings. + finish(); + } + }); + } + + + @Override + public void onPause() { + super.onPause(); + + if (jClient != null) { + jClient.abort(Constants.JPAKE_ERROR_USERABORT); + } + if (pairWithPin) { + finish(); + } + } + + @Override + public void onNewIntent(Intent intent) { + Logger.debug(LOG_TAG, "Started SetupSyncActivity with new intent."); + setIntent(intent); + } + + @Override + public void onDestroy() { + Logger.debug(LOG_TAG, "onDestroy() called."); + super.onDestroy(); + } + + /* Click Handlers */ + public void manualClickHandler(View target) { + Intent accountIntent = new Intent(this, AccountActivity.class); + accountIntent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); + startActivityForResult(accountIntent, 0); + overridePendingTransition(0, 0); + } + + public void cancelClickHandler(View target) { + finish(); + } + + public void connectClickHandler(View target) { + Logger.debug(LOG_TAG, "Connect clicked."); + // Set UI feedback. + pinError.setVisibility(View.INVISIBLE); + enablePinEntry(false); + connectButton.requestFocus(); + activateButton(connectButton, false); + + // Extract PIN. + String pin = row1.getText().toString(); + pin += row2.getText().toString() + row3.getText().toString(); + + // Start J-PAKE. + if (jClient != null) { + // Cancel previous J-PAKE exchange. + jClient.finished = true; + } + jClient = new JPakeClient(this); + jClient.pairWithPin(pin); + } + + /** + * Handler when "Show me how" link is clicked. + * @param target + * View that received the click. + */ + public void showClickHandler(View target) { + Uri uri = null; + // TODO: fetch these from fennec + if (pairWithPin) { + uri = Uri.parse(Constants.LINK_FIND_CODE); + } else { + uri = Uri.parse(Constants.LINK_FIND_ADD_DEVICE); + } + Intent intent = new Intent(this, WebViewActivity.class); + intent.setData(uri); + startActivity(intent); + } + + /* Controller methods */ + + /** + * Display generated PIN to user. + * @param pin + * 12-character string generated for J-PAKE. + */ + public void displayPin(String pin) { + if (pin == null) { + Logger.warn(LOG_TAG, "Asked to display null pin."); + return; + } + // Format PIN for display. + int charPerLine = pin.length() / 3; + final String pin1 = pin.substring(0, charPerLine); + final String pin2 = pin.substring(charPerLine, 2 * charPerLine); + final String pin3 = pin.substring(2 * charPerLine, pin.length()); + + runOnUiThread(new Runnable() { + @Override + public void run() { + TextView view1 = pinTextView1; + TextView view2 = pinTextView2; + TextView view3 = pinTextView3; + if (view1 == null || view2 == null || view3 == null) { + Logger.warn(LOG_TAG, "Couldn't find view to display PIN."); + return; + } + view1.setText(pin1); + view1.setContentDescription(pin1.replaceAll("\\B", ", ")); + + view2.setText(pin2); + view2.setContentDescription(pin2.replaceAll("\\B", ", ")); + + view3.setText(pin3); + view3.setContentDescription(pin3.replaceAll("\\B", ", ")); + } + }); + } + + /** + * Abort current J-PAKE pairing. Clear forms/restart pairing. + * @param error + */ + public void displayAbort(String error) { + if (!Constants.JPAKE_ERROR_USERABORT.equals(error) && !hasInternet()) { + runOnUiThread(new Runnable() { + @Override + public void run() { + setContentView(R.layout.sync_setup_nointernet); + } + }); + return; + } + if (pairWithPin) { + // Clear PIN entries and display error. + runOnUiThread(new Runnable() { + @Override + public void run() { + enablePinEntry(true); + row1.setText(""); + row2.setText(""); + row3.setText(""); + row1.requestFocus(); + + // Display error. + pinError.setVisibility(View.VISIBLE); + } + }); + return; + } + + // Start new JPakeClient for restarting J-PAKE. + Logger.debug(LOG_TAG, "abort reason: " + error); + if (!Constants.JPAKE_ERROR_USERABORT.equals(error)) { + jClient = new JPakeClient(this); + runOnUiThread(new Runnable() { + @Override + public void run() { + displayReceiveNoPin(); + jClient.receiveNoPin(); + } + }); + } + } + + @SuppressWarnings({ "unchecked", "static-method" }) + protected JSONObject makeAccountJSON(String username, String password, + String syncKey, String serverURL) { + + JSONObject jAccount = new JSONObject(); + + // Hack to try to keep Java 1.7 from complaining about unchecked types, + // despite the presence of SuppressWarnings. + HashMap fields = (HashMap) jAccount; + + fields.put(Constants.JSON_KEY_SYNCKEY, syncKey); + fields.put(Constants.JSON_KEY_ACCOUNT, username); + fields.put(Constants.JSON_KEY_PASSWORD, password); + fields.put(Constants.JSON_KEY_SERVER, serverURL); + + if (Logger.LOG_PERSONAL_INFORMATION) { + Logger.pii(LOG_TAG, "Extracted account data: " + jAccount.toJSONString()); + } + return jAccount; + } + + /** + * Device has finished key exchange, waiting for remote device to set up or + * link to a Sync account. Display "waiting for other device" dialog. + */ + public void onPaired() { + // Extract Sync account data. + Account[] accts = mAccountManager.getAccountsByType(SyncConstants.ACCOUNTTYPE_SYNC); + if (accts.length == 0) { + // Error, no account present. + Logger.error(LOG_TAG, "No accounts present."); + displayAbort(Constants.JPAKE_ERROR_INVALID); + return; + } + + // TODO: Single account supported. Create account selection if spec changes. + Account account = accts[0]; + String username = account.name; + String password = mAccountManager.getPassword(account); + String syncKey = mAccountManager.getUserData(account, Constants.OPTION_SYNCKEY); + String serverURL = mAccountManager.getUserData(account, Constants.OPTION_SERVER); + + JSONObject jAccount = makeAccountJSON(username, password, syncKey, serverURL); + try { + jClient.sendAndComplete(jAccount); + } catch (JPakeNoActivePairingException e) { + Logger.error(LOG_TAG, "No active J-PAKE pairing.", e); + displayAbort(Constants.JPAKE_ERROR_INVALID); + } + } + + /** + * J-PAKE pairing has started, but when this device has generated the PIN for + * pairing, does not require UI feedback to user. + */ + public void onPairingStart() { + if (!pairWithPin) { + runOnUiThread(new Runnable() { + @Override + public void run() { + setContentView(R.layout.sync_setup_jpake_waiting); + } + }); + return; + } + } + + /** + * On J-PAKE completion, store the Sync Account credentials sent by other + * device. Display progress to user. + * + * @param jCreds + */ + public void onComplete(JSONObject jCreds) { + if (!pairWithPin) { + // Create account from received credentials. + String accountName = (String) jCreds.get(Constants.JSON_KEY_ACCOUNT); + String password = (String) jCreds.get(Constants.JSON_KEY_PASSWORD); + String syncKey = (String) jCreds.get(Constants.JSON_KEY_SYNCKEY); + String serverURL = (String) jCreds.get(Constants.JSON_KEY_SERVER); + + // The password we get is double-encoded. + try { + password = Utils.decodeUTF8(password); + } catch (UnsupportedEncodingException e) { + Logger.warn(LOG_TAG, "Unsupported encoding when decoding UTF-8 ASCII J-PAKE message. Ignoring."); + } + + final SyncAccountParameters syncAccount = new SyncAccountParameters(mContext, mAccountManager, accountName, + syncKey, password, serverURL); + createAccountOnThread(syncAccount); + } else { + // No need to create an account; just clean up. + displayResultAndFinish(true); + } + } + + private void displayResultAndFinish(final boolean isSuccess) { + jClient = null; + runOnUiThread(new Runnable() { + @Override + public void run() { + int result = isSuccess ? RESULT_OK : RESULT_CANCELED; + setResult(result); + displayResult(isSuccess); + } + }); + } + + private void createAccountOnThread(final SyncAccountParameters syncAccount) { + ThreadPool.run(new Runnable() { + @Override + public void run() { + Account account = SyncAccounts.createSyncAccount(syncAccount); + boolean isSuccess = (account != null); + if (isSuccess) { + Bundle resultBundle = new Bundle(); + resultBundle.putString(AccountManager.KEY_ACCOUNT_NAME, syncAccount.username); + resultBundle.putString(AccountManager.KEY_ACCOUNT_TYPE, SyncConstants.ACCOUNTTYPE_SYNC); + resultBundle.putString(AccountManager.KEY_AUTHTOKEN, SyncConstants.ACCOUNTTYPE_SYNC); + setAccountAuthenticatorResult(resultBundle); + } + displayResultAndFinish(isSuccess); + } + }); + } + + /* + * Helper functions + */ + private void activateButton(Button button, boolean toActivate) { + button.setEnabled(toActivate); + button.setClickable(toActivate); + } + + private void enablePinEntry(boolean toEnable) { + row1.setEnabled(toEnable); + row2.setEnabled(toEnable); + row3.setEnabled(toEnable); + } + + /** + * Displays Sync account setup result to user. + * + * @param isSetup + * true if account was set up successfully, false otherwise. + */ + private void displayResult(boolean isSuccess) { + Intent intent = null; + if (isSuccess) { + intent = new Intent(mContext, SetupSuccessActivity.class); + intent.setFlags(Constants.FLAG_ACTIVITY_REORDER_TO_FRONT_NO_ANIMATION); + intent.putExtra(Constants.INTENT_EXTRA_IS_SETUP, !pairWithPin); + startActivity(intent); + finish(); + } else { + intent = new Intent(mContext, SetupFailureActivity.class); + intent.putExtra(Constants.INTENT_EXTRA_IS_ACCOUNTERROR, true); + intent.setFlags(Constants.FLAG_ACTIVITY_REORDER_TO_FRONT_NO_ANIMATION); + intent.putExtra(Constants.INTENT_EXTRA_IS_SETUP, !pairWithPin); + startActivity(intent); + // Do not finish, so user can retry setup by hitting "back." + } + } + + /** + * Validate PIN entry fields to check if the three PIN entry fields are all + * filled in. + * + * @return true, if all PIN fields have 4 characters, false otherwise + */ + private boolean pinEntryCompleted() { + if (row1.length() == 4 && + row2.length() == 4 && + row3.length() == 4) { + return true; + } + return false; + } + + private boolean hasInternet() { + Logger.debug(LOG_TAG, "Checking internet connectivity."); + ConnectivityManager connManager = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo network = connManager.getActiveNetworkInfo(); + + if (network != null && network.isConnected()) { + Logger.debug(LOG_TAG, network + " is connected."); + return true; + } + Logger.debug(LOG_TAG, "No connected networks."); + return false; + } + + /** + * Displays layout for entering a PIN from another device. + * A Sync Account has already been set up. + */ + private void displayPairWithPin() { + Logger.debug(LOG_TAG, "PairWithPin initiated."); + runOnUiThread(new Runnable() { + + @Override + public void run() { + setContentView(R.layout.sync_setup_pair); + connectButton = (Button) findViewById(R.id.pair_button_connect); + pinError = (LinearLayout) findViewById(R.id.pair_error); + + row1 = (EditText) findViewById(R.id.pair_row1); + row2 = (EditText) findViewById(R.id.pair_row2); + row3 = (EditText) findViewById(R.id.pair_row3); + + row1.addTextChangedListener(new TextWatcher() { + @Override + public void afterTextChanged(Editable s) { + activateButton(connectButton, pinEntryCompleted()); + if (s.length() == 4) { + row2.requestFocus(); + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, + int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + }); + row2.addTextChangedListener(new TextWatcher() { + @Override + public void afterTextChanged(Editable s) { + activateButton(connectButton, pinEntryCompleted()); + if (s.length() == 4) { + row3.requestFocus(); + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, + int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + }); + + row3.addTextChangedListener(new TextWatcher() { + @Override + public void afterTextChanged(Editable s) { + activateButton(connectButton, pinEntryCompleted()); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, + int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + }); + + row1.requestFocus(); + } + }); + } + + /** + * Displays layout with PIN for pairing with another device. + * No Sync Account has been set up yet. + */ + private void displayReceiveNoPin() { + Logger.debug(LOG_TAG, "ReceiveNoPin initiated"); + runOnUiThread(new Runnable(){ + + @Override + public void run() { + setContentView(R.layout.sync_setup); + + // Set up UI. + pinTextView1 = ((TextView) findViewById(R.id.text_pin1)); + pinTextView2 = ((TextView) findViewById(R.id.text_pin2)); + pinTextView3 = ((TextView) findViewById(R.id.text_pin3)); + } + }); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (resultCode) { + case Activity.RESULT_OK: + // Setup completed in manual setup. + finish(); + } + } +}