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

Trong phiên bản 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à máy ảnh tuỳ khả năng chi trả của người dùng trong 3 lượt tương tác từ trình khởi chạy mặc định. Nhà sản xuất thiết bị gốc sẽ chọn trình chạy nào được sử dụng. Trang web tổng hợp thiết bị (chẳng hạn như Google Home) và ứ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 hướng dẫn bạn cách hiển thị các nút điều khiển thiết bị trong không gian này và liên kết các nút điều khiển đó với ứng dụng điều khiể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 hỗ trợ dựa trên chế độ điều khiển định sẵn, sau đó tạo kê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 cho các chế độ điều khiển thiết bị
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 thanh trượt phạm vi cho các chế độ điều khiển thiết bị
Phạm vi (không thể bật hoặc tắt)
Tiện ích điều khiển bật/tắt không trạng thái
Bật/tắt trạng thái
Tiện ích bảng nhiệt độ ở trạng thái đóng
Bảng nhiệt độ (đã đóng)
Hình 2. Bộ sưu tập các tiện ích theo mẫu.

Chạm và giữ một tiện ích để chuyển đến ứng dụng và 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 màu và biểu tượng mặc định nếu bộ mặc định khớp với thiết bị.

Tiện ích bảng nhiệt độ ở trạng thái mở
Hình 3. Mở tiện ích bảng nhiệt độ.

Tạo dịch vụ

Phần này cho biết cách tạo ControlsProviderService. Dịch vụ này 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 nút điều khiển thiết bị phải hiển thị trong khu vực Điều khiển thiết bị của giao diện người dùng Android.

ControlsProviderService API giả định là đã làm quen với luồng phản hồi, như được xác định ở Dự án GitHub của Reactive Stream và triển khai trong Giao diện Luồng của Java 9. API này được xây dựng dựa trên 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à họ có thể yêu cầu nhà xuất bản cung cấp một số chế độ điều khiển.
  • Gói thuê bao: khung thời gian mà nhà xuất bản có thể gửi thông tin cập nhật đến 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 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 ứ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 nút đ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à đặt tệp này mở rộng ControlsProviderService:

class MyCustomControlService : 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 dùng để xác định 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 dành cho người dùng, vị trí thiết bị (ví dụ: nhà bếp) và các phần tử văn bản khác trên giao diện người dùng liên quan đến tùy chọn đ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 thành phầ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, nghĩa là ControlAction. Bảng sau trình bày một số mẫu có sẵn và thao tác các mẫu đó hỗ trợ:
Mẫu Hành động Mô tả
ControlTemplate.getNoTemplateObject() None Ứng dụng có thể sử dụng thông tin này để truyền đạt biện pháp điều khiển nhưng người dùng không thể tương tác với nó.
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 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 tùy chọn điều khiển.
RangeTemplate FloatAction Biểu thị 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 kèm theo giá trị cập nhật.
ToggleRangeTemplate BooleanAction, FloatAction Mẫu này là kết hợp giữa ToggleTemplate và RangeTemplate. Tính năng này hỗ trợ các sự kiện chạm cũng như thanh trượt, ví dụ như để điều chỉnh độ sáng của đèn.
TemperatureControlTemplate ModeAction, BooleanAction, FloatAction Ngoài việc tổng hợp các thao tác trước đó, mẫu này còn cho phép người dùng đặ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 để chỉ ra một chức năng điều khiển có cảm ứng nhưng không thể xác định trạng thái của chức năng đó, chẳng hạn như TV hồng ngoại điều khiển từ xa. 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 thành phần điều khiển:

  • Sử dụng lớp trình tạo Control.StatelessBuilder khi không xác định được trạng thái của tuỳ 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:

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() {
    // ...
}

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

Sau khi được tạo, tùy chọn điều khiển này cần 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 Publisher cho tất cả các chế độ điều khiển có trong ứng dụng. Sử dụng Control.StatelessBuilder để tạo đối tượng Control cho nhà xuất bản này.
  • createPublisherFor: tạo Publisher cho danh sách các tùy chọn điều khiển nhất định, như được xác định theo giá trị nhận dạng chuỗi của các tùy chọn đó. Sử dụng Control.StatefulBuilder để tạo các đối tượng Control này, vì nhà xuất bản cần chỉ định một trạng thái cho mỗi tùy chọn điều khiển.

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

Khi ứng dụng phát hành chế độ điều khiển trên giao diện người dùng của hệ thống lần đầu, ứ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 biết 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 chế độ đ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ì không xác định được trạng thái của mỗi tùy chọn điều khiển.

Sau khi các chế độ đ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 chế độ điều khiển yêu thích.

Để sử dụng coroutine Kotlin nhằm tạo ControlsProviderService, hãy thêm một 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 bạn đồng bộ hoá các tệp Gradle, hãy thêm đoạn mã sau vào Service để triển khai createPublisherForAllAvailable:

class MyCustomControlService : ControlsProviderService() {

    override fun createPublisherForAllAvailable(): Flow.Publisher<Control> =
        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<String>): Flow.Publisher<Control> {
        TODO()
    }

    override fun performControlAction(
        controlId: String,
        action: ControlAction,
        consumer: Consumer<Int>,
    ) {
        TODO()
    }
}

Vuốt trình đơn hệ thống xuống và tìm nút Điều khiển thiết bị, như trong hình 4:

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

Khi nhấn vào Device controls (Điều khiển thiết bị), bạn sẽ chuyển đến màn hình thứ hai, nơi 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 trình đơn hệ thống tuỳ chỉnh cho thấy các chế độ điều khiển mới, như trong hình 5:

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

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

private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.IO + job)
private val controlFlows = mutableMapOf<String, MutableSharedFlow<Control>>()

private var toggleState = false
private var rangeState = 18f

override fun createPublisherFor(controlIds: List<String>): Flow.Publisher<Control> {
    val flow = MutableSharedFlow<Control>(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 <T> 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()
}

Trong ví dụ này, phương thức createPublisherFor chứa một phương thức triển khai giả về 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 của thiết bị và phát trạng thái đó cho hệ thống.

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

  1. Tạo Flow.
  2. Chờ một giây.
  3. Tạo và phát trạng thái của đèn thông minh.
  4. Chờ 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 cho biết thời điểm người dùng tương tác với một tùy chọn đ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ức năng điều khiển đã định, 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:

override fun performControlAction(
    controlId: String,
    action: ControlAction,
    consumer: Consumer<Int>,
) {
    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)
}

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

Các nút điều khiển hiển thị đèn và máy điều nhiệt
Hình 6. Điều khiển đèn và máy điều nhiệt.