Talk to your Credit Card (Android NFC Java)

AndroidCrypto
14 min readApr 12, 2023

--

This is an article series about reading a Credit Card (or in general a Payment Card) with your Android device. Im using Java as framework in Android Studio.

image of a sample credit card
Airodyssey at English Wikipedia, CC BY-SA 3.0 http://creativecommons.org/licenses/by-sa/3.0/, via Wikimedia Commons

As there are only 5 to 7 steps to get informations like the Credit Card number (called “Primary Account Number” or “PAN”) or card’s Expiration Date I split up the complete story into single articles covering each step. Each step is accompanied by a link to the step specific GitHub repository so you can easily follow the steps with the complete source code. The repository of the (final) seventh step has the compiled app available as APK file.

In general, a Credit Card readable by a contactless reader (NFC) follows the EMV specifications written in a lot of books, all available from the specification holder’s website https://www.emvco.com/specifications/. As the specifications provide a lot of information it can be a pain to go through them all to understand what you need to do.

When using “Credit Card” I mean all types of payment cards that are accessible using the NFC technology, e.g. a German “GiroCard”. As there maybe are sub-specifications for each type of card I can test my workflow only on card I own - the complete workflow is tested with these cards:

  • American Express type “Credit”
  • MasterCard types “Credit” and “Debit”
  • VisaCard types “Credit” and “Debit”
  • German GiroCards type “Debit”

These are the steps to read a payment card, it is a kind of “question & answer” workflow:

  1. ask the card which applications are available on the card (“select PPSE”)(“Paypass Payment System Environment”)
  2. analyze the card’s response and identify one or more of the application number or application id (“AID”)
  3. select one application on the card to work with (“select AID”) [or iterate through the applications and run the following steps for each application]
  4. analyze the card’s response to find out what data the card needs to proceed (find the “processing options data object list” (PDOL))
  5. analyze the card’s response and get the content of the element “Application File Locator” (AFL) list
  6. read all files given in the AFL list and find the file where there are the elements “Application Primary Account Number” and “Application Expiration Date”
  7. find and print out the “Application Primary Account Number” (“PAN”) = card number and “Application Expiration Date” = expiration date of the card.

At this point I’m placing two serious warnings: Please use a card that is outdated or terminated for your tests as you may damage your card irrevocably. Second: do not publish or send the the card data revealed by this app using an email as it may contain confidential data. Some data elements contain e.g. the card number in encrypted form so you can’t see that there are confidential data.

Before we are starting our journey I give you some information on the “empty” card reader application (part 0) that will be the basis of our work.

To run the code we do need two permissions granted in the AndroidManifest.xml:

<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.VIBRATE" />

The first one is to enable NFC support in our project and the second to enable an “card is read” indication vibration.

We do need 3 dependencies to run the app:

  1. BER-TLV: as we need to search for and extract “Tag-Length-Value” data we do need this library
  2. EMV-NFC-Paycard-Enrollment: although this library is a fully card reading library we use the library for nice “pretty-printing” of data we recieve as respond from our Credit Card
  3. Android-About-Page: just a convenience library to sho an “About” page

build.gradle (app):

    // parsing BER-TLV encoded data, e.g. a credit card
// source: https://github.com/evsinev/ber-tlv
implementation 'com.payneteasy:ber-tlv:1.0-11'

// pretty printing of card's responses
// source: https://github.com/devnied/EMV-NFC-Paycard-Enrollment
implementation 'com.github.devnied.emvnfccard:library:3.0.1'

// implementing an about page
implementation 'io.github.medyo:android-about-page:2.0.0'

The layout of the app is very simple — we just need two EditText components to display the reader’s “dataFound” and “logfile” and a progress bar indicating a running read process. Additionally there is a toolbar that has a small menu for copying the logFile data to the ClipBoard, save the content to a file, show the dependencies licenses and showing an “about” page:

activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">

<androidx.appcompat.widget.Toolbar
android:id="@+id/main_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorPrimary"
android:elevation="@dimen/toolbar_elevation"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />

<TextView
android:id="@+id/tvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="Talk to your CreditCard"
android:textAlignment="center"
android:textSize="20sp"
android:textStyle="bold" />

<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:orientation="vertical">

<TextView
android:id="@+id/tv1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textAlignment="center"
android:textSize="16sp"
android:background="@drawable/round_rect_shape"
android:text="we do need permissions for NFC and Vibration" />

