Mengontrol perangkat eksternal

Di Android 11 dan yang lebih tinggi, fitur Kontrol Perangkat Akses Cepat memungkinkan pengguna melihat dan mengontrol perangkat eksternal dengan cepat seperti lampu, termostat, dan kamera dari affordance pengguna dalam tiga interaksi dari peluncur default. OEM perangkat memilih peluncur yang akan digunakan. Agregator perangkat—misalnya, Google Home—dan aplikasi vendor pihak ketiga dapat menyediakan perangkat untuk ditampilkan di ruang ini. Halaman ini menunjukkan cara menampilkan kontrol perangkat di ruang ini dan menautkannya ke aplikasi kontrol Anda.

Gambar 1. Ruang kontrol perangkat di UI Android.

Untuk menambahkan dukungan ini, buat dan deklarasikan ControlsProviderService. Buat kontrol yang didukung aplikasi Anda berdasarkan jenis kontrol yang telah ditentukan, lalu buat penerbit untuk kontrol ini.

Antarmuka pengguna

Perangkat ditampilkan di bagian Kontrol perangkat sebagai widget template. Tersedia lima widget kontrol perangkat, seperti yang ditunjukkan pada gambar berikut:

Mengaktifkan/menonaktifkan widget untuk kontrol perangkat
Aktifkan/nonaktifkan
Widget tombol alih dengan penggeser
Tombol dengan penggeser
Widget penggeser rentang untuk kontrol perangkat
Rentang (tidak dapat diaktifkan atau dinonaktifkan)
Widget kontrol tombol stateless
Tombol stateless
Widget panel suhu dalam status tertutup
Panel suhu (ditutup)
Gambar 2. Kumpulan widget dengan template.

Menyentuh lama widget akan mengarahkan Anda ke aplikasi untuk kontrol yang lebih mendalam. Anda dapat menyesuaikan ikon dan warna di setiap widget, tetapi untuk pengalaman pengguna terbaik, gunakan ikon dan warna default jika setelan default cocok dengan perangkat.

Widget panel suhu dalam status terbuka
Gambar 3. Buka widget panel suhu.

Membuat layanan

Bagian ini menunjukkan cara membuat ControlsProviderService. Layanan ini memberi tahu UI sistem Android bahwa aplikasi Anda berisi kontrol perangkat yang harus ditampilkan di area Kontrol perangkat pada UI Android.

ControlsProviderService API mengasumsikan pemahaman tentang reactive stream, seperti yang didefinisikan dalam project GitHub Reactive Streams dan diimplementasikan dalam antarmuka Java 9 Flow. API ini dibuat berdasarkan konsep berikut:

  • Penayang: aplikasi Anda adalah penayang.
  • Pelanggan: UI sistem adalah pelanggan dan dapat meminta sejumlah kontrol dari penayang.
  • Langganan: jangka waktu saat penayang dapat mengirim update ke UI Sistem. Penayang atau pelanggan dapat menutup jendela ini.

Mendeklarasikan layanan

Aplikasi Anda harus mendeklarasikan layanan—seperti MyCustomControlService—dalam manifes aplikasinya.

Layanan harus menyertakan filter intent untuk ControlsProviderService. Filter ini memungkinkan aplikasi menyumbangkan kontrol ke UI sistem.

Anda juga memerlukan label yang ditampilkan dalam kontrol di UI sistem.

Contoh berikut menunjukkan cara mendeklarasikan layanan:

<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>

Selanjutnya, buat file Kotlin baru bernama MyCustomControlService.kt dan buat file tersebut memperluas ControlsProviderService:

class MyCustomControlService : ControlsProviderService() {
    // ...
}

Memilih jenis kontrol yang tepat

API menyediakan metode builder untuk membuat kontrol. Untuk mengisi builder, tentukan perangkat yang ingin Anda kontrol dan cara pengguna berinteraksi dengan perangkat tersebut. Lakukan langkah-langkah berikut:

  1. Pilih jenis perangkat yang diwakili oleh kontrol. Class DeviceTypes adalah enumerasi dari semua perangkat yang didukung. Jenis ini digunakan untuk menentukan ikon dan warna perangkat di UI.
  2. Tentukan nama yang ditampilkan kepada pengguna, lokasi perangkat—misalnya, dapur—dan elemen tekstual UI lainnya yang terkait dengan kontrol.
  3. Pilih template terbaik untuk mendukung interaksi pengguna. Kontrol diberi ControlTemplate dari aplikasi. Template ini langsung menampilkan status kontrol kepada pengguna serta metode input yang tersedia—yaitu, ControlAction. Tabel berikut menguraikan beberapa template yang tersedia dan tindakan yang didukung:
