Điều khiển các thiết bị bên ngoài

Trong Android 11 trở lên, tính năng Điều khiển thiết bị truy cập nhanh cho phép người dùng xem và điều khiển nhanh các thiết bị bên ngoài như đèn, bộ điều nhiệt và camera thông qua một khả năng tương tác của người dùng trong vòng 3 lượt tương tác trên một trình chạy mặc định. Nhà sản xuất thiết bị gốc sẽ chọn trình chạy họ sử dụng. Bộ tổng hợp thiết bị (ví dụ: Google Home) và các ứng dụng của nhà cung cấp bên thứ ba có thể cung cấp thiết bị để hiển thị trong không gian này. Trang này cho bạn biết cách hiển thị các chế độ điều khiển thiết bị trong không gian này và liên kết các chế độ đó với ứng dụng điều khiển của bạn.

Hình 1. Không gian điều khiển thiết bị trong giao diện người dùng Android.

Để thêm tính năng hỗ trợ này, hãy tạo và khai báo ControlsProviderService. Tạo các chế độ điều khiển mà ứng dụng của bạn hỗ trợ dựa trên các loại chế độ điều khiển định sẵn, sau đó tạo nhà xuất bản cho các chế độ điều khiển này.

Giao diện người dùng

Các thiết bị hiển thị trong phần điều khiển thiết bị dưới dạng tiện ích mẫu. Có 5 tiện ích điều khiển thiết bị, như trong hình sau:

Bật/tắt tiện ích
Bật/tắt
Bật/tắt bằng tiện ích thanh trượt
Bật/tắt bằng thanh trượt
Tiện ích phạm vi
Phạm vi (không thể bật/tắt)
Bật/tắt tiện ích không trạng thái
Bật/tắt trạng thái
Tiện ích bảng nhiệt độ (đã đóng)
Bảng nhiệt độ (đã đóng)
Hình 2. Tập hợp các tiện ích theo mẫu.

Thao tác chạm và giữ một tiện ích sẽ đưa bạn đến ứng dụng để kiểm soát sâu hơn. Bạn có thể tuỳ chỉnh biểu tượng và màu sắc trên từng tiện ích, nhưng để có trải nghiệm người dùng tốt nhất, hãy sử dụng biểu tượng và màu mặc định nếu bộ mặc định khớp với thiết bị.

Hình ảnh hiển thị tiện ích bảng nhiệt độ (mở)
Hình 3. Mở tiện ích bảng nhiệt độ.

Tạo dịch vụ

Phần này trình bày cách tạo ControlsProviderService. Dịch vụ này thông báo cho giao diện người dùng hệ thống Android biết ứng dụng của bạn chứa các chế độ điều khiển thiết bị phải hiển thị trong phần Device controls (Điều khiển thiết bị) trên giao diện người dùng Android.

API ControlsProviderService giả định rằng bạn đã quen thuộc với các luồng phản ứng, như định nghĩa trong dự án GitHub về Luồng phản ứng và được triển khai trong giao diện Luồng phản ứng Java 9. API được xây dựng xoay quanh các khái niệm sau:

  • Nhà xuất bản: ứng dụng của bạn là nhà xuất bản.
  • Người đăng ký: giao diện người dùng hệ thống là người đăng ký và có thể yêu cầu nhà xuất bản cung cấp một số chế độ kiểm soát.
  • Gói thuê bao: khung thời gian mà nhà xuất bản có thể gửi các bản cập nhật cho Giao diện người dùng hệ thống. Nhà xuất bản hoặc người đăng ký có thể đóng cửa sổ này.

Khai báo dịch vụ

Ứng dụng của bạn phải khai báo một dịch vụ (chẳng hạn như MyCustomControlService) trong tệp kê khai ứng dụng.

Dịch vụ phải có một bộ lọc ý định cho ControlsProviderService. Bộ lọc này cho phép các ứng dụng đóng góp quyền điều khiển cho giao diện người dùng hệ thống.

Bạn cũng cần có một label hiển thị trong các chế độ điều khiển trên giao diện người dùng hệ thống.

Ví dụ sau đây cho thấy cách khai báo một dịch vụ:

<service
    android:name="MyCustomControlService"
    android:label="My Custom Controls"
    android:permission="android.permission.BIND_CONTROLS"
    android:exported="true"
    >
    <intent-filter>
      <action android:name="android.service.controls.ControlsProviderService" />
    </intent-filter>
</service>

Tiếp theo, hãy tạo một tệp Kotlin mới có tên là MyCustomControlService.kt và làm cho tệp đó mở rộng ControlsProviderService():

