Mengontrol perangkat eksternal

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

Gambar 1. Ruang kontrol perangkat di UI Android.

Untuk menambahkan dukungan ini, buat dan deklarasikan ControlsProviderService. Buat mengontrol yang didukung aplikasi berdasarkan jenis kontrol yang telah ditetapkan sebelumnya, lalu membuat penayang untuk kontrol ini.

Antarmuka pengguna

Perangkat ditampilkan di bagian Kontrol perangkat sebagai widget template. Lima widget kontrol perangkat tersedia, seperti yang ditunjukkan di gambar berikut:

Widget tombol alih
Tombol alih
Widget tombol alih dengan penggeser
Tombol alih dengan penggeser
Widget rentang
Rentang (tidak dapat diaktifkan atau dinonaktifkan)
Widget tombol alih stateless
Tombol alih stateless
Widget panel suhu (tertutup)
Panel suhu (tertutup)
Gambar 2. Kumpulan widget dengan template.

Menyentuh & menahan widget akan membawa Anda ke aplikasi untuk kontrol yang lebih mendalam. Anda dapat menyesuaikan ikon dan warna pada setiap widget, tetapi untuk pengalaman pengguna terbaik, gunakan ikon dan warna default jika kumpulan default cocok dengan perangkat.

Gambar yang menampilkan widget panel suhu (terbuka)
Gambar 3. Widget panel suhu terbuka.

Membuat layanan

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

ControlsProviderService API mengasumsikan bahwa Anda telah memahami aliran reaktif, yang ditentukan di Reactive Streams GitHub project dan diimplementasikan dalam Flow Java 9 antarmuka. API ini dibuat berdasarkan konsep berikut:

  • Penayang: aplikasi Anda adalah penerbit.
  • Pelanggan: UI sistem adalah pelanggan dan dapat meminta nomor kontrol dari penayang.
  • Langganan: jangka waktu penerbit dapat mengirim pembaruan ke UI Sistem. Penerbit atau pelanggan dapat menutupnya jendela.

Mendeklarasikan layanan

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

Layanan harus menyertakan filter intent untuk ControlsProviderService. Ini filter memungkinkan aplikasi memberikan kontrol ke UI sistem.

Anda juga memerlukan label yang ditampilkan di 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 perluas ControlsProviderService():

Kotlin

    class MyCustomControlService : ControlsProviderService() {
        ...
    }
    

Java

    public class MyCustomJavaControlService extends ControlsProviderService {
        ...
    }
    

Memilih jenis kontrol yang tepat

API menyediakan metode builder untuk membuat kontrol. Untuk mengisi builder, tentukan perangkat yang ingin dikontrol dan cara pengguna berinteraksi dengannya. 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 untuk perangkat di UI.
  2. Tentukan nama dan lokasi perangkat yang dilihat pengguna, misalnya, dapur—dan elemen teks UI lainnya yang terkait dengan kontrol.
  3. Pilih template terbaik untuk mendukung interaksi pengguna. Kontrol ditetapkan ke ControlTemplate dari aplikasi. Template ini secara langsung menunjukkan status kontrol kepada pengguna, beserta metode input yang tersedia—yaitu ControlAction. Tabel berikut menguraikan beberapa template yang tersedia dan tindakannya yang mereka dukung:
Template Tindakan Deskripsi
ControlTemplate.getNoTemplateObject() None Aplikasi mungkin menggunakannya 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 mewakili status baru yang diminta saat pengguna mengetuk kontrol.
RangeTemplate FloatAction Mewakili widget penggeser dengan nilai min, maks, dan langkah yang ditentukan. Kapan pengguna berinteraksi dengan penggeser, mengirim FloatAction baru kembali ke aplikasi dengan nilai yang diupdate.
ToggleRangeTemplate BooleanAction, FloatAction Template ini adalah kombinasi dari ToggleTemplate dan RangeTemplate. OpenVPN 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 memberikan kemampuan sentuh tetapi statusnya tidak dapat ditentukan, seperti pengaturan jarak jauh 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 hal berikut konstanta untuk MyCustomControlService Anda:

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;
 
    ...
    }
    

Membuat penayang untuk kontrol

Setelah dibuat, kontrol 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 membuat objek Control bagi penayang ini.
  • createPublisherFor(): membuat Publisher untuk daftar kontrol tertentu, seperti yang diidentifikasi oleh ID stringnya. Gunakan Control.StatefulBuilder untuk membangun objek Control ini, karena penayang harus menetapkan status ke tiap kontrol.

Membuat penayang

Saat pertama kali memublikasikan kontrol ke UI sistem, aplikasi tidak mengetahui status setiap kontrol. Mendapatkan status bisa menjadi operasi yang memakan waktu yang melibatkan banyak hop di jaringan penyedia perangkat. Gunakan metode createPublisherForAllAvailable() untuk memberi tahu sistem tentang kontrol yang tersedia. 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 Anda menyinkronkan file Gradle, tambahkan cuplikan berikut ke Service Anda untuk menerapkan 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<String, ReplayProcessor> 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);
        }
    }
    

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

Gambar yang menampilkan UI sistem untuk kontrol perangkat
Gambar 4. Kontrol perangkat di menu sistem.

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

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

Sekarang, terapkan metode createPublisherFor(), tambahkan kode berikut ke Service Anda:

Kotlin

    private val job = SupervisorJob()
    private val scope = CoroutineScope(Dispatchers.IO + job)
    private val controlFlows = mutableMapOf<String, MutableSharedFlow>()
 
    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();
    }
    

Dalam contoh ini, metode createPublisherFor() berisi objek palsu apa yang harus dilakukan aplikasi Anda: berkomunikasi dengan perangkat Anda untuk mengambil statusnya, dan memberikan status itu 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 sebentar lagi.
  5. Membuat dan memunculkan status termostat.

Menangani tindakan

Metode performControlAction() akan memberikan sinyal saat pengguna berinteraksi dengan kontrol yang dipublikasikan. Jenis ControlAction yang dikirim menentukan tindakan. Melakukan tindakan yang sesuai untuk kontrol yang diberikan, lalu mengupdate statusnya perangkat di UI Android.

Untuk melengkapi contoh, tambahkan kode berikut ke Service Anda:

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

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

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