Android 11 이상에서는 사용자가 Android 전원 메뉴에서 빠른 액세스 기기 제어 기능을 사용하여 조명, 온도 조절기, 카메라와 같은 외부 기기를 신속하게 확인하고 제어할 수 있습니다. 기기 애그리게이터(예: Google Home) 및 타사 공급업체 앱은 이 공간에 표시되는 기기를 제공할 수 있습니다. 이 가이드에서는 이 공간에 기기 컨트롤을 표시하고 제어 앱에 연결하는 방법을 설명합니다.
이 지원을 추가하려면 ControlsProviderService
를 만들고 선언하고 사전 정의된 컨트롤 유형에 따라 앱에서 지원하는 컨트롤을 만든 다음 이러한 컨트롤의 게시자를 만듭니다.
사용자 인터페이스
기기는 기기 제어 아래에 템플릿 위젯으로 표시됩니다. 5가지 기기 제어 위젯을 사용할 수 있습니다.
![]() |
![]() |
![]() |
![]() |
![]() |

위젯을 길게 누르면 앱을 더 세부적으로 제어할 수 있습니다. 각 위젯의 아이콘과 색상을 맞춤설정할 수 있지만 최적의 사용자 환경을 위해서는 기본 설정이 기기와 일치하지 않는 경우를 제외하고 기본 아이콘과 색상을 사용해야 합니다.
서비스 만들기
이 섹션에서는 ControlsProviderService
를 만드는 방법을 보여줍니다.
이 서비스는 앱에 Android UI의 기기 제어 영역에 표시되어야 하는 기기 제어가 포함되어 있다고 Android 시스템 UI에 알려줍니다.
ControlsProviderService
API는 반응형 스트림 GitHub 프로젝트에서 정의되고 자바 9 흐름 인터페이스에서 구현된 대로 반응형 스트림에 익숙하다고 가정합니다.
이 API는 다음과 같은 개념에 기반합니다.
- 게시자: 애플리케이션이 게시자입니다.
- 구독자: 시스템 UI가 구독자이며 게시자에게 여러 컨트롤을 요청할 수 있습니다.
- 구독: 게시자가 시스템 UI에 업데이트를 전송할 수 있는 기간입니다. 이 기간은 게시자나 구독자가 종료할 수 있습니다.
서비스 선언
앱은 앱 매니페스트에서 서비스를 선언해야 합니다. BIND_CONTROLS
권한을 포함해야 합니다.
서비스에는 ControlsProviderService
의 인텐트 필터가 포함되어야 합니다. 이 필터를 사용하면 애플리케이션이 시스템 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>
올바른 컨트롤 유형 선택
이 API는 컨트롤을 만드는 빌더 메서드를 제공합니다. 빌더를 채우려면 제어하려는 기기와 사용자가 기기와 상호작용하는 방법을 결정해야 합니다. 특히 다음을 실행해야 합니다.
- 컨트롤이 나타내는 기기 유형을 선택합니다.
DeviceTypes
클래스는 현재 지원되는 모든 기기를 열거합니다. 유형은 UI에서 기기의 아이콘과 색상을 결정하는 데 사용됩니다. - 사용자에게 표시되는 이름, 기기 위치(예: 주방) 및 컨트롤과 연결된 기타 UI 텍스트 요소를 결정합니다.
- 사용자 상호작용을 지원하는 최적의 템플릿을 선택합니다. 컨트롤에는 애플리케이션의
ControlTemplate
이 할당됩니다. 이 템플릿은 컨트롤 상태와 사용할 수 있는 입력 방법(ControlAction
)을 사용자에게 직접 보여줍니다. 다음 표는 사용 가능한 템플릿과 템플릿에서 지원하는 작업을 간략히 설명합니다.
템플릿 | 작업 | 설명 |
ControlTemplate.getNoTemplateObject()
|
None
|
애플리케이션은 이를 사용하여 컨트롤에 관한 정보를 전달할 수 있지만 사용자는 컨트롤과 상호작용할 수 없습니다. |
ToggleTemplate
|
BooleanAction
|
사용 설정 및 사용 중지 상태 간에 전환할 수 있는 컨트롤을 나타냅니다. BooleanAction 객체에는 사용자가 컨트롤을 터치할 때 요청된 새 상태를 나타내기 위해 변경되는 필드가 포함되어 있습니다.
|
RangeTemplate
|
FloatAction
|
지정된 최솟값, 최댓값, 단계 값이 있는 슬라이더 위젯을 나타냅니다. 사용자가 슬라이더와 상호작용하면 새 FloatAction 객체를 업데이트된 값이 있는 애플리케이션으로 다시 전송해야 합니다.
|
ToggleRangeTemplate
|
BooleanAction, FloatAction
|
이 템플릿은 ToggleTemplate 과 RangeTemplate 의 조합으로, 터치 이벤트와 슬라이더(예: 밝기 조절이 가능한 조명의 제어)를 지원합니다.
|
TemperatureControlTemplate
|
ModeAction, BooleanAction, FloatAction
|
위 작업 중 하나를 캡슐화하는 것 외에도 이 템플릿으로 사용자는 모드를 설정할 수 있습니다(예: 난방, 냉방, 절전, 사용 중지). |
StatelessTemplate
|
CommandAction
|
터치 기능을 제공하지만 상태를 확인할 수 없는 컨트롤(예: IR TV 리모컨)을 나타내는 데 사용됩니다. 이 템플릿을 사용하여 컨트롤 및 상태 변경의 집계인 루틴 또는 매크로를 정의할 수 있습니다. |
이 정보로 이제 개발자는 컨트롤을 만들 수 있습니다.
- 컨트롤의 상태를 알 수 없는 경우
Control.StatelessBuilder
빌더 클래스를 사용합니다. - 컨트롤의 상태가 알려진 경우
Control.StatefulBuilder
빌더 클래스를 사용합니다.
컨트롤의 게시자 만들기
컨트롤을 만든 후에는 게시자가 있어야 합니다. 게시자는 시스템 UI에 컨트롤의 존재를 알립니다. ControlsProviderService
클래스에는 애플리케이션 코드에서 재정의해야 하는 두 가지 게시자 메서드가 있습니다.
createPublisherForAllAvailable()
: 앱에서 사용할 수 있는 모든 컨트롤의Publisher
를 만듭니다.Control.StatelessBuilder()
를 사용하여이 게시자의Controls
를 빌드합니다.createPublisherFor()
: 문자열 식별자로 식별된 대로 지정된 컨트롤 목록의Publisher
를 만듭니다. 게시자가 각 컨트롤에 상태를 할당해야 하므로Control.StatefulBuilder
를 사용하여 이러한Controls
를 빌드합니다.
게시자 만들기
앱이 시스템 UI에 처음으로 컨트롤을 게시하면 앱은 각 컨트롤의 상태를 알 수 없습니다. 상태 가져오기는 기기 제공업체 네트워크의 여러 홉이 관련되는 시간이 많이 걸리는 작업일 수 있습니다. createPublisherForAllAvailable()
메서드를 사용하여 사용 가능한 컨트롤을 시스템에 알립니다. 이 메서드는 Control.StatelessBuilder
빌더 클래스를 사용합니다. 각 컨트롤의 상태를 알 수 없기 때문입니다.
컨트롤이 Android UI에 표시되면 사용자는 관심 있는 컨트롤을 선택(즉, 즐겨찾기 선택)할 수 있습니다.
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)) } }
자바
/* 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)); } }
사용자가 일련의 컨트롤을 선택하고 나면 선택한 컨트롤의 게시자만 만듭니다. createPublisherFor()
메서드를 사용하세요. 이 메서드는 Control.StatefulBuilder
빌더 클래스를 사용하고 이 클래스는 각 컨트롤의 현재 상태(예: 사용 설정 또는 사용 중지)를 제공하기 때문입니다.
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) } }
자바
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); }
작업 처리
performControlAction()
메서드는 사용자가 게시된 컨트롤과 상호작용했음을 나타냅니다. 이 작업은 전송된 ControlAction
유형에 따라 결정됩니다. 지정된 컨트롤에 적절한 작업을 실행한 다음 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) } } }
자바
@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); } }