<LinearLayout
android:id="@+id/loading_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical"
android:visibility="gone">

<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="read data - do not move the card" />

</LinearLayout>

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/etDataLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="Data:"
android:visibility="visible"
app:boxCornerRadiusBottomEnd="5dp"
app:boxCornerRadiusBottomStart="5dp"
app:boxCornerRadiusTopEnd="5dp"
app:boxCornerRadiusTopStart="5dp"
app:endIconMode="clear_text">

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:focusable="false"
android:fontFamily="monospace"
android:text=""
android:textSize="14sp"
android:visibility="visible"
tools:ignore="KeyboardInaccessibleWidget" />
</com.google.android.material.textfield.TextInputLayout>

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/etLogLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="Log:"
android:visibility="visible"
app:boxCornerRadiusBottomEnd="5dp"
app:boxCornerRadiusBottomStart="5dp"
app:boxCornerRadiusTopEnd="5dp"
app:boxCornerRadiusTopStart="5dp"
app:endIconMode="clear_text">

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etLog"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:focusable="false"
android:fontFamily="monospace"
android:text=""
android:textSize="14sp"
android:visibility="visible"
tools:ignore="KeyboardInaccessibleWidget" />
</com.google.android.material.textfield.TextInputLayout>

<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:dividerInsetEnd="16dp"
app:dividerInsetStart="16dp"
app:dividerThickness="4dp" />

</LinearLayout>
</ScrollView>

</LinearLayout>

menu_activity_main.xml:

<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">

<item
android:id="@+id/action_copy_data"
android:title="copy data"
app:showAsAction="never"/>

<item
android:id="@+id/action_export_text_file"
android:title="Export text File"
app:showAsAction="never"/>

<item
android:id="@+id/action_licenses"
android:title="Licenses"
app:showAsAction="never"/>

<item
android:id="@+id/action_about"
android:title="About the app"
app:showAsAction="never"/>

</menu>

The res/raw folder contains two MP3-files for the “ping”-ringtones that are played when reading of the card starts and ends.

The MainActivity.java class implements NfcAdapter.ReaderCallback so I’m using the NFC Reader mode to get access to the card. This way the application is not started by an intent when an NFC tag is detected — the read process starts only when the application is in the foreground. On the other hand this process is more reliable because our app get’s the “first access” to the data.

MainActivity.java:

package de.androidcrypto.talktoyourcreditcard;

import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.media.MediaPlayer;
import android.net.Uri;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.nfc.tech.IsoDep;
import android.os.Build;
import android.os.Bundle;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.webkit.WebView;
import android.widget.TextView;
import android.widget.Toast;

import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;

import com.github.devnied.emvnfccard.enums.CommandEnum;
import com.github.devnied.emvnfccard.exception.CommunicationException;
import com.github.devnied.emvnfccard.iso7816emv.EmvTags;
import com.github.devnied.emvnfccard.iso7816emv.impl.DefaultTerminalImpl;
import com.github.devnied.emvnfccard.utils.CommandApdu;
import com.github.devnied.emvnfccard.utils.TlvUtil;
import com.payneteasy.tlv.BerTag;
import com.payneteasy.tlv.BerTlv;
import com.payneteasy.tlv.BerTlvParser;
import com.payneteasy.tlv.BerTlvs;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;

import mehdi.sakout.aboutpage.AboutPage;
import mehdi.sakout.aboutpage.Element;

public class MainActivity extends AppCompatActivity implements NfcAdapter.ReaderCallback {

private final String TAG = "MainAct";
private com.google.android.material.textfield.TextInputEditText etData, etLog;
private View loadingLayout;
private NfcAdapter mNfcAdapter;

private String outputString = ""; // used for the UI output
private String exportString = ""; // used for exporting the log to a text file
private String exportStringFileName = "emv.html";
private final String stepSeparatorString = "*********************************";
private final String lineSeparatorString = "---------------------------------";
Context context;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar myToolbar = (Toolbar) findViewById(R.id.main_toolbar);
setSupportActionBar(myToolbar);

etData = findViewById(R.id.etData);
etLog = findViewById(R.id.etLog);
loadingLayout = findViewById(R.id.loading_layout);

context = getApplicationContext();

mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
}

/**
* section for NFC
*/

/**
* This method is run in another thread when a card is discovered
* This method cannot cannot direct interact with the UI Thread
* Use `runOnUiThread` method to change the UI from this method
*
* @param tag discovered tag
*/

