| /* |
| * Copyright (C) 2013 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.nfc.cardemulation; |
| |
| import android.annotation.SdkConstant; |
| import android.annotation.SdkConstant.SdkConstantType; |
| import android.app.Activity; |
| import android.app.ActivityThread; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.pm.IPackageManager; |
| import android.content.pm.PackageManager; |
| import android.nfc.INfcCardEmulation; |
| import android.nfc.NfcAdapter; |
| import android.os.RemoteException; |
| import android.os.UserHandle; |
| import android.provider.Settings; |
| import android.provider.Settings.SettingNotFoundException; |
| import android.util.Log; |
| |
| import java.util.HashMap; |
| import java.util.List; |
| |
| /** |
| * This class can be used to query the state of |
| * NFC card emulation services. |
| * |
| * For a general introduction into NFC card emulation, |
| * please read the <a href="{@docRoot}guide/topics/connectivity/nfc/hce.html"> |
| * NFC card emulation developer guide</a>.</p> |
| * |
| * <p class="note">Use of this class requires the |
| * {@link PackageManager#FEATURE_NFC_HOST_CARD_EMULATION} to be present |
| * on the device. |
| */ |
| public final class CardEmulation { |
| static final String TAG = "CardEmulation"; |
| |
| /** |
| * Activity action: ask the user to change the default |
| * card emulation service for a certain category. This will |
| * show a dialog that asks the user whether he wants to |
| * replace the current default service with the service |
| * identified with the ComponentName specified in |
| * {@link #EXTRA_SERVICE_COMPONENT}, for the category |
| * specified in {@link #EXTRA_CATEGORY} |
| */ |
| @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) |
| public static final String ACTION_CHANGE_DEFAULT = |
| "android.nfc.cardemulation.action.ACTION_CHANGE_DEFAULT"; |
| |
| /** |
| * The category extra for {@link #ACTION_CHANGE_DEFAULT}. |
| * |
| * @see #ACTION_CHANGE_DEFAULT |
| */ |
| public static final String EXTRA_CATEGORY = "category"; |
| |
| /** |
| * The service {@link ComponentName} object passed in as an |
| * extra for {@link #ACTION_CHANGE_DEFAULT}. |
| * |
| * @see #ACTION_CHANGE_DEFAULT |
| */ |
| public static final String EXTRA_SERVICE_COMPONENT = "component"; |
| |
| /** |
| * Category used for NFC payment services. |
| */ |
| public static final String CATEGORY_PAYMENT = "payment"; |
| |
| /** |
| * Category that can be used for all other card emulation |
| * services. |
| */ |
| public static final String CATEGORY_OTHER = "other"; |
| |
| /** |
| * Return value for {@link #getSelectionModeForCategory(String)}. |
| * |
| * <p>In this mode, the user has set a default service for this |
| * category. |
| * |
| * <p>When using ISO-DEP card emulation with {@link HostApduService} |
| * or {@link OffHostApduService}, if a remote NFC device selects |
| * any of the Application IDs (AIDs) |
| * that the default service has registered in this category, |
| * that service will automatically be bound to to handle |
| * the transaction. |
| */ |
| public static final int SELECTION_MODE_PREFER_DEFAULT = 0; |
| |
| /** |
| * Return value for {@link #getSelectionModeForCategory(String)}. |
| * |
| * <p>In this mode, when using ISO-DEP card emulation with {@link HostApduService} |
| * or {@link OffHostApduService}, whenever an Application ID (AID) of this category |
| * is selected, the user is asked which service he wants to use to handle |
| * the transaction, even if there is only one matching service. |
| */ |
| public static final int SELECTION_MODE_ALWAYS_ASK = 1; |
| |
| /** |
| * Return value for {@link #getSelectionModeForCategory(String)}. |
| * |
| * <p>In this mode, when using ISO-DEP card emulation with {@link HostApduService} |
| * or {@link OffHostApduService}, the user will only be asked to select a service |
| * if the Application ID (AID) selected by the reader has been registered by multiple |
| * services. If there is only one service that has registered for the AID, |
| * that service will be invoked directly. |
| */ |
| public static final int SELECTION_MODE_ASK_IF_CONFLICT = 2; |
| |
| static boolean sIsInitialized = false; |
| static HashMap<Context, CardEmulation> sCardEmus = new HashMap<Context, CardEmulation>(); |
| static INfcCardEmulation sService; |
| |
| final Context mContext; |
| |
| private CardEmulation(Context context, INfcCardEmulation service) { |
| mContext = context.getApplicationContext(); |
| sService = service; |
| } |
| |
| /** |
| * Helper to get an instance of this class. |
| * |
| * @param adapter A reference to an NfcAdapter object. |
| * @return |
| */ |
| public static synchronized CardEmulation getInstance(NfcAdapter adapter) { |
| if (adapter == null) throw new NullPointerException("NfcAdapter is null"); |
| Context context = adapter.getContext(); |
| if (context == null) { |
| Log.e(TAG, "NfcAdapter context is null."); |
| throw new UnsupportedOperationException(); |
| } |
| if (!sIsInitialized) { |
| IPackageManager pm = ActivityThread.getPackageManager(); |
| if (pm == null) { |
| Log.e(TAG, "Cannot get PackageManager"); |
| throw new UnsupportedOperationException(); |
| } |
| try { |
| if (!pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION, 0)) { |
| Log.e(TAG, "This device does not support card emulation"); |
| throw new UnsupportedOperationException(); |
| } |
| } catch (RemoteException e) { |
| Log.e(TAG, "PackageManager query failed."); |
| throw new UnsupportedOperationException(); |
| } |
| sIsInitialized = true; |
| } |
| CardEmulation manager = sCardEmus.get(context); |
| if (manager == null) { |
| // Get card emu service |
| INfcCardEmulation service = adapter.getCardEmulationService(); |
| if (service == null) { |
| Log.e(TAG, "This device does not implement the INfcCardEmulation interface."); |
| throw new UnsupportedOperationException(); |
| } |
| manager = new CardEmulation(context, service); |
| sCardEmus.put(context, manager); |
| } |
| return manager; |
| } |
| |
| /** |
| * Allows an application to query whether a service is currently |
| * the default service to handle a card emulation category. |
| * |
| * <p>Note that if {@link #getSelectionModeForCategory(String)} |
| * returns {@link #SELECTION_MODE_ALWAYS_ASK} or {@link #SELECTION_MODE_ASK_IF_CONFLICT}, |
| * this method will always return false. That is because in these |
| * selection modes a default can't be set at the category level. For categories where |
| * the selection mode is {@link #SELECTION_MODE_ALWAYS_ASK} or |
| * {@link #SELECTION_MODE_ASK_IF_CONFLICT}, use |
| * {@link #isDefaultServiceForAid(ComponentName, String)} to determine whether a service |
| * is the default for a specific AID. |
| * |
| * @param service The ComponentName of the service |
| * @param category The category |
| * @return whether service is currently the default service for the category. |
| * |
| * <p class="note">Requires the {@link android.Manifest.permission#NFC} permission. |
| */ |
| public boolean isDefaultServiceForCategory(ComponentName service, String category) { |
| try { |
| return sService.isDefaultServiceForCategory(mContext.getUserId(), service, category); |
| } catch (RemoteException e) { |
| // Try one more time |
| recoverService(); |
| if (sService == null) { |
| Log.e(TAG, "Failed to recover CardEmulationService."); |
| return false; |
| } |
| try { |
| return sService.isDefaultServiceForCategory(mContext.getUserId(), service, |
| category); |
| } catch (RemoteException ee) { |
| Log.e(TAG, "Failed to recover CardEmulationService."); |
| return false; |
| } |
| } |
| } |
| |
| /** |
| * |
| * Allows an application to query whether a service is currently |
| * the default handler for a specified ISO7816-4 Application ID. |
| * |
| * @param service The ComponentName of the service |
| * @param aid The ISO7816-4 Application ID |
| * @return whether the service is the default handler for the specified AID |
| * |
| * <p class="note">Requires the {@link android.Manifest.permission#NFC} permission. |
| */ |
| public boolean isDefaultServiceForAid(ComponentName service, String aid) { |
| try { |
| return sService.isDefaultServiceForAid(mContext.getUserId(), service, aid); |
| } catch (RemoteException e) { |
| // Try one more time |
| recoverService(); |
| if (sService == null) { |
| Log.e(TAG, "Failed to recover CardEmulationService."); |
| return false; |
| } |
| try { |
| return sService.isDefaultServiceForAid(mContext.getUserId(), service, aid); |
| } catch (RemoteException ee) { |
| Log.e(TAG, "Failed to reach CardEmulationService."); |
| return false; |
| } |
| } |
| } |
| |
| /** |
| * Returns whether the user has allowed AIDs registered in the |
| * specified category to be handled by a service that is preferred |
| * by the foreground application, instead of by a pre-configured default. |
| * |
| * Foreground applications can set such preferences using the |
| * {@link #setPreferredService(Activity, ComponentName)} method. |
| * |
| * @param category The category, e.g. {@link #CATEGORY_PAYMENT} |
| * @return whether AIDs in the category can be handled by a service |
| * specified by the foreground app. |
| */ |
| public boolean categoryAllowsForegroundPreference(String category) { |
| if (CATEGORY_PAYMENT.equals(category)) { |
| boolean preferForeground = false; |
| try { |
| preferForeground = Settings.Secure.getInt(mContext.getContentResolver(), |
| Settings.Secure.NFC_PAYMENT_FOREGROUND) != 0; |
| } catch (SettingNotFoundException e) { |
| } |
| return preferForeground; |
| } else { |
| // Allowed for all other categories |
| return true; |
| } |
| } |
| |
| /** |
| * Returns the service selection mode for the passed in category. |
| * Valid return values are: |
| * <p>{@link #SELECTION_MODE_PREFER_DEFAULT} the user has requested a default |
| * service for this category, which will be preferred. |
| * <p>{@link #SELECTION_MODE_ALWAYS_ASK} the user has requested to be asked |
| * every time what service he would like to use in this category. |
| * <p>{@link #SELECTION_MODE_ASK_IF_CONFLICT} the user will only be asked |
| * to pick a service if there is a conflict. |
| * @param category The category, for example {@link #CATEGORY_PAYMENT} |
| * @return the selection mode for the passed in category |
| */ |
| public int getSelectionModeForCategory(String category) { |
| if (CATEGORY_PAYMENT.equals(category)) { |
| String defaultComponent = Settings.Secure.getString(mContext.getContentResolver(), |
| Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT); |
| if (defaultComponent != null) { |
| return SELECTION_MODE_PREFER_DEFAULT; |
| } else { |
| return SELECTION_MODE_ALWAYS_ASK; |
| } |
| } else { |
| return SELECTION_MODE_ASK_IF_CONFLICT; |
| } |
| } |
| |
| /** |
| * Registers a list of AIDs for a specific category for the |
| * specified service. |
| * |
| * <p>If a list of AIDs for that category was previously |
| * registered for this service (either statically |
| * through the manifest, or dynamically by using this API), |
| * that list of AIDs will be replaced with this one. |
| * |
| * <p>Note that you can only register AIDs for a service that |
| * is running under the same UID as the caller of this API. Typically |
| * this means you need to call this from the same |
| * package as the service itself, though UIDs can also |
| * be shared between packages using shared UIDs. |
| * |
| * @param service The component name of the service |
| * @param category The category of AIDs to be registered |
| * @param aids A list containing the AIDs to be registered |
| * @return whether the registration was successful. |
| */ |
| public boolean registerAidsForService(ComponentName service, String category, |
| List<String> aids) { |
| AidGroup aidGroup = new AidGroup(aids, category); |
| try { |
| return sService.registerAidGroupForService(mContext.getUserId(), service, aidGroup); |
| } catch (RemoteException e) { |
| // Try one more time |
| recoverService(); |
| if (sService == null) { |
| Log.e(TAG, "Failed to recover CardEmulationService."); |
| return false; |
| } |
| try { |
| return sService.registerAidGroupForService(mContext.getUserId(), service, |
| aidGroup); |
| } catch (RemoteException ee) { |
| Log.e(TAG, "Failed to reach CardEmulationService."); |
| return false; |
| } |
| } |
| } |
| |
| /** |
| * Retrieves the currently registered AIDs for the specified |
| * category for a service. |
| * |
| * <p>Note that this will only return AIDs that were dynamically |
| * registered using {@link #registerAidsForService(ComponentName, String, List)} |
| * method. It will *not* return AIDs that were statically registered |
| * in the manifest. |
| * |
| * @param service The component name of the service |
| * @param category The category for which the AIDs were registered, |
| * e.g. {@link #CATEGORY_PAYMENT} |
| * @return The list of AIDs registered for this category, or null if it couldn't be found. |
| */ |
| public List<String> getAidsForService(ComponentName service, String category) { |
| try { |
| AidGroup group = sService.getAidGroupForService(mContext.getUserId(), service, |
| category); |
| return (group != null ? group.getAids() : null); |
| } catch (RemoteException e) { |
| recoverService(); |
| if (sService == null) { |
| Log.e(TAG, "Failed to recover CardEmulationService."); |
| return null; |
| } |
| try { |
| AidGroup group = sService.getAidGroupForService(mContext.getUserId(), service, |
| category); |
| return (group != null ? group.getAids() : null); |
| } catch (RemoteException ee) { |
| Log.e(TAG, "Failed to recover CardEmulationService."); |
| return null; |
| } |
| } |
| } |
| |
| /** |
| * Removes a previously registered list of AIDs for the specified category for the |
| * service provided. |
| * |
| * <p>Note that this will only remove AIDs that were dynamically |
| * registered using the {@link #registerAidsForService(ComponentName, String, List)} |
| * method. It will *not* remove AIDs that were statically registered in |
| * the manifest. If dynamically registered AIDs are removed using |
| * this method, and a statically registered AID group for the same category |
| * exists in the manifest, the static AID group will become active again. |
| * |
| * @param service The component name of the service |
| * @param category The category of the AIDs to be removed, e.g. {@link #CATEGORY_PAYMENT} |
| * @return whether the group was successfully removed. |
| */ |
| public boolean removeAidsForService(ComponentName service, String category) { |
| try { |
| return sService.removeAidGroupForService(mContext.getUserId(), service, category); |
| } catch (RemoteException e) { |
| // Try one more time |
| recoverService(); |
| if (sService == null) { |
| Log.e(TAG, "Failed to recover CardEmulationService."); |
| return false; |
| } |
| try { |
| return sService.removeAidGroupForService(mContext.getUserId(), service, category); |
| } catch (RemoteException ee) { |
| Log.e(TAG, "Failed to reach CardEmulationService."); |
| return false; |
| } |
| } |
| } |
| |
| /** |
| * Allows a foreground application to specify which card emulation service |
| * should be preferred while a specific Activity is in the foreground. |
| * |
| * <p>The specified Activity must currently be in resumed state. A good |
| * paradigm is to call this method in your {@link Activity#onResume}, and to call |
| * {@link #unsetPreferredService(Activity)} in your {@link Activity#onPause}. |
| * |
| * <p>This method call will fail in two specific scenarios: |
| * <ul> |
| * <li> If the service registers one or more AIDs in the {@link #CATEGORY_PAYMENT} |
| * category, but the user has indicated that foreground apps are not allowed |
| * to override the default payment service. |
| * <li> If the service registers one or more AIDs in the {@link #CATEGORY_OTHER} |
| * category that are also handled by the default payment service, and the |
| * user has indicated that foreground apps are not allowed to override the |
| * default payment service. |
| * </ul> |
| * |
| * <p> Use {@link #categoryAllowsForegroundPreference(String)} to determine |
| * whether foreground apps can override the default payment service. |
| * |
| * <p>Note that this preference is not persisted by the OS, and hence must be |
| * called every time the Activity is resumed. |
| * |
| * @param activity The activity which prefers this service to be invoked |
| * @param service The service to be preferred while this activity is in the foreground |
| * @return whether the registration was successful |
| */ |
| public boolean setPreferredService(Activity activity, ComponentName service) { |
| // Verify the activity is in the foreground before calling into NfcService |
| if (activity == null || service == null) { |
| throw new NullPointerException("activity or service or category is null"); |
| } |
| if (!activity.isResumed()) { |
| throw new IllegalArgumentException("Activity must be resumed."); |
| } |
| try { |
| return sService.setPreferredService(service); |
| } catch (RemoteException e) { |
| // Try one more time |
| recoverService(); |
| if (sService == null) { |
| Log.e(TAG, "Failed to recover CardEmulationService."); |
| return false; |
| } |
| try { |
| return sService.setPreferredService(service); |
| } catch (RemoteException ee) { |
| Log.e(TAG, "Failed to reach CardEmulationService."); |
| return false; |
| } |
| } |
| } |
| |
| /** |
| * Unsets the preferred service for the specified Activity. |
| * |
| * <p>Note that the specified Activity must still be in resumed |
| * state at the time of this call. A good place to call this method |
| * is in your {@link Activity#onPause} implementation. |
| * |
| * @param activity The activity which the service was registered for |
| * @return true when successful |
| */ |
| public boolean unsetPreferredService(Activity activity) { |
| if (activity == null) { |
| throw new NullPointerException("activity is null"); |
| } |
| if (!activity.isResumed()) { |
| throw new IllegalArgumentException("Activity must be resumed."); |
| } |
| try { |
| return sService.unsetPreferredService(); |
| } catch (RemoteException e) { |
| // Try one more time |
| recoverService(); |
| if (sService == null) { |
| Log.e(TAG, "Failed to recover CardEmulationService."); |
| return false; |
| } |
| try { |
| return sService.unsetPreferredService(); |
| } catch (RemoteException ee) { |
| Log.e(TAG, "Failed to reach CardEmulationService."); |
| return false; |
| } |
| } |
| } |
| |
| /** |
| * Some devices may allow an application to register all |
| * AIDs that starts with a certain prefix, e.g. |
| * "A000000004*" to register all MasterCard AIDs. |
| * |
| * Use this method to determine whether this device |
| * supports registering AID prefixes. |
| * |
| * @return whether AID prefix registering is supported on this device. |
| */ |
| public boolean supportsAidPrefixRegistration() { |
| try { |
| return sService.supportsAidPrefixRegistration(); |
| } catch (RemoteException e) { |
| recoverService(); |
| if (sService == null) { |
| Log.e(TAG, "Failed to recover CardEmulationService."); |
| return false; |
| } |
| try { |
| return sService.supportsAidPrefixRegistration(); |
| } catch (RemoteException ee) { |
| Log.e(TAG, "Failed to reach CardEmulationService."); |
| return false; |
| } |
| } |
| } |
| |
| /** |
| * @hide |
| */ |
| public boolean setDefaultServiceForCategory(ComponentName service, String category) { |
| try { |
| return sService.setDefaultServiceForCategory(mContext.getUserId(), service, category); |
| } catch (RemoteException e) { |
| // Try one more time |
| recoverService(); |
| if (sService == null) { |
| Log.e(TAG, "Failed to recover CardEmulationService."); |
| return false; |
| } |
| try { |
| return sService.setDefaultServiceForCategory(mContext.getUserId(), service, |
| category); |
| } catch (RemoteException ee) { |
| Log.e(TAG, "Failed to reach CardEmulationService."); |
| return false; |
| } |
| } |
| } |
| |
| /** |
| * @hide |
| */ |
| public boolean setDefaultForNextTap(ComponentName service) { |
| try { |
| return sService.setDefaultForNextTap(mContext.getUserId(), service); |
| } catch (RemoteException e) { |
| // Try one more time |
| recoverService(); |
| if (sService == null) { |
| Log.e(TAG, "Failed to recover CardEmulationService."); |
| return false; |
| } |
| try { |
| return sService.setDefaultForNextTap(mContext.getUserId(), service); |
| } catch (RemoteException ee) { |
| Log.e(TAG, "Failed to reach CardEmulationService."); |
| return false; |
| } |
| } |
| } |
| |
| /** |
| * @hide |
| */ |
| public List<ApduServiceInfo> getServices(String category) { |
| try { |
| return sService.getServices(mContext.getUserId(), category); |
| } catch (RemoteException e) { |
| // Try one more time |
| recoverService(); |
| if (sService == null) { |
| Log.e(TAG, "Failed to recover CardEmulationService."); |
| return null; |
| } |
| try { |
| return sService.getServices(mContext.getUserId(), category); |
| } catch (RemoteException ee) { |
| Log.e(TAG, "Failed to reach CardEmulationService."); |
| return null; |
| } |
| } |
| } |
| |
| /** |
| * A valid AID according to ISO/IEC 7816-4: |
| * <ul> |
| * <li>Has >= 5 bytes and <=16 bytes (>=10 hex chars and <= 32 hex chars) |
| * <li>Consist of only hex characters |
| * <li>Additionally, we allow an asterisk at the end, to indicate |
| * a prefix |
| * <li>Additinally we allow an (#) at symbol at the end, to indicate |
| * a subset |
| * </ul> |
| * |
| * @hide |
| */ |
| public static boolean isValidAid(String aid) { |
| if (aid == null) |
| return false; |
| |
| // If a prefix/subset AID, the total length must be odd (even # of AID chars + '*') |
| if ((aid.endsWith("*") || aid.endsWith("#")) && ((aid.length() % 2) == 0)) { |
| Log.e(TAG, "AID " + aid + " is not a valid AID."); |
| return false; |
| } |
| |
| // If not a prefix/subset AID, the total length must be even (even # of AID chars) |
| if ((!(aid.endsWith("*") || aid.endsWith("#"))) && ((aid.length() % 2) != 0)) { |
| Log.e(TAG, "AID " + aid + " is not a valid AID."); |
| return false; |
| } |
| |
| // Verify hex characters |
| if (!aid.matches("[0-9A-Fa-f]{10,32}\\*?\\#?")) { |
| Log.e(TAG, "AID " + aid + " is not a valid AID."); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void recoverService() { |
| NfcAdapter adapter = NfcAdapter.getDefaultAdapter(mContext); |
| sService = adapter.getCardEmulationService(); |
| } |
| |
| } |