Build an Android App for Matter

1. Welcome

Built with the goal of unifying IoT standards, Matter connects smart home devices across various ecosystems like Google Home, Zigbee, Bluetooth Mesh, Z-Wave, and more.

Mobile devices are a central interaction point with smart home devices. If you'd like to build your own Android apps to support Matter devices, we can help you get started fast.

The Google Home Sample App for Matter (GHSA for Matter) showcases the Home Mobile SDK APIs, allowing users to commission and share devices. You can also use the sample app as a learning tool to better understand key Matter concepts, as well as a tool to debug and troubleshoot interactions with Matter devices.

What you'll do

In this Codelab, you'll download the source code for the sample app and learn how to use the Home Mobile SDK to commission and share devices. You'll also learn how to use commissioning and Cluster libraries from the Matter repo (connectedhomeip).

After you download the sample app, we'll review the source code in Android Studio and implement the following Home Mobile SDK APIs:

You'll also learn more about commissioning concepts, Matter fabrics, and how to control Matter devices.

What you'll need

Before you begin, make sure to complete the following steps:

You don't need a hub, for example a Google Nest Hub (2nd Generation), to commission and control devices with the sample app.

2. Get set up

The codelab starter app is located in the codelab branch. To start working with the codelab source code, you can download the ZIP file.

You'll use this codelab ZIP file to build a working sample.

Codelab versions

The codelab branch is tagged with the 2.0.0 release of the sample app. To compare your updates as you work through each step, you can download the completed source code for this release.

If you'd like to clone the GitHub repository, follow the instructions on the Sample app README.

Dependencies

We'll guide you through the source code required to share and commission devices, but it might help to be aware of the following dependencies before you get started. Note that these dependencies are declared in file libs.versions.toml and their usage specified in file build.gradle.kts

Source code

The user interface and most of the functionality has already been created for you.

For this codelab, we'll be adding Matter functionality to the following files:

  • java/commissioning/AppCommissioningService: allows you to commission devices to the development fabric
  • java/screens/home/HomeScreen and java/screens/home/HomeViewModel.kt: includes the Home Mobile SDK commissioning functionality
  • java/screens/device/DeviceScreen and java/screens/device/DeviceViewModel: includes the Share Device API calls

Each file is commented with the code-block that you'll be modifying, for example:

// CODELAB: add commissioningFunction()

This allows you to quickly locate the corresponding section in the codelab.

3. Commission to Google

Before you can control devices and allow them to communicate with each other within the same fabric, they need to be commissioned by a Commissioner, which in this case is this sample application, the Google Home Sample App for Matter.

It's important to understand the following concepts about Matter commissioning:

  • Fabrics allow devices to communicate with each other.
  • Fabrics maintain a shared set of unique credentials.
  • Ecosystems are responsible for issuing trusted root certificates, assigning fabric IDs, and assigning unique node IDs. An ecosystem is the back-end service of a commissioner, for example the Home Graph for the Google Home ecosystem.
  • Devices can be commissioned to more than one fabric (multi-admin feature).

To commission a device, you'll need to use the CommissioningClient API. A call to .commissionDevice() returns an IntentSender, which launches the proper activity in Google Play Services:

interface CommissioningClient {
  Task<IntentSender> commissionDevice(CommissioningRequest request);
}

In the next sections, we'll go over the minimal code required to commission devices to the Google fabric.

Step 1: Activity Launcher

To handle the IntentSender from the CommissioningClient, you can use an ActivityResultLauncher:

val commissioningLauncher = registerForActivityResult(
    StartIntentSenderForResult()
) { result: ActivityResult ->
    if (result.resultCode == RESULT_OK) {
        Timber.d(TAG, "Commissioning succeeded.")
    } else {
        Timber.d(TAG, "Commissioning failed. " + result.resultCode)
    }
}

Step 2: Commissioning function

Here's a basic example that uses the CommissioningClient API to commission a device to the Google fabric.

  1. The commissioning process starts with the commissionDevice() function. First, a CommissioningRequest is defined. With this default configuration, devices are commissioned only to the Local Android fabric.
  2. Matter is the entry point for the Home Mobile SDK. In the next call, .getCommissioningClient gets a CommissioningClient by this (Activity).
  3. .commissionDevice() accepts the CommissioningRequest.
  4. And finally, .addOnSuccessListener is called to process the CommissioningResult and launch the Google Play Services (GPS) Commission Device Activity.
