ב-Android מגרסה 11 ואילך, התכונה "לחצני שליטה במכשירים בגישה מהירה" מאפשרת למשתמשים לראות מכשירים חיצוניים כמו נורות, תרמוסטטים ומצלמות ולשלוט בהם במהירות, באמצעות ממשק משתמש שדורש עד שלוש אינטראקציות ממרכז האפליקציות שמוגדר כברירת מחדל. יצרן הציוד המקורי (OEM) של המכשיר בוחר באיזה מרכז אפליקציות להשתמש. אפליקציות של ספקי צד שלישי ואפליקציות לאיחוד מכשירים, כמו Google Home, יכולות לספק מכשירים להצגה במרחב הזה. בדף הזה מוסבר איך להציג את אמצעי הבקרה של המכשיר במרחב הזה ולקשר אותם לאפליקציית הבקרה.
כדי להוסיף את התמיכה הזו, צריך ליצור ולהצהיר על ControlsProviderService. יוצרים את אמצעי הבקרה שהאפליקציה תומכת בהם על סמך סוגי אמצעי בקרה מוגדרים מראש, ואז יוצרים מפרסמים לאמצעי הבקרה האלה.
ממשק משתמש
המכשירים מוצגים בקטע אמצעי בקרה למכשיר כווידג'טים עם תבניות. יש חמישה ווידג'טים לשליטה במכשירים, כמו שמוצג בתמונה הבאה:
|
|
|
|
|
לחיצה ארוכה על ווידג'ט תעביר אתכם לאפליקציה כדי שתוכלו לשלוט בה בצורה מעמיקה יותר. אפשר להתאים אישית את הסמל והצבע של כל ווידג'ט, אבל כדי לספק את חוויית המשתמש הטובה ביותר, מומלץ להשתמש בסמל ובצבע שמוגדרים כברירת מחדל אם הם תואמים למכשיר.
יצירת השירות
בקטע הזה מוסבר איך ליצור את ControlsProviderService. השירות הזה מודיע לממשק המשתמש של מערכת Android שהאפליקציה מכילה אמצעי בקרה למכשירים שצריך להציג באזור אמצעי בקרה למכשירים בממשק המשתמש של Android.
כדי להשתמש ב-API של ControlsProviderService, צריך להכיר את המושג "סטרימינג תגובתי" (reactive streams), כפי שמוגדר בפרויקט Reactive Streams ב-GitHub ומוטמע בממשקי Java 9 Flow.
ה-API מבוסס על המושגים הבאים:
- בעל תוכן דיגיטלי: האפליקציה שלכם היא בעל התוכן הדיגיטלי.
- מנוי: ממשק המשתמש של המערכת הוא המנוי, והוא יכול לבקש מספר פקדים מהמפרסם.
- מינוי: מסגרת הזמן שבה בעל האפליקציה יכול לשלוח עדכונים לממשק המשתמש של המערכת. גם בעל התוכן הדיגיטלי וגם המנוי יכולים לסגור את החלון הזה.
הצהרה על השירות
האפליקציה צריכה להצהיר על שירות, כמו MyCustomControlService, בקובץ מניפסט של אפליקציה.
השירות חייב לכלול מסנן Intent עבור ControlsProviderService. המסנן הזה מאפשר לאפליקציות להוסיף אמצעי בקרה לממשק המשתמש של המערכת.
צריך גם label שמוצג בפקדים בממשק המשתמש של המערכת.
בדוגמה הבאה אפשר לראות איך להצהיר על שירות:
<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 מספק שיטות ליצירת פקדים. כדי לאכלס את הכלי ליצירת תסריטים, צריך לקבוע באיזה מכשיר רוצים לשלוט ואיך המשתמש יוכל להשתמש בו. כך עושים את זה:
- בוחרים את סוג המכשיר שאמצעי הבקרה מייצג. הכיתה
DeviceTypesהיא רשימה של כל המכשירים הנתמכים. הסוג משמש לקביעת הסמלים והצבעים של המכשיר בממשק המשתמש. - קובעים את השם שגלוי למשתמשים, את מיקום המכשיר (לדוגמה, מטבח) ורכיבי טקסט אחרים בממשק המשתמש שמשויכים לפקד.
- בחרו את התבנית שהכי מתאימה לאינטראקציה עם המשתמשים. לאמצעי הבקרה מוקצה
ControlTemplateמהאפליקציה. התבנית הזו מציגה למשתמש באופן ישיר את מצב הפקד ואת שיטות הקלט הזמינות, כלומר אתControlAction. בטבלה הבאה מפורטות חלק מהתבניות הזמינות והפעולות שהן תומכות בהן:
| תבנית | פעולה | תיאור |
ControlTemplate.getNoTemplateObject()
|
None
|
האפליקציה יכולה להשתמש בזה כדי להעביר מידע על הפקד, אבל המשתמש לא יכול לבצע איתו אינטראקציה. |
ToggleTemplate
|
BooleanAction
|
מייצג פקד שאפשר להעביר בין מצב מופעל למצב מושבת. האובייקט BooleanAction מכיל שדה שמשתנה כדי לייצג את המצב החדש שהמשתמש ביקש כשהוא לחץ על הפקד.
|
RangeTemplate
|
FloatAction
|
מייצג ווידג'ט של פס הזזה עם ערכי מינימום, מקסימום ושלבים מוגדרים. כשהמשתמש
מזיז את הסליידר, שולחים אובייקט FloatAction חדש
בחזרה לאפליקציה עם הערך המעודכן.
|
ToggleRangeTemplate
|
BooleanAction, FloatAction
|
התבנית הזו היא שילוב של ToggleTemplate ו-RangeTemplate. הוא תומך באירועי מגע וגם בסליידר,
למשל כדי לשלוט באורות שניתן לעמעם.
|
TemperatureControlTemplate
|
ModeAction, BooleanAction, FloatAction
|
בנוסף לפעולות הקודמות, התבנית הזו מאפשרת למשתמש להגדיר מצב, כמו חימום, קירור, חימום/קירור, חיסכון או כיבוי. |
StatelessTemplate
|
CommandAction
|
משמש לציון פקד שמספק יכולת מגע אבל לא ניתן לקבוע את המצב שלו, כמו שלט רחוק של טלוויזיה עם אינפרה-אדום. אפשר להשתמש בתבנית הזו כדי להגדיר תרחיש או פקודת מאקרו, שהם אוסף של שינויים במצב ובבקרה. |
בעזרת המידע הזה, אתם יכולים ליצור את אמצעי הבקרה:
- משתמשים במחלקת ה-builder
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() { // ... }
יצירת בעלי אפליקציות לאמצעי הבקרה
אחרי שיוצרים את הפקד, צריך להוסיף לו בעל תוכן דיגיטלי. האפליקציה המפרסמת מודיעה לממשק המשתמש של המערכת על קיום הפקד. למחלקה ControlsProviderService יש שתי שיטות של בעל תוכן דיגיטלי שצריך לשנות בקוד האפליקציה:
-
createPublisherForAllAvailable: יוצרתPublisherלכל אמצעי הבקרה שזמינים באפליקציה. משתמשים ב-Control.StatelessBuilderכדי ליצור אובייקטים מסוגControlלבעל האפליקציה הזה. -
createPublisherFor: יוצרתPublisherלרשימה של פקדים נתונים, לפי המזהים שלהם במחרוזת. כדי ליצור את האובייקטים האלה שלControl, צריך להשתמש ב-Control.StatefulBuilder, כי בעל האפליקציה צריך להקצות מצב לכל פקד.
יצירת החשבון של בעל האפליקציה
כשהאפליקציה מפרסמת פקדים בממשק המשתמש של המערכת בפעם הראשונה, היא לא יודעת מה המצב של כל פקד. קבלת המצב יכולה להיות פעולה שאורכת זמן רב, וכוללת דילוגים רבים ברשת של ספק המכשיר. משתמשים ב-method createPublisherForAllAvailable כדי לפרסם את אמצעי הבקרה הזמינים למערכת. בשיטה הזו משתמשים במחלקה Control.StatelessBuilder, כי המצב של כל פקד לא ידוע.
אחרי שהאמצעים מופיעים בממשק המשתמש של Android, המשתמשים יכולים לבחור את האמצעים המועדפים.
כדי להשתמש ב-coroutines של Kotlin ליצירת ControlsProviderService, צריך להוסיף תלות חדשה לקובץ 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:
לחיצה על אמצעי בקרה למכשיר מעבירה למסך שני שבו אפשר לבחור את האפליקציה. אחרי שבוחרים את האפליקציה, אפשר לראות איך קטע הקוד הקודם יוצר תפריט מערכת בהתאמה אישית שבו מוצגים אמצעי הבקרה החדשים, כמו שמוצג באיור 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() }
בדוגמה הזו, ה-method createPublisherFor מכיל הטמעה מזויפת של מה שהאפליקציה צריכה לעשות: לתקשר עם המכשיר כדי לאחזר את הסטטוס שלו, ולשלוח את הסטטוס הזה למערכת.
ה-method createPublisherFor משתמשת ב-coroutines וב-flows של Kotlin כדי לעמוד בדרישות של Reactive Streams API, באופן הבא:
- יצירת
Flow. - המתנה של שנייה אחת.
- יוצרת ומפיקה את המצב של הנורה החכמה.
- ממתין עוד שנייה.
- יוצרת ומפיקה את המצב של התרמוסטט.
טיפול בפעולות
השיטה performControlAction מאותתת כשהמשתמש יוצר אינטראקציה עם פקד שפורסם. הפעולה נקבעת לפי סוג ה-ControlAction שנשלח.
מבצעים את הפעולה המתאימה לאמצעי הבקרה הנתון ואז מעדכנים את מצב המכשיר בממשק המשתמש של 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) }
מפעילים את האפליקציה, נכנסים לתפריט אמצעי בקרה למכשירים ורואים את אמצעי הבקרה של התאורה והתרמוסטט.