You have a project and want to hire us?

Let's talk

Please enter a valid email address.
Please enter a valid phone number
hire-us

HOME / BLOG / Host-based Card Emulation (HCE) with Android and Flutter

Host-based Card Emulation (HCE) with Android and Flutter

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.
sequent.com

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.

Keycard emulation

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

The app itself has three main challenges to accomplish:

  1. Communicating with the HCE reader
  2. Forwarding the results to native Android UI layer
  3. Forwarding those results to Flutter
HCE flow in our application

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 AndroidManifest.xml:

class HCEService : HostApduService(){}
HCEService.kt
<service
      android:name=".HCEService"
      android:exported="true"
      android:permission="android.permission.BIND_NFC_SERVICE">
      <intent-filter>
        <action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" />
      </intent-filter>
      <meta-data
        android:name="android.nfc.cardemulation.host_apdu_service"
        android:resource="@xml/apduservice" />
</service>
service in AndroidManifest.xml

Notice the 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.

<host-apdu-service 		xmlns:android="http://schemas.android.com/apk/res/android"
  android:description="@string/service_description"
  android:requireDeviceUnlock="false">
  <aid-group
    android:category="other"
    android:description="@string/appdu_description">
    <aid-filter android:name="F0010203040506" />
  </aid-group>
</host-apdu-service>
apduservice.xml

Our 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:

class HCEService : HostApduService() {

  override fun processCommandApdu(commandApdu: ByteArray?, extras: Bundle?): ByteArray {
    if (commandApdu != null) {
      val success: Boolean = processCommand(commandApdu)
      forwardTheResult(success)
      return if (success) RESULT_SUCCESS.toByteArray() else RESULT_FAILURE.toByteArray()
    }
    return RESULT_EMPTY.toByteArray()
  }

}
HCEService.kt

The 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 launchMode to 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:

private fun forwardTheResult(){
	startActivity(
        Intent(this, MainActivity::class.java)
          .apply {
            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            putExtra("success", true)
          }
      )
}
HCEService.kt

Then we can update our activity to handle the payload like this:

class MainActivity : FlutterActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    GeneratedPluginRegistrant.registerWith(this)
    if (intent.hasExtra("success")) {
      onHCEResult(intent)
    }
  }

  override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
    if (intent?.hasExtra("success") == true) {
      onHCEResult(intent)
    }
  }

  private fun onHCEResult(intent: Intent) {
    // handle the result
  }

}
MainActivity.kt

As mentioned before, we have to consider two scenarios:

  1. The scenario in which an activity is already running, and it just needs to receive the payload
  2. 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 onHCEResult() immediately.

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.
api.flutter.dev

We can now update our MainActivity to look like this:

class MainActivity : FlutterActivity() {

  private val channel: MethodChannel by lazy { MethodChannel(flutterView, "hce") }

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    GeneratedPluginRegistrant.registerWith(this)
    if (intent.hasExtra("success")) {
      onHCEResult(intent)
    }
  }

  override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
    if (intent != null) {
      onHCEResult(intent)
    }
  }

  private fun onHCEResult(intent: Intent) = intent.getBooleanExtra("success", false).let { success ->
    channel.invokeMethod("onHCEResult", success)
  }

}
MainActivity.kt

As you can see, we've created a MethodChannel named "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.

Minor improvement

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:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    GeneratedPluginRegistrant.registerWith(this)
    flutterView.addFirstFrameListener {
      if (intent.hasExtra("success")) {
        onHCEResult(intent)
      }
    }
}
MainActivity.kt

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:

DotHub Lock HCE showcase

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:

const _platform = MethodChannel("hce");

class App extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _AppState();
}

class _AppState extends State<Test> {

  bool _unlocked = false;

  _AppState() : super() {
    _platform.setMethodCallHandler((MethodCall call) async {
   	  bool unlocked = call.arguments["success"];
      switch (call.method) {
        case "onHCEResult":
          setState(() {
            _unlocked = unlocked;
          });
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Lock(unlocked = _unlocked);
  }
}
app.dart

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.

Testing

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 SELECT command.

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.

Final thoughts

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.

YOU MIGHT ALSO LIKE

LET'S GET THIS PARTY STARTED

mail contact