private fun commissionDevice() {
    val request: CommissioningRequest = CommissioningRequest.builder().build()
    Matter.getCommissioningClient(this)
        .commissionDevice(request)
        .addOnSuccessListener { result ->
            commissioningLauncher.launch(IntentSenderRequest.Builder(result).build())
        }
}

The Local Android Fabric can be leveraged via Android settings to simplify the process of commissioning its devices to other fabrics.

Next, you'll learn how to commission a device to a development fabric.

For an overview of the user interface during the commissioning process, refer to the Google Home Sample App for Matter Guide.

4. Commission to a development fabric

Devices can be commissioned to more than one fabric. To manage trusted pairings, devices store a FabricTable containing various FabricInfo members, for example:

  • Fabric identification
  • Node Id assigned by the fabric to the device
  • Vendor Id
  • Fabric Id
  • Device operational credentials

The administrative domain manager (ADM) defines fabric credentials. In the previous scenario, Google Play Services is the ecosystem that acts as a trusted root certificate authority (CA). When you commission devices to the Local Android fabric, every device includes the same set of fabric credentials, and the same set of CAs.

Custom Commissioning Services

To commission to the Local Android fabric, we used the default parameters to build the CommissioningRequest in the CommissioningClient API:

val request: CommissioningRequest = CommissioningRequest.builder().build()

If you'd like to control and manage new devices from your app, you need to create a local development fabric and obtain the operational credentials to commission devices. In this scenario, your app becomes a unique, independent ecosystem that assigns devices the appropriate node credentials.

You can inform Home Mobile SDK that you'd like to commission devices to your own fabric by passing a custom service to the CommissioningRequest:

class CommissioningRequest {
  static CommissioningRequest.Builder builder();

  class Builder {
    Builder setCommissioningService(@Nullable ComponentName commissioningService);

    CommissioningRequest build();
  }
}

In the next steps, we'll modify the commissionDevice() function to use a custom service. We'll also add an Activity Launcher to the Home fragment and use LiveData objects to manage the API flow.

Step 1: Create a GPS Activity Launcher

First, let's create an Activity Launcher to handle the IntentSender from the CommissioningClient API.

  1. Open HomeScreen in the java/screens/home/ folder.
  2. Replace the // CODELAB: commissionDeviceLauncher definition comment with the following code to register and handle the commissioning Activity result:
    val commissionDeviceLauncher =
      rememberLauncherForActivityResult(
        contract = ActivityResultContracts.StartIntentSenderForResult()
      ) { result ->
        // Commission Device Step 5.
        // The Commission Device activity in GPS (step 4) has completed.
        val resultCode = result.resultCode
        if (resultCode == Activity.RESULT_OK) {
          Timber.d("CommissionDevice: Success")
          // We let the ViewModel know that GPS commissioning has completed successfully.
          // The ViewModel knows that we still need to capture the device name and will\
          // update UI state to trigger the NewDeviceAlertDialog.
          homeViewModel.gpsCommissioningDeviceSucceeded(result)
        } else {
          homeViewModel.commissionDeviceFailed(resultCode)
        }
      }
    

Step 2: Trigger the commission device action

In this step, the user triggers the "Commission Device" action by clicking on the "+" button at the bottom right of the Home screen. A call is then made to commissionDevice().

val onCommissionDevice = {
  ...
  commissionDevice(activity!!.applicationContext, commissionDeviceLauncher)
}

Step 3: Call the API

  1. Still in HomeScreen.kt in the java/screens/home/ folder.
  2. Replace the // CODELAB: commissionDevice comment with the following commissionDeviceRequest. setCommissioningService binds AppCommissioningService to a CommissioningService instance, returned in a callback function. When you pass a custom service, Home Mobile SDK will first commission devices to the Android local fabric, then send the onboarding payload back to the AppCommissioningService.
    val commissionDeviceRequest =
        CommissioningRequest.builder()
            .setCommissioningService(ComponentName(
                context, AppCommissioningService::class.java))
            .build()
    
  3. Call .getCommissioningClient(), then call .commissionDevice().
Matter.getCommissioningClient(context)
    .commissionDevice(commissionDeviceRequest)

To complete our commissionDevice function, add an addOnSuccessListener and addOnFailureListener:

    .addOnSuccessListener { result ->
      commissionDeviceLauncher.launch(IntentSenderRequest.Builder(result).build())
    }
    .addOnFailureListener { error ->
      Timber.e(error)
    }

5. Create a CommissioningService

