Skip to content

Commit 0f99d90

Browse files
committed
✨ apdu service by using Type 4
1 parent c9e10d9 commit 0f99d90

File tree

6 files changed

+338
-37
lines changed

6 files changed

+338
-37
lines changed

app/src/main/AndroidManifest.xml

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
android:supportsRtl="true"
1919
android:theme="@style/AppTheme"
2020
tools:ignore="AllowBackup,GoogleAppIndexingWarning">
21-
<activity android:name=".cardEmulatorActivity">
21+
<activity android:name=".CardEmulatorActivity">
2222
<intent-filter>
2323
<action android:name="android.intent.action.MAIN"/>
2424

@@ -58,7 +58,7 @@
5858
<meta-data android:name="android.nfc.cardemulation.off_host_apdu_service"
5959
android:resource="@xml/apduservice"/>
6060
</service>-->
61-
<service android:name=".cardEmulation.MyHostApduService"
61+
<service android:name=".cardEmulation.KHostApduService"
6262
android:exported="true"
6363
android:enabled="true"
6464
android:permission="android.permission.BIND_NFC_SERVICE">

app/src/main/java/com/qifan/nfcbank/cardEmulatorActivity.java renamed to app/src/main/java/com/qifan/nfcbank/CardEmulatorActivity.java

+18-14
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,20 @@
11
package com.qifan.nfcbank;
22

3-
import android.app.Activity;
43
import android.content.Intent;
54
import android.nfc.NfcAdapter;
6-
import android.nfc.NfcAdapter.ReaderCallback;
7-
import android.nfc.Tag;
8-
import android.nfc.tech.IsoDep;
95
import android.os.Bundle;
106
import android.support.v7.app.AppCompatActivity;
7+
import android.text.TextUtils;
118
import android.view.View;
129
import android.widget.Button;
13-
import android.widget.ListView;
14-
import com.qifan.nfcbank.cardEmulation.IsoDepAdapter;
15-
import com.qifan.nfcbank.cardEmulation.IsoDepTransceiver;
16-
import com.qifan.nfcbank.cardEmulation.MyHostApduService;
10+
import android.widget.EditText;
11+
import android.widget.Toast;
12+
import com.qifan.nfcbank.cardEmulation.KHostApduService;
1713