Kotlin

    class MyCustomControlService : ControlsProviderService() {
        ...
    }
    

Java

    public class MyCustomJavaControlService extends ControlsProviderService {
        ...
    }
    

Chọn loại điều khiển chính xác

API này cung cấp phương thức trình tạo để tạo các tùy chọn điều khiển. Để điền trình tạo, hãy xác định thiết bị bạn muốn điều khiển và cách người dùng tương tác với thiết bị đó. Thực hiện các bước sau đây:

  1. Chọn loại thiết bị mà tùy chọn điều khiển đại diện. Lớp DeviceTypes là bảng liệt kê tất cả các thiết bị được hỗ trợ. Loại này được dùng để xác định các biểu tượng và màu sắc cho thiết bị trong giao diện người dùng.
  2. Xác định tên mà người dùng nhìn thấy, vị trí thiết bị (ví dụ: nhà bếp) và các thành phần văn bản khác trên giao diện người dùng liên kết với chế độ điều khiển này.
  3. Chọn mẫu tốt nhất để hỗ trợ tương tác cho người dùng. Các tùy chọn điều khiển đã gán ControlTemplate từ ứng dụng. Mẫu này trực tiếp hiển thị trạng thái điều khiển cho người dùng cũng như các phương thức nhập có sẵn, tức là ControlAction. Bảng sau đây trình bày một số mẫu có sẵn và thao tác mà các mẫu đó hỗ trợ:
Template Hành động Mô tả
ControlTemplate.getNoTemplateObject() None Ứng dụng có thể sử dụng thông tin này để truyền tải thông tin về chế độ điều khiển nhưng người dùng không thể tương tác với chế độ đó.
ToggleTemplate BooleanAction Biểu thị một nút điều khiển có thể chuyển đổi giữa trạng thái đang bật và đã tắt. Đối tượng BooleanAction chứa một trường thay đổi để biểu thị trạng thái mới được yêu cầu khi người dùng nhấn vào chế độ điều khiển.
RangeTemplate FloatAction Đại diện cho một tiện ích thanh trượt có các giá trị tối thiểu, tối đa và bước được chỉ định. Khi người dùng tương tác với thanh trượt, hãy gửi lại đối tượng FloatAction mới cho ứng dụng cùng với giá trị đã cập nhật.
ToggleRangeTemplate BooleanAction, FloatAction Mẫu này là kết hợp giữa ToggleTemplateRangeTemplate. Tính năng này hỗ trợ các sự kiện chạm cũng như thanh trượt, chẳng hạn như để điều khiển các đèn có thể điều chỉnh độ sáng.
TemperatureControlTemplate ModeAction, BooleanAction, FloatAction Ngoài việc đóng gói các thao tác trước đó, mẫu này còn cho phép người dùng đặt một chế độ như sưởi ấm, làm mát, sưởi ấm/làm mát, tiết kiệm năng lượng hoặc tắt.
StatelessTemplate CommandAction Dùng để cho biết một chức năng điều khiển có khả năng cảm ứng nhưng không xác định được trạng thái của chức năng này, chẳng hạn như điều khiển từ xa của TV hồng ngoại. Bạn có thể sử dụng mẫu này để xác định một quy trình hoặc macro, tổng hợp các tùy chọn điều khiển và thay đổi trạng thái.

Với thông tin này, bạn có thể tạo chế độ kiểm soát:

  • Sử dụng lớp trình tạo Control.StatelessBuilder khi không xác định được trạng thái của tùy chọn điều khiển này.
  • Sử dụng lớp trình tạo Control.StatefulBuilder khi xác định được trạng thái của tùy chọn điều khiển này.

Ví dụ: để điều khiển bóng đèn thông minh và máy điều nhiệt, hãy thêm các hằng số sau vào MyCustomControlService:

Kotlin

    private const val LIGHT_ID = 1234
    private const val LIGHT_TITLE = "My fancy light"
    private const val LIGHT_TYPE = DeviceTypes.TYPE_LIGHT
    private const val THERMOSTAT_ID = 5678
    private const val THERMOSTAT_TITLE = "My fancy thermostat"
    private const val THERMOSTAT_TYPE = DeviceTypes.TYPE_THERMOSTAT
 
    class MyCustomControlService : ControlsProviderService() {
      ...
    }
    