In the commissionDevice() function, we requested to get a CommissioningService from the CommissioningClient API. In this flow, the CommissioningClient API commissions devices to the Local Android fabric first, then returns a callback that includes the CommissioningRequestMetadata object:

public interface CommissioningService {
interface Callback {
    void onCommissioningRequested(CommissioningRequestMetadata metadata);
  }
}

Now, we have to inherit the CommissioningService.Callback and provide the functionality required to commission devices to our sample app. Here's an example of a basic CommissioningService implementation:

class MatterCommissioningService : Service(), CommissioningService.Callback {
   private val commissioningServiceDelegate =
     CommissioningService.Builder(this)
       .setCallback(this)
       .build()

   override fun onBind(intent: Intent) = commissioningServiceDelegate.asBinder()

   override fun onCommissioningRequested(metadata: CommissioningRequestMetadata) {
     // perform commissioning

     commissioningServiceDelegate
       .sendCommissioningComplete(CommissioningCompleteMetadata.builder().build())
   }
 }

Step 1: Explore the custom AppCommissioningService

To help you get started, we've already defined the basic class structure for our custom CommissioningService. Here's a quick overview of the service functionality. To follow along, open AppCommissioningService in java/commissioning.

We've added the following imports for the Home Mobile SDK APIs:

import com.google.android.gms.home.matter.commissioning.CommissioningCompleteMetadata
import com.google.android.gms.home.matter.commissioning.CommissioningRequestMetadata
import com.google.android.gms.home.matter.commissioning.CommissioningService

AppCommissioningService also includes libraries from the Matter repo (connectedhomeip):

import com.google.home_sample_app_for_matter.chip.ChipClient

Finally, the service includes imports to support Hilt and Kotlin coroutines.

Next, we create the constructor and set a few things up, including the commissioningServiceDelegate, which we'll use to let Google Play Services know when commissioning is complete.

private lateinit var commissioningServiceDelegate: CommissioningService
...
commissioningServiceDelegate = CommissioningService.Builder(this).setCallback(this).build()

Now it's time to add the commissioning functions.

Step 2: Override onCommissioningRequested

