In my Android app I have an implementation of HostApduService
. Here is a snippet of it's implementation:
public final class MyHostApduService extends HostApduService {
private boolean disconnected = false;
@Override
public byte[] processCommandApdu(byte[] commandApdu, @Nullable Bundle extras) {
//process apdu in a background thread and call sendResponseApdu() when ready
Single.fromCallable(() -> processInternal(commandApdu))
.subscribeOn(nfcScheduler)
.subscribe(this::sendResponseApdu, t -> Log.e("could not create response", t));
return null;
}
...
@Override
public void onDeactivated(int reason) {
disconnected = true;
}
private void processInternal(byte[] apdu) {
//business logic
if(!disconnected) {
//last message was probably received by the terminal
}
}
}
So in my observation the onDeactivated()
callback can come right in the middle of a processCommandApdu()
and even then the OS seems to recognize a bit earlier that the NFC field is lost than onDeactivated()
is called.
Here is an example of a lost field during the communication:
16:21:16.808 I/MyHostApduService : processApdu[request|13bytes] 0A4040007A000000004306000
16:21:16.811 D/MyHostApduService : do business logic
16:21:16.890 D/HostEmulationManager: notifyHostEmulationDeactivated
16:21:16.890 D/HostEmulationManager: Unbinding from service ComponentInfo{app.debug/internal.MyHostApduService}
16:21:16.890 I/MyHostApduService : onDeactivated LINK_LOSS
16:21:16.898 I/MyHostApduService : processApdu[response|2bytes|90ms] 6A82
The problem is that I need to confidently check if the last message was received or dropped, because some important finalization code has to be executed (but only if the terminal receives the message). Is there a better way to check if a message was received than to use onDeactivated()
(which seems to be quite non-deterministic in its timing)?
You can't. Instead you will need to adapt your communication protocol if you really need to reliably detect that case.
The problem is not really Android but the underlying communication protocols (ISO/IEC 7816-4 over ISO/IEC 14443-4). These protocols were built for communication with regular smartcards. Smartcards being fully passive devices which can't continue processing (due to lack of energy) when pulled out of a reader or away from the NFC RF field.
The protocol stack is designed for interrogator driven communication (where the interrogator is the terminal). Communication is performed in command-response sequences. In principal, each command-response sequence consists of the following steps (with a few additional corner cases):
Neither the application protocol (ISO/IEC 7816-4) nor the transmission protocol (ISO/IEC 14443-4 aka ISO-DEP) confirm the smartcard response by any form of acknowledgement. Once the smartcard sent its response it is deemed to have finished processing.
In effect this would not be an issue for a classic smartcard (contact or contactless). An interrupted communication would cause the card to be power-cycled (either because the link-loss also implies a power-loss or because the terminal performs an explicit reset). So the smartcard would not be able to rely on cleanup sequences at that point.
However, that does not mean that there are no ways to overcome that limitation. Classic smartcards maintain persistent state even across power-cycles. Critical operations are performed as atomic transactions. In case of power-loss, cleanup/rollback is typically performed upon reset (boot-up). However, that's not quite easy to map to Android since link-loss does not cause execution on the HCE side to be interrupted. Consequently, there's no way to detect that the HCE smartcard was pulled before a response was sent back to the reader. Nevertheless, there's also no atomic transactions that would be interrupted by the link-loss. Hence, reset (i.e. the reception of the SELECT (by DF name) command that selects your application) is still the right place to perform cleanup such as resetting application state.
With regard to your specific requirement, a typical approach would be to adapt the application-level protocol and add an acknowledgment command that confirms reception of the (then second-)last response. I.e. if you currently have something like:
T---> SELECT APPLICATION <---C FCI | 9000 T---> PERFORM CRITICAL OPERATION <---C CRITICAL OPERATION RESULT
You could adapt the protocol to include a final acknowledgement:
T---> SELECT APPLICATION <---C FCI | 9000 T---> PERFORM CRITICAL OPERATION <---C CRITICAL OPERATION RESULT T---> CONFIRM RECEPTION OF RESULT <---C 9000
Now you would not really care if the final response (9000) is lost on its way to the terminal.