Mengontrol perangkat eksternal

Di Android 11 dan yang lebih baru, fitur Kontrol Perangkat Akses Cepat memungkinkan pengguna melihat dan mengontrol perangkat eksternal dengan cepat, seperti lampu, termostat, dan kamera dari menu daya Android. Agregator perangkat (misalnya, Google Home) dan aplikasi vendor pihak ketiga dapat menyediakan perangkat untuk ditampilkan di ruang ini. Panduan ini menunjukkan cara memunculkan kontrol perangkat di ruang ini dan menautkannya ke aplikasi kontrol.

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 penayang untuk kontrol ini.

Antarmuka pengguna

Perangkat ditampilkan di bagian Kontrol perangkat sebagai widget template. Tersedia lima widget kontrol perangkat:

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)
Widget panel suhu (terbuka)
Widget panel suhu (terbuka)

Menekan lama widget akan mengarahkan Anda ke aplikasi untuk kontrol yang lebih lengkap. Anda dapat menyesuaikan ikon dan warna setiap widget, tetapi untuk pengalaman pengguna terbaik, Anda harus menggunakan ikon dan warna default, kecuali kumpulan default yang tersedia tidak cocok dengan perangkat.

Membuat layanan

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

ControlsProviderService API mengasumsikan pengetahuan tentang aliran reaktif, seperti yang dijelaskan dalam project Reactive Streams GitHub dan diimplementasikan dalam antarmuka Flow Java 9. API ini dibuat berdasarkan konsep berikut:

  • Penayang: Aplikasi Anda adalah penayang
  • Pelanggan: UI sistem adalah pelanggan dan dapat meminta sejumlah kontrol dari penayang
  • Langganan: Periode waktu saat penayang dapat mengirimkan update ke UI Sistem; periode ini dapat diakhiri oleh penayang atau pelanggan

Deklarasikan layanan

Aplikasi Anda harus mendeklarasikan layanan dalam manifes aplikasinya. Pastikan menyertakan izin BIND_CONTROLS.

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

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

Pilih jenis kontrol yang tepat

API menyediakan metode builder untuk membuat kontrol. Untuk mengisi builder, Anda perlu menentukan perangkat yang ingin dikontrol dan cara pengguna berinteraksi dengannya. Secara khusus, Anda perlu melakukan hal berikut:

  1. Pilih jenis perangkat yang diwakili oleh kontrol. Class DeviceTypes adalah enumerasi dari semua perangkat yang saat ini didukung. Jenis ini digunakan untuk menentukan ikon dan warna perangkat di UI.
  2. Tentukan nama yang akan dilihat pengguna, lokasi perangkat (dapur, misalnya), dan elemen teks UI lainnya yang diatribusikan 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 masukan yang tersedia (yaitu ControlAction). Tabel berikut menguraikan beberapa template yang tersedia dan tindakan yang didukungnya:
Template Tindakan Deskripsi
ControlTemplate.getNoTemplateObject() None Aplikasi dapat 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 menyentuh kontrol.
RangeTemplate FloatAction Mewakili widget penggeser dengan nilai minimal, maksimal, dan langkah yang ditetapkan. Saat pengguna berinteraksi dengan penggeser, objek FloatAction baru akan dikirim kembali ke aplikasi dengan nilai yang sudah diperbarui.
ToggleRangeTemplate BooleanAction, FloatAction Template ini adalah kombinasi dari ToggleTemplate dan RangeTemplate. Template ini mendukung peristiwa sentuh serta penggeser, seperti pada kontrol untuk lampu yang dapat diredupkan, misalnya.
TemperatureControlTemplate ModeAction, BooleanAction, FloatAction Selain mengenkapsulasi salah satu tindakan di atas, template ini memungkinkan pengguna menentukan mode, seperti hangat, dingin, hangat/dingin, hemat energi, atau nonaktif.
StatelessTemplate CommandAction Digunakan untuk menunjukkan kontrol yang menyediakan kemampuan sentuh tetapi statusnya tidak dapat ditentukan, misalnya remote televisi IR. Anda dapat menggunakan template ini untuk menentukan rutinitas atau makro, yang merupakan agregasi perubahan status dan kontrol.

Dengan informasi ini, Anda kini dapat membuat kontrol:

Buat 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 Controls bagi penayang ini.
  • createPublisherFor(): Membuat Publisher untuk daftar kontrol tertentu, seperti yang diidentifikasi oleh ID stringnya. Gunakan Control.StatefulBuilder untuk membuat Controls ini karena penayang harus menetapkan status bagi setiap kontrol.

Membuat penayang

Saat menayangkan kontrol ke UI sistem untuk kali pertama, aplikasi tidak mengetahui status setiap kontrol. Akan memerlukan banyak waktu untuk mendapatkan status ini, yang melibatkan banyak lonjakan pada jaringan penyedia perangkat. Gunakan metode createPublisherForAllAvailable() untuk memberi tahu sistem tentang kontrol yang tersedia. Perlu diperhatikan bahwa metode ini menggunakan class builder Control.StatelessBuilder karena status setiap kontrol tidak diketahui.

Setelah kontrol muncul di UI Android, pengguna dapat memilih kontrol (memilih favorit) yang diinginkan.

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

Java

/* 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 = new 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));
    }
}

Setelah pengguna memilih kumpulan kontrol, buat penayang hanya untuk kontrol tersebut. Gunakan metode createPublisherFor() karena metode ini menggunakan class builder Control.StatefulBuilder, yang menyediakan status saat ini dari setiap kontrol (misalnya aktif atau tidak aktif).

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

Java

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

Tangani tindakan

Metode performControlAction() memberikan sinyal bahwa pengguna telah berinteraksi dengan kontrol yang dipublikasikan. Tindakan tersebut didikte oleh jenis ControlAction yang dikirimkan. Lakukan tindakan yang sesuai untuk kontrol yang ditentukan, lalu perbarui status perangkat di UI Android.

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

Java

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