To commission devices to the app's development fabric, complete the following steps:

  1. Open AppCommissioningService in java/commissioning.
  2. Locate the onCommissioningRequested() function. We've provided a log message that prints out the CommissioningRequestMetadata. Replace the // CODELAB: onCommissioningRequested() comment to start the serviceScope coroutine and get the deviceId.
    // Perform commissioning on custom fabric for the sample app.
    serviceScope.launch {
      val deviceId = devicesRepository.incrementAndReturnLastDeviceId()
    
  3. Perform commissioning. For this step, we can pass the device information returned in the CommissioningRequestMetadata object. The ChipClient uses this metadata information to create a secure channel between the GHSA for Matter app and your device.
    try {
      Timber.d(
          "Commissioning: App fabric -> ChipClient.establishPaseConnection(): deviceId [${deviceId}]")
      chipClient.awaitEstablishPaseConnection(
          deviceId,
          metadata.networkLocation.ipAddress.hostAddress!!,
          metadata.networkLocation.port,
          metadata.passcode)
      Timber.d(
          "Commissioning: App fabric -> ChipClient.commissionDevice(): deviceId [${deviceId}]")
      chipClient.awaitCommissionDevice(deviceId, null)
    } catch (e: Exception) {
      Timber.e(e, "onCommissioningRequested() failed")
      // No way to determine whether this was ATTESTATION_FAILED or DEVICE_UNREACHABLE.
      commissioningServiceDelegate
          .sendCommissioningError(CommissioningError.OTHER)
          .addOnSuccessListener {
            Timber.d(
                "Commissioning: commissioningServiceDelegate.sendCommissioningError() succeeded")
          }
          .addOnFailureListener { e2 ->
            Timber.e(e2, "Commissioning: commissioningServiceDelegate.sendCommissioningError() failed")
          }
      return@launch
    }
    
  4. Use the commissioningServiceDelegate to let Google Play Services know that commissioning is complete. In .sendCommissioningComplete(), pass the CommissioningCompleteMetadata.
    commissioningServiceDelegate
        .sendCommissioningComplete(
            CommissioningCompleteMetadata.builder().setToken(deviceId.toString()).build())
        .addOnSuccessListener {
          Timber.d("Commissioning: commissioningServiceDelegate.sendCommissioningComplete() succeeded")
        }
        .addOnFailureListener { e ->
          Timber.e(e, "Commissioning: commissioningServiceDelegate.sendCommissioningComplete() failed")
        }
    

Run the app

Now that all of the required code is in place to commission to our local fabric, it's time to test it. Choose your Android device, and run the app. From the Home screen, tap Add Device and complete the steps to commission your device.

When commissioning completes, your device now participates in two fabrics: the Local Android fabric, and your local development fabric. Each fabric has its own set of credentials and a unique, 64-bit fabric ID.

6. Control devices

Commissioning to a development fabric allows you to use the libraries from the Matter repo (connectedhomeip) to control devices from the sample app.

We've created some helper classes to make it easier to access device Clusters and send commands. To learn more, open ClustersHelper in java/clusters. This Singleton helper imports the following libraries to access device information:

import chip.devicecontroller.ChipClusters
import chip.devicecontroller.ChipStructs

We can use this class to get the On/Off Cluster for a device, then call .toggle:

suspend fun toggleDeviceStateOnOffCluster(deviceId: Long, endpoint: Int) {
  Timber.d("toggleDeviceStateOnOffCluster())")
  val connectedDevicePtr =
      try {
        chipClient.getConnectedDevicePointer(deviceId)
      } catch (e: IllegalStateException) {
        Timber.e("Can't get connectedDevicePointer.")
        return
      }
  return suspendCoroutine { continuation ->
    getOnOffClusterForDevice(connectedDevicePtr, endpoint)
        .toggle(
            object : ChipClusters.DefaultClusterCallback {
              override fun onSuccess() {
                continuation.resume(Unit)
              }
              override fun onError(ex: Exception) {
                Timber.e("readOnOffAttribute command failure: $ex")
                continuation.resumeWithException(ex)
              }
            })
  }
}

Toggle a device

After you commission a device, the payload returned in the CommissioningResult gets added to the DataStore. This gives our app access to device information that we can use to send commands.

Matter apps are event driven. When the Matter stack is initialized, Cluster services listen for incoming messages. Once a device is commissioned, Matter clients send commands over the secure operational channel that was established during device commissioning.

On the device, packets are validated, decrypted, then dispatched with a callback. Callback functions include the EndpointId, ClusterId, and AttributeId, accessible from the attributePath. For example, this code can be implemented on a Matter device:

void MatterPostAttributeChangeCallback(const chip::app::ConcreteAttributePath & attributePath, uint8_t mask, uint8_t type,
                                       uint16_t size, uint8_t * value)
{
    // handle callback
    ClusterId clusterId     = attributePath.mClusterId;
    AttributeId attributeId = attributePath.mAttributeId;
}

In the next steps, you'll use the Matter SDK and ClustersHelper to toggle a device.

  1. Go to DeviceViewModel in java/screens/device.
  2. Locate the updateDeviceStateOn function.
  3. Replace the // CODELAB: toggle comment with the code to call clustersHelper, then update the device repository:
    Timber.d("Handling real device")
        try {
          clustersHelper.setOnOffDeviceStateOnOffCluster(deviceUiModel.device.deviceId, isOn, 1)
          devicesStateRepository.updateDeviceState(deviceUiModel.device.deviceId, true, isOn)
        } catch (e: Throwable) {
          Timber.e("Failed setting on/off state")
        }
    

This function is called from DeviceScreen:

// On/Off Switch click.
val onOnOffClick: (value: Boolean) -> Unit = { value ->
  deviceViewModel.updateDeviceStateOn(deviceUiModel!!, value)
}

Run the app

Run the app to reload your updates. From the Home screen, toggle your device on and off.

7. Share devices with other ecosystems

Sharing a device is referred to as multi-admin flow in the Matter specification.

In the previous steps, we learned that the Home Mobile SDK makes it possible to commission devices to the Local Android fabric and also to a development fabric for the sample app. This is an example of multi-admin flow, where devices can be commissioned to more than one fabric.

Now, you may want to share devices with even more fabrics, especially if this is a household where people have their own preferences when it comes to applications and platforms.

The Home Mobile SDK provides this functionality in the ShareDeviceRequest API, allowing you to:

  1. Open a temporary commissioning window for devices.
  2. Change the state of your devices, enabling them to be commissioned to another fabric.
  3. Control your devices from other apps and ecosystems.

In the next steps, you'll use the Home Mobile SDK to share devices.

Step 1: Create a GPS Activity Launcher

Similar to the Commissioning Activity Launcher that we created when we commissioned to a development fabric, we've created a Share Device Activity Launcher to handle the IntentSender from the CommissioningClient API.

  1. Open DeviceScreen in the java/screens/device/ folder.
  2. Replace the // CODELAB: shareDeviceLauncher definition comment with the following code to register and handle the .shareDevice() Activity result:
    val shareDeviceLauncher = rememberLauncherForActivityResult(
      contract = ActivityResultContracts.StartIntentSenderForResult()
    ) { result ->
      // Commission Device Step 5.
      // The Share Device activity in GPS (step 4) has completed.
      val resultCode = result.resultCode
      if (resultCode == Activity.RESULT_OK) {
        deviceViewModel.shareDeviceSucceeded()
      } else {
        deviceViewModel.shareDeviceFailed(resultCode)
      }
    }
    

Step 2: Trigger the share device action

In this step, the user triggers the "Share Device" action by clicking on the "Share" button on the device screen. A call is then made to the deviceViewModel to open a Pairing Window for device sharing.

// Share Device button click.
val onShareDevice: () -> Unit = {
 deviceViewModel.openPairingWindow(deviceUiModel!!.device.deviceId)
}

After successfully opening the paring window, the deviceViewModel communicates that fact to the UI. Communication between the ViewModel and the UI is done via StateFlow objects.

// Communicate to the UI that the pairing window is open.
// UI can then launch the GPS activity for device sharing.
_pairingWindowOpenForDeviceSharing.value = true

Upon seeing the change to the StateFlow object, DeviceScreen makes the following call:

shareDevice(activity!!.applicationContext, shareDeviceLauncher, deviceViewModel)

Step 3: Call the API

Now it's time to initiate a share device task.

  1. Open DeviceScreen.kt in the java/screens/device/ folder.
  2. Locate the shareDevice() function. Replace the // CODELAB: shareDevice comment with the ShareDeviceRequest. The DeviceDescriptor provides specific information about the device such as its Vendor Id, Product Id, and deviceType. In this example, we hard-code the values.
    val shareDeviceRequest =
      ShareDeviceRequest.builder()
        .setDeviceDescriptor(DeviceDescriptor.builder().build())
        .setDeviceName("GHSAFM temp device name")
    
  3. Set the CommissioningWindow and parameters.
        .setCommissioningWindow(
            CommissioningWindow.builder()
                .setDiscriminator(Discriminator.forLongValue(DISCRIMINATOR))
                .setPasscode(SETUP_PIN_CODE)
                .setWindowOpenMillis(SystemClock.elapsedRealtime())
                .setDurationSeconds(OPEN_COMMISSIONING_WINDOW_DURATION_SECONDS.toLong())
                .build())
        .build()
    
  4. Call .getCommissioningClient(), only this time, use the .shareDevice() API.
    Matter.getCommissioningClient(context)
        .shareDevice(shareDeviceRequest)
    

The success callback of the commissioningClient.shareDevice() API provides the IntentSender to be used to launch the Share Device Activity in Google Play Services.

  1. To complete our shareDevice function, add an addOnSuccessListener and addOnFailureListener. On success, launch is called on shareDeviceLauncher to launch the GPS activity for device sharing.
        .addOnSuccessListener { result ->
          Timber.d("ShareDevice: Success getting the IntentSender: result [${result}]")
          shareDeviceLauncher.launch(IntentSenderRequest.Builder(result).build())
        }
        .addOnFailureListener { error ->
          Timber.e(error)
          deviceViewModel.showMsgDialog("Share device failed", error.toString())
        }
    

Run the app

To share your Matter device with other ecosystems, you'll need to have another platform installed on your Android device. We've created another instance of the sample app that you can use as the target commissioner.

Once you have the target commissioner installed on your Android device, verify that you can share your Matter device. The target commissioner app is labeled GHSAFM-TC.

Your devices can now participate in three fabrics:

  1. The Local Android fabric.
  2. Your development fabric (this app).
  3. This third fabric that you've just shared the device with.

8. Next Steps

Congratulations

Congratulations, you've successfully completed this Codelab and learned how to commission and share devices using the Home Mobile SDK.

If you're having issues with the sample app, try completing the steps to verify your environment:

If you have questions about using the sample app or discover a code bug, you can submit issues to the Issue Tracker in the GitHub repository:

To get official guidance from Google on technical questions, use the Smart Home Developer Forum:

To get technical support from the community, use the google-smart-home tag on Stack Overflow: