외부 기기 제어

Android 11부터 사용할 수 있는 빠른 액세스 기기 제어 기능으로 사용자는 Android 전원 메뉴에서 조명, 온도 조절기, 카메라와 같은 외부 기기를 빠르게 확인하고 제어할 수 있습니다. 기기 애그리게이터(예: Google Home) 및 타사 공급업체 앱은 이 공간에 표시할 기기를 제공할 수 있습니다. 이 가이드에서는 컨트롤 앱에 기기 제어 지원을 추가하는 방법을 보여줍니다.

Android UI의 기기 제어 공간

이 지원을 추가하려면 ControlsProviderService를 만들고 선언하고 사전 정의된 컨트롤 유형에 따라 앱에서 지원하는 컨트롤을 만든 다음 이러한 컨트롤의 게시자를 만듭니다.

사용자 인터페이스

기기는 기기 제어 아래에 템플릿 위젯으로 표시됩니다. 5가지 기기 제어 위젯을 사용할 수 있습니다.

전환 위젯
전환
슬라이더로 전환 위젯
슬라이더로 전환
범위 위젯
범위(사용 설정 또는 사용 중지할 수 없음)
스테이트리스(Stateless) 전환 위젯
스테이트리스(Stateless) 전환
온도 패널 위젯(닫힘)
온도 패널(닫힘)
온도 패널 위젯(열림)
온도 패널(열림)

위젯을 길게 누르면 앱을 더 세부적으로 제어할 수 있습니다. 각 위젯의 아이콘과 색상을 맞춤설정할 수 있지만 최적의 사용자 환경을 위해서는 기본 설정이 기기와 일치하지 않는 경우를 제외하고 기본 아이콘과 색상을 사용해야 합니다.

서비스 만들기

이 섹션에서는 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는 컨트롤을 만드는 빌더 메서드를 제공합니다. 빌더를 채우려면 제어하려는 기기와 사용자가 기기와 상호작용하는 방법을 결정해야 합니다. 특히 다음을 실행해야 합니다.

  1. 컨트롤이 나타내는 기기 유형을 선택합니다. DeviceTypes 클래스는 현재 지원되는 모든 기기를 열거합니다. 유형은 UI에서 기기의 아이콘과 색상을 결정하는 데 사용됩니다.
  2. 사용자에게 표시되는 이름, 기기 위치(예: 주방) 및 컨트롤과 연결된 기타 UI 텍스트 요소를 결정합니다.
  3. 사용자 상호작용을 지원하는 최적의 템플릿을 선택합니다. 컨트롤에는 애플리케이션의 ControlTemplate이 할당됩니다. 이 템플릿은 컨트롤 상태와 사용할 수 있는 입력 방법(ControlAction)을 사용자에게 직접 보여줍니다. 다음 표는 사용 가능한 템플릿과 템플릿에서 지원하는 작업을 간략히 설명합니다.
템플릿 작업 설명
ControlTemplate.getNoTemplateObject() None 애플리케이션은 이를 사용하여 컨트롤에 관한 정보를 전달할 수 있지만 사용자는 컨트롤과 상호작용할 수 없습니다.
ToggleTemplate BooleanAction 사용 설정 및 사용 중지 상태 간에 전환할 수 있는 컨트롤을 나타냅니다. BooleanAction 객체에는 사용자가 컨트롤을 터치할 때 요청된 새 상태를 나타내기 위해 변경되는 필드가 포함되어 있습니다.
RangeTemplate FloatAction 지정된 최솟값, 최댓값, 단계 값이 있는 슬라이더 위젯을 나타냅니다. 사용자가 슬라이더와 상호작용하면 새 FloatAction 객체를 업데이트된 값이 있는 애플리케이션으로 다시 전송해야 합니다.
ToggleRangeTemplate BooleanAction, FloatAction 이 템플릿은 ToggleTemplateRangeTemplate의 조합으로, 터치 이벤트와 슬라이더(예: 밝기 조절이 가능한 조명의 제어)를 지원합니다.
TemperatureControlTemplate ModeAction, BooleanAction, FloatAction 위 작업 중 하나를 캡슐화하는 것 외에도 이 템플릿으로 사용자는 모드를 설정할 수 있습니다(예: 난방, 냉방, 절전, 사용 중지).
StatelessTemplate CommandAction 터치 기능을 제공하지만 상태를 확인할 수 없는 컨트롤(예: IR TV 리모컨)을 나타내는 데 사용됩니다. 이 템플릿을 사용하여 컨트롤 및 상태 변경의 집계인 루틴 또는 매크로를 정의할 수 있습니다.

이 정보로 이제 개발자는 컨트롤을 만들 수 있습니다.

컨트롤의 게시자 만들기

컨트롤을 만든 후에는 게시자가 있어야 합니다. 게시자는 시스템 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 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 = 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: 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)
    }
}

자바

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 = 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,
    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 = 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);
  }
}