Java

    public class MyCustomJavaControlService extends ControlsProviderService {
 
    private final int LIGHT_ID = 1337;
    private final String LIGHT_TITLE = "My fancy light";
    private final int LIGHT_TYPE = DeviceTypes.TYPE_LIGHT;
    private final int THERMOSTAT_ID = 1338;
    private final String THERMOSTAT_TITLE = "My fancy thermostat";
    private final int THERMOSTAT_TYPE = DeviceTypes.TYPE_THERMOSTAT;
 
    ...
    }
    

Tạo trình xuất bản cho các chế độ kiểm soát

Sau khi tạo chế độ kiểm soát, bạn cần có một nhà xuất bản. Nhà xuất bản sẽ thông báo cho giao diện người dùng hệ thống về tính khả dụng của tùy chọn điều khiển. Lớp ControlsProviderServicec ó hai phương thức mà nhà xuất bản phải viết hàm đè trong mã ứng dụng:

  • createPublisherForAllAvailable(): tạo một Publisher cho tất cả các chế độ điều khiển có trong ứng dụng của bạn. Sử dụng Control.StatelessBuilder() để tạo các đối tượng Control cho nhà xuất bản này.
  • createPublisherFor(): tạo một Publisher cho một danh sách các chế độ điều khiển nhất định, như được xác định bằng giá trị nhận dạng chuỗi của các chế độ đó. Sử dụng Control.StatefulBuilder để tạo các đối tượng Control này, vì nhà xuất bản phải chỉ định một trạng thái cho từng chế độ điều khiển.

Tạo trình xuất bản

Trong lần đầu phát hành các chế độ điều khiển lên giao diện người dùng hệ thống, ứng dụng sẽ không biết trạng thái của từng chế độ điều khiển. Việc nhận trạng thái có thể là một hoạt động tốn thời gian liên quan đến nhiều bước trong mạng của nhà cung cấp thiết bị. Sử dụng phương thức createPublisherForAllAvailable() để quảng cáo các tùy chọn điều khiển có sẵn cho hệ thống. Phương thức này sử dụng lớp trình tạo Control.StatelessBuilder, vì trạng thái của mỗi chế độ điều khiển là không xác định.

Sau khi các nút điều khiển xuất hiện trong giao diện người dùng Android , người dùng có thể chọn các nút điều khiển yêu thích.

Để sử dụng coroutine của Kotlin nhằm tạo ControlsProviderService, hãy thêm phần phụ thuộc mới vào build.gradle:

Groovy

dependencies {
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk9:1.6.4"
}

Kotlin

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk9:1.6.4")
}

Sau khi đồng bộ hoá các tệp Gradle, hãy thêm đoạn mã sau vào Service để triển khai createPublisherForAllAvailable():

Kotlin

    class MyCustomControlService : ControlsProviderService() {
 
      override fun createPublisherForAllAvailable(): Flow.Publisher =
          flowPublish {
              send(createStatelessControl(LIGHT_ID, LIGHT_TITLE, LIGHT_TYPE))
              send(createStatelessControl(THERMOSTAT_ID, THERMOSTAT_TITLE, THERMOSTAT_TYPE))
          }
 
      private fun createStatelessControl(id: Int, title: String, type: Int): Control {
          val intent = Intent(this, MainActivity::class.java)
              .putExtra(EXTRA_MESSAGE, title)
              .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
          val action = PendingIntent.getActivity(
              this,
              id,
              intent,
              PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
          )
 
          return Control.StatelessBuilder(id.toString(), action)
              .setTitle(title)
              .setDeviceType(type)
              .build()
      }
 
          override fun createPublisherFor(controlIds: List): Flow.Publisher {
           TODO()
        }
 
        override fun performControlAction(
            controlId: String,
            action: ControlAction,
            consumer: Consumer
        ) {
            TODO()
        }
    }
    

Java

    public class MyCustomJavaControlService extends ControlsProviderService {
 
        private final int LIGHT_ID = 1337;
        private final String LIGHT_TITLE = "My fancy light";
        private final int LIGHT_TYPE = DeviceTypes.TYPE_LIGHT;
        private final int THERMOSTAT_ID = 1338;
        private final String THERMOSTAT_TITLE = "My fancy thermostat";
        private final int THERMOSTAT_TYPE = DeviceTypes.TYPE_THERMOSTAT;
 
        private boolean toggleState = false;
        private float rangeState = 18f;
        private final Map> controlFlows = new HashMap<>();
 
        @NonNull
        @Override
        public Flow.Publisher createPublisherForAllAvailable() {
            List controls = new ArrayList<>();
            controls.add(createStatelessControl(LIGHT_ID, LIGHT_TITLE, LIGHT_TYPE));
            controls.add(createStatelessControl(THERMOSTAT_ID, THERMOSTAT_TITLE, THERMOSTAT_TYPE));
            return FlowAdapters.toFlowPublisher(Flowable.fromIterable(controls));
        }
 
        @NonNull
        @Override
        public Flow.Publisher createPublisherFor(@NonNull List controlIds) {
            ReplayProcessor updatePublisher = ReplayProcessor.create();
 
            controlIds.forEach(control -> {
                controlFlows.put(control, updatePublisher);
                updatePublisher.onNext(createLight());
                updatePublisher.onNext(createThermostat());
            });
 
            return FlowAdapters.toFlowPublisher(updatePublisher);
        }
    }
    

