ควบคุมอุปกรณ์ภายนอก

ใน Android 11 ขึ้นไป ฟีเจอร์ระบบควบคุมอุปกรณ์ที่เข้าถึงด่วนช่วยให้ผู้ใช้ดูและควบคุมอุปกรณ์ภายนอก เช่น หลอดไฟ ตัวควบคุมอุณหภูมิ และกล้องได้อย่างรวดเร็วจากความสามารถของผู้ใช้ภายใน 3 การโต้ตอบจาก Launcher เริ่มต้น OEM ของอุปกรณ์จะเลือก Launcher ที่จะใช้ ผู้รวบรวมอุปกรณ์ เช่น Google Home และแอปของผู้ให้บริการบุคคลที่สามสามารถจัดหาอุปกรณ์เพื่อแสดงในพื้นที่นี้ได้ หน้านี้จะแสดงวิธีแสดงระบบควบคุมอุปกรณ์ในพื้นที่นี้และลิงก์กับแอปควบคุมของคุณ

รูปที่ 1 พื้นที่ควบคุมอุปกรณ์ใน UI ของ Android

หากต้องการเพิ่มการรองรับนี้ ให้สร้างและประกาศ ControlsProviderService สร้างตัวควบคุมที่แอปของคุณรองรับตามประเภทตัวควบคุมที่กำหนดไว้ล่วงหน้า แล้วสร้างผู้เผยแพร่สำหรับตัวควบคุมเหล่านี้

อินเทอร์เฟซผู้ใช้

อุปกรณ์จะแสดงในส่วนระบบควบคุมอุปกรณ์เป็นวิดเจ็ตที่มีเทมเพลต วิดเจ็ตระบบควบคุมอุปกรณ์มีให้เลือก 5 รายการ ดังที่แสดงในรูปภาพต่อไปนี้

เปิด/ปิดวิดเจ็ตสำหรับระบบควบคุมอุปกรณ์
สลับ
สลับด้วยวิดเจ็ตแถบเลื่อน
สลับด้วยแถบเลื่อน
วิดเจ็ตแถบเลื่อนช่วงสำหรับการควบคุมอุปกรณ์
ช่วง (เปิดหรือปิดไม่ได้)
วิดเจ็ตควบคุมการเปิด/ปิดแบบไม่เก็บสถานะ
ปุ่มเปิด/ปิดแบบไม่เก็บสถานะ
วิดเจ็ตแผงควบคุมอุณหภูมิในสถานะปิด
แผงอุณหภูมิ (ปิด)
รูปที่ 2 คอลเล็กชันวิดเจ็ตที่มีเทมเพลต

การแตะวิดเจ็ตค้างไว้จะนำคุณไปยังแอปเพื่อการควบคุมที่ละเอียดยิ่งขึ้น คุณสามารถปรับแต่งไอคอนและสีในวิดเจ็ตแต่ละรายการได้ แต่เพื่อประสบการณ์ของผู้ใช้ที่ดีที่สุด ให้ใช้ไอคอนและสีเริ่มต้นหากชุดเริ่มต้นตรงกับอุปกรณ์

วิดเจ็ตแผงอุณหภูมิในสถานะเปิด
รูปที่ 3 เปิดวิดเจ็ตแผงอุณหภูมิ

สร้างบริการ

ส่วนนี้จะแสดงวิธีสร้าง ControlsProviderService บริการนี้จะบอก UI ของระบบ Android ว่าแอปของคุณมีระบบควบคุมอุปกรณ์ที่ต้องแสดงในส่วนระบบควบคุมอุปกรณ์ของ UI ของ Android

ControlsProviderService API ถือว่าคุณคุ้นเคยกับสตรีมแบบรีแอ็กทีฟตามที่ระบุไว้ในโปรเจ็กต์ GitHub ของสตรีมแบบรีแอ็กทีฟและใช้งานในอินเทอร์เฟซ Java 9 Flow API สร้างขึ้นจากแนวคิดต่อไปนี้

  • ผู้เผยแพร่: แอปพลิเคชันของคุณเป็นผู้เผยแพร่
  • ผู้สมัครใช้บริการ: UI ของระบบคือผู้สมัครใช้บริการและสามารถขอการควบคุมจำนวนหนึ่งจากผู้เผยแพร่เนื้อหาได้
  • การสมัครใช้บริการ: กรอบเวลาที่ผู้เผยแพร่โฆษณาสามารถส่งการอัปเดตไปยัง UI ของระบบได้ ผู้เผยแพร่หรือผู้สมัครใช้บริการสามารถปิดหน้าต่างนี้ได้

ประกาศบริการ

แอปของคุณต้องประกาศบริการ เช่น MyCustomControlService ในไฟล์ Manifest ของแอป

บริการต้องมีตัวกรอง Intent สำหรับ ControlsProviderService ตัวกรองนี้ช่วยให้แอปพลิเคชันมีส่วนร่วมในการควบคุม UI ของระบบ

นอกจากนี้ คุณยังต้องมี label ซึ่งแสดงในการควบคุมใน UI ของระบบ

ตัวอย่างต่อไปนี้แสดงวิธีประกาศบริการ

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

