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

สร้างบริการ
ส่วนนี้แสดงวิธีสร้าง ControlsProviderService
บริการนี้จะบอก UI ของระบบ Android ว่าแอปของคุณมีตัวควบคุมอุปกรณ์
ที่ต้องแสดงในส่วนตัวควบคุมอุปกรณ์ของ UI ของ Android
ControlsProviderService
API นี้ถือว่าผู้ใช้มีความคุ้นเคยกับสตรีมแบบรีแอกทีฟตามที่กำหนดไว้ในโปรเจ็กต์ Reactive Streams 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()
ดังนี้
Kotlin
class MyCustomControlService : ControlsProviderService() { ... }
Java
public class MyCustomJavaControlService extends ControlsProviderService { ... }
เลือกประเภทการควบคุมที่ถูกต้อง
API มีเมธอด Builder เพื่อสร้างตัวควบคุม หากต้องการป้อนข้อมูลใน เครื่องมือสร้าง ให้ระบุอุปกรณ์ที่ต้องการควบคุมและวิธีที่ผู้ใช้โต้ตอบ กับอุปกรณ์ ทำตามขั้นตอนต่อไปนี้
- เลือกประเภทอุปกรณ์ที่ตัวควบคุมแสดง คลาส
DeviceTypes
คือ การแจงนับอุปกรณ์ที่รองรับทั้งหมด ระบบจะใช้ประเภทเพื่อกำหนดไอคอนและสีสำหรับอุปกรณ์ใน UI - กำหนดชื่อที่แสดงต่อผู้ใช้ ตำแหน่งอุปกรณ์ เช่น ห้องครัว และองค์ประกอบข้อความอื่นๆ ของ UI ที่เชื่อมโยงกับการควบคุม
- เลือกเทมเพลตที่ดีที่สุดเพื่อรองรับการโต้ตอบของผู้ใช้ ระบบจะกำหนด
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
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; ... }
สร้างผู้เผยแพร่โฆษณาสำหรับการควบคุม
หลังจากสร้างตัวควบคุมแล้ว ตัวควบคุมจะต้องมีผู้เผยแพร่ ผู้เผยแพร่เนื้อหาจะแจ้งให้ UI ของระบบทราบว่ามีตัวควบคุมอยู่ คลาส ControlsProviderService
มีเมธอดของผู้เผยแพร่โฆษณา 2 รายการที่คุณต้องลบล้างในโค้ดแอปพลิเคชัน
createPublisherForAllAvailable()
: สร้างPublisher
สำหรับการควบคุมทั้งหมดที่มีในแอป ใช้Control.StatelessBuilder()
เพื่อสร้างออบเจ็กต์Control
สำหรับผู้เผยแพร่โฆษณารายนี้createPublisherFor()
: สร้างPublisher
สำหรับรายการการควบคุมที่ระบุ ตามตัวระบุสตริง ใช้Control.StatefulBuilder
เพื่อสร้างออบเจ็กต์Control
เหล่านี้ เนื่องจากผู้เผยแพร่โฆษณาต้องกำหนดสถานะให้กับแต่ละตัวควบคุม
สร้างผู้เผยแพร่
เมื่อแอปเผยแพร่ตัวควบคุมไปยัง UI ของระบบเป็นครั้งแรก แอปจะไม่ทราบ
สถานะของตัวควบคุมแต่ละรายการ การรับสถานะอาจเป็นการดำเนินการที่ใช้เวลานาน
ซึ่งเกี่ยวข้องกับการข้ามหลายครั้งในเครือข่ายของผู้ให้บริการอุปกรณ์ ใช้วิธี
createPublisherForAllAvailable()
เพื่อโฆษณาการควบคุมที่ใช้ได้ในระบบ วิธีนี้ใช้คลาสบิลเดอร์ Control.StatelessBuilder
เนื่องจากสถานะของตัวควบคุมแต่ละรายการไม่ทราบ
เมื่อการควบคุมปรากฏใน UI ของ Android แล้ว ผู้ใช้จะเลือกการควบคุมที่ชื่นชอบได้
หากต้องการใช้ Kotlin Coroutines เพื่อสร้าง ControlsProviderService
ให้เพิ่มการอ้างอิงใหม่
ลงใน 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") }
เมื่อซิงค์ไฟล์ Gradle แล้ว ให้เพิ่มข้อมูลโค้ดต่อไปนี้ลงใน Service
เพื่อ
ใช้ 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); } }
ปัดเมนูระบบลง แล้วหาปุ่มการควบคุมอุปกรณ์ตามที่แสดงใน รูปที่ 4

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

ตอนนี้ ให้ใช้เมธอด createPublisherFor()
โดยเพิ่มข้อมูลต่อไปนี้ลงใน
Service
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.PublishercreatePublisherFor(@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(); }
ในตัวอย่างนี้ เมธอด createPublisherFor()
มีการใช้งานแบบจำลองของสิ่งที่แอปของคุณต้องทำ ได้แก่ สื่อสารกับอุปกรณ์เพื่อดึงข้อมูลสถานะ และส่งสถานะดังกล่าวไปยังระบบ
เมธอด createPublisherFor()
ใช้โครูทีนและโฟลว์ของ Kotlin เพื่อให้เป็นไปตาม
API ของ Reactive Streams ที่จำเป็นโดยทำดังนี้
- สร้าง
Flow
- รอ 1 วินาที
- สร้างและปล่อยสถานะของหลอดไฟอัจฉริยะ
- รออีก 1 วินาที
- สร้างและส่งสถานะของตัวควบคุมอุณหภูมิ
จัดการการดำเนินการ
performControlAction()
วิธีนี้จะส่งสัญญาณเมื่อผู้ใช้โต้ตอบกับตัวควบคุมที่เผยแพร่ ประเภทของ ControlAction
ที่ส่งจะเป็นตัวกำหนดการดำเนินการ
ดำเนินการที่เหมาะสมกับการควบคุมที่ระบุ จากนั้นอัปเดตสถานะ
ของอุปกรณ์ใน UI ของ Android
หากต้องการทำให้ตัวอย่างเสร็จสมบูรณ์ ให้เพิ่มข้อมูลต่อไปนี้ลงใน Service
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 Consumerconsumer) { 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()); } }
เรียกใช้แอป เข้าถึงเมนูการควบคุมอุปกรณ์ และดูการควบคุมหลอดไฟและ เทอร์โมสตัท
