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.app.Activity;
20 import android.content.ComponentName;
21 import android.content.Context;
22 import android.content.pm.PackageManager;
23 import android.nfc.INfcFCardEmulation;
24 import android.nfc.NfcAdapter;
25 import android.os.RemoteException;
26 import android.util.Log;
27 
28 import java.util.HashMap;
29 import java.util.List;
30 
31 /**
32  * This class can be used to query the state of
33  * NFC-F card emulation services.
34  *
35  * For a general introduction into NFC card emulation,
36  * please read the <a href="{@docRoot}guide/topics/connectivity/nfc/hce.html">
37  * NFC card emulation developer guide</a>.</p>
38  *
39  * <p class="note">Use of this class requires the
40  * {@link PackageManager#FEATURE_NFC_HOST_CARD_EMULATION_NFCF}
41  * to be present on the device.
42  */
43 public final class NfcFCardEmulation {
44     static final String TAG = "NfcFCardEmulation";
45 
46     static boolean sIsInitialized = false;
47     static HashMap<Context, NfcFCardEmulation> sCardEmus = new HashMap<Context, NfcFCardEmulation>();
48     static INfcFCardEmulation sService;
49 
50     final Context mContext;
51 
NfcFCardEmulation(Context context, INfcFCardEmulation service)52     private NfcFCardEmulation(Context context, INfcFCardEmulation service) {
53         mContext = context.getApplicationContext();
54         sService = service;
55     }
56 
57     /**
58      * Helper to get an instance of this class.
59      *
60      * @param adapter A reference to an NfcAdapter object.
61      * @return
62      */
getInstance(NfcAdapter adapter)63     public static synchronized NfcFCardEmulation getInstance(NfcAdapter adapter) {
64         if (adapter == null) throw new NullPointerException("NfcAdapter is null");
65         Context context = adapter.getContext();
66         if (context == null) {
67             Log.e(TAG, "NfcAdapter context is null.");
68             throw new UnsupportedOperationException();
69         }
70         if (!sIsInitialized) {
71             PackageManager pm = context.getPackageManager();
72             if (pm == null) {
73                 Log.e(TAG, "Cannot get PackageManager");
74                 throw new UnsupportedOperationException();
75             }
76             if (!pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION_NFCF)) {
77                 Log.e(TAG, "This device does not support NFC-F card emulation");
78                 throw new UnsupportedOperationException();
79             }
80             sIsInitialized = true;
81         }
82         NfcFCardEmulation manager = sCardEmus.get(context);
83         if (manager == null) {
84             // Get card emu service
85             INfcFCardEmulation service = adapter.getNfcFCardEmulationService();
86             if (service == null) {
87                 Log.e(TAG, "This device does not implement the INfcFCardEmulation interface.");
88                 throw new UnsupportedOperationException();
89             }
90             manager = new NfcFCardEmulation(context, service);
91             sCardEmus.put(context, manager);
92         }
93         return manager;
94     }
95 
96     /**
97      * Retrieves the current System Code for the specified service.
98      *
99      * <p>Before calling {@link #registerSystemCodeForService(ComponentName, String)},
100      * the System Code contained in the Manifest file is returned. After calling
101      * {@link #registerSystemCodeForService(ComponentName, String)}, the System Code
102      * registered there is returned. After calling
103      * {@link #unregisterSystemCodeForService(ComponentName)}, "null" is returned.
104      *
105      * @param service The component name of the service
106      * @return the current System Code
107      */
getSystemCodeForService(ComponentName service)108     public String getSystemCodeForService(ComponentName service) throws RuntimeException {
109         if (service == null) {
110             throw new NullPointerException("service is null");
111         }
112         try {
113             return sService.getSystemCodeForService(mContext.getUser().getIdentifier(), service);
114         } catch (RemoteException e) {
115             // Try one more time
116             recoverService();
117             if (sService == null) {
118                 Log.e(TAG, "Failed to recover CardEmulationService.");
119                 return null;
120             }
121             try {
122                 return sService.getSystemCodeForService(mContext.getUser().getIdentifier(),
123                         service);
124             } catch (RemoteException ee) {
125                 Log.e(TAG, "Failed to reach CardEmulationService.");
126                 ee.rethrowAsRuntimeException();
127                 return null;
128             }
129         }
130     }
131 
132     /**
133      * Registers a System Code for the specified service.
134      *
135      * <p>The System Code must be in range from "4000" to "4FFF" (excluding "4*FF").
136      *
137      * <p>If a System Code was previously registered for this service
138      * (either statically through the manifest, or dynamically by using this API),
139      * it will be replaced with this one.
140      *
141      * <p>Even if the same System Code is already registered for another service,
142      * this method succeeds in registering the System Code.
143      *
144      * <p>Note that you can only register a System Code for a service that
145      * is running under the same UID as the caller of this API. Typically
146      * this means you need to call this from the same
147      * package as the service itself, though UIDs can also
148      * be shared between packages using shared UIDs.
149      *
150      * @param service The component name of the service
151      * @param systemCode The System Code to be registered
152      * @return whether the registration was successful.
153      */
registerSystemCodeForService(ComponentName service, String systemCode)154     public boolean registerSystemCodeForService(ComponentName service, String systemCode)
155             throws RuntimeException {
156         if (service == null || systemCode == null) {
157             throw new NullPointerException("service or systemCode is null");
158         }
159         try {
160             return sService.registerSystemCodeForService(mContext.getUser().getIdentifier(),
161                     service, systemCode);
162         } catch (RemoteException e) {
163             // Try one more time
164             recoverService();
165             if (sService == null) {
166                 Log.e(TAG, "Failed to recover CardEmulationService.");
167                 return false;
168             }
169             try {
170                 return sService.registerSystemCodeForService(mContext.getUser().getIdentifier(),
171                         service, systemCode);
172             } catch (RemoteException ee) {
173                 Log.e(TAG, "Failed to reach CardEmulationService.");
174                 ee.rethrowAsRuntimeException();
175                 return false;
176             }
177         }
178     }
179 
180     /**
181      * Removes a registered System Code for the specified service.
182      *
183      * @param service The component name of the service
184      * @return whether the System Code was successfully removed.
185      */
unregisterSystemCodeForService(ComponentName service)186     public boolean unregisterSystemCodeForService(ComponentName service) throws RuntimeException {
187         if (service == null) {
188             throw new NullPointerException("service is null");
189         }
190         try {
191             return sService.removeSystemCodeForService(mContext.getUser().getIdentifier(), service);
192         } catch (RemoteException e) {
193             // Try one more time
194             recoverService();
195             if (sService == null) {
196                 Log.e(TAG, "Failed to recover CardEmulationService.");
197                 return false;
198             }
199             try {
200                 return sService.removeSystemCodeForService(mContext.getUser().getIdentifier(),
201                         service);
202             } catch (RemoteException ee) {
203                 Log.e(TAG, "Failed to reach CardEmulationService.");
204                 ee.rethrowAsRuntimeException();
205                 return false;
206             }
207         }
208     }
209 
210     /**
211      * Retrieves the current NFCID2 for the specified service.
212      *
213      * <p>Before calling {@link #setNfcid2ForService(ComponentName, String)},
214      * the NFCID2 contained in the Manifest file is returned. If "random" is specified
215      * in the Manifest file, a random number assigned by the system at installation time
216      * is returned. After setting an NFCID2
217      * with {@link #setNfcid2ForService(ComponentName, String)}, this NFCID2 is returned.
218      *
219      * @param service The component name of the service
220      * @return the current NFCID2
221      */
getNfcid2ForService(ComponentName service)222     public String getNfcid2ForService(ComponentName service) throws RuntimeException {
223         if (service == null) {
224             throw new NullPointerException("service is null");
225         }
226         try {
227             return sService.getNfcid2ForService(mContext.getUser().getIdentifier(), service);
228         } catch (RemoteException e) {
229             // Try one more time
230             recoverService();
231             if (sService == null) {
232                 Log.e(TAG, "Failed to recover CardEmulationService.");
233                 return null;
234             }
235             try {
236                 return sService.getNfcid2ForService(mContext.getUser().getIdentifier(), service);
237             } catch (RemoteException ee) {
238                 Log.e(TAG, "Failed to reach CardEmulationService.");
239                 ee.rethrowAsRuntimeException();
240                 return null;
241             }
242         }
243     }
244 
245     /**
246      * Set a NFCID2 for the specified service.
247      *
248      * <p>The NFCID2 must be in range from "02FE000000000000" to "02FEFFFFFFFFFFFF".
249      *
250      * <p>If a NFCID2 was previously set for this service
251      * (either statically through the manifest, or dynamically by using this API),
252      * it will be replaced.
253      *
254      * <p>Note that you can only set the NFCID2 for a service that
255      * is running under the same UID as the caller of this API. Typically
256      * this means you need to call this from the same
257      * package as the service itself, though UIDs can also
258      * be shared between packages using shared UIDs.
259      *
260      * @param service The component name of the service
261      * @param nfcid2 The NFCID2 to be registered
262      * @return whether the setting was successful.
263      */
setNfcid2ForService(ComponentName service, String nfcid2)264     public boolean setNfcid2ForService(ComponentName service, String nfcid2)
265             throws RuntimeException {
266         if (service == null || nfcid2 == null) {
267             throw new NullPointerException("service or nfcid2 is null");
268         }
269         try {
270             return sService.setNfcid2ForService(mContext.getUser().getIdentifier(),
271                     service, nfcid2);
272         } catch (RemoteException e) {
273             // Try one more time
274             recoverService();
275             if (sService == null) {
276                 Log.e(TAG, "Failed to recover CardEmulationService.");
277                 return false;
278             }
279             try {
280                 return sService.setNfcid2ForService(mContext.getUser().getIdentifier(),
281                         service, nfcid2);
282             } catch (RemoteException ee) {
283                 Log.e(TAG, "Failed to reach CardEmulationService.");
284                 ee.rethrowAsRuntimeException();
285                 return false;
286             }
287         }
288     }
289 
290     /**
291      * Allows a foreground application to specify which card emulation service
292      * should be enabled while a specific Activity is in the foreground.
293      *
294      * <p>The specified HCE-F service is only enabled when the corresponding application is
295      * in the foreground and this method has been called. When the application is moved to
296      * the background, {@link #disableService(Activity)} is called, or
297      * NFCID2 or System Code is replaced, the HCE-F service is disabled.
298      *
299      * <p>The specified Activity must currently be in resumed state. A good
300      * paradigm is to call this method in your {@link Activity#onResume}, and to call
301      * {@link #disableService(Activity)} in your {@link Activity#onPause}.
302      *
303      * <p>Note that this preference is not persisted by the OS, and hence must be
304      * called every time the Activity is resumed.
305      *
306      * @param activity The activity which prefers this service to be invoked
307      * @param service The service to be preferred while this activity is in the foreground
308      * @return whether the registration was successful
309      */
enableService(Activity activity, ComponentName service)310     public boolean enableService(Activity activity, ComponentName service) throws RuntimeException {
311         if (activity == null || service == null) {
312             throw new NullPointerException("activity or service is null");
313         }
314         // Verify the activity is in the foreground before calling into NfcService
315         if (!activity.isResumed()) {
316             throw new IllegalArgumentException("Activity must be resumed.");
317         }
318         try {
319             return sService.enableNfcFForegroundService(service);
320         } catch (RemoteException e) {
321             // Try one more time
322             recoverService();
323             if (sService == null) {
324                 Log.e(TAG, "Failed to recover CardEmulationService.");
325                 return false;
326             }
327             try {
328                 return sService.enableNfcFForegroundService(service);
329             } catch (RemoteException ee) {
330                 Log.e(TAG, "Failed to reach CardEmulationService.");
331                 ee.rethrowAsRuntimeException();
332                 return false;
333             }
334         }
335     }
336 
337     /**
338      * Disables the service for the specified Activity.
339      *
340      * <p>Note that the specified Activity must still be in resumed
341      * state at the time of this call. A good place to call this method
342      * is in your {@link Activity#onPause} implementation.
343      *
344      * @param activity The activity which the service was registered for
345      * @return true when successful
346      */
disableService(Activity activity)347     public boolean disableService(Activity activity) throws RuntimeException {
348         if (activity == null) {
349             throw new NullPointerException("activity is null");
350         }
351         if (!activity.isResumed()) {
352             throw new IllegalArgumentException("Activity must be resumed.");
353         }
354         try {
355             return sService.disableNfcFForegroundService();
356         } catch (RemoteException e) {
357             // Try one more time
358             recoverService();
359             if (sService == null) {
360                 Log.e(TAG, "Failed to recover CardEmulationService.");
361                 return false;
362             }
363             try {
364                 return sService.disableNfcFForegroundService();
365             } catch (RemoteException ee) {
366                 Log.e(TAG, "Failed to reach CardEmulationService.");
367                 ee.rethrowAsRuntimeException();
368                 return false;
369             }
370         }
371     }
372 
373     /**
374      * @hide
375      */
getNfcFServices()376     public List<NfcFServiceInfo> getNfcFServices() {
377         try {
378             return sService.getNfcFServices(mContext.getUser().getIdentifier());
379         } catch (RemoteException e) {
380             // Try one more time
381             recoverService();
382             if (sService == null) {
383                 Log.e(TAG, "Failed to recover CardEmulationService.");
384                 return null;
385             }
386             try {
387                 return sService.getNfcFServices(mContext.getUser().getIdentifier());
388             } catch (RemoteException ee) {
389                 Log.e(TAG, "Failed to reach CardEmulationService.");
390                 return null;
391             }
392         }
393     }
394 
395     /**
396      * @hide
397      */
getMaxNumOfRegisterableSystemCodes()398     public int getMaxNumOfRegisterableSystemCodes() {
399         try {
400             return sService.getMaxNumOfRegisterableSystemCodes();
401         } catch (RemoteException e) {
402             // Try one more time
403             recoverService();
404             if (sService == null) {
405                 Log.e(TAG, "Failed to recover CardEmulationService.");
406                 return -1;
407             }
408             try {
409                 return sService.getMaxNumOfRegisterableSystemCodes();
410             } catch (RemoteException ee) {
411                 Log.e(TAG, "Failed to reach CardEmulationService.");
412                 return -1;
413             }
414         }
415     }
416 
417     /**
418      * @hide
419      */
isValidSystemCode(String systemCode)420     public static boolean isValidSystemCode(String systemCode) {
421         if (systemCode == null) {
422             return false;
423         }
424         if (systemCode.length() != 4) {
425             Log.e(TAG, "System Code " + systemCode + " is not a valid System Code.");
426             return false;
427         }
428         // check if the value is between "4000" and "4FFF" (excluding "4*FF")
429         if (!systemCode.startsWith("4") || systemCode.toUpperCase().endsWith("FF")) {
430             Log.e(TAG, "System Code " + systemCode + " is not a valid System Code.");
431             return false;
432         }
433         try {
434             Integer.parseInt(systemCode, 16);
435         } catch (NumberFormatException e) {
436             Log.e(TAG, "System Code " + systemCode + " is not a valid System Code.");
437             return false;
438         }
439         return true;
440     }
441 
442     /**
443      * @hide
444      */
isValidNfcid2(String nfcid2)445     public static boolean isValidNfcid2(String nfcid2) {
446         if (nfcid2 == null) {
447             return false;
448         }
449         if (nfcid2.length() != 16) {
450             Log.e(TAG, "NFCID2 " + nfcid2 + " is not a valid NFCID2.");
451             return false;
452         }
453         // check if the the value starts with "02FE"
454         if (!nfcid2.toUpperCase().startsWith("02FE")) {
455             Log.e(TAG, "NFCID2 " + nfcid2 + " is not a valid NFCID2.");
456             return false;
457         }
458         try {
459             Long.parseLong(nfcid2, 16);
460         } catch (NumberFormatException e) {
461             Log.e(TAG, "NFCID2 " + nfcid2 + " is not a valid NFCID2.");
462             return false;
463         }
464         return true;
465     }
466 
recoverService()467     void recoverService() {
468         NfcAdapter adapter = NfcAdapter.getDefaultAdapter(mContext);
469         sService = adapter.getNfcFCardEmulationService();
470     }
471 
472 }
473 
474