Thread Network SDK for Android

The Thread Network SDK provides functionality that's similar to a digital keychain, allowing your Android apps to share Thread network credentials with Google Play services. This allows your apps to set up any Thread device from any smart home ecosystem, without exposing credentials and user data directly.

With just a few API calls, you can:

  1. Request preferred Thread network credentials from Google Play services.
  2. Set up new border routers and add your Thread network credentials to Google Play services.
  3. If you already have in-field border routers, you can check if your border routers are in the preferred network and migrate them, if necessary.

There are several user and developer journeys to consider. We'll cover most of them in this guide, along with other key features and recommended usage.

Key terminology and API concepts

Before you get started, it's helpful to understand the following terms:

  • Thread Network Credentials: Binary blob of Thread TLVs that encodes Thread Network Name, Network Key and other properties that are required by a Thread device to join a given Thread network.

  • Preferred Thread Network Credentials: The auto-selected Thread network credentials that can be shared with apps of different vendors using the getPreferredCredentials API.

  • Border Agent ID: A 16-byte globally unique ID for a Thread Border Router device. This ID is created and managed by border router vendors.

  • Thread Border Router Setup app: This is your Android app that sets up new Thread Border Router devices and adds the Thread network credentials to Google Play services. Your app is the authoritative owner of the added credentials and has access to them.

Many of the Thread Network APIs return a Task that completes asynchronously. You can use addOnSuccessListener and addOnFailureListener to register callbacks for receiving the result. To learn more, refer to the Task documentation.

Credentials ownership and maintenance

The app that adds the Thread network credentials becomes the owner of the credentials, and has full permissions to access to the credentials. If you try to access credentials added by other apps, you'll receive a PERMISSION_DENIED error.

As the app owner, it's recommended that you keep credentials stored in Google Play services up-to-date when the Thread Border Router network is updated. This means adding credentials when required, updating credentials when the border router's Thread network credentials change, and removing credentials when the Thread Border Router is removed or factory reset.

Border Agent discovery

Credentials must be saved with a Border Agent ID. You'll need to make sure that your Thread Border Router Setup app is able to determine the Border Agent IDs of your Thread border routers.

Thread Border Routers must use mDNS to advertise Thread network information, including the Network Name, Extended Pan ID, and Border Agent ID. The corresponding txt values for these attributes are nn, xp, and id, respectively.

For networks with Google border routers, Google Play services automatically gets Google Thread network credentials for use.

Integrate the SDK into your Android app

To get started, complete the following steps:

  1. Follow the instructions provided at Set up Google Play services.

  2. Add the Google Play services dependency to your build.gradle file:

    implementation 'com.google.android.gms:play-services-threadnetwork:16.2.1'
    
  3. Optional: Define a BorderAgent data class to store border router information. We'll use this data throughout this guide:

    data class BorderAgentInfo(
      // Network Name max 16 len
      val networkName: String = "",
      val extPanId: ByteArray = ByteArray(16),
      val borderAgentId: ByteArray = ByteArray(16),
      ...
    )
    

Next, we'll go over the recommended steps to add and manage preferred credentials.

New border router setups

Before you create a new network for new border routers, it's important that you try using the preferred network credentials first. This ensures that Thread devices are connected to a single Thread network when possible.

A call to getPreferredCredentials launches an Activity, prompting users to allow the network request. If network credentials have been stored in the Thread SDK digital keychain, the credentials are returned to your app.

Request credentials

