1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.example.android.cardemulation;
18 
19 import android.nfc.cardemulation.HostApduService;
20 import android.os.Bundle;
21 import com.example.android.common.logger.Log;
22 
23 import java.util.Arrays;
24 
25 /**
26  * This is a sample APDU Service which demonstrates how to interface with the card emulation support
27  * added in Android 4.4, KitKat.
28  *
29  * <p>This sample replies to any requests sent with the string "Hello World". In real-world
30  * situations, you would need to modify this code to implement your desired communication
31  * protocol.
32  *
33  * <p>This sample will be invoked for any terminals selecting AIDs of 0xF11111111, 0xF22222222, or
34  * 0xF33333333. See src/main/res/xml/aid_list.xml for more details.
35  *
36  * <p class="note">Note: This is a low-level interface. Unlike the NdefMessage many developers
37  * are familiar with for implementing Android Beam in apps, card emulation only provides a
38  * byte-array based communication channel. It is left to developers to implement higher level
39  * protocol support as needed.
40  */
41 public class CardService extends HostApduService {
42     private static final String TAG = "CardService";
43     // AID for our loyalty card service.
44     private static final String SAMPLE_LOYALTY_CARD_AID = "F222222222";
45     // ISO-DEP command HEADER for selecting an AID.
46     // Format: [Class | Instruction | Parameter 1 | Parameter 2]
47     private static final String SELECT_APDU_HEADER = "00A40400";
48     // "OK" status word sent in response to SELECT AID command (0x9000)
49     private static final byte[] SELECT_OK_SW = HexStringToByteArray("9000");
50     // "UNKNOWN" status word sent in response to invalid APDU command (0x0000)
51     private static final byte[] UNKNOWN_CMD_SW = HexStringToByteArray("0000");
52     private static final byte[] SELECT_APDU = BuildSelectApdu(SAMPLE_LOYALTY_CARD_AID);
53 
54     /**
55      * Called if the connection to the NFC card is lost, in order to let the application know the
56      * cause for the disconnection (either a lost link, or another AID being selected by the
57      * reader).
58      *
59      * @param reason Either DEACTIVATION_LINK_LOSS or DEACTIVATION_DESELECTED
60      */
61     @Override
onDeactivated(int reason)62     public void onDeactivated(int reason) { }
63 
64     /**
65      * This method will be called when a command APDU has been received from a remote device. A
66      * response APDU can be provided directly by returning a byte-array in this method. In general
67      * response APDUs must be sent as quickly as possible, given the fact that the user is likely
68      * holding their device over an NFC reader when this method is called.
69      *
70      * <p class="note">If there are multiple services that have registered for the same AIDs in
71      * their meta-data entry, you will only get called if the user has explicitly selected your
72      * service, either as a default or just for the next tap.
73      *
74      * <p class="note">This method is running on the main thread of your application. If you
75      * cannot return a response APDU immediately, return null and use the {@link
76      * #sendResponseApdu(byte[])} method later.
77      *
78      * @param commandApdu The APDU that received from the remote device
79      * @param extras A bundle containing extra data. May be null.
80      * @return a byte-array containing the response APDU, or null if no response APDU can be sent
81      * at this point.
82      */
83     // BEGIN_INCLUDE(processCommandApdu)
84     @Override
processCommandApdu(byte[] commandApdu, Bundle extras)85     public byte[] processCommandApdu(byte[] commandApdu, Bundle extras) {
86         Log.i(TAG, "Received APDU: " + ByteArrayToHexString(commandApdu));
87         // If the APDU matches the SELECT AID command for this service,
88         // send the loyalty card account number, followed by a SELECT_OK status trailer (0x9000).
89         if (Arrays.equals(SELECT_APDU, commandApdu)) {
90             String account = AccountStorage.GetAccount(this);
91             byte[] accountBytes = account.getBytes();
92             Log.i(TAG, "Sending account number: " + account);
93             return ConcatArrays(accountBytes, SELECT_OK_SW);
94         } else {
95             return UNKNOWN_CMD_SW;
96         }
97     }
98     // END_INCLUDE(processCommandApdu)
99 
100     /**
101      * Build APDU for SELECT AID command. This command indicates which service a reader is
102      * interested in communicating with. See ISO 7816-4.
103      *
104      * @param aid Application ID (AID) to select
105      * @return APDU for SELECT AID command
106      */
BuildSelectApdu(String aid)107     public static byte[] BuildSelectApdu(String aid) {
108         // Format: [CLASS | INSTRUCTION | PARAMETER 1 | PARAMETER 2 | LENGTH | DATA]
109         return HexStringToByteArray(SELECT_APDU_HEADER + String.format("%02X",
110                 aid.length() / 2) + aid);
111     }
112 
113     /**
114      * Utility method to convert a byte array to a hexadecimal string.
115      *
116      * @param bytes Bytes to convert
117      * @return String, containing hexadecimal representation.
118      */
ByteArrayToHexString(byte[] bytes)119     public static String ByteArrayToHexString(byte[] bytes) {
120         final char[] hexArray = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
121         char[] hexChars = new char[bytes.length * 2]; // Each byte has two hex characters (nibbles)
122         int v;
123         for (int j = 0; j < bytes.length; j++) {
124             v = bytes[j] & 0xFF; // Cast bytes[j] to int, treating as unsigned value
125             hexChars[j * 2] = hexArray[v >>> 4]; // Select hex character from upper nibble
126             hexChars[j * 2 + 1] = hexArray[v & 0x0F]; // Select hex character from lower nibble
127         }
128         return new String(hexChars);
129     }
130 
131     /**
132      * Utility method to convert a hexadecimal string to a byte string.
133      *
134      * <p>Behavior with input strings containing non-hexadecimal characters is undefined.
135      *
136      * @param s String containing hexadecimal characters to convert
137      * @return Byte array generated from input
138      * @throws java.lang.IllegalArgumentException if input length is incorrect
139      */
HexStringToByteArray(String s)140     public static byte[] HexStringToByteArray(String s) throws IllegalArgumentException {
141         int len = s.length();
142         if (len % 2 == 1) {
143             throw new IllegalArgumentException("Hex string must have even number of characters");
144         }
145         byte[] data = new byte[len / 2]; // Allocate 1 byte per 2 hex characters
146         for (int i = 0; i < len; i += 2) {
147             // Convert each character into a integer (base-16), then bit-shift into place
148             data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
149                     + Character.digit(s.charAt(i+1), 16));
150         }
151         return data;
152     }
153 
154     /**
155      * Utility method to concatenate two byte arrays.
156      * @param first First array
157      * @param rest Any remaining arrays
158      * @return Concatenated copy of input arrays
159      */
ConcatArrays(byte[] first, byte[]... rest)160     public static byte[] ConcatArrays(byte[] first, byte[]... rest) {
161         int totalLength = first.length;
162         for (byte[] array : rest) {
163             totalLength += array.length;
164         }
165         byte[] result = Arrays.copyOf(first, totalLength);
166         int offset = first.length;
167         for (byte[] array : rest) {
168             System.arraycopy(array, 0, result, offset, array.length);
169             offset += array.length;
170         }
171         return result;
172     }
173 }
174