blob: cb364fb3298ee52286f82a2ed103bb969a04b69b [file] [log] [blame]
/*
* 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.Manifest;
import android.annotation.CallbackExecutor;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresFeature;
import android.annotation.RequiresPermission;
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
import android.annotation.SystemApi;
import android.annotation.UserHandleAware;
import android.annotation.UserIdInt;
import android.app.Activity;
import android.app.role.RoleManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.nfc.ComponentNameAndUser;
import android.nfc.Constants;
import android.nfc.Flags;
import android.nfc.INfcCardEmulation;
import android.nfc.INfcEventListener;
import android.nfc.NfcAdapter;
import android.os.Build;
import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.Settings;
import android.provider.Settings.SettingNotFoundException;
import android.telephony.SubscriptionManager;
import android.util.ArrayMap;
import android.util.Log;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.HashMap;
import java.util.HexFormat;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.regex.Pattern;
/**
* 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 {
private static final Pattern AID_PATTERN = Pattern.compile("[0-9A-Fa-f]{10,32}\\*?\\#?");
private static final Pattern PLPF_PATTERN = Pattern.compile("[0-9A-Fa-f,\\?,\\*\\.]*");
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 they want 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}. There is an optional
* extra field using {@link Intent#EXTRA_USER} to specify
* the {@link UserHandle} of the user that owns the app.
*
* @deprecated Please use {@link android.app.role.RoleManager#createRequestRoleIntent(String)}
* with {@link android.app.role.RoleManager#ROLE_WALLET} parameter
* and {@link Activity#startActivityForResult(Intent, int)} instead.
*/
@Deprecated
@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 they want 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;
/**
* Route to Device Host (DH).
*/
@FlaggedApi(Flags.FLAG_NFC_OVERRIDE_RECOVER_ROUTING_TABLE)
public static final int PROTOCOL_AND_TECHNOLOGY_ROUTE_DH = 0;
/**
* Route to eSE.
*/
@FlaggedApi(Flags.FLAG_NFC_OVERRIDE_RECOVER_ROUTING_TABLE)
public static final int PROTOCOL_AND_TECHNOLOGY_ROUTE_ESE = 1;
/**
* Route to UICC.
*/
@FlaggedApi(Flags.FLAG_NFC_OVERRIDE_RECOVER_ROUTING_TABLE)
public static final int PROTOCOL_AND_TECHNOLOGY_ROUTE_UICC = 2;
/**
* Route to the default value in config file.
*/
@FlaggedApi(Flags.FLAG_NFC_OVERRIDE_RECOVER_ROUTING_TABLE)
public static final int PROTOCOL_AND_TECHNOLOGY_ROUTE_DEFAULT = 3;
/**
* Route unset.
*/
@FlaggedApi(Flags.FLAG_NFC_OVERRIDE_RECOVER_ROUTING_TABLE)
public static final int PROTOCOL_AND_TECHNOLOGY_ROUTE_UNSET = -1;
/**
* Status code returned when {@link #setServiceEnabledForCategoryOther(ComponentName, boolean)}
* succeeded.
* @hide
*/
@SystemApi
@FlaggedApi(Flags.FLAG_NFC_SET_SERVICE_ENABLED_FOR_CATEGORY_OTHER)
public static final int SET_SERVICE_ENABLED_STATUS_OK = 0;
/**
* Status code returned when {@link #setServiceEnabledForCategoryOther(ComponentName, boolean)}
* failed due to the unsupported feature.
* @hide
*/
@SystemApi
@FlaggedApi(Flags.FLAG_NFC_SET_SERVICE_ENABLED_FOR_CATEGORY_OTHER)
public static final int SET_SERVICE_ENABLED_STATUS_FAILURE_FEATURE_UNSUPPORTED = 1;
/**
* Status code returned when {@link #setServiceEnabledForCategoryOther(ComponentName, boolean)}
* failed due to the invalid service.
* @hide
*/
@SystemApi
@FlaggedApi(Flags.FLAG_NFC_SET_SERVICE_ENABLED_FOR_CATEGORY_OTHER)
public static final int SET_SERVICE_ENABLED_STATUS_FAILURE_INVALID_SERVICE = 2;
/**
* Status code returned when {@link #setServiceEnabledForCategoryOther(ComponentName, boolean)}
* failed due to the service is already set to the requested status.
* @hide
*/
@SystemApi
@FlaggedApi(Flags.FLAG_NFC_SET_SERVICE_ENABLED_FOR_CATEGORY_OTHER)
public static final int SET_SERVICE_ENABLED_STATUS_FAILURE_ALREADY_SET = 3;
/**
* Status code returned when {@link #setServiceEnabledForCategoryOther(ComponentName, boolean)}
* failed due to unknown error.
* @hide
*/
@SystemApi
@FlaggedApi(Flags.FLAG_NFC_SET_SERVICE_ENABLED_FOR_CATEGORY_OTHER)
public static final int SET_SERVICE_ENABLED_STATUS_FAILURE_UNKNOWN_ERROR = 4;
/**
* Status code returned by {@link #setServiceEnabledForCategoryOther(ComponentName, boolean)}
* @hide
*/
@IntDef(prefix = "SET_SERVICE_ENABLED_STATUS_", value = {
SET_SERVICE_ENABLED_STATUS_OK,
SET_SERVICE_ENABLED_STATUS_FAILURE_FEATURE_UNSUPPORTED,
SET_SERVICE_ENABLED_STATUS_FAILURE_INVALID_SERVICE,
SET_SERVICE_ENABLED_STATUS_FAILURE_ALREADY_SET,
SET_SERVICE_ENABLED_STATUS_FAILURE_UNKNOWN_ERROR
})
@Retention(RetentionPolicy.SOURCE)
public @interface SetServiceEnabledStatusCode {}
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) {
PackageManager pm = context.getPackageManager();
if (pm == null) {
Log.e(TAG, "Cannot get PackageManager");
throw new UnsupportedOperationException();
}
if (!pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION)) {
Log.e(TAG, "This device does not support card emulation");
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) {
return callServiceReturn(() ->
sService.isDefaultServiceForCategory(
mContext.getUser().getIdentifier(), service, category), 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) {
return callServiceReturn(() ->
sService.isDefaultServiceForAid(
mContext.getUser().getIdentifier(), service, aid), false);
}
/**
* <p>
* 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.
* <p class="note">
* Starting with {@link Build.VERSION_CODES#VANILLA_ICE_CREAM}, this method will always
* return true.
*
* @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.
*/
@SuppressWarnings("NonUserGetterCalled")
public boolean categoryAllowsForegroundPreference(String category) {
Context contextAsUser = mContext.createContextAsUser(
UserHandle.of(UserHandle.myUserId()), 0);
RoleManager roleManager = contextAsUser.getSystemService(RoleManager.class);
if (roleManager.isRoleAvailable(RoleManager.ROLE_WALLET)) {
return true;
}
if (CATEGORY_PAYMENT.equals(category)) {
boolean preferForeground = false;
try {
preferForeground = Settings.Secure.getInt(
contextAsUser.getContentResolver(),
Constants.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 they 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.
*
* <p class="note">
* Starting with {@link Build.VERSION_CODES#VANILLA_ICE_CREAM}, the default service defined
* by the holder of {@link android.app.role.RoleManager#ROLE_WALLET} and is category agnostic.
*
* @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)) {
boolean paymentRegistered = callServiceReturn(() ->
sService.isDefaultPaymentRegistered(), false);
if (paymentRegistered) {
return SELECTION_MODE_PREFER_DEFAULT;
} else {
return SELECTION_MODE_ALWAYS_ASK;
}
} else {
return SELECTION_MODE_ASK_IF_CONFLICT;
}
}
/**
* Sets whether when this service becomes the preferred service, if the NFC stack
* should enable observe mode or disable observe mode. The default is to not enable observe
* mode when a service either the foreground default service or the default payment service so
* not calling this method will preserve that behavior.
*
* @param service The component name of the service
* @param enable Whether the service should default to observe mode or not
* @return whether the change was successful.
*/
@FlaggedApi(Flags.FLAG_NFC_OBSERVE_MODE)
public boolean setShouldDefaultToObserveModeForService(@NonNull ComponentName service,
boolean enable) {
return callServiceReturn(() ->
sService.setShouldDefaultToObserveModeForService(
mContext.getUser().getIdentifier(), service, enable), false);
}
/**
* Register a polling loop filter (PLF) for a HostApduService and indicate whether it should
* auto-transact or not. The PLF can be sequence of an
* even number of at least 2 hexadecimal numbers (0-9, A-F or a-f), representing a series of
* bytes. When non-standard polling loop frame matches this sequence exactly, it may be
* delivered to {@link HostApduService#processPollingFrames(List)}. If auto-transact
* is set to true and this service is currently preferred or there are no other services
* registered for this filter then observe mode will also be disabled.
* @param service The HostApduService to register the filter for
* @param pollingLoopFilter The filter to register
* @param autoTransact true to have the NFC stack automatically disable observe mode and allow
* transactions to proceed when this filter matches, false otherwise
* @return true if the filter was registered, false otherwise
* @throws IllegalArgumentException if the passed in string doesn't parse to at least one byte
*/
@FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP)
public boolean registerPollingLoopFilterForService(@NonNull ComponentName service,
@NonNull String pollingLoopFilter, boolean autoTransact) {
final String pollingLoopFilterV = validatePollingLoopFilter(pollingLoopFilter);
return callServiceReturn(() ->
sService.registerPollingLoopFilterForService(
mContext.getUser().getIdentifier(), service, pollingLoopFilterV, autoTransact),
false);
}
/**
* Unregister a polling loop filter (PLF) for a HostApduService. If the PLF had previously been
* registered via {@link #registerPollingLoopFilterForService(ComponentName, String, boolean)}
* for this service it will be removed.
* @param service The HostApduService to unregister the filter for
* @param pollingLoopFilter The filter to unregister
* @return true if the filter was removed, false otherwise
* @throws IllegalArgumentException if the passed in string doesn't parse to at least one byte
*/
@FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP)
public boolean removePollingLoopFilterForService(@NonNull ComponentName service,
@NonNull String pollingLoopFilter) {
final String pollingLoopFilterV = validatePollingLoopFilter(pollingLoopFilter);
return callServiceReturn(() ->
sService.removePollingLoopFilterForService(
mContext.getUser().getIdentifier(), service, pollingLoopFilterV), false);
}
/**
* Register a polling loop pattern filter (PLPF) for a HostApduService and indicate whether it
* should auto-transact or not. The pattern may include the characters 0-9 and A-F as well as
* the regular expression operators `.`, `?` and `*`. When the beginning of anon-standard
* polling loop frame matches this sequence exactly, it may be delivered to
* {@link HostApduService#processPollingFrames(List)}. If auto-transact is set to true and this
* service is currently preferred or there are no other services registered for this filter
* then observe mode will also be disabled.
* @param service The HostApduService to register the filter for
* @param pollingLoopPatternFilter The pattern filter to register, must to be compatible with
* {@link java.util.regex.Pattern#compile(String)} and only contain hexadecimal numbers
* and `.`, `?` and `*` operators
* @param autoTransact true to have the NFC stack automatically disable observe mode and allow
* transactions to proceed when this filter matches, false otherwise
* @return true if the filter was registered, false otherwise
* @throws IllegalArgumentException if the filter containst elements other than hexadecimal
* numbers and `.`, `?` and `*` operators
* @throws java.util.regex.PatternSyntaxException if the regex syntax is invalid
*/
@FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP)
public boolean registerPollingLoopPatternFilterForService(@NonNull ComponentName service,
@NonNull String pollingLoopPatternFilter, boolean autoTransact) {
final String pollingLoopPatternFilterV =
validatePollingLoopPatternFilter(pollingLoopPatternFilter);
return callServiceReturn(() ->
sService.registerPollingLoopPatternFilterForService(
mContext.getUser().getIdentifier(), service, pollingLoopPatternFilterV,
autoTransact),
false);
}
/**
* Unregister a polling loop pattern filter (PLPF) for a HostApduService. If the PLF had
* previously been registered via
* {@link #registerPollingLoopFilterForService(ComponentName, String, boolean)} for this
* service it will be removed.
* @param service The HostApduService to unregister the filter for
* @param pollingLoopPatternFilter The filter to unregister, must to be compatible with
* {@link java.util.regex.Pattern#compile(String)} and only contain hexadecimal numbers
* and`.`, `?` and `*` operators
* @return true if the filter was removed, false otherwise
* @throws IllegalArgumentException if the filter containst elements other than hexadecimal
* numbers and `.`, `?` and `*` operators
* @throws java.util.regex.PatternSyntaxException if the regex syntax is invalid
*/
@FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP)
public boolean removePollingLoopPatternFilterForService(@NonNull ComponentName service,
@NonNull String pollingLoopPatternFilter) {
final String pollingLoopPatternFilterV =
validatePollingLoopPatternFilter(pollingLoopPatternFilter);
return callServiceReturn(() ->
sService.removePollingLoopPatternFilterForService(
mContext.getUser().getIdentifier(), service, pollingLoopPatternFilterV), false);
}
/**
* 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) {
final AidGroup aidGroup = new AidGroup(aids, category);
return callServiceReturn(() ->
sService.registerAidGroupForService(
mContext.getUser().getIdentifier(), service, aidGroup), false);
}
/**
* Unsets the off-host Secure Element for the given service.
*
* <p>Note that this will only remove Secure Element that was dynamically
* set using the {@link #setOffHostForService(ComponentName, String)}
* and resets it to a value that was statically assigned using manifest.
*
* <p>Note that you can only unset off-host SE 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
* @return whether the registration was successful.
*/
@RequiresPermission(android.Manifest.permission.NFC)
@NonNull
public boolean unsetOffHostForService(@NonNull ComponentName service) {
return callServiceReturn(() ->
sService.unsetOffHostForService(
mContext.getUser().getIdentifier(), service), false);
}
/**
* Sets the off-host Secure Element for the given service.
*
* <p>If off-host SE was initially set (either statically
* through the manifest, or dynamically by using this API),
* it will be replaced with this one. All AIDs registered by
* this service will be re-routed to this Secure Element if
* successful. AIDs that was statically assigned using manifest
* will re-route to off-host SE that stated in manifest after NFC
* toggle.
*
* <p>Note that you can only set off-host SE 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.
*
* <p>Registeration will be successful only if the Secure Element
* exists on the device.
*
* @param service The component name of the service
* @param offHostSecureElement Secure Element to register the AID to. Only accept strings with
* prefix SIM or prefix eSE.
* Ref: GSMA TS.26 - NFC Handset Requirements
* TS26_NFC_REQ_069: For UICC, Secure Element Name SHALL be
* SIM[smartcard slot]
* (e.g. SIM/SIM1, SIM2… SIMn).
* TS26_NFC_REQ_070: For embedded SE, Secure Element Name SHALL be
* eSE[number]
* (e.g. eSE/eSE1, eSE2, etc.).
* @return whether the registration was successful.
*/
@RequiresPermission(android.Manifest.permission.NFC)
@NonNull
public boolean setOffHostForService(@NonNull ComponentName service,
@NonNull String offHostSecureElement) {
NfcAdapter adapter = NfcAdapter.getDefaultAdapter(mContext);
if (adapter == null || offHostSecureElement == null) {
return false;
}
List<String> validSE = adapter.getSupportedOffHostSecureElements();
if ((offHostSecureElement.startsWith("eSE") && !validSE.contains("eSE"))
|| (offHostSecureElement.startsWith("SIM") && !validSE.contains("SIM"))) {
return false;
}
if (!offHostSecureElement.startsWith("eSE") && !offHostSecureElement.startsWith("SIM")) {
return false;
}
if (offHostSecureElement.equals("eSE")) {
offHostSecureElement = "eSE1";
} else if (offHostSecureElement.equals("SIM")) {
offHostSecureElement = "SIM1";
}
final String offHostSecureElementV = new String(offHostSecureElement);
return callServiceReturn(() ->
sService.setOffHostForService(
mContext.getUser().getIdentifier(), service, offHostSecureElementV), 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) {
AidGroup group = callServiceReturn(() ->
sService.getAidGroupForService(
mContext.getUser().getIdentifier(), service, category), null);
return (group != null ? group.getAids() : 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) {
return callServiceReturn(() ->
sService.removeAidGroupForService(
mContext.getUser().getIdentifier(), service, category), 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");
}
return callServiceReturn(() -> sService.setPreferredService(service), 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");
}
return callServiceReturn(() -> sService.unsetPreferredService(), 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() {
return callServiceReturn(() -> sService.supportsAidPrefixRegistration(), false);
}
/**
* Retrieves the registered AIDs for the preferred payment service.
*
* @return The list of AIDs registered for this category, or null if it couldn't be found.
*/
@RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO)
@Nullable
public List<String> getAidsForPreferredPaymentService() {
ApduServiceInfo serviceInfo = callServiceReturn(() ->
sService.getPreferredPaymentService(mContext.getUser().getIdentifier()), null);
return (serviceInfo != null ? serviceInfo.getAids() : null);
}
/**
* Retrieves the route destination for the preferred payment service.
*
* <p class="note">
* Starting with {@link Build.VERSION_CODES#VANILLA_ICE_CREAM}, the preferred payment service
* no longer exists and is replaced by {@link android.app.role.RoleManager#ROLE_WALLET}. This
* will return the route for one of the services registered by the role holder (if any). If
* there are multiple services registered, it is unspecified which of those will be used to
* determine the route.
*
* @return The route destination secure element name of the preferred payment service.
* HCE payment: "Host"
* OffHost payment: 1. String with prefix SIM or prefix eSE string.
* Ref: GSMA TS.26 - NFC Handset Requirements
* TS26_NFC_REQ_069: For UICC, Secure Element Name SHALL be
* SIM[smartcard slot]
* (e.g. SIM/SIM1, SIM2… SIMn).
* TS26_NFC_REQ_070: For embedded SE, Secure Element Name SHALL be
* eSE[number]
* (e.g. eSE/eSE1, eSE2, etc.).
* 2. "OffHost" if the payment service does not specify secure element
* name.
*/
@RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO)
@Nullable
public String getRouteDestinationForPreferredPaymentService() {
ApduServiceInfo serviceInfo = callServiceReturn(() ->
sService.getPreferredPaymentService(mContext.getUser().getIdentifier()), null);
if (serviceInfo != null) {
if (!serviceInfo.isOnHost()) {
return serviceInfo.getOffHostSecureElement() == null ?
"OffHost" : serviceInfo.getOffHostSecureElement();
}
return "Host";
}
return null;
}
/**
* Returns a user-visible description of the preferred payment service.
*
* <p class="note">
* Starting with {@link Build.VERSION_CODES#VANILLA_ICE_CREAM}, the preferred payment service
* no longer exists and is replaced by {@link android.app.role.RoleManager#ROLE_WALLET}. This
* will return the description for one of the services registered by the role holder (if any).
* If there are multiple services registered, it is unspecified which of those will be used
* to obtain the service description here.
*
* @return the preferred payment service description
*/
@RequiresPermission(Manifest.permission.NFC_PREFERRED_PAYMENT_INFO)
@Nullable
public CharSequence getDescriptionForPreferredPaymentService() {
ApduServiceInfo serviceInfo = callServiceReturn(() ->
sService.getPreferredPaymentService(mContext.getUser().getIdentifier()), null);
return (serviceInfo != null ? serviceInfo.getDescription() : null);
}
/**
* @hide
*/
public boolean setDefaultServiceForCategory(ComponentName service, String category) {
return callServiceReturn(() ->
sService.setDefaultServiceForCategory(
mContext.getUser().getIdentifier(), service, category), false);
}
/**
* @hide
*/
public boolean setDefaultForNextTap(ComponentName service) {
return callServiceReturn(() ->
sService.setDefaultForNextTap(
mContext.getUser().getIdentifier(), service), false);
}
/**
* @hide
*/
public boolean setDefaultForNextTap(int userId, ComponentName service) {
return callServiceReturn(() ->
sService.setDefaultForNextTap(userId, service), false);
}
/**
* @hide
*/
public List<ApduServiceInfo> getServices(String category) {
return callServiceReturn(() ->
sService.getServices(
mContext.getUser().getIdentifier(), category), null);
}
/**
* Retrieves list of services registered of the provided category for the provided user.
*
* @param category Category string, one of {@link #CATEGORY_PAYMENT} or {@link #CATEGORY_OTHER}
* @param userId the user handle of the user whose information is being requested.
* @hide
*/
@SystemApi
@FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE)
@NonNull
public List<ApduServiceInfo> getServices(@NonNull String category, @UserIdInt int userId) {
return callServiceReturn(() ->
sService.getServices(userId, category), null);
}
/**
* Tests the validity of the polling loop filter.
* @param pollingLoopFilter The polling loop filter to test.
*
* @hide
*/
@FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP)
public static @NonNull String validatePollingLoopFilter(@NonNull String pollingLoopFilter) {
// Verify hex characters
byte[] plfBytes = HexFormat.of().parseHex(pollingLoopFilter);
if (plfBytes.length == 0) {
throw new IllegalArgumentException(
"Polling loop filter must contain at least one byte.");
}
return HexFormat.of().withUpperCase().formatHex(plfBytes);
}
/**
* Tests the validity of the polling loop pattern filter.
* @param pollingLoopPatternFilter The polling loop filter to test.
*
* @hide
*/
@FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP)
public static @NonNull String validatePollingLoopPatternFilter(
@NonNull String pollingLoopPatternFilter) {
// Verify hex characters
if (!PLPF_PATTERN.matcher(pollingLoopPatternFilter).matches()) {
throw new IllegalArgumentException(
"Polling loop pattern filters may only contain hexadecimal numbers, ?s and *s");
}
return Pattern.compile(pollingLoopPatternFilter.toUpperCase(Locale.ROOT)).toString();
}
/**
* 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_PATTERN.matcher(aid).matches()) {
Log.e(TAG, "AID " + aid + " is not a valid AID.");
return false;
}
return true;
}
/**
* Allows to set or unset preferred service (category other) to avoid AID Collision. The user
* should use corresponding context using {@link Context#createContextAsUser(UserHandle, int)}
*
* @param service The ComponentName of the service
* @param status true to enable, false to disable
* @return status code defined in {@link SetServiceEnabledStatusCode}
*
* @hide
*/
@SystemApi
@FlaggedApi(Flags.FLAG_NFC_SET_SERVICE_ENABLED_FOR_CATEGORY_OTHER)
@RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
@SetServiceEnabledStatusCode
public int setServiceEnabledForCategoryOther(@NonNull ComponentName service,
boolean status) {
return callServiceReturn(() ->
sService.setServiceEnabledForCategoryOther(mContext.getUser().getIdentifier(),
service, status), SET_SERVICE_ENABLED_STATUS_FAILURE_UNKNOWN_ERROR);
}
/** @hide */
@IntDef(prefix = "PROTOCOL_AND_TECHNOLOGY_ROUTE_",
value = {
PROTOCOL_AND_TECHNOLOGY_ROUTE_DH,
PROTOCOL_AND_TECHNOLOGY_ROUTE_ESE,
PROTOCOL_AND_TECHNOLOGY_ROUTE_UICC,
PROTOCOL_AND_TECHNOLOGY_ROUTE_UNSET,
PROTOCOL_AND_TECHNOLOGY_ROUTE_DEFAULT
})
@Retention(RetentionPolicy.SOURCE)
public @interface ProtocolAndTechnologyRoute {}
/**
* Setting NFC controller routing table, which includes Protocol Route and Technology Route,
* while this Activity is in the foreground.
*
* The parameter set to {@link #PROTOCOL_AND_TECHNOLOGY_ROUTE_UNSET}
* can be used to keep current values for that entry. Either
* Protocol Route or Technology Route should be override when calling this API, otherwise
* throw {@link IllegalArgumentException}.
* <p>
* Example usage in an Activity that requires to set proto route to "ESE" and keep tech route:
* <pre>
* protected void onResume() {
* mNfcAdapter.overrideRoutingTable(
* this, {@link #PROTOCOL_AND_TECHNOLOGY_ROUTE_ESE},
* {@link #PROTOCOL_AND_TECHNOLOGY_ROUTE_UNSET});
* }</pre>
* </p>
* Also activities must call {@link #recoverRoutingTable(Activity)}
* when it goes to the background. Only the package of the
* currently preferred service (the service set as preferred by the current foreground
* application via {@link CardEmulation#setPreferredService(Activity, ComponentName)} or the
* current Default Wallet Role Holder {@link RoleManager#ROLE_WALLET}),
* otherwise a call to this method will fail and throw {@link SecurityException}.
* @param activity The Activity that requests NFC controller routing table to be changed.
* @param protocol ISO-DEP route destination, where the possible inputs are defined
* in {@link ProtocolAndTechnologyRoute}.
* @param technology Tech-A, Tech-B and Tech-F route destination, where the possible inputs
* are defined in {@link ProtocolAndTechnologyRoute}
* @throws SecurityException if the caller is not the preferred NFC service
* @throws IllegalArgumentException if the activity is not resumed or the caller is not in the
* foreground.
* <p>
* This is a high risk API and only included to support mainline effort
* @hide
*/
@SystemApi
@RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
@FlaggedApi(Flags.FLAG_NFC_OVERRIDE_RECOVER_ROUTING_TABLE)
public void overrideRoutingTable(
@NonNull Activity activity, @ProtocolAndTechnologyRoute int protocol,
@ProtocolAndTechnologyRoute int technology) {
if (!activity.isResumed()) {
throw new IllegalArgumentException("Activity must be resumed.");
}
String protocolRoute = routeIntToString(protocol);
String technologyRoute = routeIntToString(technology);
callService(() ->
sService.overrideRoutingTable(
mContext.getUser().getIdentifier(),
protocolRoute,
technologyRoute,
mContext.getPackageName()));
}
/**
* Restore the NFC controller routing table,
* which was changed by {@link #overrideRoutingTable(Activity, int, int)}
*
* @param activity The Activity that requested NFC controller routing table to be changed.
* @throws IllegalArgumentException if the caller is not in the foreground.
*
* @hide
*/
@SystemApi
@RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
@FlaggedApi(Flags.FLAG_NFC_OVERRIDE_RECOVER_ROUTING_TABLE)
public void recoverRoutingTable(@NonNull Activity activity) {
if (!activity.isResumed()) {
throw new IllegalArgumentException("Activity must be resumed.");
}
callService(() ->
sService.recoverRoutingTable(
mContext.getUser().getIdentifier()));
}
/**
* Is EUICC supported as a Secure Element EE which supports off host card emulation.
*
* @return true if the device supports EUICC for off host card emulation, false otherwise.
*/
@FlaggedApi(android.nfc.Flags.FLAG_ENABLE_CARD_EMULATION_EUICC)
public boolean isEuiccSupported() {
return callServiceReturn(() -> sService.isEuiccSupported(), false);
}
/**
* Setting the default subscription ID succeeded.
* @hide
*/
@SystemApi
@FlaggedApi(android.nfc.Flags.FLAG_ENABLE_CARD_EMULATION_EUICC)
public static final int SET_SUBSCRIPTION_ID_STATUS_SUCCESS = 0;
/**
* Setting the default subscription ID failed because the subscription ID is invalid.
* @hide
*/
@SystemApi
@FlaggedApi(android.nfc.Flags.FLAG_ENABLE_CARD_EMULATION_EUICC)
public static final int SET_SUBSCRIPTION_ID_STATUS_FAILED_INVALID_SUBSCRIPTION_ID = 1;
/**
* Setting the default subscription ID failed because there was an internal error processing
* the request. For ex: NFC service died in the middle of handling the API.
* @hide
*/
@SystemApi
@FlaggedApi(android.nfc.Flags.FLAG_ENABLE_CARD_EMULATION_EUICC)
public static final int SET_SUBSCRIPTION_ID_STATUS_FAILED_INTERNAL_ERROR = 2;
/**
* Setting the default subscription ID failed because this feature is not supported on the
* device.
* @hide
*/
@SystemApi
@FlaggedApi(android.nfc.Flags.FLAG_ENABLE_CARD_EMULATION_EUICC)
public static final int SET_SUBSCRIPTION_ID_STATUS_FAILED_NOT_SUPPORTED = 3;
/** @hide */
@IntDef(prefix = "SET_SUBSCRIPTION_ID_STATUS_",
value = {
SET_SUBSCRIPTION_ID_STATUS_SUCCESS,
SET_SUBSCRIPTION_ID_STATUS_FAILED_INVALID_SUBSCRIPTION_ID,
SET_SUBSCRIPTION_ID_STATUS_FAILED_INTERNAL_ERROR,
SET_SUBSCRIPTION_ID_STATUS_FAILED_NOT_SUPPORTED,
})
@Retention(RetentionPolicy.SOURCE)
public @interface SetSubscriptionIdStatus {}
/**
* Sets the system's default NFC subscription id.
*
* <p> For devices with multiple UICC/EUICC that is configured to be NFCEE, this sets the
* default UICC NFCEE that will handle NFC offhost CE transactoions </p>
*
* @param subscriptionId the default NFC subscription Id to set.
* @return status of the operation.
*
* @throws UnsupportedOperationException If the device does not have
* {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
* @hide
*/
@SystemApi
@RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)
@RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
@FlaggedApi(android.nfc.Flags.FLAG_ENABLE_CARD_EMULATION_EUICC)
public @SetSubscriptionIdStatus int setDefaultNfcSubscriptionId(int subscriptionId) {
return callServiceReturn(() ->
sService.setDefaultNfcSubscriptionId(
subscriptionId, mContext.getPackageName()),
SET_SUBSCRIPTION_ID_STATUS_FAILED_INTERNAL_ERROR);
}
/**
* Returns the system's default NFC subscription id.
*
* <p> For devices with multiple UICC/EUICC that is configured to be NFCEE, this returns the
* default UICC NFCEE that will handle NFC offhost CE transactoions </p>
* <p> If the device has no UICC that can serve as NFCEE, this will return
* {@link SubscriptionManager#INVALID_SUBSCRIPTION_ID}.</p>
*
* @return the default NFC subscription Id if set,
* {@link SubscriptionManager#INVALID_SUBSCRIPTION_ID} otherwise.
*
* @throws UnsupportedOperationException If the device does not have
* {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
*/
@RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)
@FlaggedApi(android.nfc.Flags.FLAG_ENABLE_CARD_EMULATION_EUICC)
public int getDefaultNfcSubscriptionId() {
return callServiceReturn(() ->
sService.getDefaultNfcSubscriptionId(mContext.getPackageName()),
SubscriptionManager.INVALID_SUBSCRIPTION_ID);
}
/**
* Returns the value of {@link Settings.Secure#NFC_PAYMENT_DEFAULT_COMPONENT}.
*
* @param context A context
* @return A ComponentName for the setting value, or null.
*
* @hide
*/
@SystemApi
@UserHandleAware
@RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO)
@SuppressWarnings("AndroidFrameworkClientSidePermissionCheck")
@FlaggedApi(android.permission.flags.Flags.FLAG_WALLET_ROLE_ENABLED)
@Nullable
public static ComponentName getPreferredPaymentService(@NonNull Context context) {
context.checkCallingOrSelfPermission(Manifest.permission.NFC_PREFERRED_PAYMENT_INFO);
String defaultPaymentComponent = Settings.Secure.getString(context.getContentResolver(),
Constants.SETTINGS_SECURE_NFC_PAYMENT_DEFAULT_COMPONENT);
if (defaultPaymentComponent == null) {
return null;
}
return ComponentName.unflattenFromString(defaultPaymentComponent);
}
/** @hide */
interface ServiceCall {
void call() throws RemoteException;
}
/** @hide */
public static void callService(ServiceCall call) {
try {
if (sService == null) {
NfcAdapter.attemptDeadServiceRecovery(
new RemoteException("NFC CardEmulation Service is null"));
sService = NfcAdapter.getCardEmulationService();
}
call.call();
} catch (RemoteException e) {
NfcAdapter.attemptDeadServiceRecovery(e);
sService = NfcAdapter.getCardEmulationService();
try {
call.call();
} catch (RemoteException ee) {
ee.rethrowAsRuntimeException();
}
}
}
/** @hide */
interface ServiceCallReturn<T> {
T call() throws RemoteException;
}
/** @hide */
public static <T> T callServiceReturn(ServiceCallReturn<T> call, T defaultReturn) {
try {
if (sService == null) {
NfcAdapter.attemptDeadServiceRecovery(
new RemoteException("NFC CardEmulation Service is null"));
sService = NfcAdapter.getCardEmulationService();
}
return call.call();
} catch (RemoteException e) {
NfcAdapter.attemptDeadServiceRecovery(e);
sService = NfcAdapter.getCardEmulationService();
// Try one more time
try {
return call.call();
} catch (RemoteException ee) {
ee.rethrowAsRuntimeException();
}
}
return defaultReturn;
}
/** @hide */
public static String routeIntToString(@ProtocolAndTechnologyRoute int route) {
return switch (route) {
case PROTOCOL_AND_TECHNOLOGY_ROUTE_DH -> "DH";
case PROTOCOL_AND_TECHNOLOGY_ROUTE_ESE -> "eSE";
case PROTOCOL_AND_TECHNOLOGY_ROUTE_UICC -> "SIM";
case PROTOCOL_AND_TECHNOLOGY_ROUTE_UNSET -> null;
case PROTOCOL_AND_TECHNOLOGY_ROUTE_DEFAULT -> "default";
default -> throw new IllegalStateException("Unexpected value: " + route);
};
}
@FlaggedApi(android.nfc.Flags.FLAG_NFC_EVENT_LISTENER)
public static final int NFC_INTERNAL_ERROR_UNKNOWN = 0;
/**
* This error is reported when the NFC command watchdog restarts the NFC stack.
*/
@FlaggedApi(android.nfc.Flags.FLAG_NFC_EVENT_LISTENER)
public static final int NFC_INTERNAL_ERROR_NFC_CRASH_RESTART = 1;
/**
* This error is reported when the NFC controller does not respond or there's an NCI transport
* error.
*/
@FlaggedApi(android.nfc.Flags.FLAG_NFC_EVENT_LISTENER)
public static final int NFC_INTERNAL_ERROR_NFC_HARDWARE_ERROR = 2;
/**
* This error is reported when the NFC stack times out while waiting for a response to a command
* sent to the NFC hardware.
*/
@FlaggedApi(android.nfc.Flags.FLAG_NFC_EVENT_LISTENER)
public static final int NFC_INTERNAL_ERROR_COMMAND_TIMEOUT = 3;
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@FlaggedApi(android.nfc.Flags.FLAG_NFC_EVENT_LISTENER)
@IntDef(prefix = "NFC_INTERNAL_ERROR_", value = {
NFC_INTERNAL_ERROR_UNKNOWN,
NFC_INTERNAL_ERROR_NFC_CRASH_RESTART,
NFC_INTERNAL_ERROR_NFC_HARDWARE_ERROR,
NFC_INTERNAL_ERROR_COMMAND_TIMEOUT,
})
public @interface NfcInternalErrorType {}
/** Listener for preferred service state changes. */
@FlaggedApi(android.nfc.Flags.FLAG_NFC_EVENT_LISTENER)
public interface NfcEventListener {
/**
* This method is called when this package gains or loses preferred Nfc service status,
* either the Default Wallet Role holder (see {@link
* android.app.role.RoleManager#ROLE_WALLET}) or the preferred service of the foreground
* activity set with {@link #setPreferredService(Activity, ComponentName)}
*
* @param isPreferred true is this service has become the preferred Nfc service, false if it
* is no longer the preferred service
*/
@FlaggedApi(android.nfc.Flags.FLAG_NFC_EVENT_LISTENER)
default void onPreferredServiceChanged(boolean isPreferred) {}
/**
* This method is called when observe mode has been enabled or disabled.
*
* @param isEnabled true if observe mode has been enabled, false if it has been disabled
*/
@FlaggedApi(android.nfc.Flags.FLAG_NFC_EVENT_LISTENER)
default void onObserveModeStateChanged(boolean isEnabled) {}
/**
* This method is called when an AID conflict is detected during an NFC transaction. This
* can happen when multiple services are registered for the same AID.
*
* @param aid The AID that is in conflict
*/
@FlaggedApi(android.nfc.Flags.FLAG_NFC_EVENT_LISTENER)
default void onAidConflictOccurred(@NonNull String aid) {}
/**
* This method is called when an AID is not routed to any service during an NFC
* transaction. This can happen when no service is registered for the given AID.
*
* @param aid the AID that was not routed
*/
@FlaggedApi(android.nfc.Flags.FLAG_NFC_EVENT_LISTENER)
default void onAidNotRouted(@NonNull String aid) {}
/**
* This method is called when the NFC state changes.
*
* @see NfcAdapter#getAdapterState()
*
* @param state The new NFC state
*/
@FlaggedApi(android.nfc.Flags.FLAG_NFC_EVENT_LISTENER)
default void onNfcStateChanged(@NfcAdapter.AdapterState int state) {}
/**
* This method is called when the NFC controller is in card emulation mode and an NFC
* reader's field is either detected or lost.
*
* @param isDetected true if an NFC reader is detected, false if it is lost
*/
@FlaggedApi(android.nfc.Flags.FLAG_NFC_EVENT_LISTENER)
default void onRemoteFieldChanged(boolean isDetected) {}
/**
* This method is called when an internal error is reported by the NFC stack.
*
* No action is required in response to these events as the NFC stack will automatically
* attempt to recover. These errors are reported for informational purposes only.
*
* Note that these errors can be reported when performing various internal NFC operations
* (such as during device shutdown) and cannot always be explicitly correlated with NFC
* transaction failures.
*
* @param errorType The type of the internal error
*/
@FlaggedApi(android.nfc.Flags.FLAG_NFC_EVENT_LISTENER)
default void onInternalErrorReported(@NfcInternalErrorType int errorType) {}
}
private final ArrayMap<NfcEventListener, Executor> mNfcEventListeners = new ArrayMap<>();
final INfcEventListener mINfcEventListener =
new INfcEventListener.Stub() {
public void onPreferredServiceChanged(ComponentNameAndUser componentNameAndUser) {
if (!android.nfc.Flags.nfcEventListener()) {
return;
}
boolean isPreferred =
componentNameAndUser != null
&& componentNameAndUser.getUserId()
== mContext.getUser().getIdentifier()
&& componentNameAndUser.getComponentName() != null
&& Objects.equals(
mContext.getPackageName(),
componentNameAndUser.getComponentName()
.getPackageName());
callListeners(listener -> listener.onPreferredServiceChanged(isPreferred));
}
public void onObserveModeStateChanged(boolean isEnabled) {
if (!android.nfc.Flags.nfcEventListener()) {
return;
}
callListeners(listener -> listener.onObserveModeStateChanged(isEnabled));
}
public void onAidConflictOccurred(String aid) {
if (!android.nfc.Flags.nfcEventListener()) {
return;
}
callListeners(listener -> listener.onAidConflictOccurred(aid));
}
public void onAidNotRouted(String aid) {
if (!android.nfc.Flags.nfcEventListener()) {
return;
}
callListeners(listener -> listener.onAidNotRouted(aid));
}
public void onNfcStateChanged(int state) {
if (!android.nfc.Flags.nfcEventListener()) {
return;
}
callListeners(listener -> listener.onNfcStateChanged(state));
}
public void onRemoteFieldChanged(boolean isDetected) {
if (!android.nfc.Flags.nfcEventListener()) {
return;
}
callListeners(listener -> listener.onRemoteFieldChanged(isDetected));
}
public void onInternalErrorReported(@NfcInternalErrorType int errorType) {
if (!android.nfc.Flags.nfcEventListener()) {
return;
}
callListeners(listener -> listener.onInternalErrorReported(errorType));
}
interface ListenerCall {
void invoke(NfcEventListener listener);
}
private void callListeners(ListenerCall listenerCall) {
synchronized (mNfcEventListeners) {
mNfcEventListeners.forEach(
(listener, executor) -> {
executor.execute(() -> listenerCall.invoke(listener));
});
}
}
};
/**
* Register a listener for NFC Events.
*
* @param executor The Executor to run the call back with
* @param listener The listener to register
*/
@FlaggedApi(android.nfc.Flags.FLAG_NFC_EVENT_LISTENER)
public void registerNfcEventListener(
@NonNull @CallbackExecutor Executor executor, @NonNull NfcEventListener listener) {
if (!android.nfc.Flags.nfcEventListener()) {
return;
}
synchronized (mNfcEventListeners) {
mNfcEventListeners.put(listener, executor);
if (mNfcEventListeners.size() == 1) {
callService(() -> sService.registerNfcEventListener(mINfcEventListener));
}
}
}
/**
* Unregister a preferred service listener that was previously registered with {@link
* #registerNfcEventListener(Executor, NfcEventListener)}
*
* @param listener The previously registered listener to unregister
*/
@FlaggedApi(android.nfc.Flags.FLAG_NFC_EVENT_LISTENER)
public void unregisterNfcEventListener(@NonNull NfcEventListener listener) {
if (!android.nfc.Flags.nfcEventListener()) {
return;
}
synchronized (mNfcEventListeners) {
mNfcEventListeners.remove(listener);
if (mNfcEventListeners.size() == 0) {
callService(() -> sService.unregisterNfcEventListener(mINfcEventListener));
}
}
}
}