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.
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:
![]() |
![]() |
![]() |
![]() |
![]() |

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:
- 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. - Determine the user-facing name, device location (for example, kitchen), and other UI textual elements associated with the control.
- 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, theControlAction
). 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:
- Use the
Control.StatelessBuilder
builder class when the state of the control is unknown. - Use the
Control.StatefulBuilder
builder class when the state of the control is known.
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 aPublisher
for all of the controls available in your app. UseControl.StatelessBuilder()
to buildControls
for this publisher.createPublisherFor()
: Creates aPublisher
for a list of given controls, as identified by their string identifiers. UseControl.StatefulBuilder
to build theseControls
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 PublishercreatePublisherForAllAvailable() { 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: ReplayProcessoroverride 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 ReplayProcessorupdatePublisher; @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, Consumerconsumer) { /* 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); } }