To prompt the user for preferred credentials:

  1. Declare an ActivityLauncher:

    private lateinit var preferredCredentialsLauncher: ActivityResultLauncher<IntentSenderRequest>
    
  2. Handle the Activity result, returned as ThreadNetworkCredentials:

    preferredCredentialsLauncher =
     registerForActivityResult(
       StartIntentSenderForResult()
     ) { result: ActivityResult ->
       if (result.resultCode == RESULT_OK) {
         val threadNetworkCredentials = ThreadNetworkCredentials.fromIntentSenderResultData(result.data!!)
         Log.d("debug", threadNetworkCredentials.networkName)
       } else {
         Log.d("debug", "User denied request.")
       }
     }
    
  3. If you're setting up a new Thread Border Router, it's advised that you call preferredCredentials and launch the Activity. This call will ensure that your new Thread Border Router will use the same credentials already stored as preferred in the phone, promoting the convergence of different TBRs to the same network.

    private fun getPreferredThreadNetworkCredentials() {
      ThreadNetwork.getClient(this)
        .preferredCredentials
      .addOnSuccessListener { intentSenderResult ->
        intentSenderResult.intentSender?.let {
          preferredCredentialsLauncher.launch(IntentSenderRequest.Builder(it).build())
          } ?: Log.d("debug", "No preferred credentials found.")
        }
      .addOnFailureListener { e: Exception -> Log.d(TAG, "ERROR: [${e}]") }
    }
    
  4. If your use case relates to setting up non-TBR devices, such as a new Matter-over-Thread end device, it's advised that you use the allActiveCredentials api to fetch credentials. This call will scan for TBRs found in the local network and thus won't return credentials that are not available by an existing TBR locally.

    // Creates the IntentSender result launcher for the getAllActiveCredentials API
    private val getAllActiveCredentialsLauncher =
      registerForActivityResult(
        StartIntentSenderForResult()
      ) { result: ActivityResult ->
        if (result.resultCode == RESULT_OK) {
          val activeCredentials: List<ThreadNetworkCredentials> =
            ThreadNetworkCredentials.parseListFromIntentSenderResultData(
              result.data!!
            )
          // Use the activeCredentials list
        } else {
          // The user denied to share!
        }
      }
    
    // Invokes the getAllActiveCredentials API and starts the dialog activity with the returned
    // IntentSender
    threadNetworkClient
    .getAllActiveCredentials()
    .addOnSuccessListener { intentSenderResult: IntentSenderResult ->
      val intentSender = intentSenderResult.intentSender
      if (intentSender != null) {
        getAllActiveCredentialsLauncher.launch(
          IntentSenderRequest.Builder(intentSender).build()
        )
      } else {
        // No active network credentials found!
      }
    }
    // Handles the failure
    .addOnFailureListener { e: Exception ->
      // Handle the exception
    }
    

Create a new Thread network

If there are neither preferred Thread network credentials nor active thread credentials available in a user's Thread network, then you can use the addCredentials API to add credentials to Google Play Services. To do this, you'll need to create a ThreadBorderAgent, and also supply a ThreadNetworkCredentials object.

To create a random network, call the newRandomizeBuilder:

val threadCredentials = ThreadNetworkCredentials.newRandomizedBuilder().build()

To specify the Thread network Name:

val threadCredentials = ThreadNetworkCredentials.newRandomizedBuilder()
  .setNetworkName("ThreadNetworkSDK")
  .build()

Add credentials

To make your Thread network credentials available for other Thread vendors, we need to add them to Google Play services. Before we can add our new credentials, we also need to know to which border router device this Thread network belongs to.

In this example, we'll create a ThreadBorderAgent from a Border Agent ID, and pass the new Thread network credentials that you just created:

private fun addCredentials(borderAgentInfo: BorderAgentInfo, credentialsToBeAdded: ThreadNetworkCredentials) {

  val threadBorderAgent = ThreadBorderAgent.newBuilder(borderAgentInfo.borderAgentId).build()
  Log.d("debug", "border router id:" + threadBorderAgent.id)

  ThreadNetwork.getClient(this)
    .addCredentials(threadBorderAgent, credentialsToBeAdded)
      .addOnSuccessListener {
        Log.d("debug", "Credentials added.")
      }
      .addOnFailureListener { e: Exception -> Log.d(TAG, "ERROR: [${e}]") }
}

Detect and migrate in-field border routers

If you currently have in-field border routers, you can use isPreferredCredentials to determine if your border routers belong to the preferred network. This API does not prompt the user for permission, and checks border router credentials against what's stored in Google Play services.

isPreferredCredentails returns 0 for not matched, and 1 for matched, as an Int datatype. You can use IsPreferredCredentialsResult to check your results.

public @interface IsPreferredCredentialsResult {
    int PREFERRED_CREDENTIALS_NOT_FOUND = -1;
    int PREFERRED_CREDENTIALS_NOT_MATCHED = 0;
    int PREFERRED_CREDENTIALS_MATCHED = 1;
}

To use isPreferredCredentials, you'll need to create a ThreadNetworkCredentials object first. There are several ways to instantiate ThreadNetworkCredentials. In the next steps, we'll go over these options.

Thread network credentials by Operational Dataset

