blob: f78161e7cad0202355063970e987338f75b64e9e [file] [log] [blame]
/*
* Copyright (C) 2024 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;
import static android.nfc.cardemulation.CardEmulation.PROTOCOL_AND_TECHNOLOGY_ROUTE_DH;
import static android.nfc.cardemulation.CardEmulation.PROTOCOL_AND_TECHNOLOGY_ROUTE_ESE;
import static android.nfc.cardemulation.CardEmulation.PROTOCOL_AND_TECHNOLOGY_ROUTE_UICC;
import static android.nfc.cardemulation.CardEmulation.routeIntToString;
import android.Manifest;
import android.annotation.CallbackExecutor;
import android.annotation.DurationMillisLong;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.nfc.cardemulation.ApduServiceInfo;
import android.nfc.cardemulation.CardEmulation;
import android.nfc.cardemulation.CardEmulation.ProtocolAndTechnologyRoute;
import android.os.Binder;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.se.omapi.Reader;
import android.util.Log;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* Used for OEM extension APIs.
* This class holds all the APIs and callbacks defined for OEMs/vendors to extend the NFC stack
* for their proprietary features.
*
* @hide
*/
@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
@SystemApi
public final class NfcOemExtension {
private static final String TAG = "NfcOemExtension";
private static final int OEM_EXTENSION_RESPONSE_THRESHOLD_MS = 2000;
private static final int TYPE_TECHNOLOGY = 0;
private static final int TYPE_PROTOCOL = 1;
private static final int TYPE_AID = 2;
private static final int TYPE_SYSTEMCODE = 3;
private final NfcAdapter mAdapter;
private final NfcOemExtensionCallback mOemNfcExtensionCallback;
private boolean mIsRegistered = false;
private final Map<Callback, Executor> mCallbackMap = new HashMap<>();
private final Context mContext;
private final Object mLock = new Object();
private boolean mCardEmulationActivated = false;
private boolean mRfFieldActivated = false;
private boolean mRfDiscoveryStarted = false;
private boolean mEeListenActivated = false;
/**
* Broadcast Action: Sent on NFC stack initialization when NFC OEM extensions are enabled.
* <p> OEM extension modules should use this intent to start their extension service </p>
* @hide
*/
public static final String ACTION_OEM_EXTENSION_INIT = "android.nfc.action.OEM_EXTENSION_INIT";
/**
* Mode Type for {@link #setControllerAlwaysOnMode(int)}.
* Enables the controller in default mode when NFC is disabled (existing API behavior).
* works same as {@link NfcAdapter#setControllerAlwaysOn(boolean)}.
* @hide
*/
@SystemApi
@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
public static final int ENABLE_DEFAULT = NfcAdapter.CONTROLLER_ALWAYS_ON_MODE_DEFAULT;
/**
* Mode Type for {@link #setControllerAlwaysOnMode(int)}.
* Enables the controller in transparent mode when NFC is disabled.
* @hide
*/
@SystemApi
@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
public static final int ENABLE_TRANSPARENT = 2;
/**
* Mode Type for {@link #setControllerAlwaysOnMode(int)}.
* Enables the controller and initializes and enables the EE subsystem when NFC is disabled.
* @hide
*/
@SystemApi
@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
public static final int ENABLE_EE = 3;
/**
* Mode Type for {@link #setControllerAlwaysOnMode(int)}.
* Disable the Controller Always On Mode.
* works same as {@link NfcAdapter#setControllerAlwaysOn(boolean)}.
* @hide
*/
@SystemApi
@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
public static final int DISABLE = NfcAdapter.CONTROLLER_ALWAYS_ON_DISABLE;
/**
* Possible controller modes for {@link #setControllerAlwaysOnMode(int)}.
*
* @hide
*/
@IntDef(prefix = { "" }, value = {
ENABLE_DEFAULT,
ENABLE_TRANSPARENT,
ENABLE_EE,
DISABLE,
})
@Retention(RetentionPolicy.SOURCE)
public @interface ControllerMode{}
/**
* Technology Type for {@link #getActiveNfceeList()}.
* @hide
*/
@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
public static final int NFCEE_TECH_NONE = 0;
/**
* Technology Type for {@link #getActiveNfceeList()}.
* @hide
*/
@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
public static final int NFCEE_TECH_A = 1;
/**
* Technology Type for {@link #getActiveNfceeList()}.
* @hide
*/
@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
public static final int NFCEE_TECH_B = 1 << 1;
/**
* Technology Type for {@link #getActiveNfceeList()}.
* @hide
*/
@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
public static final int NFCEE_TECH_F = 1 << 2;
/**
* Nfc technology flags for {@link #getActiveNfceeList()}.
*
* @hide
*/
@IntDef(flag = true, value = {
NFCEE_TECH_NONE,
NFCEE_TECH_A,
NFCEE_TECH_B,
NFCEE_TECH_F,
})
@Retention(RetentionPolicy.SOURCE)
public @interface NfceeTechnology {}
/**
* Event that Host Card Emulation is activated.
*/
public static final int HCE_ACTIVATE = 1;
/**
* Event that some data is transferred in Host Card Emulation.
*/
public static final int HCE_DATA_TRANSFERRED = 2;
/**
* Event that Host Card Emulation is deactivated.
*/
public static final int HCE_DEACTIVATE = 3;
/**
* Possible events from {@link Callback#onHceEventReceived}.
*
* @hide
*/
@IntDef(value = {
HCE_ACTIVATE,
HCE_DATA_TRANSFERRED,
HCE_DEACTIVATE
})
@Retention(RetentionPolicy.SOURCE)
public @interface HostCardEmulationAction {}
/**
* Status code returned when the polling state change request succeeded.
* @see #pausePolling()
* @see #resumePolling()
*/
public static final int POLLING_STATE_CHANGE_SUCCEEDED = 1;
/**
* Status code returned when the polling state change request is already in
* required state.
* @see #pausePolling()
* @see #resumePolling()
*/
public static final int POLLING_STATE_CHANGE_ALREADY_IN_REQUESTED_STATE = 2;
/**
* Possible status codes for {@link #pausePolling()} and
* {@link #resumePolling()}.
* @hide
*/
@IntDef(value = {
POLLING_STATE_CHANGE_SUCCEEDED,
POLLING_STATE_CHANGE_ALREADY_IN_REQUESTED_STATE,
})
@Retention(RetentionPolicy.SOURCE)
public @interface PollingStateChangeStatusCode {}
/**
* Status OK
*/
public static final int STATUS_OK = 0;
/**
* Status unknown error
*/
public static final int STATUS_UNKNOWN_ERROR = 1;
/**
* Status codes passed to OEM extension callbacks.
*
* @hide
*/
@IntDef(value = {
STATUS_OK,
STATUS_UNKNOWN_ERROR
})
@Retention(RetentionPolicy.SOURCE)
public @interface StatusCode {}
/**
* Routing commit succeeded.
*/
public static final int COMMIT_ROUTING_STATUS_OK = 0;
/**
* Routing commit failed.
*/
public static final int COMMIT_ROUTING_STATUS_FAILED = 3;
/**
* Routing commit failed due to the update is in progress.
*/
public static final int COMMIT_ROUTING_STATUS_FAILED_UPDATE_IN_PROGRESS = 6;
/**
* Status codes returned when calling {@link #forceRoutingTableCommit()}
* @hide
*/
@IntDef(prefix = "COMMIT_ROUTING_STATUS_", value = {
COMMIT_ROUTING_STATUS_OK,
COMMIT_ROUTING_STATUS_FAILED,
COMMIT_ROUTING_STATUS_FAILED_UPDATE_IN_PROGRESS,
})
@Retention(RetentionPolicy.SOURCE)
public @interface CommitRoutingStatusCode {}
/**
* Interface for Oem extensions for NFC.
*/
public interface Callback {
/**
* Notify Oem to tag is connected or not
* ex - if tag is connected notify cover and Nfctest app if app is in testing mode
*
* @param connected status of the tag true if tag is connected otherwise false
*/
void onTagConnected(boolean connected);
/**
* Update the Nfc Adapter State
* @param state new state that need to be updated
*/
void onStateUpdated(@NfcAdapter.AdapterState int state);
/**
* Check if NfcService apply routing method need to be skipped for
* some feature.
* @param isSkipped The {@link Consumer} to be completed. If apply routing can be skipped,
* the {@link Consumer#accept(Object)} should be called with
* {@link Boolean#TRUE}, otherwise call with {@link Boolean#FALSE}.
*/
void onApplyRouting(@NonNull Consumer<Boolean> isSkipped);
/**
* Check if NfcService ndefRead method need to be skipped To skip
* and start checking for presence of tag
* @param isSkipped The {@link Consumer} to be completed. If Ndef read can be skipped,
* the {@link Consumer#accept(Object)} should be called with
* {@link Boolean#TRUE}, otherwise call with {@link Boolean#FALSE}.
*/
void onNdefRead(@NonNull Consumer<Boolean> isSkipped);
/**
* Method to check if Nfc is allowed to be enabled by OEMs.
* @param isAllowed The {@link Consumer} to be completed. If enabling NFC is allowed,
* the {@link Consumer#accept(Object)} should be called with
* {@link Boolean#TRUE}, otherwise call with {@link Boolean#FALSE}.
* false if NFC cannot be enabled at this time.
*/
void onEnableRequested(@NonNull Consumer<Boolean> isAllowed);
/**
* Method to check if Nfc is allowed to be disabled by OEMs.
* @param isAllowed The {@link Consumer} to be completed. If disabling NFC is allowed,
* the {@link Consumer#accept(Object)} should be called with
* {@link Boolean#TRUE}, otherwise call with {@link Boolean#FALSE}.
* false if NFC cannot be disabled at this time.
*/
void onDisableRequested(@NonNull Consumer<Boolean> isAllowed);
/**
* Callback to indicate that Nfc starts to boot.
*/
void onBootStarted();
/**
* Callback to indicate that Nfc starts to enable.
*/
void onEnableStarted();
/**
* Callback to indicate that Nfc starts to disable.
*/
void onDisableStarted();
/**
* Callback to indicate if NFC boots successfully or not.
* @param status the status code indicating if boot finished successfully
*/
void onBootFinished(@StatusCode int status);
/**
* Callback to indicate if NFC is successfully enabled.
* @param status the status code indicating if enable finished successfully
*/
void onEnableFinished(@StatusCode int status);
/**
* Callback to indicate if NFC is successfully disabled.
* @param status the status code indicating if disable finished successfully
*/
void onDisableFinished(@StatusCode int status);
/**
* Check if NfcService tag dispatch need to be skipped.
* @param isSkipped The {@link Consumer} to be completed. If tag dispatch can be skipped,
* the {@link Consumer#accept(Object)} should be called with
* {@link Boolean#TRUE}, otherwise call with {@link Boolean#FALSE}.
*/
void onTagDispatch(@NonNull Consumer<Boolean> isSkipped);
/**
* Notifies routing configuration is changed.
* @param isCommitRoutingSkipped The {@link Consumer} to be
* completed. If routing commit should be skipped,
* the {@link Consumer#accept(Object)} should be called with
* {@link Boolean#TRUE}, otherwise call with {@link Boolean#FALSE}.
*/
void onRoutingChanged(@NonNull Consumer<Boolean> isCommitRoutingSkipped);
/**
* API to activate start stop cpu boost on hce event.
*
* <p>When HCE is activated, transferring data, and deactivated,
* must call this method to activate, start and stop cpu boost respectively.
* @param action Flag indicating actions to activate, start and stop cpu boost.
*/
void onHceEventReceived(@HostCardEmulationAction int action);
/**
* API to notify when reader option has been changed using
* {@link NfcAdapter#enableReaderOption(boolean)} by some app.
* @param enabled Flag indicating ReaderMode enabled/disabled
*/
void onReaderOptionChanged(boolean enabled);
/**
* Notifies NFC is activated in listen mode.
* NFC Forum NCI-2.3 ch.5.2.6 specification
*
* <p>NFCC is ready to communicate with a Card reader
*
* @param isActivated true, if card emulation activated, else de-activated.
*/
void onCardEmulationActivated(boolean isActivated);
/**
* Notifies the Remote NFC Endpoint RF Field is activated.
* NFC Forum NCI-2.3 ch.5.3 specification
*
* @param isActivated true, if RF Field is ON, else RF Field is OFF.
*/
void onRfFieldActivated(boolean isActivated);
/**
* Notifies the NFC RF discovery is started or in the IDLE state.
* NFC Forum NCI-2.3 ch.5.2 specification
*
* @param isDiscoveryStarted true, if RF discovery started, else RF state is Idle.
*/
void onRfDiscoveryStarted(boolean isDiscoveryStarted);
/**
* Notifies the NFCEE (NFC Execution Environment) Listen has been activated.
*
* @param isActivated true, if EE Listen is ON, else EE Listen is OFF.
*/
void onEeListenActivated(boolean isActivated);
/**
* Notifies that some NFCEE (NFC Execution Environment) has been updated.
*
* <p> This indicates that some applet has been installed/updated/removed in
* one of the NFCEE's.
* </p>
*/
void onEeUpdated();
/**
* Gets the intent to find the OEM package in the OEM App market. If the consumer returns
* {@code null} or a timeout occurs, the intent from the first available package will be
* used instead.
*
* @param packages the OEM packages name stored in the tag
* @param intentConsumer The {@link Consumer} to be completed.
* The {@link Consumer#accept(Object)} should be called with
* the Intent required.
*
*/
void onGetOemAppSearchIntent(@NonNull List<String> packages,
@NonNull Consumer<Intent> intentConsumer);
/**
* Checks if the NDEF message contains any specific OEM package executable content
*
* @param tag the {@link android.nfc.Tag Tag}
* @param message NDEF Message to read from tag
* @param hasOemExecutableContent The {@link Consumer} to be completed. If there is
* OEM package executable content, the
* {@link Consumer#accept(Object)} should be called with
* {@link Boolean#TRUE}, otherwise call with
* {@link Boolean#FALSE}.
*/
void onNdefMessage(@NonNull Tag tag, @NonNull NdefMessage message,
@NonNull Consumer<Boolean> hasOemExecutableContent);
/**
* Callback to indicate the app chooser activity should be launched for handling CE
* transaction. This is invoked for example when there are more than 1 app installed that
* can handle the HCE transaction. OEMs can launch the Activity based
* on their requirement.
*
* @param selectedAid the selected AID from APDU
* @param services {@link ApduServiceInfo} of the service triggering the activity
* @param failedComponent the component failed to be resolved
* @param category the category of the service
*/
void onLaunchHceAppChooserActivity(@NonNull String selectedAid,
@NonNull List<ApduServiceInfo> services,
@NonNull ComponentName failedComponent,
@NonNull String category);
/**
* Callback to indicate tap again dialog should be launched for handling HCE transaction.
* This is invoked for example when a CE service needs the device to unlocked before
* handling the transaction. OEMs can launch the Activity based on their requirement.
*
* @param service {@link ApduServiceInfo} of the service triggering the dialog
* @param category the category of the service
*/
void onLaunchHceTapAgainDialog(@NonNull ApduServiceInfo service, @NonNull String category);
/**
* Callback to indicate that routing table is full and the OEM can optionally launch a
* dialog to request the user to remove some Card Emulation apps from the device to free
* routing table space.
*/
void onRoutingTableFull();
/**
* Callback when OEM specified log event are notified.
* @param item the log items that contains log information of NFC event.
*/
void onLogEventNotified(@NonNull OemLogItems item);
/**
* Callback to to extract OEM defined packages from given NDEF message when
* a NFC tag is detected. These are used to handle NFC tags encoded with a
* proprietary format for storing app name (Android native app format).
*
* @param message NDEF message containing OEM package names
* @param packageConsumer The {@link Consumer} to be completed.
* The {@link Consumer#accept(Object)} should be called with
* the list of package names.
*/
void onExtractOemPackages(@NonNull NdefMessage message,
@NonNull Consumer<List<String>> packageConsumer);
}
/**
* Constructor to be used only by {@link NfcAdapter}.
*/
NfcOemExtension(@NonNull Context context, @NonNull NfcAdapter adapter) {
mContext = context;
mAdapter = adapter;
mOemNfcExtensionCallback = new NfcOemExtensionCallback();
}
/**
* Get an instance of {@link T4tNdefNfcee} object for performing T4T (Type-4 Tag)
* NDEF (NFC Data Exchange Format) NFCEE (NFC Execution Environment) operations.
* This can be used to write NDEF data to emulate a T4T tag in an NFCEE
* (NFC Execution Environment - eSE, SIM, etc). Refer to the NFC forum specification
* "NFCForum-TS-NCI-2.3 section 10.4" and "NFCForum-TS-T4T-1.1 section 4.2" for more details.
*
* This is a singleton object which shall be used by OEM extension module to do NDEF-NFCEE
* read/write operations.
*
* <p>Returns {@link T4tNdefNfcee}
* <p>Does not cause any RF activity and does not block.
* @return NFC Data Exchange Format (NDEF) NFC Execution Environment (NFCEE) object
* @hide
*/
@SystemApi
@NonNull
@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
public T4tNdefNfcee getT4tNdefNfcee() {
return T4tNdefNfcee.getInstance();
}
/**
* Register an {@link Callback} to listen for NFC oem extension callbacks
* Multiple clients can register and callbacks will be invoked asynchronously.
*
* <p>The provided callback will be invoked by the given {@link Executor}.
* As part of {@link #registerCallback(Executor, Callback)} the
* {@link Callback} will be invoked with current NFC state
* before the {@link #registerCallback(Executor, Callback)} function completes.
*
* @param executor an {@link Executor} to execute given callback
* @param callback oem implementation of {@link Callback}
*/
@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
@RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
public void registerCallback(@NonNull @CallbackExecutor Executor executor,
@NonNull Callback callback) {
synchronized (mLock) {
if (executor == null || callback == null) {
Log.e(TAG, "Executor and Callback must not be null!");
throw new IllegalArgumentException();
}
if (mCallbackMap.containsKey(callback)) {
Log.e(TAG, "Callback already registered. Unregister existing callback before"
+ "registering");
throw new IllegalArgumentException();
}
mCallbackMap.put(callback, executor);
if (!mIsRegistered) {
NfcAdapter.callService(() -> {
NfcAdapter.sService.registerOemExtensionCallback(mOemNfcExtensionCallback);
mIsRegistered = true;
});
} else {
updateNfCState(callback, executor);
}
}
}
private void updateNfCState(Callback callback, Executor executor) {
if (callback != null) {
Log.i(TAG, "updateNfCState");
executor.execute(() -> {
callback.onCardEmulationActivated(mCardEmulationActivated);
callback.onRfFieldActivated(mRfFieldActivated);
callback.onRfDiscoveryStarted(mRfDiscoveryStarted);
callback.onEeListenActivated(mEeListenActivated);
});
}
}
/**
* Unregister the specified {@link Callback}
*
* <p>The same {@link Callback} object used when calling
* {@link #registerCallback(Executor, Callback)} must be used.
*
* <p>Callbacks are automatically unregistered when an application process goes away
*
* @param callback oem implementation of {@link Callback}
*/
@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
@RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
public void unregisterCallback(@NonNull Callback callback) {
synchronized (mLock) {
if (!mCallbackMap.containsKey(callback) || !mIsRegistered) {
Log.e(TAG, "Callback not registered");
throw new IllegalArgumentException();
}
if (mCallbackMap.size() == 1) {
NfcAdapter.callService(() -> {
NfcAdapter.sService.unregisterOemExtensionCallback(mOemNfcExtensionCallback);
mIsRegistered = false;
mCallbackMap.remove(callback);
});
} else {
mCallbackMap.remove(callback);
}
}
}
/**
* Clear NfcService preference, interface method to clear NFC preference values on OEM specific
* events. For ex: on soft reset, Nfc default values needs to be overridden by OEM defaults.
*/
@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
@RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
public void clearPreference() {
NfcAdapter.callService(() -> NfcAdapter.sService.clearPreference());
}
/**
* Get the screen state from system and set it to current screen state.
*/
@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
@RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
public void synchronizeScreenState() {
NfcAdapter.callService(() -> NfcAdapter.sService.setScreenState());
}
/**
* Check if the firmware needs updating.
*
* <p>If an update is needed, a firmware will be triggered when NFC is disabled.
*/
@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
@RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
public void maybeTriggerFirmwareUpdate() {
NfcAdapter.callService(() -> NfcAdapter.sService.checkFirmware());
}
/**
* Get the Active NFCEE (NFC Execution Environment) List
*
* @see Reader#getName() for the list of possible NFCEE names.
*
* @return Map< String, @NfceeTechnology Integer >
* A HashMap where keys are activated secure elements and
* the values are bitmap of technologies supported by each secure element
* on success keys can contain "eSE" and "UICC", otherwise empty map.
*/
@NonNull
@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
public Map<String, Integer> getActiveNfceeList() {
return NfcAdapter.callServiceReturn(() ->
NfcAdapter.sService.fetchActiveNfceeList(), new HashMap<String, Integer>());
}
/**
* Sets NFC controller always on feature.
* <p>This API is for the NFCC internal state management. It allows to discriminate
* the controller function from the NFC function by keeping the NFC controller on without
* any NFC RF enabled if necessary.
* <p>This call is asynchronous, register listener {@link NfcAdapter.ControllerAlwaysOnListener}
* by {@link NfcAdapter#registerControllerAlwaysOnListener} to find out when the operation is
* complete.
* <p> Note: This adds more always on modes on top of existing
* {@link NfcAdapter#setControllerAlwaysOn(boolean)} API which can be used to set the NFCC in
* only {@link #ENABLE_DEFAULT} and {@link #DISABLE} modes.
* @param mode one of {@link ControllerMode} modes
* @throws UnsupportedOperationException if
* <li> if FEATURE_NFC, FEATURE_NFC_HOST_CARD_EMULATION, FEATURE_NFC_HOST_CARD_EMULATION_NFCF,
* FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC and FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE
* are unavailable </li>
* <li> if the feature is unavailable @see NfcAdapter#isNfcControllerAlwaysOnSupported() </li>
* @hide
* @see NfcAdapter#setControllerAlwaysOn(boolean)
*/
@SystemApi
@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
@RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON)
public void setControllerAlwaysOnMode(@ControllerMode int mode) {
if (!NfcAdapter.sHasNfcFeature && !NfcAdapter.sHasCeFeature) {
throw new UnsupportedOperationException();
}
NfcAdapter.callService(() -> NfcAdapter.sService.setControllerAlwaysOn(mode));
}
/**
* Triggers NFC initialization. If OEM extension is registered
* (indicated via `enable_oem_extension` NFC overlay), the NFC stack initialization at bootup
* is delayed until the OEM extension app triggers the initialization via this call.
*/
@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
@RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
public void triggerInitialization() {
NfcAdapter.callService(() -> NfcAdapter.sService.triggerInitialization());
}
/**
* Gets the last user toggle status.
* @return true if NFC is set to ON, false otherwise
*/
@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
@RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
public boolean hasUserEnabledNfc() {
return NfcAdapter.callServiceReturn(() -> NfcAdapter.sService.getSettingStatus(), false);
}
/**
* Checks if the tag is present or not.
* @return true if the tag is present, false otherwise
*/
@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
@RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
public boolean isTagPresent() {
return NfcAdapter.callServiceReturn(() -> NfcAdapter.sService.isTagPresent(), false);
}
/**
* Pauses NFC tag reader mode polling for a {@code timeoutInMs} millisecond.
* In case of {@code timeoutInMs} is zero or invalid polling will be stopped indefinitely.
* Use {@link #resumePolling()} to resume the polling.
* Use {@link #getMaxPausePollingTimeoutMs()} to check the max timeout value.
* @param timeoutInMs the pause polling duration in millisecond.
* @return status of the operation
* @throws IllegalArgumentException if timeoutInMs value is invalid
* (0 < timeoutInMs < max).
*/
@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
@RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
public @PollingStateChangeStatusCode int pausePolling(@DurationMillisLong long timeoutInMs) {
return NfcAdapter.callServiceReturn(() ->
NfcAdapter.sService.pausePolling(timeoutInMs),
POLLING_STATE_CHANGE_ALREADY_IN_REQUESTED_STATE);
}
/**
* Resumes default NFC tag reader mode polling for the current device state if polling is
* paused. Calling this while already in polling is a no-op.
* @return status of the operation
*/
@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
@RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
public @PollingStateChangeStatusCode int resumePolling() {
return NfcAdapter.callServiceReturn(() ->
NfcAdapter.sService.resumePolling(),
POLLING_STATE_CHANGE_ALREADY_IN_REQUESTED_STATE);
}
/**
* Gets the max pause polling timeout value in millisecond.
* @return long integer representing the max timeout
*/
@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
@RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
@DurationMillisLong
public long getMaxPausePollingTimeoutMills() {
return NfcAdapter.callServiceReturn(() ->
NfcAdapter.sService.getMaxPausePollingTimeoutMs(), 0L);
}
/**
* Set whether to enable auto routing change or not (enabled by default).
* If disabled, routing targets are limited to a single off-host destination.
*
* @param state status of auto routing change, true if enable, otherwise false
*/
@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
@RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
public void setAutoChangeEnabled(boolean state) {
NfcAdapter.callService(() ->
NfcAdapter.sCardEmulationService.setAutoChangeStatus(state));
}
/**
* Check if auto routing change is enabled or not.
*
* @return true if enabled, otherwise false
*/
@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
@RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
public boolean isAutoChangeEnabled() {
return NfcAdapter.callServiceReturn(() ->
NfcAdapter.sCardEmulationService.isAutoChangeEnabled(), false);
}
/**
* Get current routing status
*
* @return {@link RoutingStatus} indicating the default route, default ISO-DEP
* route and default off-host route.
*/
@NonNull
@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
@RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
public RoutingStatus getRoutingStatus() {
List<String> status = NfcAdapter.callServiceReturn(() ->
NfcAdapter.sCardEmulationService.getRoutingStatus(), new ArrayList<>());
return new RoutingStatus(routeStringToInt(status.get(0)),
routeStringToInt(status.get(1)),
routeStringToInt(status.get(2)));
}
/**
* Overwrites NFC controller routing table, which includes Protocol Route, Technology Route,
* and Empty AID Route.
*
* The parameter set to
* {@link ProtocolAndTechnologyRoute#PROTOCOL_AND_TECHNOLOGY_ROUTE_UNSET}
* can be used to keep current values for that entry. At least one route should be overridden
* when calling this API, otherwise throw {@link IllegalArgumentException}.
*
* @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}
* @param emptyAid Zero-length AID route destination, where the possible inputs are defined in
* {@link ProtocolAndTechnologyRoute}
* @param systemCode System Code route destination, where the possible inputs are defined in
* {@link ProtocolAndTechnologyRoute}
*/
@RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
public void overwriteRoutingTable(
@CardEmulation.ProtocolAndTechnologyRoute int protocol,
@CardEmulation.ProtocolAndTechnologyRoute int technology,
@CardEmulation.ProtocolAndTechnologyRoute int emptyAid,
@CardEmulation.ProtocolAndTechnologyRoute int systemCode) {
String protocolRoute = routeIntToString(protocol);
String technologyRoute = routeIntToString(technology);
String emptyAidRoute = routeIntToString(emptyAid);
String systemCodeRoute = routeIntToString(systemCode);
NfcAdapter.callService(() ->
NfcAdapter.sCardEmulationService.overwriteRoutingTable(
mContext.getUser().getIdentifier(),
emptyAidRoute,
protocolRoute,
technologyRoute,
systemCodeRoute
));
}
/**
* Gets current routing table entries.
* @return List of {@link NfcRoutingTableEntry} representing current routing table
*/
@NonNull
@RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
public List<NfcRoutingTableEntry> getRoutingTable() {
List<Entry> entryList = NfcAdapter.callServiceReturn(() ->
NfcAdapter.sService.getRoutingTableEntryList(), null);
List<NfcRoutingTableEntry> result = new ArrayList<>();
for (Entry entry : entryList) {
switch (entry.getType()) {
case TYPE_TECHNOLOGY -> result.add(
new RoutingTableTechnologyEntry(entry.getNfceeId(),
RoutingTableTechnologyEntry.techStringToInt(entry.getEntry()))
);
case TYPE_PROTOCOL -> result.add(
new RoutingTableProtocolEntry(entry.getNfceeId(),
RoutingTableProtocolEntry.protocolStringToInt(entry.getEntry()))
);
case TYPE_AID -> result.add(
new RoutingTableAidEntry(entry.getNfceeId(), entry.getEntry())
);
case TYPE_SYSTEMCODE -> result.add(
new RoutingTableSystemCodeEntry(entry.getNfceeId(),
entry.getEntry().getBytes(StandardCharsets.UTF_8))
);
}
}
return result;
}
/**
* API to force a routing table commit.
* @return a {@link StatusCode} to indicate if commit routing succeeded or not
*/
@RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
@CommitRoutingStatusCode
public int forceRoutingTableCommit() {
return NfcAdapter.callServiceReturn(
() -> NfcAdapter.sService.commitRouting(), COMMIT_ROUTING_STATUS_FAILED);
}
private final class NfcOemExtensionCallback extends INfcOemExtensionCallback.Stub {
@Override
public void onTagConnected(boolean connected) throws RemoteException {
mCallbackMap.forEach((cb, ex) ->
handleVoidCallback(connected, cb::onTagConnected, ex));
}
@Override
public void onCardEmulationActivated(boolean isActivated) throws RemoteException {
mCardEmulationActivated = isActivated;
mCallbackMap.forEach((cb, ex) ->
handleVoidCallback(isActivated, cb::onCardEmulationActivated, ex));
}
@Override
public void onRfFieldActivated(boolean isActivated) throws RemoteException {
mRfFieldActivated = isActivated;
mCallbackMap.forEach((cb, ex) ->
handleVoidCallback(isActivated, cb::onRfFieldActivated, ex));
}
@Override
public void onRfDiscoveryStarted(boolean isDiscoveryStarted) throws RemoteException {
mRfDiscoveryStarted = isDiscoveryStarted;
mCallbackMap.forEach((cb, ex) ->
handleVoidCallback(isDiscoveryStarted, cb::onRfDiscoveryStarted, ex));
}
@Override
public void onEeListenActivated(boolean isActivated) throws RemoteException {
mEeListenActivated = isActivated;
mCallbackMap.forEach((cb, ex) ->
handleVoidCallback(isActivated, cb::onEeListenActivated, ex));
}
@Override
public void onEeUpdated() throws RemoteException {
mCallbackMap.forEach((cb, ex) ->
handleVoidCallback(null, (Object input) -> cb.onEeUpdated(), ex));
}
@Override
public void onStateUpdated(int state) throws RemoteException {
mCallbackMap.forEach((cb, ex) ->
handleVoidCallback(state, cb::onStateUpdated, ex));
}
@Override
public void onApplyRouting(ResultReceiver isSkipped) throws RemoteException {
mCallbackMap.forEach((cb, ex) ->
handleVoidCallback(
new ReceiverWrapper<>(isSkipped), cb::onApplyRouting, ex));
}
@Override
public void onNdefRead(ResultReceiver isSkipped) throws RemoteException {
mCallbackMap.forEach((cb, ex) ->
handleVoidCallback(
new ReceiverWrapper<>(isSkipped), cb::onNdefRead, ex));
}
@Override
public void onEnable(ResultReceiver isAllowed) throws RemoteException {
mCallbackMap.forEach((cb, ex) ->
handleVoidCallback(
new ReceiverWrapper<>(isAllowed), cb::onEnableRequested, ex));
}
@Override
public void onDisable(ResultReceiver isAllowed) throws RemoteException {
mCallbackMap.forEach((cb, ex) ->
handleVoidCallback(
new ReceiverWrapper<>(isAllowed), cb::onDisableRequested, ex));
}
@Override
public void onBootStarted() throws RemoteException {
mCallbackMap.forEach((cb, ex) ->
handleVoidCallback(null, (Object input) -> cb.onBootStarted(), ex));
}
@Override
public void onEnableStarted() throws RemoteException {
mCallbackMap.forEach((cb, ex) ->
handleVoidCallback(null, (Object input) -> cb.onEnableStarted(), ex));
}
@Override
public void onDisableStarted() throws RemoteException {
mCallbackMap.forEach((cb, ex) ->
handleVoidCallback(null, (Object input) -> cb.onDisableStarted(), ex));
}
@Override
public void onBootFinished(int status) throws RemoteException {
mCallbackMap.forEach((cb, ex) ->
handleVoidCallback(status, cb::onBootFinished, ex));
}
@Override
public void onEnableFinished(int status) throws RemoteException {
mCallbackMap.forEach((cb, ex) ->
handleVoidCallback(status, cb::onEnableFinished, ex));
}
@Override
public void onDisableFinished(int status) throws RemoteException {
mCallbackMap.forEach((cb, ex) ->
handleVoidCallback(status, cb::onDisableFinished, ex));
}
@Override
public void onTagDispatch(ResultReceiver isSkipped) throws RemoteException {
mCallbackMap.forEach((cb, ex) ->
handleVoidCallback(
new ReceiverWrapper<>(isSkipped), cb::onTagDispatch, ex));
}
@Override
public void onRoutingChanged(ResultReceiver isSkipped) throws RemoteException {
mCallbackMap.forEach((cb, ex) ->
handleVoidCallback(
new ReceiverWrapper<>(isSkipped), cb::onRoutingChanged, ex));
}
@Override
public void onHceEventReceived(int action) throws RemoteException {
mCallbackMap.forEach((cb, ex) ->
handleVoidCallback(action, cb::onHceEventReceived, ex));
}
@Override
public void onReaderOptionChanged(boolean enabled) throws RemoteException {
mCallbackMap.forEach((cb, ex) ->
handleVoidCallback(enabled, cb::onReaderOptionChanged, ex));
}
public void onRoutingTableFull() throws RemoteException {
mCallbackMap.forEach((cb, ex) ->
handleVoidCallback(null,
(Object input) -> cb.onRoutingTableFull(), ex));
}
@Override
public void onGetOemAppSearchIntent(List<String> packages, ResultReceiver intentConsumer)
throws RemoteException {
mCallbackMap.forEach((cb, ex) ->
handleVoid2ArgCallback(packages, new ReceiverWrapper<>(intentConsumer),
cb::onGetOemAppSearchIntent, ex));
}
@Override
public void onNdefMessage(Tag tag, NdefMessage message,
ResultReceiver hasOemExecutableContent) throws RemoteException {
mCallbackMap.forEach((cb, ex) -> {
synchronized (mLock) {
final long identity = Binder.clearCallingIdentity();
try {
ex.execute(() -> cb.onNdefMessage(
tag, message, new ReceiverWrapper<>(hasOemExecutableContent)));
} catch (RuntimeException exception) {
throw exception;
} finally {
Binder.restoreCallingIdentity(identity);
}
}
});
}
@Override
public void onLaunchHceAppChooserActivity(String selectedAid,
List<ApduServiceInfo> services,
ComponentName failedComponent, String category)
throws RemoteException {
mCallbackMap.forEach((cb, ex) -> {
synchronized (mLock) {
final long identity = Binder.clearCallingIdentity();
try {
ex.execute(() -> cb.onLaunchHceAppChooserActivity(
selectedAid, services, failedComponent, category));
} catch (RuntimeException exception) {
throw exception;
} finally {
Binder.restoreCallingIdentity(identity);
}
}
});
}
@Override
public void onLaunchHceTapAgainActivity(ApduServiceInfo service, String category)
throws RemoteException {
mCallbackMap.forEach((cb, ex) ->
handleVoid2ArgCallback(service, category, cb::onLaunchHceTapAgainDialog, ex));
}
@Override
public void onLogEventNotified(OemLogItems item) throws RemoteException {
mCallbackMap.forEach((cb, ex) ->
handleVoidCallback(item, cb::onLogEventNotified, ex));
}
@Override
public void onExtractOemPackages(NdefMessage message, ResultReceiver packageConsumer)
throws RemoteException {
mCallbackMap.forEach((cb, ex) ->
handleVoid2ArgCallback(message,
new ReceiverWrapper<>(packageConsumer),
cb::onExtractOemPackages, ex));
}
private <T> void handleVoidCallback(
T input, Consumer<T> callbackMethod, Executor executor) {
synchronized (mLock) {
final long identity = Binder.clearCallingIdentity();
try {
executor.execute(() -> callbackMethod.accept(input));
} catch (RuntimeException ex) {
throw ex;
} finally {
Binder.restoreCallingIdentity(identity);
}
}
}
private <T1, T2> void handleVoid2ArgCallback(
T1 input1, T2 input2, BiConsumer<T1, T2> callbackMethod, Executor executor) {
synchronized (mLock) {
final long identity = Binder.clearCallingIdentity();
try {
executor.execute(() -> callbackMethod.accept(input1, input2));
} catch (RuntimeException ex) {
throw ex;
} finally {
Binder.restoreCallingIdentity(identity);
}
}
}
private <S, T> S handleNonVoidCallbackWithInput(
S defaultValue, T input, Function<T, S> callbackMethod) throws RemoteException {
synchronized (mLock) {
final long identity = Binder.clearCallingIdentity();
S result = defaultValue;
try {
ExecutorService executor = Executors.newSingleThreadExecutor();
FutureTask<S> futureTask = new FutureTask<>(() -> callbackMethod.apply(input));
var unused = executor.submit(futureTask);
try {
result = futureTask.get(
OEM_EXTENSION_RESPONSE_THRESHOLD_MS, TimeUnit.MILLISECONDS);
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
} catch (TimeoutException e) {
Log.w(TAG, "Callback timed out: " + callbackMethod);
e.printStackTrace();
} finally {
executor.shutdown();
}
} finally {
Binder.restoreCallingIdentity(identity);
}
return result;
}
}
private <T> T handleNonVoidCallbackWithoutInput(T defaultValue, Supplier<T> callbackMethod)
throws RemoteException {
synchronized (mLock) {
final long identity = Binder.clearCallingIdentity();
T result = defaultValue;
try {
ExecutorService executor = Executors.newSingleThreadExecutor();
FutureTask<T> futureTask = new FutureTask<>(callbackMethod::get);
var unused = executor.submit(futureTask);
try {
result = futureTask.get(
OEM_EXTENSION_RESPONSE_THRESHOLD_MS, TimeUnit.MILLISECONDS);
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
} catch (TimeoutException e) {
Log.w(TAG, "Callback timed out: " + callbackMethod);
e.printStackTrace();
} finally {
executor.shutdown();
}
} finally {
Binder.restoreCallingIdentity(identity);
}
return result;
}
}
}
private @CardEmulation.ProtocolAndTechnologyRoute int routeStringToInt(String route) {
if (route.equals("DH")) {
return PROTOCOL_AND_TECHNOLOGY_ROUTE_DH;
} else if (route.startsWith("eSE")) {
return PROTOCOL_AND_TECHNOLOGY_ROUTE_ESE;
} else if (route.startsWith("SIM")) {
return PROTOCOL_AND_TECHNOLOGY_ROUTE_UICC;
} else {
throw new IllegalStateException("Unexpected value: " + route);
}
}
private class ReceiverWrapper<T> implements Consumer<T> {
private final ResultReceiver mResultReceiver;
ReceiverWrapper(ResultReceiver resultReceiver) {
mResultReceiver = resultReceiver;
}
@Override
public void accept(T result) {
if (result instanceof Boolean) {
mResultReceiver.send((Boolean) result ? 1 : 0, null);
} else if (result instanceof Intent) {
Bundle bundle = new Bundle();
bundle.putParcelable("intent", (Intent) result);
mResultReceiver.send(0, bundle);
} else if (result instanceof List<?> list) {
if (list.stream().allMatch(String.class::isInstance)) {
Bundle bundle = new Bundle();
bundle.putStringArray("packageNames",
list.stream().map(pkg -> (String) pkg).toArray(String[]::new));
mResultReceiver.send(0, bundle);
}
}
}
@Override
public Consumer<T> andThen(Consumer<? super T> after) {
return Consumer.super.andThen(after);
}
}
}