@Override
public void onTagDiscovered(Tag tag) {
clearData();
Log.d(TAG, "NFC tag discovered");
writeToUiAppend("NFC tag discovered");
playSinglePing();
setLoadingLayoutVisibility(true);
byte[] tagId = tag.getId();
writeToUiAppend("TagId: " + bytesToHexNpe(tagId));
String[] techList = tag.getTechList();
writeToUiAppend("TechList found with these entries:");
boolean isoDepInTechList = false;
for (String s : techList) {
writeToUiAppend(s);
if (s.equals("android.nfc.tech.IsoDep")) isoDepInTechList = true;
}
// proceed only if tag has IsoDep in the techList
if (isoDepInTechList) {
IsoDep nfc = null;
nfc = IsoDep.get(tag);
if (nfc != null) {
try {
nfc.connect();
Log.d(TAG, "connection with card success");
writeToUiAppend("connection with card success");
// here we are going to start our journey through the card

printStepHeader(0, "our journey begins");
writeToUiAppend(etData, "00 reading of the card started");

writeToUiAppend("increase IsoDep timeout for long lasting reading");
writeToUiAppend("timeout old: " + nfc.getTimeout() + " ms");
nfc.setTimeout(10000);
writeToUiAppend("timeout new: " + nfc.getTimeout() + " ms");


/**
* step 1 code start
*/



/**
* step 1 code end
*/

printStepHeader(99, "our journey ends");
writeToUiAppend(etData, "99 reading of the card completed");
vibrate();
} catch (IOException e) {
writeToUiAppend("connection with card failure");
writeToUiAppend(e.getMessage());
// throw new RuntimeException(e);
startEndSequence(nfc);
return;
}
}
} else {
// if (isoDepInTechList) {
writeToUiAppend("The discovered NFC tag does not have an IsoDep interface.");
}
// final cleanup
playDoublePing();
writeToUiFinal(etLog);
setLoadingLayoutVisibility(false);
}



/**
* section for emv reading
*/


/**
* add blanks to a string on right side up to a length of len
* if the data.length >= len one character is deleted to get minimum one blank
* @param data
* @param len
* @return
*/
private String trimStringRight(String data, int len) {
if (data.length() >= len) {
data = data.substring(0, (len - 1));
}
while (data.length() < len) {
data = data + " ";
}
return data;
}

/**
* checks if the response has an 0x'9000' at the end means success
* and the method returns the data without 0x'9000' at the end
* if any other trailing bytes show up the method returns NULL
*
* @param data
* @return
*/
private byte[] checkResponse(@NonNull byte[] data) {
// simple sanity check
if (data.length < 5) {
return null;
} // not ok
int status = ((0xff & data[data.length - 2]) << 8) | (0xff & data[data.length - 1]);
if (status != 0x9000) {
return null;
} else {
return Arrays.copyOfRange(data, 0, data.length - 2);
}
}

/**
* section for conversion utils
*/

/**
* converts a byte array to a hex encoded string
* This method is Null Pointer Exception (NPE) safe
*
* @param bytes
* @return hex encoded string
*/
public static String bytesToHexNpe(byte[] bytes) {
if (bytes != null) {
StringBuffer result = new StringBuffer();
for (byte b : bytes)
result.append(Integer.toString((b & 0xff) + 0x100, 16).substring(1));
return result.toString();
} else {
return "";
}
}

/**
* converts a byte array to a hex encoded string
* This method is Null Pointer Exception (NPE) safe
* @param bytes
* @return hex encoded string with a blank after each value
*/
public static String bytesToHexBlankNpe(byte[] bytes) {
if (bytes == null) return "";
StringBuffer result = new StringBuffer();
for (byte b : bytes) result.append(Integer.toString((b & 0xff) + 0x100, 16).substring(1)).append(" ");
return result.toString();
}

/**
* converts a hex encoded string to a byte array
*
* @param str
* @return
*/
public static byte[] hexToBytes(String str) {
byte[] bytes = new byte[str.length() / 2];
for (int i = 0; i < bytes.length; i++) {
bytes[i] = (byte) Integer.parseInt(str.substring(2 * i, 2 * i + 2),
16);
}
return bytes;
}

/**
* converts a byte to int
*
* @param b
* @return
*/
public static int byteToInt(byte b) {
return (int) b & 0xFF;
}

