Host-based Card Emulation (HCE) with Android and Flutter
Making a simple proof of concept app by combining HCE, Flutter and a little bit of native Android (Kotlin) code.
Here at Zero Molecule, we love to experiment with new technologies to evaluate if and how to use them on our next projects. This time we wanted to try out HCE so that we can build an app that can serve as a keycard for the electronic lock.
Although I will go over some necessary information about HCE, this post is not the go-to resource to learn about the HCE technology itself, just as it isn't a go-to resource to learn about the basics of Android and Flutter. If you wish to learn more about HCE, I suggest you look over at Wikipedia, Clearbridge, Sequent, and Android Documentation for a more detailed explanation.
What is HCE?
Host Card Emulation (HCE) is the term describing on-device technology that permits a phone to perform card emulation on an NFC-enabled device without relying on access to a secure element.
This means that your phone can be used to mimic (emulate) the behavior of any contactless card. A most known case for this technology is contactless payment with your device offered by applications such as Google Pay or Apple Pay. Those services leverage the HCE capability of your device by emulating your contactless payment cards. But even though payment is the most known usage, it isn't the only one. Except for iOS devices whose HCE capabilities are an Apple Pay exclusive and cannot be used for 3rd party apps (because, well... Apple).
Fortunately, Android devices (those with Android 4.4 or above) allow you to use HCE for implementing the whole variety of uses for the technology. Those uses can be customer loyalty card emulation, public transit card emulation, and many more such as keycard emulation described in this post.
We've decided to add another dot in our DotHub platform, which allows users to unlock their doors with their phones. For this post, I won't bother you with the details of the whole project but will instead explain just the app part related to HCE and Flutter.
The app itself has three main challenges to accomplish:
- Communicating with the HCE reader
- Forwarding the results to native Android UI layer
- Forwarding those results to Flutter
On the image above, dashed arrows represent the communication flow which we cannot control. Notice that one of them is red. We will get to that part later on, but it means that we do have some say in that particular section of the flow.
Let's analyze part of the flow that we control:
Communicating with the HCE reader
This component allows our Android application to handle the HCE logic by communicating with the HCE reader (the lock), processing that data, and forwarding the result to the native part of the UI (our 2nd component).
The first important step is creating our service and declaring it in
android:exported=true attribute. We're exporting our service externally to let the Android OS know that our app has the service for handling HCE.
Other attributes are pretty much self-explanatory. However, we still have to create our
apduservice.xml file, which we are referencing in the
<meta-data /> tag of our service manifest declaration.
apduservice.xml file tells the Android OS that our application can handle HCE events for readers that require category
other and select the AID
F0010203040506. This way, when the reader sends a request such as
SELECT F0010203040506, the Android OS knows that our app can take it from there. That triggers our
HCEService.processCommandApdu() method, and then we can handle the command and any payload that it sends.
This is why the red dashed line from the image above isn't black like the others: we do have a say in which AID's our app can handle, but it is not a guarantee since other apps can also filter the same AID's. In that case, it is up to the user to decide which app should handle the process.
Now that we've declared our service, we can implement our logic within:
processCommandApdu function is the entry point of our service. The function also returns the value as a response back to the reader. Since we usually have to do some processing here, I'd like to point out one important note from the official Android documentation:
This method is running on the main thread of your application. If you cannot return a response APDU immediately, return null and use the sendResponseApdu(byte) method later.
This means that if we have some logic which requires an API connection or some other potentially time-consuming process, we should not "stall" the user but rather return the result later on. In this example, we return some status code to let our HCE reader know if the operation was a success or not.
Forwarding the results to native Android UI layer
Now that we've finished building our service, we can focus on our native part of the UI. Since DotHub Lock is primarily a Flutter app, we want to do as minimal work as possible in the native UI department. The only reason we wrote Android native code at all is that there is no other way of declaring and implementing the
HCEService. This way, the native UI part only serves as a glue between our service and the real UI, written in Flutter.
Another thing to consider is that the
HCEService doesn't require the application to be active to handle the response. That means that our UI may or may not be shown at the time. There are various ways of sending the data from an Android Service to an Android Activity. Still, only one of them supports the option to either refresh the data of an existing Activity or starting the new Activity if it isn't already running.
We've set the
singleTop (it is already a default value when creating a Flutter project). Also, we've made sure that our
Intent has the
FLAG_ACTIVITY_NEW_TASK flag set, which is a standard for starting an Activity from a Service. The whole
forwardTheResult() implementation is shown below:
Then we can update our activity to handle the payload like this:
As mentioned before, we have to consider two scenarios:
- The scenario in which an activity is already running, and it just needs to receive the payload
- The scenario in which activity needs to be launched and immediately receive the payload.
Our implementation does both. If it is already running, we are only receiving the payload in the
onNewIntent() method, which we can then pass to the
onHCEResult(). On the other hand, we also need to check in case our activity was launched with the payload and call the
Forwarding the results from native Android UI layer to Flutter layer
Now that we've handled the Service - Activity flow, we still have to implement the Activity - Flutter flow. Fortunately for us, Flutter has an elegant way of achieving this by using a thing called Platform Channels. These channels enable two-way communication between the native part of our code and Flutter. For this app, we only need one of those directions, and we are using a Method Channel, which is just one of the Platform Channels offered by Flutter.
Method Channel is a named channel for communicating with platform plugins using asynchronous method calls.
We can now update our
MainActivity to look like this:
As you can see, we've created a
"hce" and we are invoking a channel method
onHCEResult and passing the
success boolean to let our Flutter part know if the operation was a success or not.
Although we've finished the native part, it has a potential bug. In our
onCreate method we are checking if there was any payload and forwarded it immediately. This could cause some unwanted behavior because we don't know if our Flutter part of the app is yet initialized and ready to handle our method call. That is why we must first wait until that finishes and then do our payload check. We can achieve that by using
addFirstFrameListener which is triggered when the Flutter application has rendered its first frame on the screen. Now our
onCreate looks like this:
Now that we've implemented and improved our 2nd component, the only thing left is to handle the result on Flutter part of our application.
Last (but not least) component is the Flutter UI within our application. Here we need to show to the user if the doors are locked or not. The final result looks like this:
Just as we had to use Platform Channels on the native Android part of the app, we have to use them on the Flutter part as well. This is how we did it:
We've set the method call handler for
onHCEResult method to check if the unlock operation was successful or not. Then we update the application state accordingly, which then triggers the UI update displayed above.
Honestly, this was one of the most challenging parts of developing this app since we didn't have the reader device developed when we've started with our proof of concept app. After trying out about a dozen of different apps on the Play Store with the HCE emulation capabilities, we've decided to use the APDU Debug application. It was the first and only app that we've tried, which triggered our service, so we went with that for the rest of our testing (until we built our reader, of course).
We had to make the reader app send a
SELECT <AID> command. But the trick was that it had to be sent as "raw" hexadecimal code, so we had to convert it to send
00A4040007<AID>, where the
00A4040007 actually represented a
Another unique challenge was Android Beam. If you press two Android devices with Android Beam enabled, they initiate the "Beam Share" UI and allow you to transfer your content from one device to another. It is a pretty neat feature, but for our testing purposes, it was a nuisance. Lucky for us, Android Beam can be disabled (or so we thought). Even though we've disabled it on both devices, it was still intercepting the NFC events, but it didn't initiate the share UI. That is why on most attempts, the simulated reader was able to communicate with our app, but those few attempts the Android Beam took over, which is why we've decided to create our reader even for the POC app.
Both HCE and Flutter are gaining their popularity by the day, and we cannot wait to see where both technologies go from here. Our only hope for the HCE part is that Apple soon enables its usage on their devices and OS so that the iOS users can also enjoy the wonders of this technology. As for the Flutter, we will continue experimenting with it and, hopefully, sometimes soon, build some great products with it.