Control external devices

Stay organized with collections Save and categorize content based on your preferences.

In Android 11 and later, the Quick Access Device Controls feature allows the user to quickly view and control external devices such as lights, thermostats, and cameras from a user affordance within three interactions from a default launcher. (The device OEM chooses what launcher is used.) Device aggregators (for example, Google Home) and third-party vendor apps can provide devices for display in this space. This guide shows you how to surface device controls in this space and link them to your control app.

Device control space in the Android UI

To add this support, create and declare a ControlsProviderService, create the controls your app supports based on predefined control types, and then create publishers for these controls.

User interface

Devices are displayed under Device controls as templated widgets. Five different device control widgets are available:

Toggle widget
Toggle
Toggle with slider widget
Toggle with slider
Range widget
Range (cannot be toggled on or off)
Stateless toggle widget
Stateless toggle
Temperature panel widget (closed)
Temperature panel (closed)
Temperature panel widget (open)
Temperature panel (open)

Long pressing a widget takes you to the app for deeper control. You can customize the icon and color on each widget, but for the best user experience, you should use the default icon and color unless the default set does not match the device.

Create the service

This section shows you how to create the ControlsProviderService. This service tells the Android system UI that your app contains device controls that should be surfaced in the Device controls area of the Android UI.

The ControlsProviderService API assumes familiarity with reactive streams, as defined in the Reactive Streams GitHub project and implemented in the Java 9 Flow interfaces. The API is built around these concepts:

  • Publisher: Your application is the publisher
  • Subscriber: The system UI is the subscriber and it can request a number of controls from the publisher
  • Subscription: A window of time when the publisher can send updates to System UI; this window can be closed by either publisher or subscriber

Declare the service

Your app must declare a service in its app manifest. Make sure to include the BIND_CONTROLS permission.

The service must include an intent filter for ControlsProviderService. This filter enables applications to contribute controls to the system UI.

<!-- New signature permission to ensure only systemui can bind to these services -->
<service android:name="YOUR-SERVICE-NAME" android:label="YOUR-SERVICE-LABEL"
    android:permission="android.permission.BIND_CONTROLS">
    <intent-filter>
      <action android:name="android.service.controls.ControlsProviderService" />
    </intent-filter>
</service>

Select the correct control type

The API provides builder methods to create the controls. To populate the builder, you need to determine the device you want to control and how the user should interact with it. In particular, you need to do the following:

  1. Pick the type of device the control represents. The DeviceTypes class is an enumeration of all currently-supported devices. The type is used to determine icons and colors for the device in the UI.
  2. Determine the user-facing name, device location (for example, kitchen), and other UI textual elements associated with the control.
  3. Pick the best template to support user interaction. Controls are assigned a ControlTemplate from the application. This template directly shows the control state to the user as well as the available input methods (that is, the ControlAction). The following table outlines some of the available templates and the actions they support:
Template Action Description
ControlTemplate.getNoTemplateObject() None The application may use this to convey information about the control, but the user can't interact with it.
ToggleTemplate BooleanAction Represents a control that can be switched between enabled and disabled states. The BooleanAction object contains a field that changes to represent the requested new state when the user touches the control.
RangeTemplate FloatAction Represents a slider widget with a specified min, max, and step values. When the user interacts with the slider, a new FloatAction object should be sent back to the application with the updated value.
ToggleRangeTemplate BooleanAction, FloatAction This template is a combination of the ToggleTemplate and RangeTemplate. It supports touch events as well as a slider, as in, for example, a control for dimmable lights.
TemperatureControlTemplate ModeAction, BooleanAction, FloatAction In addition to encapsulating one of the above actions, this template allows the user to set a mode, such as heat, cool, heat/cool, eco, or off.
StatelessTemplate CommandAction Used to indicate a control that provides touch capability but whose state cannot be determined, such as an IR television remote. You can use this template to define a routine or macro, which is an aggregation of control and state changes.

With this information, you can now create the control:

Create publishers for the controls

After the control is created, it needs a publisher. The publisher informs the system UI of the existence of the control. The ControlsProviderService class has two publisher methods that you must override in your application code:

  • createPublisherForAllAvailable(): Creates a Publisher for all of the controls available in your app. Use Control.StatelessBuilder() to build Controls for this publisher.
  • createPublisherFor(): Creates a Publisher for a list of given controls, as identified by their string identifiers. Use Control.StatefulBuilder to build these Controls since the publisher must assign a state to each control.

Create the publisher

When your app first publishes controls to the system UI, the app does not know the state of each control. Getting the state could be a time-consuming operation involving many hops in the device-provider’s network. Use the createPublisherForAllAvailable() method to advertise the available controls to the system. Note that this method uses the Control.StatelessBuilder builder class since the state of each control is unknown.

Once the controls appear in the Android UI , the user can select controls (that is, pick favorites) of interest.

Kotlin

