1 /*
2  * Copyright (C) 2015 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 android.nfc.cardemulation;
18 
19 import android.annotation.SdkConstant;
20 import android.annotation.SdkConstant.SdkConstantType;
21 import android.app.Service;
22 import android.content.ComponentName;
23 import android.content.Intent;
24 import android.os.Bundle;
25 import android.os.Handler;
26 import android.os.IBinder;
27 import android.os.Message;
28 import android.os.Messenger;
29 import android.os.RemoteException;
30 import android.util.Log;
31 
32 /**
33  * <p>HostNfcFService is a convenience {@link Service} class that can be
34  * extended to emulate an NFC-F card inside an Android service component.
35  *
36  * <h3>NFC Protocols</h3>
37  * <p>Cards emulated by this class are based on the NFC-Forum NFC-F
38  * protocol (based on the JIS-X 6319-4 specification.)</p>
39  *
40  * <h3>System Code and NFCID2 registration</h3>
41  * <p>A {@link HostNfcFService HostNfcFService service} can register
42  * exactly one System Code and one NFCID2. For details about the use of
43  * System Code and NFCID2, see the NFC Forum Digital specification.</p>
44  * <p>To statically register a System Code and NFCID2 with the service, a {@link #SERVICE_META_DATA}
45  * entry must be included in the declaration of the service.
46  *
47  * <p>All {@link HostNfcFService HostNfcFService} declarations in the manifest must require the
48  * {@link android.Manifest.permission#BIND_NFC_SERVICE} permission
49  * in their &lt;service&gt; tag, to ensure that only the platform can bind to your service.</p>
50  *
51  * <p>An example of a HostNfcFService manifest declaration is shown below:
52  *
53  * <pre> &lt;service android:name=".MyHostNfcFService" android:exported="true" android:permission="android.permission.BIND_NFC_SERVICE"&gt;
54  *     &lt;intent-filter&gt;
55  *         &lt;action android:name="android.nfc.cardemulation.action.HOST_NFCF_SERVICE"/&gt;
56  *     &lt;/intent-filter&gt;
57  *     &lt;meta-data android:name="android.nfc.cardemulation.host_nfcf_service" android:resource="@xml/nfcfservice"/&gt;
58  * &lt;/service&gt;</pre>
59  *
60  * This meta-data tag points to an nfcfservice.xml file.
61  * An example of this file with a System Code and NFCID2 declaration is shown below:
62  * <pre>
63  * &lt;host-nfcf-service xmlns:android="http://schemas.android.com/apk/res/android"
64  *           android:description="@string/servicedesc"&gt;
65  *       &lt;system-code-filter android:name="4000"/&gt;
66  *       &lt;nfcid2-filter android:name="02FE000000000000"/&gt;
67          &lt;t3tPmm-filter android:name="FFFFFFFFFFFFFFFF"/&gt;
68  * &lt;/host-nfcf-service&gt;
69  * </pre>
70  *
71  * <p>The {@link android.R.styleable#HostNfcFService &lt;host-nfcf-service&gt;} is required
72  * to contain a
73  * {@link android.R.styleable#HostApduService_description &lt;android:description&gt;}
74  * attribute that contains a user-friendly description of the service that may be shown in UI.
75  * <p>The {@link android.R.styleable#HostNfcFService &lt;host-nfcf-service&gt;} must
76  * contain:
77  * <ul>
78  * <li>Exactly one {@link android.R.styleable#SystemCodeFilter &lt;system-code-filter&gt;} tag.</li>
79  * <li>Exactly one {@link android.R.styleable#Nfcid2Filter &lt;nfcid2-filter&gt;} tag.</li>
80  * <li>Zero or one {@link android.R.styleable#T3tPmmFilter &lt;t3tPmm-filter&gt;} tag.</li>
81  * </ul>
82  * </p>
83  *
84  * <p>Alternatively, the System Code and NFCID2 can be dynamically registererd for a service
85  * by using the {@link NfcFCardEmulation#registerSystemCodeForService(ComponentName, String)} and
86  * {@link NfcFCardEmulation#setNfcid2ForService(ComponentName, String)} methods.
87  * </p>
88  *
89  * <h3>Service selection</h3>
90  * <p>When a remote NFC devices wants to communicate with your service, it
91  * sends a SENSF_REQ command to the NFC controller, requesting a System Code.
92  * If a {@link NfcFCardEmulation NfcFCardEmulation service} has registered
93  * this system code and has been enabled by the foreground application, the
94  * NFC controller will respond with the NFCID2 that is registered for this service.
95  * The reader can then continue data exchange with this service by using the NFCID2.</p>
96  *
97  * <h3>Data exchange</h3>
98  * <p>After service selection, all frames addressed to the NFCID2 of this service will
99  * be sent through {@link #processNfcFPacket(byte[], Bundle)}, until the NFC link is
100  * broken.<p>
101  *
102  * <p>When the NFC link is broken, {@link #onDeactivated(int)} will be called.</p>
103  */
104 public abstract class HostNfcFService extends Service {
105     /**
106      * The {@link Intent} action that must be declared as handled by the service.
107      */
108     @SdkConstant(SdkConstantType.SERVICE_ACTION)
109     public static final String SERVICE_INTERFACE =
110             "android.nfc.cardemulation.action.HOST_NFCF_SERVICE";
111 
112     /**
113      * The name of the meta-data element that contains
114      * more information about this service.
115      */
116     public static final String SERVICE_META_DATA =
117             "android.nfc.cardemulation.host_nfcf_service";
118 
119     /**
120      * Reason for {@link #onDeactivated(int)}.
121      * Indicates deactivation was due to the NFC link
122      * being lost.
123      */
124     public static final int DEACTIVATION_LINK_LOSS = 0;
125 
126     static final String TAG = "NfcFService";
127 
128     /**
129      * MSG_COMMAND_PACKET is sent by NfcService when
130      * a NFC-F command packet has been received.
131      *
132      * @hide
133      */
134     public static final int MSG_COMMAND_PACKET = 0;
135 
136     /**
137      * MSG_RESPONSE_PACKET is sent to NfcService to send
138      * a response packet back to the remote device.
139      *
140      * @hide
141      */
142     public static final int MSG_RESPONSE_PACKET = 1;
143 
144     /**
145      * MSG_DEACTIVATED is sent by NfcService when
146      * the current session is finished; because
147      * the NFC link was deactivated.
148      *
149      * @hide
150      */
151     public static final int MSG_DEACTIVATED = 2;
152 
153    /**
154      * @hide
155      */
156     public static final String KEY_DATA = "data";
157 
158     /**
159      * @hide
160      */
161     public static final String KEY_MESSENGER = "messenger";
162 
163     /**
164      * Messenger interface to NfcService for sending responses.
165      * Only accessed on main thread by the message handler.
166      *
167      * @hide
168      */
169     Messenger mNfcService = null;
170 
171     final Messenger mMessenger = new Messenger(new MsgHandler());
172 
173     final class MsgHandler extends Handler {
174         @Override
handleMessage(Message msg)175         public void handleMessage(Message msg) {
176             switch (msg.what) {
177             case MSG_COMMAND_PACKET:
178                 Bundle dataBundle = msg.getData();
179                 if (dataBundle == null) {
180                     return;
181                 }
182                 if (mNfcService == null) mNfcService = msg.replyTo;
183 
184                 byte[] packet = dataBundle.getByteArray(KEY_DATA);
185                 if (packet != null) {
186                     byte[] responsePacket = processNfcFPacket(packet, null);
187                     if (responsePacket != null) {
188                         if (mNfcService == null) {
189                             Log.e(TAG, "Response not sent; service was deactivated.");
190                             return;
191                         }
192                         Message responseMsg = Message.obtain(null, MSG_RESPONSE_PACKET);
193                         Bundle responseBundle = new Bundle();
194                         responseBundle.putByteArray(KEY_DATA, responsePacket);
195                         responseMsg.setData(responseBundle);
196                         responseMsg.replyTo = mMessenger;
197                         try {
198                             mNfcService.send(responseMsg);
199                         } catch (RemoteException e) {
200                             Log.e("TAG", "Response not sent; RemoteException calling into " +
201                                     "NfcService.");
202                         }
203                     }
204                 } else {
205                     Log.e(TAG, "Received MSG_COMMAND_PACKET without data.");
206                 }
207                 break;
208             case MSG_RESPONSE_PACKET:
209                 if (mNfcService == null) {
210                     Log.e(TAG, "Response not sent; service was deactivated.");
211                     return;
212                 }
213                 try {
214                     msg.replyTo = mMessenger;
215                     mNfcService.send(msg);
216                 } catch (RemoteException e) {
217                     Log.e(TAG, "RemoteException calling into NfcService.");
218                 }
219                 break;
220             case MSG_DEACTIVATED:
221                 // Make sure we won't call into NfcService again
222                 mNfcService = null;
223                 onDeactivated(msg.arg1);
224                 break;
225             default:
226                 super.handleMessage(msg);
227             }
228         }
229     }
230 
231     @Override
onBind(Intent intent)232     public final IBinder onBind(Intent intent) {
233         return mMessenger.getBinder();
234     }
235 
236     /**
237      * Sends a response packet back to the remote device.
238      *
239      * <p>Note: this method may be called from any thread and will not block.
240      * @param responsePacket A byte-array containing the response packet.
241      */
sendResponsePacket(byte[] responsePacket)242     public final void sendResponsePacket(byte[] responsePacket) {
243         Message responseMsg = Message.obtain(null, MSG_RESPONSE_PACKET);
244         Bundle dataBundle = new Bundle();
245         dataBundle.putByteArray(KEY_DATA, responsePacket);
246         responseMsg.setData(dataBundle);
247         try {
248             mMessenger.send(responseMsg);
249         } catch (RemoteException e) {
250             Log.e("TAG", "Local messenger has died.");
251         }
252     }
253 
254     /**
255      * <p>This method will be called when a NFC-F packet has been received
256      * from a remote device. A response packet can be provided directly
257      * by returning a byte-array in this method. Note that in general
258      * response packets must be sent as quickly as possible, given the fact
259      * that the user is likely holding their device over an NFC reader
260      * when this method is called.
261      *
262      * <p class="note">This method is running on the main thread of your application.
263      * If you cannot return a response packet immediately, return null
264      * and use the {@link #sendResponsePacket(byte[])} method later.
265      *
266      * @param commandPacket The NFC-F packet that was received from the remote device
267      * @param extras A bundle containing extra data. May be null.
268      * @return a byte-array containing the response packet, or null if no
269      *         response packet can be sent at this point.
270      */
processNfcFPacket(byte[] commandPacket, Bundle extras)271     public abstract byte[] processNfcFPacket(byte[] commandPacket, Bundle extras);
272 
273     /**
274      * This method will be called in following possible scenarios:
275      * <li>The NFC link has been lost
276      * @param reason {@link #DEACTIVATION_LINK_LOSS}
277      */
onDeactivated(int reason)278     public abstract void onDeactivated(int reason);
279 }
280