ถัดไป ให้สร้างไฟล์ Kotlin ใหม่ชื่อ MyCustomControlService.kt และทำให้ไฟล์ดังกล่าวขยาย ControlsProviderService:

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

เลือกประเภทการควบคุมที่ถูกต้อง

API มีเมธอด Builder เพื่อสร้างตัวควบคุม หากต้องการป้อนข้อมูลในเครื่องมือสร้าง ให้กำหนดอุปกรณ์ที่ต้องการควบคุมและวิธีที่ผู้ใช้โต้ตอบกับอุปกรณ์นั้น ทำตามขั้นตอนต่อไปนี้

  1. เลือกประเภทอุปกรณ์ที่ตัวควบคุมแสดง คลาส DeviceTypes คือการแจกแจงอุปกรณ์ที่รองรับทั้งหมด ระบบจะใช้ประเภทเพื่อกำหนดไอคอนและสีสำหรับอุปกรณ์ใน UI
  2. กำหนดชื่อที่แสดงต่อผู้ใช้ ตำแหน่งของอุปกรณ์ เช่น ห้องครัว และองค์ประกอบข้อความอื่นๆ ของ UI ที่เชื่อมโยงกับการควบคุม
  3. เลือกเทมเพลตที่ดีที่สุดเพื่อรองรับการโต้ตอบของผู้ใช้ ระบบจะกำหนด ControlTemplate จากแอปพลิเคชันให้กับการควบคุม เทมเพลตนี้จะแสดงสถานะการควบคุมต่อผู้ใช้โดยตรง รวมถึงวิธีการป้อนข้อมูลที่ใช้ได้ ซึ่งก็คือ ControlAction ตารางต่อไปนี้แสดงเทมเพลตบางส่วนที่พร้อมใช้งานและการดำเนินการที่รองรับ
เทมเพลต การดำเนินการ คำอธิบาย
ControlTemplate.getNoTemplateObject() None แอปพลิเคชันอาจใช้ข้อมูลนี้เพื่อสื่อข้อมูลเกี่ยวกับการควบคุม แต่ผู้ใช้จะโต้ตอบกับข้อมูลนี้ไม่ได้
ToggleTemplate BooleanAction แสดงถึงการควบคุมที่สลับระหว่างสถานะเปิดใช้และปิดใช้ได้ ออบเจ็กต์ BooleanAction มีช่องที่เปลี่ยนเพื่อแสดงสถานะใหม่ที่ขอเมื่อผู้ใช้แตะตัวควบคุม
RangeTemplate FloatAction แสดงวิดเจ็ตแถบเลื่อนที่มีค่าต่ำสุด สูงสุด และค่าขั้นที่ระบุ เมื่อผู้ใช้โต้ตอบกับแถบเลื่อน ให้ส่งออบเจ็กต์ FloatAction ใหม่กลับไปยังแอปพลิเคชันพร้อมค่าที่อัปเดต
ToggleRangeTemplate BooleanAction, FloatAction เทมเพลตนี้เป็นการผสมผสานระหว่าง ToggleTemplate และ RangeTemplate รองรับการโต้ตอบแบบสัมผัสและแถบเลื่อน เช่น เพื่อควบคุมไฟที่หรี่แสงได้
TemperatureControlTemplate ModeAction, BooleanAction, FloatAction นอกจากจะรวมการดำเนินการก่อนหน้าไว้แล้ว เทมเพลตนี้ยังช่วยให้ผู้ใช้ตั้งค่าโหมดได้ เช่น ทำความร้อน ทำความเย็น ทำความร้อน/ทำความเย็น อีโค หรือปิด
StatelessTemplate CommandAction ใช้เพื่อระบุการควบคุมที่ให้ความสามารถในการสัมผัส แต่ไม่สามารถระบุสถานะได้ เช่น รีโมตทีวี IR คุณใช้เทมเพลตนี้เพื่อกำหนดกิจวัตรหรือมาโคร ซึ่งเป็นการรวมการควบคุมและการเปลี่ยนแปลงสถานะได้

เมื่อมีข้อมูลนี้แล้ว คุณจะสร้างการควบคุมได้โดยทำดังนี้

  • ใช้คลาสเครื่องมือสร้าง Control.StatelessBuilder เมื่อไม่ทราบสถานะของตัวควบคุม
  • ใช้คลาสเครื่องมือสร้าง Control.StatefulBuilder เมื่อทราบสถานะของการควบคุม

ตัวอย่างเช่น หากต้องการควบคุมหลอดไฟอัจฉริยะและตัวควบคุมอุณหภูมิ ให้เพิ่มค่าคงที่ต่อไปนี้ลงใน 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() {
    // ...
}

สร้างผู้เผยแพร่สำหรับการควบคุม