/* If you choose to use Reactive Streams API, you will need to put the following
 * into your module's build.gradle file:
 * implementation 'org.reactivestreams:reactive-streams:1.0.3'
 * implementation 'io.reactivex.rxjava2:rxjava:2.2.0'
 */
class MyCustomControlService : ControlsProviderService() {

    override fun createPublisherForAllAvailable(): Flow.Publisher {
        val context: Context = baseContext
        val i = Intent()
        val pi =
            PendingIntent.getActivity(
                context, CONTROL_REQUEST_CODE, i,
                PendingIntent.FLAG_UPDATE_CURRENT
            )
        val controls = mutableListOf()
        val control =
            Control.StatelessBuilder(MY-UNIQUE-DEVICE-ID, pi)
                // Required: The name of the control
                .setTitle(MY-CONTROL-TITLE)
                // Required: Usually the room where the control is located
                .setSubtitle(MY-CONTROL-SUBTITLE)
                // Optional: Structure where the control is located, an example would be a house
                .setStructure(MY-CONTROL-STRUCTURE)
                // Required: Type of device, i.e., thermostat, light, switch
                .setDeviceType(DeviceTypes.DEVICE-TYPE) // For example, DeviceTypes.TYPE_THERMOSTAT
                .build()
        controls.add(control)
        // Create more controls here if needed and add it to the ArrayList

        // Uses the RxJava 2 library
        return FlowAdapters.toFlowPublisher(Flowable.fromIterable(controls))
    }
}

Java

/* If you choose to use Reactive Streams API, you will need to put the following
 * into your module's build.gradle file:
 * implementation 'org.reactivestreams:reactive-streams:1.0.3'
 * implementation 'io.reactivex.rxjava2:rxjava:2.2.0'
 */
public class MyCustomControlService extends ControlsProviderService {

    @Override
    public Publisher createPublisherForAllAvailable() {
        Context context = getBaseContext();
        Intent i = new Intent();
        PendingIntent pi = PendingIntent.getActivity(context, 1, i, PendingIntent.FLAG_UPDATE_CURRENT);
        List controls = new ArrayList<>();
        Control control = new Control.StatelessBuilder(MY-UNIQUE-DEVICE-ID, pi)
          // Required: The name of the control
          .setTitle(MY-CONTROL-TITLE)
          // Required: Usually the room where the control is located
          .setSubtitle(MY-CONTROL-SUBTITLE)
          // Optional: Structure where the control is located, an example would be a house
          .setStructure(MY-CONTROL-STRUCTURE)
          // Required: Type of device, i.e., thermostat, light, switch
          .setDeviceType(DeviceTypes.DEVICE-TYPE) // For example, DeviceTypes.TYPE_THERMOSTAT
          .build();
        controls.add(control);
        // Create more controls here if needed and add it to the ArrayList

        // Uses the RxJava 2 library
        return FlowAdapters.toFlowPublisher(Flowable.fromIterable(controls));
    }
}

Once the user has selected a set of controls, create a publisher for just those controls. Use the createPublisherFor() method since this method uses the Control.StatefulBuilder builder class, which supplies the current state of each control (for example, on or off).

Kotlin

class MyCustomControlService : ControlsProviderService() {
    private lateinit var updatePublisher: ReplayProcessor

    override fun createPublisherFor(controlIds: MutableList): Flow.Publisher {
        val context: Context = baseContext
        /* Fill in details for the activity related to this device. On long press,
         * this Intent will be launched in a bottomsheet. Please design the activity
         * accordingly to fit a more limited space (about 2/3 screen height).
         */
        val i = Intent(this, CustomSettingsActivity::class.java)
        val pi =
            PendingIntent.getActivity(context, CONTROL_REQUEST_CODE, i, PendingIntent.FLAG_UPDATE_CURRENT)
        updatePublisher = ReplayProcessor.create()

        if (controlIds.contains(MY-UNIQUE-DEVICE-ID)) {
            val control =
                Control.StatefulBuilder(MY-UNIQUE-DEVICE-ID, pi)
                    // Required: The name of the control
                    .setTitle(MY-CONTROL-TITLE)
                    // Required: Usually the room where the control is located
                    .setSubtitle(MY -CONTROL-SUBTITLE)
                    // Optional: Structure where the control is located, an example would be a house
                    .setStructure(MY-CONTROL-STRUCTURE)
                    // Required: Type of device, i.e., thermostat, light, switch
                    .setDeviceType(DeviceTypes.DEVICE-TYPE) // For example, DeviceTypes.TYPE_THERMOSTAT
                    // Required: Current status of the device
                    .setStatus(Control.CURRENT-STATUS) // For example, Control.STATUS_OK
                    .build()

            updatePublisher.onNext(control)
        }

        // If you have other controls, check that they have been selected here

        // Uses the Reactive Streams API
        updatePublisher.onNext(control)
    }
}

Java

private ReplayProcessor updatePublisher;