There are cases that your Thread Border Router is already set up with a Thread network, and you want to add this Thread network to Google Play services to share it with other vendors. You can create a ThreadNetworkCredential instance from a raw Thread Active Operational Dataset TLV list:

  1. Convert the Operational Dataset to a ByteArray. For example:

    val activeDataset =
          "0e080000000000010000000300000f35060004001fffe0020833333333...".dsToByteArray()
    
    fun String.dsToByteArray(): ByteArray {
      return chunked(2).map { it.toInt(16).toByte() }.toByteArray()
    }
    
  2. Use fromActiveOperationalDataset to create the ThreadNetworkCredentials. When successful, you'll be able to get the Thread network Name, Channel, and other network information. For a complete list of properties, refer to ThreadNetworkCredentials.

    val threadNetworkCredentials =
        ThreadNetworkCredentials.fromActiveOperationalDataset(activeDataset)
    Log.d(
        "threadNetworkCredentials",
        threadNetworkCredentials.channel.toString() + " - " + threadNetworkCredentials.networkName)
    
  3. Call the isPreferredCredentials API and pass the ThreadNetworkCredentials.

    ThreadNetwork.getClient(this)
    .isPreferredCredentials(threadNetworkCredentials)
    .addOnSuccessListener { result ->
      when (result) {
        IsPreferredCredentialsResult.PREFERRED_CREDENTIALS_NOT_MATCHED ->
            Log.d("isPreferredCredentials", "Credentials not matched.")
        IsPreferredCredentialsResult.PREFERRED_CREDENTIALS_MATCHED ->
            Log.d("isPreferredCredentials", "Credentials matched.")
      }
    }
    .addOnFailureListener { e: Exception -> Log.d("isPreferredCredentials", "ERROR: [${e}]") }
    

Thread network credentials by Border Agent

A Border Agent ID uniquely identifies a border router device. To use the getCredentialsByBorderAgent API, first you'll need to create a ThreadBorderAgent object and pass the Border Agent ID.

Once you've created the ThreadBorderAgent object, call getCredentialsByBorderAgent. If the credentials have been saved, check to see if they're preferred.

private fun isPreferredThreadNetworkByBorderAgent(borderAgentInfo: BorderAgentInfo) {

  val threadBorderAgent = ThreadBorderAgent.newBuilder(borderAgentInfo.borderAgentId).build()
  Log.d("debug", "border router id:" + threadBorderAgent.id)

  var isPreferred = IsPreferredCredentialsResult.PREFERRED_CREDENTIALS_NOT_FOUND
  var borderAgentCredentials: ThreadNetworkCredentials?
  val taskByBorderAgent = ThreadNetwork.getClient(this)
  taskByBorderAgent
      .getCredentialsByBorderAgent(threadBorderAgent)
      .addOnSuccessListener { result: ThreadNetworkCredentialsResult ->
        borderAgentCredentials = result.credentials
        result.credentials?.let {
          taskByBorderAgent.isPreferredCredentials(it).addOnSuccessListener { result ->
            isPreferred = result
          }
        }
      }
      .addOnFailureListener { e: Exception -> Log.d(TAG, "ERROR: [${e}]") }
}

Thread network credentials by Extended Pan ID

Similar to getPreferredCredentials, you can also prompt the user for credentials from a border router's Extended Pan ID. The getCredentialsByExtendedPanId returns an IntentSender, and the Activity result contains a ThreadNetworkCredentials object when the user approves.

private fun getCredentialsByExtPanId(borderAgentInfo: BorderAgentInfo) {
  ThreadNetwork.getClient(this)
    .getCredentialsByExtendedPanId(borderAgentInfo.extPanId)
    .addOnSuccessListener { intentSenderResult ->
      intentSenderResult.intentSender?.let {
        preferredCredentialsLauncher.launch(IntentSenderRequest.Builder(it).build())
      }
        ?: Log.d("debug", "No credentials found.")
    }
    .addOnFailureListener { e: Exception -> Log.d(TAG, "ERROR: [${e}]") }
}

Remove Credentials

When your Border Router device is removed from your home or factory reset, you need to remove its Thread network from Google Play services.

private fun removeCredentials(borderAgentInfo: BorderAgentInfo) {

  val threadBorderAgent = ThreadBorderAgent.newBuilder(borderAgentInfo.borderAgentId).build()
  Log.d("debug", "border router id:" + threadBorderAgent.id)

  ThreadNetwork.getClient(this)
      .removeCredentials(threadBorderAgent)
      .addOnSuccessListener { Log.d("debug", "Credentials removed.") }
      .addOnFailureListener { e: Exception -> Log.d(TAG, "ERROR: [${e}]") }
}

Resources

To learn more about the Thread Network SDK, refer to the API Reference.