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