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