public static int intFromByteArray(byte[] bytes) {
return new BigInteger(bytes).intValue();
}

/**
* converts a byte to its hex string representation
* @param data
* @return
*/
public static String byteToHex(byte data) {
int hex = data & 0xFF;
return Integer.toHexString(hex);
}

/**
* converts an integer to a byte array
*
* @param value
* @return
*/
public static byte[] intToByteArray(int value) {
return new BigInteger(String.valueOf(value)).toByteArray();
}

/**
* splits a byte array in chunks
*
* @param source
* @param chunksize
* @return a List<byte[]> with sets of chunksize
*/
private static List<byte[]> divideArray(byte[] source, int chunksize) {
List<byte[]> result = new ArrayList<byte[]>();
int start = 0;
while (start < source.length) {
int end = Math.min(source.length, start + chunksize);
result.add(Arrays.copyOfRange(source, start, end));
start += chunksize;
}
return result;
}

/**
* section for NFC
*/

private void showWirelessSettings() {
Toast.makeText(this, "You need to enable NFC", Toast.LENGTH_SHORT).show();
Intent intent = new Intent(Settings.ACTION_WIRELESS_SETTINGS);
startActivity(intent);
}

@Override
protected void onResume() {
super.onResume();
if (mNfcAdapter != null) {
if (!mNfcAdapter.isEnabled())
showWirelessSettings();
Bundle options = new Bundle();
// Work around for some broken Nfc firmware implementations that poll the card too fast
options.putInt(NfcAdapter.EXTRA_READER_PRESENCE_CHECK_DELAY, 250);
// Enable ReaderMode for all types of card and disable platform sounds
// the option NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK is NOT set
// to get the data of the tag afer reading
mNfcAdapter.enableReaderMode(this,
this,
NfcAdapter.FLAG_READER_NFC_A |
NfcAdapter.FLAG_READER_NFC_B |
NfcAdapter.FLAG_READER_NFC_F |
NfcAdapter.FLAG_READER_NFC_V |
NfcAdapter.FLAG_READER_NFC_BARCODE |
NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS,
options);
}
}

/**
* important is the disabling of the ReaderMode when activity is pausing
*/

@Override
protected void onPause() {
super.onPause();
if (mNfcAdapter != null)
mNfcAdapter.disableReaderMode(this);
}

/**
* section for UI
*/

/**
* shows a progress bar as long as the reading lasts
*
* @param isVisible
*/

private void setLoadingLayoutVisibility(boolean isVisible) {
runOnUiThread(() -> {
if (isVisible) {
loadingLayout.setVisibility(View.VISIBLE);
} else {
loadingLayout.setVisibility(View.GONE);
}
});
}

/**
* vibrate
*/
private void vibrate() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
((Vibrator) getSystemService(VIBRATOR_SERVICE)).vibrate(VibrationEffect.createOneShot(150, 10));
} else {
Vibrator v = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
v.vibrate(200);
}
}

/**
* Sound files downloaded from Material Design Sounds
* https://m2.material.io/design/sound/sound-resources.html
*
*/
private void playSinglePing() {
MediaPlayer mp = MediaPlayer.create(this, R.raw.notification_decorative_02);
mp.start();
}

private void playDoublePing() {
MediaPlayer mp = MediaPlayer.create(this, R.raw.notification_decorative_01);
mp.start();
}

private void startEndSequence(IsoDep nfc) {
playDoublePing();
writeToUiFinal(etLog);
setLoadingLayoutVisibility(false);
vibrate();
try {
nfc.close();
} catch (IOException e) {
// throw new RuntimeException(e);
}
return;
}

/**
* prints a nice header for each step
*
* @param step
* @param message
*/
private void printStepHeader(int step, String message) {
// message should not extend 29 characters, longer messages will get trimmed
String emptyMessage = " ";
StringBuilder sb = new StringBuilder();
sb.append(outputString); // has already a line feed at the end
sb.append("").append("\n");
sb.append(stepSeparatorString).append("\n");
sb.append("************ step ").append(String.format("%02d", step)).append(" ************").append("\n");
sb.append("* ").append((message + emptyMessage).substring(0, 29)).append(" *").append("\n");
sb.append(stepSeparatorString).append("\n");
outputString = sb.toString();
}

