package com.github.jthuraisamy.mastertap; import android.app.PendingIntent; import android.app.ProgressDialog; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.net.Uri; import android.nfc.NfcAdapter; import android.nfc.Tag; import android.nfc.cardemulation.CardEmulation; import android.nfc.tech.NfcA; import android.os.Bundle; import android.os.Environment; import android.os.Vibrator; import android.preference.PreferenceManager; import android.support.v4.app.FragmentActivity; import android.support.v4.content.LocalBroadcastManager; import android.support.v4.view.ViewPager; import android.view.Menu; import android.view.MenuItem; import android.view.WindowManager; import android.widget.Toast; import com.github.jthuraisamy.mastertap.adapters.CardFragmentPagerAdapter; import com.github.jthuraisamy.mastertap.fragments.ChangePasswordDialog; import com.github.jthuraisamy.mastertap.fragments.DeleteCardDialog; import com.github.jthuraisamy.mastertap.fragments.DisclaimerDialog; import com.github.jthuraisamy.mastertap.fragments.EnterPasswordDialog; import com.github.jthuraisamy.mastertap.fragments.ImportCardsDialog; import com.github.jthuraisamy.mastertap.fragments.NfcAdapterAbsentDialog; import com.github.jthuraisamy.mastertap.fragments.NfcAdapterDisabledDialog; import com.github.jthuraisamy.mastertap.fragments.RegisterPasswordDialog; import com.github.jthuraisamy.mastertap.fragments.RenameCardDialog; import com.github.jthuraisamy.mastertap.helpers.PortHelper; import com.github.jthuraisamy.mastertap.models.Card; import com.github.jthuraisamy.mastertap.models.SQLiteCardDao; import com.google.gson.Gson; import com.google.gson.JsonSyntaxException; import net.sqlcipher.database.SQLiteDatabase; import net.sqlcipher.database.SQLiteException; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.io.OutputStream; import java.util.List; public class MainActivity extends FragmentActivity { private static final String TAG = "MasterTapLog-" + MainActivity.class.getSimpleName(); private static final int SELECT_IMPORT_FILE = 1; // isDatabaseReKeyed = true when a password has been registered for the database. public static boolean isDatabaseReKeyed = false; // isDatabaseAuthenticated = true when the correct password/key has been set for the database. public static boolean isDatabaseAuthenticated = false; public static SQLiteCardDao cardDao; public static List<Card> cards; private Menu menu; private ViewPager viewPager; private int selectedPage = 0; private ProgressDialog paymentProgressDialog; private final Gson gson = new Gson(); public Vibrator vibrator; private NfcAdapter nfcAdapter; private final String[][] nfcTechFilter = new String[][] {new String[] {NfcA.class.getName()}}; private PendingIntent nfcIntent; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Keep screen always on to prevent card reading interruptions. getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); // Obtain instance of system vibrator. vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); // Get NFC adapter and set intent. nfcAdapter = NfcAdapter.getDefaultAdapter(this); nfcIntent = PendingIntent.getActivity(this, 0, new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0); // Register broadcast receiver for PaymentService. LocalBroadcastManager.getInstance(this).registerReceiver(apduReceiver, new IntentFilter("apduProcessing")); // Show disclaimer. SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this); boolean isDisclaimerAccepted = settings.getBoolean("isDisclaimerAccepted", false); boolean showDisclaimer = settings.getBoolean("showDisclaimer", true); if (!isDisclaimerAccepted || showDisclaimer) { if (!isFragmentVisible(DisclaimerDialog.TAG)) { DisclaimerDialog disclaimerDialog = DisclaimerDialog.create(); disclaimerDialog.show(getSupportFragmentManager(), DisclaimerDialog.TAG); } } else { // Initialize database if disclaimer was already accepted. initDatabase(); } } /** * Check if PaymentService is the default NFC payment app, and if not request user to set it. */ private void checkDefaultPaymentApp() { CardEmulation cardEmulationManager = CardEmulation.getInstance(NfcAdapter.getDefaultAdapter(this)); ComponentName paymentServiceComponent = new ComponentName(getApplicationContext(), PaymentService.class.getCanonicalName()); boolean isPaymentServiceDefault = cardEmulationManager.isDefaultServiceForCategory(paymentServiceComponent, CardEmulation.CATEGORY_PAYMENT); if (!isPaymentServiceDefault) { Intent intent = new Intent(CardEmulation.ACTION_CHANGE_DEFAULT); intent.putExtra(CardEmulation.EXTRA_CATEGORY, CardEmulation.CATEGORY_PAYMENT); intent.putExtra(CardEmulation.EXTRA_SERVICE_COMPONENT, paymentServiceComponent); startActivityForResult(intent, 0); } } /** * Initialize the database and authenticate the user. */ public void initDatabase() { // Load SQLCipher libraries and Card DAO. SQLiteDatabase.loadLibs(this); cardDao = new SQLiteCardDao(this); // Return if database is already authenticated. if (!isDatabaseAuthenticated) { // Check if database has been re-keyed. try { cardDao.setKey("defaultKey"); cardDao.open(); // Register a password if the default key worked. if (!isFragmentVisible(RegisterPasswordDialog.TAG)) { RegisterPasswordDialog registerPasswordDialog = RegisterPasswordDialog.create(); registerPasswordDialog.show(getSupportFragmentManager(), RegisterPasswordDialog.TAG); } } catch (SQLiteException e) { // An exception was raised so the database has indeed been re-keyed. isDatabaseReKeyed = true; // Prompt user to enter her password. if (!isFragmentVisible(EnterPasswordDialog.TAG)) { EnterPasswordDialog enterPasswordDialog = EnterPasswordDialog.create(); enterPasswordDialog.show(getSupportFragmentManager(), EnterPasswordDialog.TAG); } } // Retrieve stored cards. cards = cardDao.getCards(); } // Setup ViewPager. viewPager = (ViewPager) findViewById(R.id.pager); viewPager.setAdapter(new CardFragmentPagerAdapter(this)); viewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { selectedPage = position; // Ensure that the appropriate card is selected for PaymentService. if (position == 0) deselectPaymentCard(); setVisibleOptionsMenuItems(); } @Override public void onPageSelected(int position) { // Ensure that the appropriate card is selected for PaymentService. if (position > 0) selectPaymentCard(position); } @Override public void onPageScrollStateChanged(int state) { } }); } private final BroadcastReceiver apduReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(MainActivity.this); int paymentCardIndex = settings.getInt("paymentCardIndex", 0); String inboundApduDescription = intent.getStringExtra("inboundApduDescription"); boolean transactionInProgress = intent.getBooleanExtra("transactionInProgress", false); if (paymentProgressDialog == null) { paymentProgressDialog = new ProgressDialog(MainActivity.this); paymentProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); paymentProgressDialog.setTitle(R.string.payment_progress_title); } if (transactionInProgress) { paymentProgressDialog.setMessage(inboundApduDescription); paymentProgressDialog.show(); } else { paymentProgressDialog.dismiss(); } } }; @Override public boolean onCreateOptionsMenu(Menu menu) { this.menu = menu; // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); // Set visible menu items. setVisibleOptionsMenuItems(); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); Card card = cards.get(selectedPage); switch (id) { case R.id.action_lock: // Lock database. cardDao.close(); isDatabaseAuthenticated = false; // Deselect card for PaymentService. deselectPaymentCard(); // Show enter password dialog. EnterPasswordDialog enterPasswordDialog = EnterPasswordDialog.create(); enterPasswordDialog.show(getSupportFragmentManager(), EnterPasswordDialog.TAG); break; case R.id.action_change_password: // Show change password dialog. ChangePasswordDialog changePasswordDialog = ChangePasswordDialog.create(); changePasswordDialog.show(getSupportFragmentManager(), ChangePasswordDialog.TAG); break; case R.id.action_rename: // Show rename card dialog. RenameCardDialog renameCardDialog = RenameCardDialog.create(card); renameCardDialog.show(getSupportFragmentManager(), RenameCardDialog.TAG); break; case R.id.action_delete: // Show delete card dialog. DeleteCardDialog deleteCardDialog = DeleteCardDialog.create(card); deleteCardDialog.show(getSupportFragmentManager(), DeleteCardDialog.TAG); break; case R.id.action_import: importCardsFromFile(); break; case R.id.action_export: exportCardsToJsonFile(); break; } return super.onOptionsItemSelected(item); } @Override protected void onResume() { super.onResume(); if (nfcAdapter == null) { NfcAdapterAbsentDialog nfcAdapterAbsentDialog = NfcAdapterAbsentDialog.create(); nfcAdapterAbsentDialog.show(getSupportFragmentManager(), NfcAdapterAbsentDialog.TAG); } else { if (!nfcAdapter.isEnabled()) { NfcAdapterDisabledDialog nfcAdapterDisabledDialog = NfcAdapterDisabledDialog.create(); nfcAdapterDisabledDialog.show(getSupportFragmentManager(), NfcAdapterDisabledDialog.TAG); } nfcAdapter.enableForegroundDispatch(this, nfcIntent, null, nfcTechFilter); } // Ask for password if database is not authenticated. if (isDatabaseReKeyed && !isDatabaseAuthenticated) { // Prompt user to enter her password. if (!isFragmentVisible(EnterPasswordDialog.TAG)) { EnterPasswordDialog enterPasswordDialog = EnterPasswordDialog.create(); enterPasswordDialog.show(getSupportFragmentManager(), EnterPasswordDialog.TAG); } } // Check if PaymentService is the default payment app. checkDefaultPaymentApp(); // Ensure that the appropriate card is selected for PaymentService. selectPaymentCard(selectedPage); } @Override protected void onPause() { super.onPause(); // Ensure that no cards are selected for the PaymentService. deselectPaymentCard(); nfcAdapter.disableForegroundDispatch(this); } @Override protected void onDestroy() { super.onDestroy(); } protected void onNewIntent(Intent intent) { super.onNewIntent(intent); // Intent is only enabled if the user is authenticated. if (isDatabaseAuthenticated) { // Scroll to "Add Card" page. viewPager.setCurrentItem(0, true); // Toast message. toastMessage(getString(R.string.contact_established)); // Read the card. Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); new CardReaderTask(this).execute(tag); } } public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK) { if (requestCode == SELECT_IMPORT_FILE) { Uri selectedFileUri = data.getData(); String selectedFilePath = selectedFileUri.getPath(); File selectedFile = new File(selectedFilePath); importCardsFromFile(selectedFile); } } } /** * Select the currently showing card for the PaymentService. */ public void selectShownCardForPayment() { selectPaymentCard(selectedPage); } /** * Set the PaymentService card to null. */ private void deselectPaymentCard() { selectPaymentCard(0); } /** * Select the card for the PaymentService. * * @param cardIndex The index of the card in static member List<Card> cards. * If the value is zero (0), no card is selected for payment. */ private void selectPaymentCard(int cardIndex) { // Ensure that the appropriate card is selected for PaymentService. SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this); SharedPreferences.Editor settingsEditor = settings.edit(); settingsEditor.putInt("paymentCardIndex", cardIndex); settingsEditor.commit(); } /** * Return whether there are any visible fragments with the given tag. * * @param tag String * @return boolean */ public boolean isFragmentVisible(String tag) { return (getSupportFragmentManager().findFragmentByTag(tag) != null); } public void toastMessage(String message) { Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); } /** * Set options menu items depending on the selected page. */ private void setVisibleOptionsMenuItems() { if (menu == null) return; switch (selectedPage) { // Hide rename and delete card buttons when AddCardFragment is selected. case 0: menu.findItem(R.id.action_rename).setVisible(false); menu.findItem(R.id.action_delete).setVisible(false); break; // Show rename and delete card buttons when CardFragment is selected. default: menu.findItem(R.id.action_rename).setVisible(true); menu.findItem(R.id.action_delete).setVisible(true); } } /** * Refresh the view pager, then scroll to the card with the given PAN. * * @param pan String */ public void refreshViewPager(String pan) { refreshViewPager(); scrollToCard(pan); } /** * Refresh the view pager. */ public void refreshViewPager() { cards = cardDao.getCards(); viewPager.setAdapter(new CardFragmentPagerAdapter(this)); viewPager.getAdapter().notifyDataSetChanged(); } /** * Scroll to the card with the given PAN. * * @param pan String */ private void scrollToCard(String pan) { for (int i = 1; i < cards.size(); i++) { if (cards.get(i).getPan().equals(pan)) { viewPager.setCurrentItem(i, true); return; } } } /** * Import cards from the default JSON file path. */ private void importCardsFromFile() { // Path to exported JSON file. File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS); File file = new File(path, getPackageName() + ".json"); importCardsFromFile(file); } /** * Import cards from the given File. * * @param file File */ private void importCardsFromFile(File file) { // Check if external storage is readable, and return if not. if (!PortHelper.isExternalStorageReadable()) { toastMessage(getString(R.string.import_io_error)); return; } // Try reading the given file. However, if it is not found or if the file contains no // valid cards, allow the user to select a file. try { FileReader fileReader = new FileReader(file); BufferedReader bufferedReader = new BufferedReader(fileReader); Card[] cards = gson.fromJson(bufferedReader, Card[].class); List<Card> validCards = PortHelper.validateCards(cards); bufferedReader.close(); fileReader.close(); if (validCards.size() > 0) { // Prompt the user to select which cards they would like to import. if (!isFragmentVisible(ImportCardsDialog.TAG)) { ImportCardsDialog importCardsDialog = ImportCardsDialog.create(validCards); importCardsDialog.show(getSupportFragmentManager(), ImportCardsDialog.TAG); } } else { toastMessage(getString(R.string.no_valid_cards_found)); selectImportFile(); } } catch (FileNotFoundException e) { selectImportFile(); } catch (JsonSyntaxException e) { toastMessage(getString(R.string.no_valid_cards_found)); selectImportFile(); } catch (IOException e) { toastMessage(e.getMessage()); } } /** * Chose a JSON file for import. */ private void selectImportFile() { toastMessage(getString(R.string.select_cards_file)); Intent intent = new Intent(); intent.setAction(Intent.ACTION_GET_CONTENT); intent.setType("application/json"); startActivityForResult(intent, SELECT_IMPORT_FILE); } /** * Export all cards to the default JSON file path. */ private void exportCardsToJsonFile() { String jsonCards = gson.toJson(cards.subList(1, cards.size())); if (PortHelper.isExternalStorageWritable()) { File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS); File file = new File(path, getPackageName() + ".json"); try { path.mkdirs(); file.createNewFile(); OutputStream outputStream = new FileOutputStream(file); outputStream.write(jsonCards.getBytes()); outputStream.close(); toastMessage(getString(R.string.export_success)); } catch (IOException e) { e.printStackTrace(); toastMessage(getString(R.string.export_io_error)); } } } }