/* 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.sync.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<String, String> fields = (HashMap<String, String>) 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(); } } }