This is the first time I've ever had to post a question so please be patient. I've been working on an NFC application on a Samsung Galaxy S4 running Android 4.4.2. The phone is definitely able to use Host-based Card Emulation (HCE) and I want to use this to communicate with an ACR1252U-A1 NFC reader (Advanced Card Systems) that is in reader/writer mode.
The reader is connected to a PC and I have written a Java application using the javax.smartcardio library to communicate with the reader. So far I have been able to send a SELECT command from the reader to the phone, receive a response from the phone and send subsequent messages back and forth between the reader and the phone. On the Android side I extend the HostApduService class from the Android HCE API. I'm basically playing around with hardware and decided to create an Android application then sends some loyalty card information to a POS system which, in turn, sends back a till number to the device.
However, the communication between the devices only seems to work when the phone is locked. If I unlock the phone (home screen or anything else), my PC attempts to install drivers for what it calls "Smart Card" and it fails (as expected) or it just doesn't connect to the phone. Basically, I just want the Java application to work when the phone is both unlocked and locked.
Here is the main method of my Java app:
public static void main(String[] args) {
try {
TerminalFactory factory = TerminalFactory.getDefault();
List terminals = factory.terminals().list();
System.out.println("Terminals count: " + terminals.size());
System.out.println("Terminals: " + terminals);
// Get the first terminal in the list
CardTerminal terminal = (CardTerminal) terminals.get(0);
System.out.println("Using terminal: " + terminal);
System.out.println("Waiting for card present...");
terminal.waitForCardPresent(2000);
if (terminal.isCardPresent()) {
System.out.println("Card present!");
}
// Establish a connection with the card using
// "T=0", "T=1", "T=CL" or "*"
Card card = terminal.connect("*");
System.out.println("Card: " + card);
// Get ATR
byte[] baATR = card.getATR().getBytes();
System.out.println("ATR: " + TestSmartCardIO.toString(baATR));
CardChannel channel = card.getBasicChannel();
// Setup terminal device settings (i.e. buzzer and LED)
byte[] data = { (byte) 0xE0, (byte) 0x00, (byte) 0x00, (byte) 0x21,
(byte) 0x01, (byte) 0x77 };
System.out.println("Setting up terminal device...");
card.transmitControlCommand(
IOCTL_SMARTCARD_ACR1251_ACR1252_ESCAPE_COMMAND, data);
/*
* SELECT Command See GlobalPlatform Card Specification (e.g. 2.2,
* section 11.9) CLA: 00 INS: A4 P1: 04 i.e. b3 is set to 1, means
* select by name P2: 00 i.e. first or only occurence Lc: 08 i.e.
* length of AID see below Data: A0 00 00 00 03 00 00 00 AID of the
* card manager
*/
// Create select to select the correct Android application.
System.out.println("Sending SELECT command...");
byte[] selectAidApdu = createSelectAidApdu(AID_ANDROID);
System.out.println("APDU >>: "
+ TestSmartCardIO.toString(selectAidApdu));
ResponseAPDU response = channel.transmit(new CommandAPDU(
selectAidApdu));
System.out.println("APDU <<: "
+ TestSmartCardIO.toString(response.getBytes()));
// Check response to ensure successful.
if (response.getSW() == SW_OK) {
System.out.println("Selection successful.");
String ssNumber = new String(response.getData());
System.out.println("SS Number : " + ssNumber);
// Send another message to device.
System.out.println("Sending Till number.");
byte[] message = { (byte) 0x00, (byte) TILL_ID };
byte[] messageAidApdu = createMessageApdu(message);
System.out.println("APDU >>: "
+ TestSmartCardIO.toString(messageAidApdu));
response = channel.transmit(new CommandAPDU(messageAidApdu));
if (response.getSW() == SW_OK) {
System.out.println("APDU <<: "
+ TestSmartCardIO.toString(response.getBytes()));
String ack = new String(response.getData());
System.out.println("Received : " + ack);
} else {
System.out.println("SW1: " + response.getSW1());
System.out.println("SW2: " + response.getSW2());
}
} else {
System.out.println("SW1: " + response.getSW1());
System.out.println("SW2: " + response.getSW2());
}
// Disconnect
// true: reset the card after disconnecting card.
card.disconnect(true);
} catch (CardException e) {
e.printStackTrace();
}
}
And here is my service on the Android device:
import java.util.Arrays;
import android.content.Intent;
import android.content.SharedPreferences;
import android.nfc.cardemulation.HostApduService;
import android.os.Bundle;
import android.util.Log;
public class MyHostApduService extends HostApduService {
private static final String TAG = "CardService";
// AID for our loyalty card service.
private static final String SAMPLE_LOYALTY_CARD_AID = "F0010203040506";
// ISO-DEP command HEADER for selecting an AID.
// Format: [Class | Instruction | Parameter 1 | Parameter 2]
private static final String SELECT_APDU_HEADER = "00A40400";
private static final String PUT_DATA_APDU_HEADER = "00DA0000";
// "OK" status word sent in response to SELECT AID command (0x9000)
private static final byte[] SELECT_OK_SW = HexStringToByteArray("9000");
// "UNKNOWN" status word sent in response to invalid APDU command (0x0000)
private static final byte[] UNKNOWN_CMD_SW = HexStringToByteArray("0000");
private static final byte[] SELECT_APDU = BuildSelectApdu(SAMPLE_LOYALTY_CARD_AID);
public static final String PREFS_NAME = "MyPrefsFile";
public static final String SS_NUMBER = "ssNumber";
public static final String TILL_NUMBER = "tillNumber";
/**
* Called if the connection to the NFC card is lost, in order to let the
* application know the cause for the disconnection (either a lost link, or
* another AID being selected by the reader).
*
* @param reason
* Either DEACTIVATION_LINK_LOSS or DEACTIVATION_DESELECTED
*/
@Override
public void onDeactivated(int reason) {
}
/**
* This method will be called when a command APDU has been received from a
* remote device. A response APDU can be provided directly by returning a
* byte-array in this method. In general response APDUs must be sent as
* quickly as possible, given the fact that the user is likely holding his
* device over an NFC reader when this method is called.
*
* <p class="note">
* If there are multiple services that have registered for the same AIDs in
* their meta-data entry, you will only get called if the user has
* explicitly selected your service, either as a default or just for the
* next tap.
*
* <p class="note">
* This method is running on the main thread of your application. If you
* cannot return a response APDU immediately, return null and use the
* {@link #sendResponseApdu(byte[])} method later.
*
* @param commandApdu
* The APDU that received from the remote device
* @param extras
* A bundle containing extra data. May be null.
* @return a byte-array containing the response APDU, or null if no response
* APDU can be sent at this point.
*/
@Override
public byte[] processCommandApdu(byte[] commandApdu, Bundle extras) {
Log.i(TAG, "Received APDU: " + ByteArrayToHexString(commandApdu));
// Copy command section to determine type of command.
byte[] command = new byte[4];
System.arraycopy(commandApdu, 0, command, 0, 4);
// If the APDU matches the SELECT AID command for this service,
// send the loyalty card account number.
Log.i(TAG, "Command String: " + ByteArrayToHexString(command));
if (Arrays.equals(SELECT_APDU, commandApdu)) {
// Retrieve stored SS number.
SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0);
String account = settings.getString(SS_NUMBER, "00000000");
byte[] accountBytes = account.getBytes();
Log.i(TAG, "Application Selected. Sending account number: "
+ account);
return ConcatArrays(accountBytes, SELECT_OK_SW);
} else if (PUT_DATA_APDU_HEADER.equals(ByteArrayToHexString(command))) {
int dataLength = commandApdu[4];
byte[] data = new byte[dataLength];
System.arraycopy(commandApdu, 5, data, 0, dataLength);
int tillNumber = Integer.parseInt(ByteArrayToHexString(data), 16);
Log.i(TAG, "Till Number: " + tillNumber);
String ack = "ACK";
byte[] ackBytes = ack.getBytes();
Intent i = new Intent();
i.setClass(this, MainActivity.class);
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
i.putExtra(TILL_NUMBER, tillNumber);
startActivity(i);
return ConcatArrays(ackBytes, SELECT_OK_SW);
} else {
return UNKNOWN_CMD_SW;
}
}
/**
* Build APDU for SELECT AID command. This command indicates which service a
* reader is interested in communicating with. See ISO 7816-4.
*
* @param aid
* Application ID (AID) to select
* @return APDU for SELECT AID command
*/
public static byte[] BuildSelectApdu(String aid) {
// Format: [CLASS | INSTRUCTION | PARAMETER 1 | PARAMETER 2 | LENGTH |
// DATA]
return HexStringToByteArray(SELECT_APDU_HEADER
+ String.format("%02X", aid.length() / 2) + aid);
}
public static byte[] BuildCommandApdu(String command) {
// Format: [CLASS | INSTRUCTION | PARAMETER 1 | PARAMETER 2 | LENGTH |
// DATA]
return HexStringToByteArray(command);
}
/**
* Utility method to convert a byte array to a hexadecimal string.
*
* @param bytes
* Bytes to convert
* @return String, containing hexadecimal representation.
*/
public static String ByteArrayToHexString(byte[] bytes) {
final char[] hexArray = { '0', '1', '2', '3', '4', '5', '6', '7', '8',
'9', 'A', 'B', 'C', 'D', 'E', 'F' };
char[] hexChars = new char[bytes.length * 2]; // Each byte has two hex
// characters (nibbles)
int v;
for (int j = 0; j < bytes.length; j++) {
v = bytes[j] & 0xFF; // Cast bytes[j] to int, treating as unsigned
// value
hexChars[j * 2] = hexArray[v >>> 4]; // Select hex character from
// upper nibble
hexChars[j * 2 + 1] = hexArray[v & 0x0F]; // Select hex character
// from lower nibble
}
return new String(hexChars);
}
/**
* Utility method to convert a hexadecimal string to a byte string.
*
* <p>
* Behavior with input strings containing non-hexadecimal characters is
* undefined.
*
* @param s
* String containing hexadecimal characters to convert
* @return Byte array generated from input
* @throws java.lang.IllegalArgumentException
* if input length is incorrect
*/
public static byte[] HexStringToByteArray(String s)
throws IllegalArgumentException {
int len = s.length();
if (len % 2 == 1) {
throw new IllegalArgumentException(
"Hex string must have even number of characters");
}
byte[] data = new byte[len / 2]; // Allocate 1 byte per 2 hex characters
for (int i = 0; i < len; i += 2) {
// Convert each character into a integer (base-16), then bit-shift
// into place
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character
.digit(s.charAt(i + 1), 16));
}
return data;
}
/**
* Utility method to concatenate two byte arrays.
*
* @param first
* First array
* @param rest
* Any remaining arrays
* @return Concatenated copy of input arrays
*/
public static byte[] ConcatArrays(byte[] first, byte[]... rest) {
int totalLength = first.length;
for (byte[] array : rest) {
totalLength += array.length;
}
byte[] result = Arrays.copyOf(first, totalLength);
int offset = first.length;
for (byte[] array : rest) {
System.arraycopy(array, 0, result, offset, array.length);
offset += array.length;
}
return result;
}
}
You might also want to see my aid.xml file to see my AID number:
<?xml version="1.0" encoding="utf-8"?>
<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/service_name"
android:requireDeviceUnlock="false">
<aid-group android:description="@string/SS_title" android:category="other">
<aid-filter android:name="F0010203040506"/>
</aid-group>
</host-apdu-service>
This is the output log for a successful connection on the Java app:
Terminals count: 2
Terminals: [PC/SC terminal ACS ACR1252 1S CL Reader PICC 0, PC/SC terminal
ACS ACR12521S CL Reader SAM 0]
Using terminal: PC/SC terminal ACS ACR1252 1S CL Reader PICC 0
Waiting for card present...
Card present!
Card: PC/SC card in ACS ACR1252 1S CL Reader PICC 0, protocol T=1, state OK
ATR: 3B808011
Setting up terminal device...
Sending SELECT command...
APDU >>: 0A4407F0123456
APDU <<: 313132323333343435900
Selection successful.
SS Number : 112233445
Sending Till number.
APDU >>: 0DA00203
APDU <<: 41434B900
Received : ACK
This is the output log for an unsuccessful connection on the Java app:
Terminals count: 2
Terminals: [PC/SC terminal ACS ACR1252 1S CL Reader PICC 0, PC/SC terminal ACS
ACR1252 1S CL Reader SAM 0]
Using terminal: PC/SC terminal ACS ACR1252 1S CL Reader PICC 0
Waiting for card present...
Card present!
Card: PC/SC card in ACS ACR1252 1S CL Reader PICC 0, protocol T=1, state OK
ATR: 3B8F801804FCA000361103B000042
Setting up terminal device...
Sending SELECT command...
APDU >>: 0A4407F0123456
APDU <<: 641
SW1: 100
SW2: 1
Please let me know if there is anything else I can do or if I have omitted something that will help to answer my question. Thanks!
This is an old post but it still might be a reference to some. Something happened on the SELECT command that removed padding zeros.
The SELECT command appears as
While it should be (without spaces, just to see the difference easily)