/**
* used for printing the card responses in a human readable format to a string
*
* @param responseData
* @return
*/
private String prettyPrintDataToString(byte[] responseData) {
StringBuilder sb = new StringBuilder();
sb.append("------------------------------------").append("\n");
sb.append(trimLeadingLineFeeds(TlvUtil.prettyPrintAPDUResponse(responseData))).append("\n");
sb.append("------------------------------------").append("\n");
return sb.toString();
}

/**
* trim leading line feeds if existing
*
* @param input
* @return
*/
public static String trimLeadingLineFeeds(String input) {
String[] output = input.split("^\\n+", 2);
return output.length > 1 ? output[1] : output[0];
}

private void clearData() {
runOnUiThread(() -> {
outputString = "";
exportString = "";
etData.setText("");
etLog.setText("");
});
}

private void writeToUiAppend(String message) {
//System.out.println(message);
outputString = outputString + message + "\n";
}

private void writeToUiAppend(final TextView textView, String message) {
runOnUiThread(() -> {
if (TextUtils.isEmpty(textView.getText().toString())) {
if (textView == (TextView) etLog) {
} else {
textView.setText(message);
}
} else {
String newString = textView.getText().toString() + "\n" + message;
if (textView == (TextView) etLog) {
} else {
textView.setText(newString);
}
}
});
}

private void writeToUiFinal(final TextView textView) {
if (textView == (TextView) etLog) {
runOnUiThread(new Runnable() {
@Override
public void run() {
textView.setText(outputString);
System.out.println(outputString); // print the data to console
}
});
}
}

private void provideTextViewDataForExport(TextView textView) {
exportString = textView.getText().toString();
}

private void writeToUiToast(String message) {
runOnUiThread(() -> {
Toast.makeText(getApplicationContext(),
message,
Toast.LENGTH_SHORT).show();
});
}

/**
* section OptionsMenu export text file methods
*/

private void exportTextFile() {
provideTextViewDataForExport(etLog);
if (exportString.isEmpty()) {
writeToUiToast("Scan a tag first before writing files :-)");
return;
}
writeStringToExternalSharedStorage();
}

private void writeStringToExternalSharedStorage() {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
// Optionally, specify a URI for the file that should appear in the
// system file picker when it loads.
// boolean pickerInitialUri = false;
// intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri);
// get filename from edittext
String filename = exportStringFileName;
// sanity check
if (filename.equals("")) {
writeToUiToast("scan a tag before writing the content to a file :-)");
return;
}
intent.putExtra(Intent.EXTRA_TITLE, filename);
selectTextFileActivityResultLauncher.launch(intent);
}

ActivityResultLauncher<Intent> selectTextFileActivityResultLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
new ActivityResultCallback<ActivityResult>() {
@Override
public void onActivityResult(ActivityResult result) {
if (result.getResultCode() == Activity.RESULT_OK) {
// There are no request codes
Intent resultData = result.getData();
// The result data contains a URI for the document or directory that
// the user selected.
Uri uri = null;
if (resultData != null) {
uri = resultData.getData();
// Perform operations on the document using its URI.
try {
// get file content from edittext
String fileContent = exportString;
System.out.println("## data to write: " + exportString);
writeTextToUri(uri, fileContent);
writeToUiToast("file written to external shared storage: " + uri.toString());
} catch (IOException e) {
e.printStackTrace();
writeToUiToast("ERROR: " + e.toString());
return;
}
}
}
}
});

private void writeTextToUri(Uri uri, String data) throws IOException {
try {
System.out.println("** data to write: " + data);
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(getApplicationContext().getContentResolver().openOutputStream(uri));
outputStreamWriter.write(data);
outputStreamWriter.close();
} catch (IOException e) {
System.out.println("Exception File write failed: " + e.toString());
}
}

/**
* options menu show licenses
*/

// run: displayLicensesAlertDialog();
// display licenses dialog see: https://bignerdranch.com/blog/open-source-licenses-and-android/
private void displayLicensesAlertDialog() {
WebView view = (WebView) LayoutInflater.from(this).inflate(R.layout.dialog_licenses, null);
view.loadUrl("file:///android_asset/open_source_licenses.html");
android.app.AlertDialog mAlertDialog = new android.app.AlertDialog.Builder(MainActivity.this).create();
mAlertDialog = new android.app.AlertDialog.Builder(this, R.style.Theme_TalkToYourCreditCard)
.setTitle(getString(R.string.action_licenses))
.setView(view)
.setPositiveButton(android.R.string.ok, null)
.show();
}

