com.sigilance.CardEdit.MainActivity.java Source code

Java tutorial

Introduction

Here is the source code for com.sigilance.CardEdit.MainActivity.java

Source

/*
 * Copyright (C) 2015 Joey Castillo <joey@joeycastillo.com>
 *
 * This file derives in part from GPLv3 code from the OpenKeychain project:
 * Copyright (C) 2015 Dominik Schrmann <dominik@dominikschuermann.de>
 * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.sigilance.CardEdit;

import android.app.AlertDialog;
import android.app.PendingIntent;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Typeface;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.nfc.tech.IsoDep;
import android.support.v4.app.Fragment;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.text.InputFilter;
import android.text.InputType;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.TimeZone;

public class MainActivity extends AppCompatActivity {

    private abstract class PendingOperation {
        protected int mSlot;

        public int getSlot() {
            return mSlot;
        }
    }

    private class PendingPutDataOperation extends PendingOperation {
        private byte[] mData;

        public PendingPutDataOperation(int slot, byte[] data) {
            mSlot = slot;
            mData = data;
        }

        public byte[] getData() {
            return mData;
        }
    }

    private class PendingVerifyPinOperation extends PendingOperation {
        private String mPin;

        public PendingVerifyPinOperation(int mode, String pin) {
            mSlot = mode;
            mPin = pin;
        }

        public String getPin() {
            return mPin;
        }

        public void setPin(String pin) {
            mPin = pin;
        }
    }

    private class PendingChangePinOperation extends PendingOperation {
        private String mOldPin;
        private String mNewPin;

        public PendingChangePinOperation(int slot, String oldPin, String newPin) {
            mSlot = slot;
            mOldPin = oldPin;
            mNewPin = newPin;
        }

        public String getOldPin() {
            return mOldPin;
        }

        public String getNewPin() {
            return mNewPin;
        }
    }

    private IsoDep isoDep;
    private NfcAdapter mNfcAdapter;

    private boolean mPw3Verified = false;

    // NOTE: The formats for on-card data vary depending on the DO.
    // These are defined to be binary
    private byte[] mCurrentAid = null;
    private byte[] mPrivateDo1 = null;
    private byte[] mPrivateDo2 = null;
    private byte[] mPrivateDo3 = null;
    private byte[] mPrivateDo4 = null;
    private byte[] mLoginData = null;
    private byte[] mPwStatusBytes = null;

    // These are defined to be ISO 8859-1
    private String mCardholderName = null;

    // These are defined to be ASCII
    private String mUrl = null;
    private String mCardholderLanguage = null;
    private String mCardholderSex = null;

    // These are binary, but we're going to convert them to strings for display
    private String mSigKeyFingerprint = null;
    private String mEncKeyFingerprint = null;
    private String mAuthKeyFingerprint = null;

    // These are UNIX epoch timestamps. Unsigned ints, which we need to represent as Java longs
    private long mSigKeyTimestamp;
    private long mEncKeyTimestamp;
    private long mAuthKeyTimestamp;

    // And this is a three-byte array that we'll represent as an integer.
    private Integer mSignatureCount = null;

    private ArrayList<PendingOperation> mPendingOperations = new ArrayList<PendingOperation>();

    // Static stuff
    private static final String BLANK_FINGERPRINT = "0000000000000000000000000000000000000000";

    // Status words
    static final byte[] SW_ACCEPTED = { (byte) 0x90, 0x00 };

    // DO slots for reading and writing
    static final int DO_PRIVATE_1 = 0x0101;
    static final int DO_PRIVATE_2 = 0x0102;
    static final int DO_PRIVATE_3 = 0x0103;
    static final int DO_PRIVATE_4 = 0x0104;
    static final int DO_LOGIN_DATA = 0x005E;
    static final int DO_URL = 0x5F50;
    static final int DO_PW1_STATUS_BYTE = 0x00C4;

    // DO slots for reading only
    static final int DO_AID = 0x004F;
    static final int DO_CARDHOLDER_DATA = 0x0065;
    static final int DO_APPLICATION_DATA = 0x006E;
    static final int DO_SECURITY_TEMPLATE = 0x007A;
    static final int DO_PW_STATUS_BYTES = 0x00C4;

    // DO slots for writing only
    static final int DO_NAME = 0x005B;
    static final int DO_LANGUAGE = 0x5F2D;
    static final int DO_SEX = 0x5F35;

    // Tags for compound objects
    static final int TAG_FINGERPRINTS = 0xC5;
    static final int TAG_TIMESTAMPS = 0xCD;
    static final int TAG_NAME = 0x005B;
    static final int TAG_LANGUAGE = 0x5F2D;
    static final int TAG_SEX = 0x5F35;
    static final int TAG_SIG_COUNT = 0x93;

    // PIN slots
    static final int PIN_PW1 = 0x81;
    static final int PIN_PW3 = 0x83;

    /**
     * Called when the system is about to start resuming a previous activity,
     * disables NFC Foreground Dispatch
     */
    public void onPause() {
        super.onPause();
        disableNfcForegroundDispatch();
    }

    public void onStop() {
        super.onStop();
        dissociateFromCard();
    }

    /**
     * Called when the activity will start interacting with the user,
     * enables NFC Foreground Dispatch
     */
    public void onResume() {
        super.onResume();
        enableNfcForegroundDispatch();
    }

    /**
     * Receive new NFC Intents to this activity only by enabling foreground dispatch.
     * This can only be done in onResume!
     */
    public void enableNfcForegroundDispatch() {
        mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
        if (mNfcAdapter == null) {
            return;
        }
        Intent nfcI = new Intent(this, getClass())
                .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
        PendingIntent nfcPendingIntent = PendingIntent.getActivity(this, 0, nfcI,
                PendingIntent.FLAG_CANCEL_CURRENT);
        IntentFilter[] writeTagFilters = new IntentFilter[] { new IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED) };

        try {
            mNfcAdapter.enableForegroundDispatch(this, nfcPendingIntent, writeTagFilters, null);
        } catch (IllegalStateException e) {
            Toast.makeText(this, "NfcForegroundDispatch Error!", Toast.LENGTH_LONG).show();
        }
    }

    /**
     * Disable foreground dispatch in onPause!
     */
    public void disableNfcForegroundDispatch() {
        if (mNfcAdapter == null) {
            return;
        }
        mNfcAdapter.disableForegroundDispatch(this);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setTitle(R.string.app_name);
        setContentView(R.layout.activity_main);
        if (savedInstanceState == null) {
            getSupportFragmentManager().beginTransaction().add(R.id.container, new PlaceholderFragment()).commit();
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        if (mCurrentAid == null) {
            menu.findItem(R.id.action_change_pw1).setVisible(false);
            menu.findItem(R.id.action_change_pw3).setVisible(false);
            menu.findItem(R.id.action_different_card).setVisible(false);
        } else {
            menu.findItem(R.id.action_change_pw1).setVisible(true);
            menu.findItem(R.id.action_change_pw3).setVisible(true);
            menu.findItem(R.id.action_different_card).setVisible(true);
        }

        if (mPw3Verified) {
            menu.findItem(R.id.action_verify_pw3).setVisible(false);
        } else {
            menu.findItem(R.id.action_verify_pw3).setVisible(true);
        }

        return true;
    }

    void removePendingOperation(Class type, int mode) {
        for (PendingOperation op : mPendingOperations)
            if (op.getClass().equals(type) && op.getSlot() == mode)
                mPendingOperations.remove(op);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();

        switch (id) {
        case R.id.action_verify_pw3:
            removePendingOperation(PendingVerifyPinOperation.class, PIN_PW3);
            promptForVerifyPin(PIN_PW3);
            return true;
        case R.id.action_change_pw1:
            removePendingOperation(PendingChangePinOperation.class, PIN_PW1);
            promptForChangePin(PIN_PW1);
            return true;
        case R.id.action_change_pw3:
            removePendingOperation(PendingChangePinOperation.class, PIN_PW3);
            promptForChangePin(PIN_PW3);
            return true;
        case R.id.action_different_card:
            dissociateFromCard();
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    private void dissociateFromCard() {
        // Note: We don't have to null out everything else, because the UI won't show again until
        // after a successful GET DATA of all the card data.
        for (PendingOperation op : mPendingOperations)
            mPendingOperations.remove(op);
        mPw3Verified = false;
        mCurrentAid = null;
        findTextViewById(R.id.id_action_reqiured_warning).setText(R.string.warning_tap_card_to_view);
        hideUi();
    }

    private void promptForChangePin(final int mode) {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        if (mode == 0x83)
            builder.setTitle(R.string.action_change_pw3);
        else
            builder.setTitle(R.string.action_change_pw1);

        final String typeString = mode == 0x83 ? "Admin" : "User";
        String defaultString = mode == 0x83 ? "12345678" : "123456";
        builder.setMessage(String.format("REMINDER: The default %s PIN is %s", typeString, defaultString));
        final EditText oldPinInput = new EditText(this);
        oldPinInput.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
        oldPinInput.setHint(String.format("Old %s PIN", mode == 0x83 ? "Admin" : "User"));
        final EditText newPinInput = new EditText(this);
        newPinInput.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
        newPinInput.setHint(String.format("New %s PIN", mode == 0x83 ? "Admin" : "User"));
        final EditText confirmPinInput = new EditText(this);
        confirmPinInput.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
        confirmPinInput.setHint("Repeat New PIN");
        LinearLayout fields = new LinearLayout(this);
        fields.setOrientation(LinearLayout.VERTICAL);
        fields.addView(oldPinInput);
        fields.addView(newPinInput);
        fields.addView(confirmPinInput);
        builder.setView(fields);

        builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                // Placeholder; we will override this
            }
        });
        builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.cancel();
            }
        });

        final AlertDialog dialog = builder.create();
        dialog.show();
        dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (oldPinInput.getText().toString().length() == 0
                        || newPinInput.getText().toString().length() == 0) {
                    Toast.makeText(MainActivity.this, "Enter a PIN!", Toast.LENGTH_SHORT).show();
                    return;
                }
                if (!(confirmPinInput.getText().toString().equals(newPinInput.getText().toString()))) {
                    newPinInput.setText("");
                    confirmPinInput.setText("");
                    Toast.makeText(MainActivity.this, "PINs did not match.", Toast.LENGTH_SHORT).show();
                    return;
                }
                int minPinLength = (mode == 0x83) ? 8 : 6;
                if (oldPinInput.getText().toString().length() < minPinLength
                        || newPinInput.getText().toString().length() < minPinLength) {
                    newPinInput.setText("");
                    confirmPinInput.setText("");
                    Toast.makeText(MainActivity.this,
                            String.format("%s PIN must be at least %d digits.", typeString, minPinLength),
                            Toast.LENGTH_SHORT).show();
                    return;
                }
                // Once we have valid PINs, add the pending operation.
                mPendingOperations.add(new PendingChangePinOperation(mode, oldPinInput.getText().toString(),
                        newPinInput.getText().toString()));
                // And prompt the user to change the PIN.
                hideUi();
                findTextViewById(R.id.id_action_reqiured_warning).setText(R.string.warning_tap_card_to_change);
                dialog.dismiss();
            }
        });
    }

    private void promptForVerifyPin(final int mode) {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle(R.string.action_enable_edit_mode);
        builder.setMessage(
                "Enter the Admin PIN to edit data on the card.\nREMINDER: The default Admin PIN is 12345678");
        final EditText input = new EditText(this);
        input.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
        builder.setView(input);

        builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                if (input.getText().toString().length() == 0) {
                    Toast.makeText(MainActivity.this, "Enter a PIN!", Toast.LENGTH_SHORT).show();
                    return;
                }
                int minPinLength = (mode == 0x83) ? 8 : 6;
                if (input.getText().toString().length() < minPinLength) {
                    input.setText("");
                    Toast.makeText(MainActivity.this, String.format("PIN is at least %d digits.", minPinLength),
                            Toast.LENGTH_SHORT).show();
                    return;
                }
                mPendingOperations.add(new PendingVerifyPinOperation(mode, input.getText().toString()));
                hideUi();
                findTextViewById(R.id.id_action_reqiured_warning).setText(R.string.warning_tap_card_to_verify);
            }
        });
        builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.cancel();
            }
        });

        builder.create().show();
    }

    private void promptForTextDo(final int slot) {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);

        final EditText input = new EditText(this);
        final EditText input2 = new EditText(this);
        input.setInputType(InputType.TYPE_CLASS_TEXT);
        input2.setInputType(InputType.TYPE_CLASS_TEXT);
        LinearLayout fields = new LinearLayout(this);
        fields.setOrientation(LinearLayout.VERTICAL);
        fields.addView(input);

        switch (slot) {
        case DO_NAME:
            builder.setTitle(R.string.lbl_cardholder_name);
            input.setHint(R.string.hint_surname);
            input2.setHint(R.string.hint_given_name);
            input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_WORDS);
            input2.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_WORDS);

            String[] names = mCardholderName.split("<<");
            if (names.length > 1) {
                input.setText(names[0].replace('<', ' '));
                input2.setText(names[1].replace('<', ' '));
            }
            fields.addView(input2);
            break;
        case DO_LANGUAGE:
            builder.setTitle(R.string.lbl_language_prefs);
            builder.setMessage("Use a two-letter ISO 639-1 language code: en for English, es for Spanish, etc.");
            input.setFilters(new InputFilter[] { new InputFilter.LengthFilter(2) });
            input.setText(mCardholderLanguage);
            break;
        case DO_LOGIN_DATA:
            builder.setTitle(R.string.lbl_login_data);
            builder.setMessage(
                    "This is arbitrary text; you can use this field to store a username, email address or network logon.");
            input.setFilters(new InputFilter[] { new InputFilter.LengthFilter(254) });
            input.setText(new String(mLoginData));
            break;
        case DO_URL:
            builder.setTitle(R.string.lbl_url);
            input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
            input.setFilters(new InputFilter[] { new InputFilter.LengthFilter(254) });
            input.setText(mUrl);
            break;
        }

        builder.setView(fields);

        builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                byte[] data;
                String text = input.getText().toString();

                switch (slot) {
                case DO_NAME:
                    String surname = text.trim().replace(' ', '<');
                    String givenNames = input2.getText().toString().trim().replace(' ', '<');
                    data = (surname + "<<" + givenNames).getBytes(Charset.forName("ISO-8859-1"));
                    if (data.length > 39) {
                        Toast.makeText(MainActivity.this, "Name is too long!", Toast.LENGTH_LONG).show();
                        return;
                    }
                    break;
                case DO_LANGUAGE:
                    if (text.length() == 2)
                        data = text.toLowerCase().getBytes();
                    else
                        data = new byte[0];
                    break;
                case DO_URL:
                case DO_LOGIN_DATA:
                    data = text.getBytes();
                    break;
                default:
                    data = new byte[0];
                }

                mPendingOperations.add(new PendingPutDataOperation(slot, data));
                hideUi();
                findTextViewById(R.id.id_action_reqiured_warning).setText(R.string.warning_tap_card_to_save);
            }
        });
        builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.cancel();
            }
        });

        builder.create().show();
    }

    private void promptForSexDo() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle(R.string.lbl_sex);

        String[] options = new String[] { getString(R.string.lbl_male), getString(R.string.lbl_female),
                getString(R.string.lbl_gender_unspecifed) };
        int currentIndex = -1;
        switch (mCardholderSex) {
        case "1":
            currentIndex = 0;
            break;
        case "2":
            currentIndex = 1;
            break;
        case "9":
            currentIndex = 2;
            break;
        }
        builder.setSingleChoiceItems(options, currentIndex, new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int item) {
            }
        });

        builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                ListView lw = ((AlertDialog) dialog).getListView();
                switch (lw.getCheckedItemPosition()) {
                case 0:
                    mPendingOperations.add(new PendingPutDataOperation(DO_SEX, new byte[] { 0x31 }));
                    break;
                case 1:
                    mPendingOperations.add(new PendingPutDataOperation(DO_SEX, new byte[] { 0x32 }));
                    break;
                case 2:
                    mPendingOperations.add(new PendingPutDataOperation(DO_SEX, new byte[] { 0x39 }));
                    break;
                default:
                    return;
                }
                hideUi();
                findTextViewById(R.id.id_action_reqiured_warning).setText(R.string.warning_tap_card_to_save);
            }
        });
        builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.cancel();
            }
        });

        builder.create().show();
    }

    private void promptForPinBehaviorDo() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle(R.string.lbl_signature_pin);

        String[] options = new String[] { getString(R.string.lbl_pin_forced),
                getString(R.string.lbl_pin_not_forced) };
        builder.setSingleChoiceItems(options, mPwStatusBytes[0], new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int index) {

            }
        });

        builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                ListView lw = ((AlertDialog) dialog).getListView();
                byte[] data;
                if (lw.getCheckedItemPosition() == 0)
                    data = new byte[] { 0 };
                else
                    data = new byte[] { 1 };
                mPendingOperations.add(new PendingPutDataOperation(DO_PW1_STATUS_BYTE, data));
                hideUi();
                findTextViewById(R.id.id_action_reqiured_warning).setText(R.string.warning_tap_card_to_save);
            }
        });
        builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.cancel();
            }
        });

        builder.create().show();
    }

    /**
     * A placeholder fragment containing a simple view.
     */
    public static class PlaceholderFragment extends Fragment {

        public PlaceholderFragment() {
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            View rootView = inflater.inflate(R.layout.fragment_main, container, false);
            return rootView;
        }
    }

    @Override
    public void onNewIntent(Intent intent) {
        if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(intent.getAction())) {
            try {
                handleNdefDiscoveredIntent(intent);
            } catch (IOException e) {
                handleNfcError(e);
            }
        }
    }

    private void hideUi() {
        findViewById(R.id.id_action_reqiured_warning).setVisibility(View.VISIBLE);
        findViewById(R.id.id_version_container).setVisibility(View.GONE);
        findViewById(R.id.id_manufacturer_container).setVisibility(View.GONE);
        findViewById(R.id.id_serialno_container).setVisibility(View.GONE);
        findViewById(R.id.id_name_container).setVisibility(View.GONE);
        findViewById(R.id.id_lang_container).setVisibility(View.GONE);
        findViewById(R.id.id_sex_container).setVisibility(View.GONE);
        findViewById(R.id.id_url_container).setVisibility(View.GONE);
        findViewById(R.id.id_logindata_container).setVisibility(View.GONE);
        findViewById(R.id.id_forcesig_container).setVisibility(View.GONE);
        findViewById(R.id.id_sigcount_container).setVisibility(View.GONE);
        findViewById(R.id.id_sigkey_container).setVisibility(View.GONE);
        findViewById(R.id.id_enckey_container).setVisibility(View.GONE);
        findViewById(R.id.id_authkey_container).setVisibility(View.GONE);
    }

    private void showUi() {
        findViewById(R.id.id_action_reqiured_warning).setVisibility(View.GONE);
        findViewById(R.id.id_version_container).setVisibility(View.VISIBLE);
        findViewById(R.id.id_manufacturer_container).setVisibility(View.VISIBLE);
        findViewById(R.id.id_serialno_container).setVisibility(View.VISIBLE);
        findViewById(R.id.id_name_container).setVisibility(View.VISIBLE);
        findViewById(R.id.id_lang_container).setVisibility(View.VISIBLE);
        findViewById(R.id.id_sex_container).setVisibility(View.VISIBLE);
        findViewById(R.id.id_url_container).setVisibility(View.VISIBLE);
        findViewById(R.id.id_logindata_container).setVisibility(View.VISIBLE);
        findViewById(R.id.id_forcesig_container).setVisibility(View.VISIBLE);
        findViewById(R.id.id_sigcount_container).setVisibility(View.VISIBLE);
        findViewById(R.id.id_sigkey_container).setVisibility(View.VISIBLE);
        findViewById(R.id.id_enckey_container).setVisibility(View.VISIBLE);
        findViewById(R.id.id_authkey_container).setVisibility(View.VISIBLE);
    }

    private void disableEditControls() {
        findViewById(R.id.btn_edit_name).setEnabled(false);
        findViewById(R.id.btn_edit_lang).setEnabled(false);
        findViewById(R.id.btn_edit_sex).setEnabled(false);
        findViewById(R.id.btn_edit_url).setEnabled(false);
        findViewById(R.id.btn_edit_logindata).setEnabled(false);
        findViewById(R.id.btn_edit_forcesig).setEnabled(false);
    }

    private void enableEditControls() {
        findViewById(R.id.btn_edit_name).setEnabled(true);
        findViewById(R.id.btn_edit_lang).setEnabled(true);
        findViewById(R.id.btn_edit_sex).setEnabled(true);
        findViewById(R.id.btn_edit_url).setEnabled(true);
        findViewById(R.id.btn_edit_logindata).setEnabled(true);
        findViewById(R.id.btn_edit_forcesig).setEnabled(true);

        findButtonById(R.id.btn_edit_name).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                promptForTextDo(DO_NAME);
            }
        });
        findButtonById(R.id.btn_edit_lang).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                promptForTextDo(DO_LANGUAGE);
            }
        });
        findButtonById(R.id.btn_edit_sex).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                promptForSexDo();
            }
        });
        findButtonById(R.id.btn_edit_url).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                promptForTextDo(DO_URL);
            }
        });
        findButtonById(R.id.btn_edit_logindata).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                promptForTextDo(DO_LOGIN_DATA);
            }
        });
        findButtonById(R.id.btn_edit_forcesig).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                promptForPinBehaviorDo();
            }
        });

    }

    private void populateSimpleField(int id, String text) {
        TextView textView = findTextViewById(id);
        if (text.equals("") || text.equals(BLANK_FINGERPRINT)) {
            textView.setText(R.string.lbl_empty);
            textView.setTypeface(null, Typeface.ITALIC);
        } else {
            textView.setText(text);
            textView.setTypeface(null, Typeface.NORMAL);
        }
    }

    private static String formatEpochDate(long timestamp) {
        if (timestamp == 0)
            return " ";

        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        df.setTimeZone(TimeZone.getTimeZone("GMT"));
        return df.format(new Date(timestamp * 1000));
    }

    private String getManufacturer() {
        int manufacturerId = (mCurrentAid[8] << 8) | mCurrentAid[9];
        switch (manufacturerId) {
        case 0x0005:
            return "ZeitControl";
        case 0x0006:
            return "Yubico";
        case 0x7615:
            return "SIGILANCE";
        case 0xF517:
            return "FSIJ";
        case 0x0000:
        case 0xffff:
            return "test card";
        default:
            if ((manufacturerId & 0xff00) == 0xff00)
                return "unmanaged S/N range";
            else
                return "unknown";
        }
    }

    private String getVersion() {
        String aidString = hexString(mCurrentAid);
        // Spec states that this version string is BCD, so 0x10 is 10, not 16.
        int majorVersion = Integer.parseInt(aidString.substring(12, 14));
        int minorVersion = Integer.parseInt(aidString.substring(14, 16));
        return String.format("%d.%d", majorVersion, minorVersion);
    }

    private String getSerialNumber() {
        String aidString = hexString(mCurrentAid);
        return aidString.substring(20, 28);
    }

    private TextView findTextViewById(int id) {
        return (TextView) findViewById(id);
    }

    private ImageButton findButtonById(int id) {
        return (ImageButton) findViewById(id);
    }

    private void refreshUi() {
        if (mCurrentAid == null) {
            hideUi();
            return;
        }

        showUi();

        if (mPw3Verified) {
            enableEditControls();
        } else {
            disableEditControls();
        }

        populateSimpleField(R.id.id_version_content, getVersion());
        populateSimpleField(R.id.id_manufacturer_content, getManufacturer());
        populateSimpleField(R.id.id_serialno_content, getSerialNumber());

        populateSimpleField(R.id.id_name_content, mCardholderName);
        populateSimpleField(R.id.id_lang_content, mCardholderLanguage);

        populateSimpleField(R.id.id_url_content, mUrl);
        populateSimpleField(R.id.id_logindata_content, new String(mLoginData));

        populateSimpleField(R.id.id_sigcount_content, String.format("%d", mSignatureCount));

        switch (mCardholderSex) {
        case "1":
            findTextViewById(R.id.id_sex_content).setText(R.string.lbl_male);
            break;
        case "2":
            findTextViewById(R.id.id_sex_content).setText(R.string.lbl_female);
            break;
        default:
            findTextViewById(R.id.id_sex_content).setText(R.string.lbl_gender_unspecifed);
            break;
        }

        if (mPwStatusBytes[0] == 1)
            findTextViewById(R.id.id_forcesig_content).setText(R.string.lbl_pin_not_forced);
        else
            findTextViewById(R.id.id_forcesig_content).setText(R.string.lbl_pin_forced);

        populateSimpleField(R.id.id_sigkey_fingerprint_content, mSigKeyFingerprint);
        populateSimpleField(R.id.id_sigkey_timestamp_content, formatEpochDate(mSigKeyTimestamp));

        populateSimpleField(R.id.id_enckey_fingerprint_content, mEncKeyFingerprint);
        populateSimpleField(R.id.id_enckey_timestamp_content, formatEpochDate(mEncKeyTimestamp));

        populateSimpleField(R.id.id_authkey_fingerprint_content, mAuthKeyFingerprint);
        populateSimpleField(R.id.id_authkey_timestamp_content, formatEpochDate(mAuthKeyTimestamp));
    }

    public byte[] nfcCommunicate(byte[] apdu) throws IOException {
        return isoDep.transceive(apdu);
    }

    /**
     * Gets a data object from the card.
     * Supported for all data objects < 255 bytes in length. Only the cardholder certificate
     * (0x7F21) can exceed this length.
     *
     * @param dataObject The data object to get.
     */
    public byte[] nfcGetData(int dataObject) throws IOException {
        byte p1 = (byte) ((dataObject & 0xFF00) >> 8);
        byte p2 = (byte) (dataObject & 0x00FF);

        byte[] getDataApdu = { 0x00, (byte) 0xCA, p1, p2, 0x00 };
        byte[] response = nfcCommunicate(getDataApdu);

        byte[] sw = Arrays.copyOfRange(response, response.length - 2, response.length);

        if (!Arrays.equals(sw, SW_ACCEPTED)) {
            throw new IOException("GET DATA failed!");
        }

        return Arrays.copyOf(response, response.length - 2);
    }

    /**
     * Stores a data object on the card. Automatically validates the proper PIN for the operation.
     * Supported for all data objects < 255 bytes in length. Only the cardholder certificate
     * (0x7F21) can exceed this length.
     *
     * @param dataObject The data object to be stored.
     * @param data       The data to store in the object
     */
    public void nfcPutData(int dataObject, byte[] data) throws IOException {
        byte p1 = (byte) ((dataObject & 0xFF00) >> 8);
        byte p2 = (byte) (dataObject & 0x00FF);

        byte[] putDataHeader = { 0x00, (byte) 0xDA, p1, p2, (byte) data.length };
        byte[] putDataApdu = new byte[putDataHeader.length + data.length];
        System.arraycopy(putDataHeader, 0, putDataApdu, 0, putDataHeader.length);
        System.arraycopy(data, 0, putDataApdu, putDataHeader.length, data.length);

        byte[] response = nfcCommunicate(putDataApdu);

        if (!Arrays.equals(response, SW_ACCEPTED)) {
            throw new IOException("PUT DATA failed!");
        }
    }

    /**
     * Modifies the user's PW1 or PW3. Before sending, the new PIN will be validated for
     * conformance to the card's requirements for key length.
     *
     * @param slot   For PW1, this is 0x81. For PW3 (Admin PIN), mode is 0x83.
     * @param oldPin The old PW1 or PW3.
     * @param newPin The new PW1 or PW3.
     */
    public void nfcModifyPIN(int slot, String oldPin, String newPin) throws IOException {
        byte[] pins = (oldPin + newPin).getBytes();
        byte[] changePinHeader = { 0x00, 0x24, 0x00, (byte) slot, (byte) pins.length };
        byte[] changePinApdu = new byte[changePinHeader.length + pins.length];
        System.arraycopy(changePinHeader, 0, changePinApdu, 0, changePinHeader.length);
        System.arraycopy(pins, 0, changePinApdu, changePinHeader.length, pins.length);

        byte[] response = nfcCommunicate(changePinApdu); // change PIN
        if (!Arrays.equals(response, SW_ACCEPTED)) {
            showUi();
            throw new IOException("CHANGE PIN failed!");
        }
    }

    /** Verifies the user's PW1 or PW3 with the appropriate mode.
     *
     * @param mode For PW1, this is 0x81 for signing, 0x82 for everything else.
     *             For PW3 (Admin PIN), mode is 0x83.
     */
    public void nfcVerifyPIN(int mode, String pinString) throws IOException {
        if (pinString != null || mode == 0x83) {
            if (pinString == null || pinString.length() < 6)
                throw new IOException("Invalid PIN!");
            byte[] pin = pinString.getBytes();
            byte[] verifyPinHeader = { 0x00, 0x20, 0x00, (byte) mode, (byte) pin.length };
            byte[] verifyPinApdu = new byte[verifyPinHeader.length + pin.length];
            System.arraycopy(verifyPinHeader, 0, verifyPinApdu, 0, verifyPinHeader.length);
            System.arraycopy(pin, 0, verifyPinApdu, verifyPinHeader.length, pin.length);

            byte[] response = nfcCommunicate(verifyPinApdu);
            if (!Arrays.equals(response, SW_ACCEPTED)) {
                showUi();
                throw new IOException(
                        "Incorrect PIN. Do not attempt again with the same PIN, or you risk locking the card!");
            }

            if (mode == 0x83) {
                mPw3Verified = true;
            }
        }
    }

    protected void handleNdefDiscoveredIntent(Intent intent) throws IOException {
        Tag detectedTag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
        isoDep = IsoDep.get(detectedTag);
        isoDep.setTimeout(100000); // timeout is set to 100 seconds to avoid cancellation during calculation
        isoDep.connect();

        byte[] opening = { 0x00, (byte) 0xA4, 0x04, 0x00, 0x06, (byte) 0xD2, 0x76, 0x00, 0x01, 0x24, 0x01, 0x00 };
        byte[] response = nfcCommunicate(opening);
        if (!Arrays.equals(response, SW_ACCEPTED)) {
            throw new IOException("Initialization failed!");
        }

        byte[] aid = nfcGetData(DO_AID);

        // If user is touching card for the first time, cache the AID.
        if (mCurrentAid == null) {
            mCurrentAid = aid;
        }

        // Confirm that they're still tapping the same card.
        if (!Arrays.equals(aid, mCurrentAid)) {
            throw new IOException("Serial numbers did not match; did you tap a different card?");
        }

        // With safety checks out of the way, perform pending operations.
        if (mPendingOperations.size() > 0) {
            String newAdminPIN = null;

            for (PendingOperation operation : mPendingOperations) {
                if (operation instanceof PendingVerifyPinOperation) {
                    PendingVerifyPinOperation op = (PendingVerifyPinOperation) operation;
                    nfcVerifyPIN(op.getSlot(), op.getPin());
                    // NOTE: We do not remove the verify operation, because if the user wants to
                    // edit another DO, we'll need to transmit it again.
                } else if (operation instanceof PendingPutDataOperation) {
                    PendingPutDataOperation op = (PendingPutDataOperation) operation;
                    nfcPutData(op.getSlot(), op.getData());
                    mPendingOperations.remove(op);
                } else if (operation instanceof PendingChangePinOperation) {
                    PendingChangePinOperation op = (PendingChangePinOperation) operation;
                    nfcModifyPIN(op.getSlot(), op.getOldPin(), op.getNewPin());
                    if (op.getSlot() == 0x83)
                        newAdminPIN = op.getNewPin();
                    mPendingOperations.remove(op);
                    Toast.makeText(this, "PIN was changed.", Toast.LENGTH_LONG).show();
                }
            }

            // If we changed the Admin PIN, we need the VERIFY command to reflect the new PIN.
            if (newAdminPIN != null)
                for (PendingOperation operation : mPendingOperations)
                    if (operation instanceof PendingVerifyPinOperation) {
                        PendingVerifyPinOperation op = (PendingVerifyPinOperation) operation;
                        if (op.getSlot() == 0x83)
                            op.setPin(newAdminPIN);
                    }
        }

        // Finally, get all the data and show the UI.
        byte[] cardholderData = nfcGetData(DO_CARDHOLDER_DATA);
        Iso7816TLV chTlv = Iso7816TLV.readSingle(cardholderData, true);
        mCardholderName = new String(Iso7816TLV.findRecursive(chTlv, TAG_NAME).mV);
        mCardholderSex = new String(Iso7816TLV.findRecursive(chTlv, TAG_SEX).mV);
        mCardholderLanguage = new String(Iso7816TLV.findRecursive(chTlv, TAG_LANGUAGE).mV);

        mUrl = new String(nfcGetData(DO_URL));
        mLoginData = nfcGetData(DO_LOGIN_DATA);

        byte[] appData = nfcGetData(DO_APPLICATION_DATA);
        Iso7816TLV appTlv = Iso7816TLV.readSingle(appData, true);
        byte[] fingerprints = Iso7816TLV.findRecursive(appTlv, TAG_FINGERPRINTS).mV;
        mSigKeyFingerprint = hexString(Arrays.copyOfRange(fingerprints, 0, 20));
        mEncKeyFingerprint = hexString(Arrays.copyOfRange(fingerprints, 20, 40));
        mAuthKeyFingerprint = hexString(Arrays.copyOfRange(fingerprints, 40, 60));
        byte[] timestamps = Iso7816TLV.findRecursive(appTlv, TAG_TIMESTAMPS).mV;
        mSigKeyTimestamp = unsignedFromByteArray(Arrays.copyOfRange(timestamps, 0, 4));
        mEncKeyTimestamp = unsignedFromByteArray(Arrays.copyOfRange(timestamps, 4, 8));
        mAuthKeyTimestamp = unsignedFromByteArray(Arrays.copyOfRange(timestamps, 8, 12));

        mPwStatusBytes = nfcGetData(DO_PW_STATUS_BYTES);

        byte[] secData = nfcGetData(DO_SECURITY_TEMPLATE);
        Iso7816TLV secTlv = Iso7816TLV.readSingle(secData, true);
        byte[] sigCount = { 0, 0, 0, 0 };
        byte[] sigCountFromCard = Iso7816TLV.findRecursive(secTlv, TAG_SIG_COUNT).mV;
        System.arraycopy(sigCountFromCard, 0, sigCount, 1, 3);
        mSignatureCount = ByteBuffer.wrap(sigCount).getInt();

        refreshUi();
    }

    public void handleNfcError(IOException e) {
        Toast.makeText(this, "Exception: " + e.getLocalizedMessage(), Toast.LENGTH_LONG).show();
    }

    final protected static char[] HEX_CHARACTERS = "0123456789ABCDEF".toCharArray();

    public static String hexString(byte[] bytes) {
        char[] retVal = new char[bytes.length * 2];
        for (int i = 0; i < bytes.length; i++) {
            int v = bytes[i] & 0xFF;
            retVal[i * 2] = HEX_CHARACTERS[v >>> 4];
            retVal[i * 2 + 1] = HEX_CHARACTERS[v & 0x0F];
        }
        return new String(retVal);
    }

    long unsignedFromByteArray(byte[] b) {
        long l = 0;
        l |= b[0] & 0xFF;
        l <<= 8;
        l |= b[1] & 0xFF;
        l <<= 8;
        l |= b[2] & 0xFF;
        l <<= 8;
        l |= b[3] & 0xFF;
        return l;
    }

}