Template Tindakan Deskripsi
ControlTemplate.getNoTemplateObject() None Aplikasi mungkin menggunakan ini untuk menyampaikan informasi tentang kontrol, tetapi pengguna tidak dapat berinteraksi dengannya.
ToggleTemplate BooleanAction Mewakili kontrol yang statusnya dapat dialihkan antara aktif dan nonaktif. Objek BooleanAction berisi kolom yang berubah untuk merepresentasikan status baru yang diminta saat pengguna mengetuk kontrol.
RangeTemplate FloatAction Mewakili widget penggeser dengan nilai minimum, maksimum, dan langkah yang ditentukan. Saat pengguna berinteraksi dengan penggeser, kirim objek FloatAction baru kembali ke aplikasi dengan nilai yang diperbarui.
ToggleRangeTemplate BooleanAction, FloatAction Template ini adalah kombinasi dari ToggleTemplate dan RangeTemplate. Mendukung peristiwa sentuh serta penggeser, seperti untuk mengontrol lampu yang dapat diredupkan.
TemperatureControlTemplate ModeAction, BooleanAction, FloatAction Selain mengenkapsulasi tindakan sebelumnya, template ini memungkinkan pengguna menyetel mode, seperti hangat, sejuk, hangat/sejuk, hemat energi, atau nonaktif.
StatelessTemplate CommandAction Digunakan untuk menunjukkan kontrol yang menyediakan kemampuan sentuh, tetapi statusnya tidak dapat ditentukan, seperti remote televisi IR. Anda dapat menggunakan template ini untuk menentukan rutinitas atau makro, yang merupakan agregasi perubahan status dan kontrol.

Dengan informasi ini, Anda dapat membuat kontrol:

Misalnya, untuk mengontrol bohlam smart dan termostat, tambahkan konstanta berikut ke MyCustomControlService Anda:

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

Membuat penayang untuk kontrol

Setelah Anda membuat kontrol, kontrol tersebut memerlukan penayang. Penayang memberi tahu UI sistem tentang keberadaan kontrol. Class ControlsProviderService memiliki dua metode penayang yang harus Anda ganti dalam kode aplikasi:

  • createPublisherForAllAvailable: membuat Publisher untuk semua kontrol yang tersedia di aplikasi Anda. Gunakan Control.StatelessBuilder untuk mem-build objek Control untuk penayang ini.
  • createPublisherFor: membuat Publisher untuk daftar kontrol tertentu, sebagaimana diidentifikasi oleh ID string-nya. Gunakan Control.StatefulBuilder untuk membuat objek Control ini, karena penayang harus menetapkan status ke setiap kontrol.

Membuat penayang

Saat aplikasi Anda pertama kali memublikasikan kontrol ke UI sistem, aplikasi tidak mengetahui status setiap kontrol. Mendapatkan status dapat menjadi operasi yang memakan waktu, yang melibatkan banyak hop di jaringan penyedia perangkat. Gunakan metode createPublisherForAllAvailable untuk memberitahukan kontrol yang tersedia ke sistem. Metode ini menggunakan class builder Control.StatelessBuilder, karena status setiap kontrol tidak diketahui.

Setelah kontrol muncul di UI Android, pengguna dapat memilih kontrol favorit.

Untuk menggunakan coroutine Kotlin guna membuat ControlsProviderService, tambahkan dependensi baru ke 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")
}

Setelah menyinkronkan file Gradle, tambahkan cuplikan berikut ke Service untuk mengimplementasikan 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()
    }
}

Geser ke bawah menu sistem dan temukan tombol Kontrol perangkat, yang ditampilkan pada gambar 4:

Antarmuka pengguna sistem untuk kontrol perangkat
Gambar 4. Kontrol perangkat di menu sistem.

Mengetuk Kontrol perangkat akan mengarahkan ke layar kedua tempat Anda dapat memilih aplikasi. Setelah memilih aplikasi, Anda akan melihat cara cuplikan sebelumnya membuat menu sistem kustom yang menampilkan kontrol baru, seperti yang ditunjukkan pada gambar 5:

Menu sistem yang menampilkan kontrol lampu dan termostat
Gambar 5. Kontrol lampu dan termostat yang akan ditambahkan.

Sekarang, terapkan metode createPublisherFor, dengan menambahkan kode berikut ke 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()
}

Dalam contoh ini, metode createPublisherFor berisi implementasi palsu dari hal yang harus dilakukan aplikasi Anda: berkomunikasi dengan perangkat untuk mengambil statusnya, dan memunculkan status tersebut ke sistem.

Metode createPublisherFor menggunakan coroutine dan flow Kotlin untuk memenuhi Reactive Streams API yang diperlukan dengan melakukan hal berikut:

  1. Membuat Flow.
  2. Menunggu selama satu detik.
  3. Membuat dan memunculkan status lampu smart.
  4. Menunggu satu detik lagi.
  5. Membuat dan memunculkan status termostat.

Menangani tindakan

Metode performControlAction memberi sinyal saat pengguna berinteraksi dengan kontrol yang dipublikasikan. Jenis ControlAction yang dikirim menentukan tindakan. Lakukan tindakan yang sesuai untuk kontrol yang diberikan, lalu perbarui status perangkat di UI Android.

Untuk menyelesaikan contoh, tambahkan kode berikut ke Service Anda:

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)
}

Jalankan aplikasi, akses menu Kontrol perangkat, dan lihat kontrol lampu dan termostat Anda.

Kontrol yang menampilkan lampu dan termostat
Gambar 6. Kontrol lampu dan termostat.