/**
* section for OptionsMenu
*/

@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_activity_main, menu);

MenuItem mCopyData = menu.findItem(R.id.action_copy_data);
mCopyData.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(@NonNull MenuItem menuItem) {
ClipboardManager clipboard = (ClipboardManager)
getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("BasicNfcEmvReader", etLog.getText());
clipboard.setPrimaryClip(clip);
// show toast only on Android versions < 13
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2)
Toast.makeText(getApplicationContext(), "copied", Toast.LENGTH_SHORT).show();
return false;
}
});

MenuItem mExportTextFile = menu.findItem(R.id.action_export_text_file);
mExportTextFile.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
Log.i(TAG, "mExportTextFile");
exportTextFile();
return false;
}
});

MenuItem mLicenses = menu.findItem(R.id.action_licenses);
mLicenses.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
Log.i(TAG, "mLicenses");
displayLicensesAlertDialog();
return false;
}
});

MenuItem mAbout = menu.findItem(R.id.action_about);
mAbout.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
Log.i(TAG, "mLicenses");
CharSequence description = "This is the basic app for the Talk to your Credit Card application";
View aboutPage = new AboutPage(context)
.isRTL(false)
.setDescription(getString(R.string.app_description))
//.setCustomFont(String) // or Typeface
.setImage(R.drawable.ic_launcher_playstore)
.addItem(new Element().setTitle("Version 1.0"))
.addGroup("Connect with us")
.addEmail("androidcrypto@gmx.de")
.addWebsite("https://medium.com/@androidcrypto")
.addGitHub("androidcrypto")
.addItem(getCopyRightsElement())
.create();
setContentView(aboutPage);
return false;
}
});

return super.onCreateOptionsMenu(menu);
}

Element getCopyRightsElement() {
Element copyRightsElement = new Element();
final String copyrights = String.format(getString(R.string.copy_right), Calendar.getInstance().get(Calendar.YEAR));
copyRightsElement.setTitle(copyrights);
copyRightsElement.setIconDrawable(R.drawable.about_icon_copy_right);
copyRightsElement.setAutoApplyIconTint(true);
copyRightsElement.setIconTint(mehdi.sakout.aboutpage.R.color.about_item_icon_color);
copyRightsElement.setIconNightTint(android.R.color.white);
copyRightsElement.setGravity(Gravity.CENTER);
copyRightsElement.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, copyrights, Toast.LENGTH_SHORT).show();
}
});
return copyRightsElement;
}
}

Last but not least I’m using 3 additional dependencies for our work:

  1. to analyze the card’s reading response I’m using the library “BER-TLV parser and builder” written by Evgeniy (evsinev). Find the source code here.
  2. for pretty printing of the card’s response I use the “EMV-NFC-Paycard-Enrollment” written by Julien Millau (devnied). Find the source code here.
  3. for showing the “About this app” page I’m using the library “Android About Page” written by Mehdi Sakout (medyo). Find the source code here.

The code should run on devices from Android 5 to Android 13+ and is tested on real devices with Android 5.0.1 SDK Level 21 LOLLIPOP = L, Android 8.0.0 SDK Level 26 = O and Android 13 SDK Level 33 Tiramisu = T.

When running the Basic app and tapping a credit card to the device’s NFC card reader the app recognizes the NFC tag and just logs some information:

NFC tag discovered
TagId: 028eedb17074b0
TechList found with these entries:
android.nfc.tech.IsoDep
android.nfc.tech.NfcA
connection with card success

*********************************
************ step 00 ************
* our journey begins *
*********************************
increase IsoDep timeout for long reading
timeout old: 2000 ms
timeout new: 10000 ms

This information shows us that a) our Android device has enabled NFC capabilities, b) the NFC chip on the tag (Credit Card) could get read by the devices NFC system and c) — most important — that we get a connection to the card using the IsoDep class that is the basis for all further card readings. After a successful connect I’m increasing the timeout to not run in any error.

Minimal out after tapping a Credit Card to the NFC reader

The complete app code is available in my GitHub repository “TalkToYourCreditCard part 0”: TalkToYourCreditCardPart0

An additional note on the following steps: As all of the source codes run in the same package “de.androidcrypto.talktoyourcreditcard“ you will overwrite the previous app with the new app (step).

Go to the next article: step 1: ask the card which applications are available on the card (“select PPSE”)

--

--