| /* |
| * 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 android.annotation.CallbackExecutor; |
| import android.annotation.FlaggedApi; |
| import android.annotation.IntDef; |
| import android.annotation.NonNull; |
| import android.annotation.RequiresPermission; |
| import android.annotation.SuppressLint; |
| import android.annotation.SystemApi; |
| import android.content.Context; |
| import android.os.Binder; |
| import android.os.RemoteException; |
| import android.os.ResultReceiver; |
| import android.util.Log; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| 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 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; |
| |
| /** |
| * 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 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 {} |
| |
| /** |
| * 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 |
| * @param tag Tag details |
| */ |
| void onTagConnected(boolean connected, @NonNull Tag tag); |
| |
| /** |
| * 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. |
| */ |
| @SuppressLint("MethodNameTense") |
| void onEnable(@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 onDisable(@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 enable. |
| */ |
| 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. |
| */ |
| void onRoutingChanged(); |
| |
| /** |
| * 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); |
| |
| /** |
| * 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); |
| } |
| |
| |
| /** |
| * Constructor to be used only by {@link NfcAdapter}. |
| */ |
| NfcOemExtension(@NonNull Context context, @NonNull NfcAdapter adapter) { |
| mContext = context; |
| mAdapter = adapter; |
| mOemNfcExtensionCallback = new NfcOemExtensionCallback(); |
| } |
| |
| /** |
| * 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); |
| }); |
| } |
| } |
| |
| /** |
| * 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 |
| * |
| * @return List of activated secure elements on success |
| * which can contain "eSE" and "UICC", otherwise empty list. |
| */ |
| @NonNull |
| @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION) |
| public List<String> getActiveNfceeList() { |
| return NfcAdapter.callServiceReturn(() -> |
| NfcAdapter.sService.fetchActiveNfceeList(), new ArrayList<String>()); |
| } |
| |
| private final class NfcOemExtensionCallback extends INfcOemExtensionCallback.Stub { |
| |
| @Override |
| public void onTagConnected(boolean connected, Tag tag) throws RemoteException { |
| mCallbackMap.forEach((cb, ex) -> |
| handleVoid2ArgCallback(connected, tag, 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 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::onEnable, ex)); |
| } |
| @Override |
| public void onDisable(ResultReceiver isAllowed) throws RemoteException { |
| mCallbackMap.forEach((cb, ex) -> |
| handleVoidCallback( |
| new ReceiverWrapper(isAllowed), cb::onDisable, 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() throws RemoteException { |
| mCallbackMap.forEach((cb, ex) -> |
| handleVoidCallback(null, (Object input) -> cb.onRoutingChanged(), ex)); |
| } |
| @Override |
| public void onHceEventReceived(int action) throws RemoteException { |
| mCallbackMap.forEach((cb, ex) -> |
| handleVoidCallback(action, cb::onHceEventReceived, 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 class ReceiverWrapper implements Consumer<Boolean> { |
| private final ResultReceiver mResultReceiver; |
| |
| ReceiverWrapper(ResultReceiver resultReceiver) { |
| mResultReceiver = resultReceiver; |
| } |
| |
| @Override |
| public void accept(Boolean result) { |
| mResultReceiver.send(result ? 1 : 0, null); |
| } |
| |
| @Override |
| public Consumer<Boolean> andThen(Consumer<? super Boolean> after) { |
| return Consumer.super.andThen(after); |
| } |
| } |
| } |