1814
/**
1915
* Created by Qifan on 28/11/2018.
2016
*/
21-
public class cardEmulatorActivity extends AppCompatActivity{
17+
public class CardEmulatorActivity extends AppCompatActivity {
2218

2319
private NfcAdapter nfcAdapter;
2420

@@ -33,16 +29,24 @@ protected void onCreate(Bundle savedInstanceState) {
3329
public void onResume() {
3430
super.onResume();
3531
Button button = (Button) findViewById(R.id.button);
32+
3633
button.setOnClickListener(new View.OnClickListener() {
34+
EditText editText = (EditText) findViewById(R.id.editText);
35+
3736
@Override
3837
public void onClick(View v) {
39-
Intent intent = new Intent(cardEmulatorActivity.this, MyHostApduService.class);
40-
intent.putExtra("ndefMessage", "你是");
41-
startService(intent);
42-
// Intent test = new Intent(cardEmulatorActivity.this, MyHostApduService.class);
43-
// startService(test);
38+
if (TextUtils.isEmpty(editText.getText())) {
39+
Toast.makeText(CardEmulatorActivity.this, getString(R.string.toast_msg), Toast.LENGTH_LONG).show();
40+
} else {
41+
Intent intent = new Intent(CardEmulatorActivity.this, KHostApduService.class);
42+
intent.putExtra("ndefMessage", editText.getText().toString());
43+
startService(intent);
44+
}
45+
4446
}
4547
});
48+
49+
4650
}
4751

4852
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
package com.qifan.nfcbank.cardEmulation
2+
3+
import android.nfc.cardemulation.HostApduService
4+
import android.app.Service
5+
import android.content.Intent
6+
import android.nfc.NdefMessage
7+
import android.os.Bundle
8+
import android.util.Log
9+
import java.util.*
10+
import android.nfc.NdefRecord
11+
import android.widget.Toast
12+
import java.io.UnsupportedEncodingException
13+
import java.math.BigInteger
14+
15+
16+
/**
17+
* Created by Qifan on 05/12/2018.
18+
*/
19+
20+
class KHostApduService : HostApduService() {
21+
22+
private val TAG = "HostApduService"
23+
24+
private val APDU_SELECT = byteArrayOf(
25+
0x00.toByte(), // CLA - Class - Class of instruction
26+
0xA4.toByte(), // INS - Instruction - Instruction code
27+
0x04.toByte(), // P1 - Parameter 1 - Instruction parameter 1
28+
0x00.toByte(), // P2 - Parameter 2 - Instruction parameter 2
29+
0x07.toByte(), // Lc field - Number of bytes present in the data field of the command
30+
0xD2.toByte(), 0x76.toByte(), 0x00.toByte(), 0x00.toByte(), 0x85.toByte(), 0x01.toByte(), 0x01.toByte(), // NDEF Tag Application name
31+
0x00.toByte() // Le field - Maximum number of bytes expected in the data field of the response to the command
32+
)
33+
34+
private val CAPABILITY_CONTAINER_OK = byteArrayOf(
35+
0x00.toByte(), // CLA - Class - Class of instruction
36+
0xa4.toByte(), // INS - Instruction - Instruction code
37+
0x00.toByte(), // P1 - Parameter 1 - Instruction parameter 1
38+
0x0c.toByte(), // P2 - Parameter 2 - Instruction parameter 2
39+
0x02.toByte(), // Lc field - Number of bytes present in the data field of the command
40+
0xe1.toByte(), 0x03.toByte() // file identifier of the CC file
41+
)
42+
43+
private val READ_CAPABILITY_CONTAINER = byteArrayOf(
44+
0x00.toByte(), // CLA - Class - Class of instruction
45+
0xb0.toByte(), // INS - Instruction - Instruction code
46+
0x00.toByte(), // P1 - Parameter 1 - Instruction parameter 1
47+
0x00.toByte(), // P2 - Parameter 2 - Instruction parameter 2
48+
0x0f.toByte() // Lc field - Number of bytes present in the data field of the command
49+
)
50+
51+
// In the scenario that we have done a CC read, the same byte[] match
52+
// for ReadBinary would trigger and we don't want that in succession
53+
private var READ_CAPABILITY_CONTAINER_CHECK = false
54+
55+
private val READ_CAPABILITY_CONTAINER_RESPONSE = byteArrayOf(
56+
0x00.toByte(), 0x11.toByte(), // CCLEN length of the CC file
57+
0x20.toByte(), // Mapping Version 2.0
58+
0xFF.toByte(), 0xFF.toByte(), // MLe maximum
59+
0xFF.toByte(), 0xFF.toByte(), // MLc maximum
60+
0x04.toByte(), // T field of the NDEF File Control TLV
61+
0x06.toByte(), // L field of the NDEF File Control TLV
62+
0xE1.toByte(), 0x04.toByte(), // File Identifier of NDEF file
63+
0xFF.toByte(), 0xFE.toByte(), // Maximum NDEF file size of 65534 bytes
64+
0x00.toByte(), // Read access without any security
65+
0xFF.toByte(), // Write access without any security
66+
0x90.toByte(), 0x00.toByte() // A_OKAY
67+
)
68+
69+
private val NDEF_SELECT_OK = byteArrayOf(
70+
0x00.toByte(), // CLA - Class - Class of instruction
71+
0xa4.toByte(), // Instruction byte (INS) for Select command
72+
0x00.toByte(), // Parameter byte (P1), select by identifier
73+
0x0c.toByte(), // Parameter byte (P1), select by identifier
74+
0x02.toByte(), // Lc field - Number of bytes present in the data field of the command
75+
0xE1.toByte(), 0x04.toByte() // file identifier of the NDEF file retrieved from the CC file
76+
)
77+
78+
private val NDEF_READ_BINARY = byteArrayOf(
79+
0x00.toByte(), // Class byte (CLA)
80+
0xb0.toByte() // Instruction byte (INS) for ReadBinary command
81+
)
82+
83+
private val NDEF_READ_BINARY_NLEN = byteArrayOf(
84+
0x00.toByte(), // Class byte (CLA)
85+
0xb0.toByte(), // Instruction byte (INS) for ReadBinary command
86+
0x00.toByte(), 0x00.toByte(), // Parameter byte (P1, P2), offset inside the CC file
87+
0x02.toByte() // Le field
88+
)
89+
90+
private val A_OKAY = byteArrayOf(
91+
0x90.toByte(), // SW1 Status byte 1 - Command processing status
92+
0x00.toByte() // SW2 Status byte 2 - Command processing qualifier
93+
)
94+
95+
private val A_ERROR = byteArrayOf(
96+
0x6A.toByte(), // SW1 Status byte 1 - Command processing status
97+
0x82.toByte() // SW2 Status byte 2 - Command processing qualifier
98+
)
99+
100+
private val NDEF_ID = byteArrayOf(0xE1.toByte(), 0x04.toByte())
101+
102+
private var NDEF_URI = NdefMessage(createTextRecord("", "Hello world", NDEF_ID))
103+
private var NDEF_URI_BYTES = NDEF_URI.toByteArray()
104+
private var NDEF_URI_LEN = fillByteArrayToFixedDimension(BigInteger.valueOf(NDEF_URI_BYTES.size.toLong()).toByteArray(), 2)
105+
106+
107+
108+
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
109+
110+
if (intent.hasExtra("ndefMessage")) {
111+
NDEF_URI = NdefMessage(createTextRecord("", intent.getStringExtra("ndefMessage"), NDEF_ID))
112+
113+
NDEF_URI_BYTES = NDEF_URI.toByteArray()
114+
NDEF_URI_LEN = fillByteArrayToFixedDimension(BigInteger.valueOf(NDEF_URI_BYTES.size.toLong()).toByteArray(), 2)
115+
116+
Toast.makeText(applicationContext,"message has been sent",Toast.LENGTH_LONG).show()
117+
}
118+
119+
Log.i(TAG, "onStartCommand() | NDEF" + NDEF_URI.toString())
120+
121+
return Service.START_STICKY
122+
}
123+
124+
override fun processCommandApdu(commandApdu: ByteArray, extras: Bundle?): ByteArray {
125+
126+
//
127+
// The following flow is based on Appendix E "Example of Mapping Version 2.0 Command Flow"
128+
// in the NFC Forum specification
129+
//
130+
Log.i(TAG, "processCommandApdu() | incoming commandApdu: " + commandApdu.toHex())
131+
132+
//
133+
// First command: NDEF Tag Application select (Section 5.5.2 in NFC Forum spec)
134+
//
135+
if (Arrays.equals(APDU_SELECT, commandApdu)) {
136+
Log.i(TAG, "APDU_SELECT triggered. Our Response: " + A_OKAY.toHex())
137+
return A_OKAY
138+
}
139+
140+
//
141+
// Second command: Capability Container select (Section 5.5.3 in NFC Forum spec)
142+
//
143+
if (Arrays.equals(CAPABILITY_CONTAINER_OK, commandApdu)) {
144+
Log.i(TAG, "CAPABILITY_CONTAINER_OK triggered. Our Response: " + A_OKAY.toHex())
145+
return A_OKAY
146+
}
147+
148+
//
149+
// Third command: ReadBinary data from CC file (Section 5.5.4 in NFC Forum spec)
150+
//
151+
if (Arrays.equals(READ_CAPABILITY_CONTAINER, commandApdu) && !READ_CAPABILITY_CONTAINER_CHECK) {
152+
Log.i(TAG, "READ_CAPABILITY_CONTAINER triggered. Our Response: " + READ_CAPABILITY_CONTAINER_RESPONSE.toHex())
153+
READ_CAPABILITY_CONTAINER_CHECK = true
154+
return READ_CAPABILITY_CONTAINER_RESPONSE
155+
}
156+
157+
//
158+
// Fourth command: NDEF Select command (Section 5.5.5 in NFC Forum spec)
159+
//
160+
if (Arrays.equals(NDEF_SELECT_OK, commandApdu)) {
161+
Log.i(TAG, "NDEF_SELECT_OK triggered. Our Response: " + A_OKAY.toHex())
162+
return A_OKAY
163+
}
164+
165+
if (Arrays.equals(NDEF_READ_BINARY_NLEN, commandApdu)) {
166+
// Build our response
167+
val response = ByteArray(NDEF_URI_LEN.size + A_OKAY.size)
168+
System.arraycopy(NDEF_URI_LEN, 0, response, 0, NDEF_URI_LEN.size)
169+
System.arraycopy(A_OKAY, 0, response, NDEF_URI_LEN.size, A_OKAY.size)
170+
171+
Log.i(TAG, "NDEF_READ_BINARY_NLEN triggered. Our Response: " + response.toHex())
172+
173+
READ_CAPABILITY_CONTAINER_CHECK = false
174+
return response
175+
}
176+
177+
if (Arrays.equals(commandApdu.sliceArray(0..1), NDEF_READ_BINARY)) {
178+
val offset = commandApdu.sliceArray(2..3).toHex().toInt(16)
179+
val length = commandApdu.sliceArray(4..4).toHex().toInt(16)
180+
181+
val fullResponse = ByteArray(NDEF_URI_LEN.size + NDEF_URI_BYTES.size)
182+
System.arraycopy(NDEF_URI_LEN, 0, fullResponse, 0, NDEF_URI_LEN.size)
183+
System.arraycopy(NDEF_URI_BYTES, 0, fullResponse, NDEF_URI_LEN.size, NDEF_URI_BYTES.size)
184+
185+
Log.i(TAG, "NDEF_READ_BINARY triggered. Full data: " + fullResponse.toHex())
186+
Log.i(TAG, "READ_BINARY - OFFSET: " + offset + " - LEN: " + length)
187+
188+
val slicedResponse = fullResponse.sliceArray(offset..fullResponse.size)
189+
190+
// Build our response
191+
val realLength = if (slicedResponse.size <= length) slicedResponse.size else length
192+
val response = ByteArray(realLength + A_OKAY.size)
193+
194+
System.arraycopy(slicedResponse, 0, response, 0, realLength)
195+
System.arraycopy(A_OKAY, 0, response, realLength, A_OKAY.size)
196+
197+
Log.i(TAG, "NDEF_READ_BINARY triggered. Our Response: " + response.toHex())
198+
199+
READ_CAPABILITY_CONTAINER_CHECK = false
200+
return response
201+
}
202+
203+
//
204+
// We're doing something outside our scope
205+
//
206+
Log.wtf(TAG, "processCommandApdu() | I don't know what's going on!!!")
207+
return A_ERROR
208+
}
209+
210+
override fun onDeactivated(reason: Int) {
211+
Log.i(TAG, "onDeactivated() Fired! Reason: $reason")
212+
}
213+
214+
215+
216+
private val HEX_CHARS = "0123456789ABCDEF".toCharArray()
217+
218+
fun ByteArray.toHex() : String{
219+
val result = StringBuffer()
220+
221+
forEach {
222+
val octet = it.toInt()
223+
val firstIndex = (octet and 0xF0).ushr(4)
224+
val secondIndex = octet and 0x0F
225+
result.append(HEX_CHARS[firstIndex])
226+
result.append(HEX_CHARS[secondIndex])
227+
}
228+
229+
return result.toString()
230+
}
231+
232+
fun String.hexStringToByteArray() : ByteArray {
233+
234+
val result = ByteArray(length / 2)
235+
236+
for (i in 0 until length step 2) {
237+
val firstIndex = HEX_CHARS.indexOf(this[i]);
238+
val secondIndex = HEX_CHARS.indexOf(this[i + 1]);
239+
240+
val octet = firstIndex.shl(4).or(secondIndex)
241+
result.set(i.shr(1), octet.toByte())
242+
}
243+
244+
return result
245+
}
246+
247+
fun createTextRecord(language: String, text: String, id: ByteArray): NdefRecord {
248+
val languageBytes: ByteArray
249+
val textBytes: ByteArray
250+
try {
251+
languageBytes = language.toByteArray(charset("US-ASCII"))
252+
textBytes = text.toByteArray(charset("UTF-8"))
253+
} catch (e: UnsupportedEncodingException) {
254+
throw AssertionError(e)
255+
}
256+
257+
val recordPayload = ByteArray(1 + (languageBytes.size and 0x03F) + textBytes.size)
258+
259+
recordPayload[0] = (languageBytes.size and 0x03F).toByte()
260+
System.arraycopy(languageBytes, 0, recordPayload, 1, languageBytes.size and 0x03F)
261+
System.arraycopy(textBytes, 0, recordPayload, 1 + (languageBytes.size and 0x03F), textBytes.size)
262+
263+
return NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_TEXT, id, recordPayload)
264+
}
265+
266+
private fun fillByteArrayToFixedDimension(array: ByteArray, fixedSize: Int): ByteArray {
267+
if (array.size == fixedSize) {
268+
return array
269+
}
270+
271+
val start = byteArrayOf(0x00.toByte())
272+
val filledArray = ByteArray(start.size + array.size)
273+
System.arraycopy(start, 0, filledArray, 0, start.size)
274+
System.arraycopy(array, 0, filledArray, start.size, array.size)
275+
return fillByteArrayToFixedDimension(filledArray, fixedSize)
276+
}
277+
}

0 commit comments

Comments
 (0)