หลังจากสร้างการควบคุมแล้ว คุณต้องมีผู้เผยแพร่โฆษณา ผู้เผยแพร่โฆษณาจะแจ้งให้ UI ของระบบทราบถึงการมีอยู่ของการควบคุม คลาส ControlsProviderService มีเมธอดของผู้เผยแพร่โฆษณา 2 เมธอดที่คุณต้องลบล้างในโค้ดของแอปพลิเคชัน ดังนี้

  • createPublisherForAllAvailable: สร้าง Publisher สำหรับการควบคุมทั้งหมดที่มีในแอป ใช้ Control.StatelessBuilder เพื่อสร้างออบเจ็กต์ Control สำหรับผู้เผยแพร่โฆษณารายนี้
  • createPublisherFor: สร้าง Publisher สำหรับรายการการควบคุมที่ระบุ ซึ่งระบุโดยตัวระบุสตริง ใช้ Control.StatefulBuilder เพื่อสร้างออบเจ็กต์ Control เหล่านี้ เนื่องจากผู้เผยแพร่โฆษณาต้องกำหนดสถานะให้กับการควบคุมแต่ละรายการ

สร้างผู้เผยแพร่โฆษณา

เมื่อแอปเผยแพร่การควบคุมไปยัง UI ของระบบเป็นครั้งแรก แอปจะไม่ทราบสถานะของการควบคุมแต่ละรายการ การรับสถานะอาจเป็นการดำเนินการที่ใช้เวลานานซึ่งเกี่ยวข้องกับการฮ็อปหลายครั้งในเครือข่ายของผู้ให้บริการอุปกรณ์ ใช้เมธอด createPublisherForAllAvailable เพื่อโฆษณาการควบคุมที่ใช้ได้ต่อระบบ วิธีนี้ใช้คลาสเครื่องมือสร้าง Control.StatelessBuilder เนื่องจากไม่ทราบสถานะของตัวควบคุมแต่ละรายการ

เมื่อตัวควบคุมปรากฏใน Android UI ผู้ใช้จะเลือกตัวควบคุมที่ชื่นชอบได้

หากต้องการใช้โครูทีน Kotlin เพื่อสร้าง ControlsProviderService ให้เพิ่มทรัพยากร Dependency ใหม่ลงใน build.gradle ดังนี้

ดึงดูด

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

Kotlin

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

หลังจากซิงค์ไฟล์ Gradle แล้ว ให้เพิ่มข้อมูลโค้ดต่อไปนี้ลงใน Service เพื่อใช้ 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()
    }
}

ปัดเมนูระบบลงและค้นหาปุ่มระบบควบคุมอุปกรณ์ที่แสดงในรูปที่ 4

อินเทอร์เฟซผู้ใช้ของระบบสำหรับระบบควบคุมอุปกรณ์
รูปที่ 4 ระบบควบคุมอุปกรณ์ในเมนูระบบ

การแตะการควบคุมอุปกรณ์จะนำทางไปยังหน้าจอที่ 2 ซึ่งคุณสามารถเลือกแอปได้ เมื่อเลือกแอปแล้ว คุณจะเห็นว่าข้อมูลโค้ดก่อนหน้านี้สร้างเมนูระบบที่กำหนดเองซึ่งแสดงการควบคุมใหม่ได้อย่างไร ดังที่แสดงในรูปที่ 5

เมนูระบบที่แสดงการควบคุมแสงและตัวควบคุมอุณหภูมิ
รูปที่ 5 การควบคุมไฟและตัวควบคุมอุณหภูมิที่จะเพิ่ม

ตอนนี้ให้ใช้เมธอด createPublisherFor โดยเพิ่มโค้ดต่อไปนี้ลงใน 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()
}

ในตัวอย่างนี้ เมธอด createPublisherFor มีการจำลองการทำงานที่แอปของคุณต้องทำ นั่นคือสื่อสารกับอุปกรณ์เพื่อดึงสถานะ และส่งสถานะนั้นไปยังระบบ

เมธอด createPublisherFor ใช้โครูทีนและโฟลว์ของ Kotlin เพื่อให้เป็นไปตาม Reactive Streams API ที่จำเป็นโดยทำดังนี้

  1. สร้าง Flow
  2. รอ 1 วินาที
  3. สร้างและแสดงสถานะของหลอดไฟอัจฉริยะ
  4. รออีก 1 วินาที
  5. สร้างและส่งสถานะของตัวควบคุมอุณหภูมิ

จัดการการดำเนินการ

เมธอด performControlAction จะส่งสัญญาณเมื่อผู้ใช้โต้ตอบกับตัวควบคุมที่เผยแพร่ ประเภทของ ControlAction ที่ส่งจะเป็นตัวกำหนดการดำเนินการ ดำเนินการที่เหมาะสมสำหรับการควบคุมที่ระบุ แล้วอัปเดตสถานะของอุปกรณ์ใน UI ของ Android

หากต้องการทำตัวอย่างให้เสร็จสมบูรณ์ ให้เพิ่มโค้ดต่อไปนี้ลงใน 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)
}

เปิดแอป เข้าถึงเมนูระบบควบคุมอุปกรณ์ แล้วดูระบบควบคุมหลอดไฟและตัวควบคุมอุณหภูมิ

ตัวควบคุมที่แสดงหลอดไฟและตัวควบคุมอุณหภูมิ
รูปที่ 6 การควบคุมไฟและตัวควบคุมอุณหภูมิ