Vuốt trình đơn hệ thống xuống rồi tìm nút Device controls (Điều khiển thiết bị), như trong hình 4:

Hình ảnh cho thấy giao diện người dùng hệ thống để điều khiển thiết bị
Hình 4. Các chế độ điều khiển thiết bị trong trình đơn hệ thống.

Thao tác nhấn vào Device controls (Điều khiển thiết bị) sẽ chuyển đến màn hình thứ hai để bạn có thể chọn ứng dụng của mình. Sau khi chọn ứng dụng, bạn sẽ thấy cách đoạn mã trước tạo một trình đơn hệ thống tuỳ chỉnh hiển thị các chế độ điều khiển mới, như minh hoạ trong hình 5:

Hình ảnh cho thấy trình đơn hệ thống chứa bộ điều khiển đèn và máy điều nhiệt
Hình 5. Cần thêm các chế độ điều khiển ánh sáng và máy điều nhiệt.

Bây giờ, hãy triển khai phương thức createPublisherFor(), thêm nội dung sau đây vào Service:

Kotlin

    private val job = SupervisorJob()
    private val scope = CoroutineScope(Dispatchers.IO + job)
    private val controlFlows = mutableMapOf>()
 
    private var toggleState = false
    private var rangeState = 18f
 
    override fun createPublisherFor(controlIds: List): Flow.Publisher {
        val flow = MutableSharedFlow(replay = 2, extraBufferCapacity = 2)
 
        controlIds.forEach { controlFlows[it] = flow }
 
        scope.launch {
            delay(1000) // Retrieving the toggle state.
            flow.tryEmit(createLight())
 
            delay(1000) // Retrieving the range state.
            flow.tryEmit(createThermostat())
 
        }
        return flow.asPublisher()
    }
 
    private fun createLight() = createStatefulControl(
        LIGHT_ID,
        LIGHT_TITLE,
        LIGHT_TYPE,
        toggleState,
        ToggleTemplate(
            LIGHT_ID.toString(),
            ControlButton(
                toggleState,
                toggleState.toString().uppercase(Locale.getDefault())
            )
        )
    )
 
    private fun createThermostat() = createStatefulControl(
        THERMOSTAT_ID,
        THERMOSTAT_TITLE,
        THERMOSTAT_TYPE,
        rangeState,
        RangeTemplate(
            THERMOSTAT_ID.toString(),
            15f,
            25f,
            rangeState,
            0.1f,
            "%1.1f"
        )
    )
 
    private fun  createStatefulControl(id: Int, title: String, type: Int, state: T, template: ControlTemplate): Control {
        val intent = Intent(this, MainActivity::class.java)
            .putExtra(EXTRA_MESSAGE, "$title $state")
            .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        val action = PendingIntent.getActivity(
            this,
            id,
            intent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )
 
        return Control.StatefulBuilder(id.toString(), action)
            .setTitle(title)
            .setDeviceType(type)
            .setStatus(Control.STATUS_OK)
            .setControlTemplate(template)
            .build()
    }
 
    override fun onDestroy() {
        super.onDestroy()
        job.cancel()
    }
 
    