@Override
public Publisher createPublisherFor(List controlIds) {

    Context context = getBaseContext();
    /* Fill in details for the activity related to this device. On long press,
     * this Intent will be launched in a bottomsheet. Please design the activity
     * accordingly to fit a more limited space (about 2/3 screen height).
     */
    Intent i = new Intent();
    PendingIntent pi = PendingIntent.getActivity(context, 1, i, PendingIntent.FLAG_UPDATE_CURRENT);

    updatePublisher = ReplayProcessor.create();

    // For each controlId in controlIds


    if (controlIds.contains(MY-UNIQUE-DEVICE-ID)) {

      Control control = new Control.StatefulBuilder(MY-UNIQUE-DEVICE-ID, pi)
        // Required: The name of the control
        .setTitle(MY-CONTROL-TITLE)
        // Required: Usually the room where the control is located
        .setSubtitle(MY-CONTROL-SUBTITLE)
        // Optional: Structure where the control is located, an example would be a house
        .setStructure(MY-CONTROL-STRUCTURE)
        // Required: Type of device, i.e., thermostat, light, switch
        .setDeviceType(DeviceTypes.DEVICE-TYPE) // For example, DeviceTypes.TYPE_THERMOSTAT
        // Required: Current status of the device
        .setStatus(Control.CURRENT-STATUS) // For example, Control.STATUS_OK
        .build();

      updatePublisher.onNext(control);
    }
    // Uses the Reactive Streams API
    return FlowAdapters.toFlowPublisher(updatePublisher);
}

Handle actions

The performControlAction() method signals that the user has interacted with a published control. The action is dictated by the type of ControlAction that was sent. Perform the appropriate action for the given control and then update the state of the device in the Android UI.

Kotlin

class MyCustomControlService : ControlsProviderService() {

    override fun performControlAction(
        controlId: String, action: ControlAction, consumer: Consumer) {

        /* First, locate the control identified by the controlId. Once it is located, you can
         * interpret the action appropriately for that specific device. For instance, the following
         * assumes that the controlId is associated with a light, and the light can be turned on
         * or off.
         */
        if (action is BooleanAction) {

            // Inform SystemUI that the action has been received and is being processed
            consumer.accept(ControlAction.RESPONSE_OK)

            // In this example, action.getNewState() will have the requested action: true for “On”,
            // false for “Off”.

            /* This is where application logic/network requests would be invoked to update the state of
             * the device.
             * After updating, the application should use the publisher to update SystemUI with the new
             * state.
             */
            Control control = Control.StatefulBuilder (MY-UNIQUE-DEVICE-ID, pi)
            // Required: The name of the control
              .setTitle(MY-CONTROL-TITLE)
              // Required: Usually the room where the control is located
              .setSubtitle(MY-CONTROL-SUBTITLE)
              // Optional: Structure where the control is located, an example would be a house
              .setStructure(MY-CONTROL-STRUCTURE)
              // Required: Type of device, i.e., thermostat, light, switch
              .setDeviceType(DeviceTypes.DEVICE-TYPE) // For example, DeviceTypes.TYPE_THERMOSTAT
              // Required: Current status of the device
              .setStatus(Control.CURRENT-STATUS) // For example, Control.STATUS_OK
              .build()

            // This is the publisher the application created during the call to createPublisherFor()
            updatePublisher.onNext(control)
        }
    }
}

Java

@Override
public void performControlAction(String controlId, ControlAction action,
    Consumer consumer) {

  /* First, locate the control identified by the controlId. Once it is located, you can
   * interpret the action appropriately for that specific device. For instance, the following
   * assumes that the controlId is associated with a light, and the light can be turned on
   * or off.
   */
  if (action instanceof BooleanAction) {

    // Inform SystemUI that the action has been received and is being processed
    consumer.accept(ControlAction.RESPONSE_OK);

    BooleanAction action = (BooleanAction) action;
    // In this example, action.getNewState() will have the requested action: true for “On”,
    // false for “Off”.

    /* This is where application logic/network requests would be invoked to update the state of
     * the device.
     * After updating, the application should use the publisher to update SystemUI with the new
     * state.
     */
    Control control = new Control.StatefulBuilder(MY-UNIQUE-DEVICE-ID, pi)
      // Required: The name of the control
      .setTitle(MY-CONTROL-TITLE)
      // Required: Usually the room where the control is located
      .setSubtitle(MY-CONTROL-SUBTITLE)
      // Optional: Structure where the control is located, an example would be a house
      .setStructure(MY-CONTROL-STRUCTURE)
      // Required: Type of device, i.e., thermostat, light, switch
      .setDeviceType(DeviceTypes.DEVICE-TYPE) // For example, DeviceTypes.TYPE_THERMOSTAT
      // Required: Current status of the device
      .setStatus(Control.CURRENT-STATUS) // For example, Control.STATUS_OK
      .build();

    // This is the publisher the application created during the call to createPublisherFor()
    updatePublisher.onNext(control);
  }
}