org.mozilla.gecko.sync.setup.activities.SetupSyncActivity.java Source code

Java tutorial

Introduction

Here is the source code for org.mozilla.gecko.sync.setup.activities.SetupSyncActivity.java

Source

/* 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.util.HashMap;

import org.json.simple.JSONObject;
import org.mozilla.fennec_satyanarayan.R;
import org.mozilla.gecko.sync.Logger;
import org.mozilla.gecko.sync.ThreadPool;
import org.mozilla.gecko.sync.jpake.JPakeClient;
import org.mozilla.gecko.sync.jpake.JPakeNoActivePairingException;
import org.mozilla.gecko.sync.setup.Constants;

import android.accounts.Account;
import android.accounts.AccountAuthenticatorActivity;
import android.accounts.AccountManager;
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.provider.Settings;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
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 setupTitleView;
    private TextView setupNoDeviceLinkTitleView;
    private TextView setupSubtitleView;
    private TextView pinTextView1;
    private TextView pinTextView2;
    private TextView pinTextView3;
    private JPakeClient jClient;

    // Android context.
    private AccountManager mAccountManager;
    private Context mContext;

    public SetupSyncActivity() {
        super();
        Logger.info(LOG_TAG, "SetupSyncActivity constructor called.");
    }

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        setTheme(R.style.SyncTheme);
        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);
    }

    @Override
    public void onResume() {
        Logger.info(LOG_TAG, "Called SetupSyncActivity.onResume.");
        super.onResume();

        if (!hasInternet()) {
            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() {
                Account[] accts = mAccountManager.getAccountsByType(Constants.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();

                Intent intent = new Intent(Settings.ACTION_SYNC_SETTINGS);
                intent.setFlags(Constants.FLAG_ACTIVITY_REORDER_TO_FRONT_NO_ANIMATION);
                startActivity(intent);

                finish();
            }
        });
    }

    @Override
    public void onPause() {
        super.onPause();

        if (jClient != null) {
            jClient.abort(Constants.JPAKE_ERROR_USERABORT);
        }
    }

    @Override
    public void onNewIntent(Intent intent) {
        setIntent(intent);
    }

    /* Click Handlers */
    public void manualClickHandler(View target) {
        Intent accountIntent = new Intent(this, AccountActivity.class);
        accountIntent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
        startActivity(accountIntent);
        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);
        }
        startActivity(new Intent(Intent.ACTION_VIEW, uri));
    }

    /* 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);
                view2.setText(pin2);
                view3.setText(pin3);
            }
        });
    }

    /**
     * Abort current J-PAKE pairing. Clear forms/restart pairing.
     * @param error
     */
    public void displayAbort(String error) {
        if (!Constants.JPAKE_ERROR_USERABORT.equals(error) && !hasInternet()) {
            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")
    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);

        Logger.debug(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(Constants.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) {
            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);

            Logger.debug(LOG_TAG, "Using account manager " + mAccountManager);
            final Intent intent = AccountActivity.createAccount(mContext, mAccountManager, accountName, syncKey,
                    password, serverURL);
            setAccountAuthenticatorResult(intent.getExtras());

            setResult(RESULT_OK, intent);
        }

        jClient = null; // Sync is set up. Kill reference to JPakeClient object.
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                displayAccount(true);
            }
        });
    }

    /*
     * 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 completed feedback to user.
     *
     * @param isSetup
     *          boolean for whether success screen is reached during setup
     *          completion, or otherwise.
     */
    private void displayAccount(boolean isSetup) {
        Intent intent = new Intent(mContext, SetupSuccessActivity.class);
        intent.setFlags(Constants.FLAG_ACTIVITY_REORDER_TO_FRONT_NO_ANIMATION);
        intent.putExtra(Constants.INTENT_EXTRA_IS_SETUP, isSetup);
        startActivity(intent);
        finish();
    }

    /**
     * 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 wifi = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
        NetworkInfo mobile = connManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);

        if (wifi.isConnected() || mobile.isConnected()) {
            Logger.debug(LOG_TAG, "Internet connected.");
            return true;
        }
        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) {
                    }
                });
            }
        });
    }

    /**
     * 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.
                setupTitleView = ((TextView) findViewById(R.id.setup_title));
                setupSubtitleView = (TextView) findViewById(R.id.setup_subtitle);
                setupNoDeviceLinkTitleView = (TextView) findViewById(R.id.link_nodevice);
                pinTextView1 = ((TextView) findViewById(R.id.text_pin1));
                pinTextView2 = ((TextView) findViewById(R.id.text_pin2));
                pinTextView3 = ((TextView) findViewById(R.id.text_pin3));

                // UI checks.
                if (setupTitleView == null) {
                    Logger.error(LOG_TAG, "No title view.");
                }
                if (setupSubtitleView == null) {
                    Logger.error(LOG_TAG, "No subtitle view.");
                }
                if (setupNoDeviceLinkTitleView == null) {
                    Logger.error(LOG_TAG, "No 'no device' link view.");
                }
            }
        });
    }
}