Java

    @NonNull
    @Override
    public Flow.Publisher createPublisherFor(@NonNull List controlIds) {
        ReplayProcessor updatePublisher = ReplayProcessor.create();
 
        controlIds.forEach(control -> {
            controlFlows.put(control, updatePublisher);
            updatePublisher.onNext(createLight());
            updatePublisher.onNext(createThermostat());
        });
 
        return FlowAdapters.toFlowPublisher(updatePublisher);
    }
 
    private Control createStatelessControl(int id, String title, int type) {
        Intent intent = new Intent(this, MainActivity.class)
                .putExtra(EXTRA_MESSAGE, title)
                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        PendingIntent action = PendingIntent.getActivity(
                this,
                id,
                intent,
                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
        );
 
        return new Control.StatelessBuilder(id + "", action)
                .setTitle(title)
                .setDeviceType(type)
                .build();
    }
 
    private Control createLight() {
        return createStatefulControl(
                LIGHT_ID,
                LIGHT_TITLE,
                LIGHT_TYPE,
                toggleState,
                new ToggleTemplate(
                        LIGHT_ID + "",
                        new ControlButton(
                                toggleState,
                                String.valueOf(toggleState).toUpperCase(Locale.getDefault())
                        )
                )
        );
    }
 
    private Control createThermostat() {
        return createStatefulControl(
                THERMOSTAT_ID,
                THERMOSTAT_TITLE,
                THERMOSTAT_TYPE,
                rangeState,
                new RangeTemplate(
                        THERMOSTAT_ID + "",
                        15f,
                        25f,
                        rangeState,
                        0.1f,
                        "%1.1f"
                )
        );
    }
 
    private  Control createStatefulControl(int id, String title, int type, T state, ControlTemplate template) {
        Intent intent = new Intent(this, MainActivity.class)
                .putExtra(EXTRA_MESSAGE, "$title $state")
                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        PendingIntent action = PendingIntent.getActivity(
                this,
                id,
                intent,
                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
        );
 
        return new Control.StatefulBuilder(id + "", action)
                .setTitle(title)
                .setDeviceType(type)
                .setStatus(Control.STATUS_OK)
                .setControlTemplate(template)
                .build();
    }
    

Trong ví dụ này, phương thức createPublisherFor() chứa một cách triển khai giả mạo những việc ứng dụng của bạn phải làm: giao tiếp với thiết bị để truy xuất trạng thái và phát trạng thái đó cho hệ thống.

Phương thức createPublisherFor() sử dụng các coroutine và luồng Kotlin để đáp ứng API Luồng phản ứng bắt buộc bằng cách thực hiện như sau:

  1. Tạo Flow.
  2. Đợi một giây.
  3. Tạo và phát ra trạng thái của đèn thông minh.
  4. Đợi thêm một giây.
  5. Tạo và phát trạng thái của máy điều nhiệt.

Xử lý thao tác

Phương thức performControlAction() báo hiệu khi người dùng tương tác với một chế độ điều khiển đã xuất bản. Loại ControlAction được gửi sẽ xác định hành động. Thực hiện thao tác thích hợp cho chế độ điều khiển đã cho, sau đó cập nhật trạng thái của thiết bị trong giao diện người dùng Android.

Để hoàn tất ví dụ, hãy thêm đoạn mã sau vào Service:

Kotlin

    override fun performControlAction(
        controlId: String,
        action: ControlAction,
        consumer: Consumer
    ) {
        controlFlows[controlId]?.let { flow ->
            when (controlId) {
                LIGHT_ID.toString() -> {
                    consumer.accept(ControlAction.RESPONSE_OK)
                    if (action is BooleanAction) toggleState = action.newState
                    flow.tryEmit(createLight())
                }
                THERMOSTAT_ID.toString() -> {
                    consumer.accept(ControlAction.RESPONSE_OK)
                    if (action is FloatAction) rangeState = action.newValue
                    flow.tryEmit(createThermostat())
                }
                else -> consumer.accept(ControlAction.RESPONSE_FAIL)
            }
        } ?: consumer.accept(ControlAction.RESPONSE_FAIL)
    }
    

Java

    @Override
    public void performControlAction(@NonNull String controlId, @NonNull ControlAction action, @NonNull Consumer consumer) {
        ReplayProcessor processor = controlFlows.get(controlId);
        if (processor == null) return;
 
        if (controlId.equals(LIGHT_ID + "")) {
            consumer.accept(ControlAction.RESPONSE_OK);
            if (action instanceof BooleanAction) toggleState = ((BooleanAction) action).getNewState();
            processor.onNext(createLight());
        }
        if (controlId.equals(THERMOSTAT_ID + "")) {
            consumer.accept(ControlAction.RESPONSE_OK);
            if (action instanceof FloatAction) rangeState = ((FloatAction) action).getNewValue()
            processor.onNext(createThermostat());
        }
    }
    

Chạy ứng dụng, truy cập vào trình đơn Device controls (Điều khiển thiết bị) và xem các chế độ điều khiển ánh sáng cũng như máy điều nhiệt.

Hình ảnh một đèn và bộ điều khiển máy điều nhiệt
Hình 6. Bộ điều khiển đèn và máy điều nhiệt.