ב-Android 11 ואילך, התכונה 'גישה מהירה לממשק השליטה במכשירים' מאפשרת למשתמשים להציג ולשלוט במהירות במכשירים חיצוניים כמו מנורות, תרמוסטטים ומצלמות, באמצעות מזמינוּת משתמש שמופיעה בשלוש אינטראקציות ממפעיל ברירת המחדל. יצרן הציוד המקורי של המכשיר בוחר באיזה מרכז אפליקציות להשתמש. מצביעים על מכשירים שיוצגו במרחב הזה – לדוגמה, Google Home – ואפליקציות של ספקים חיצוניים. בדף הזה מוסבר איך להציג את אמצעי הבקרה של המכשיר במרחב הזה ולקשר אותם לאפליקציית הבקרה.
כדי להוסיף את התמיכה הזו, צריך ליצור ולהצהיר על ControlsProviderService. יוצרים את אמצעי הבקרה שהאפליקציה תומכת בהם על סמך סוגים מוגדרים מראש של אמצעי בקרה, ואז יוצרים מוציאים לאור עבור אמצעי הבקרה האלה.
ממשק משתמש
המכשירים מוצגים בקטע אמצעי בקרה במכשיר כווידג'טים מבוססי-תבניות. יש חמישה ווידג'טים של בקרת מכשירים, כמו שמוצג באיור הבא:
|
|
|
|
|
בלחיצה ארוכה על ווידג'ט עוברים לאפליקציה כדי לקבל שליטה מעמיקה יותר. אפשר להתאים אישית את הסמל והצבע בכל ווידג'ט, אבל כדי להעניק את חוויית המשתמש הטובה ביותר, כדאי להשתמש בסמל ובצבע שמוגדרים כברירת מחדל אם הם מתאימים למכשיר.
יצירת השירות
בקטע הזה נסביר איך ליצור את ControlsProviderService. השירות הזה מודיע לממשק המשתמש של מערכת Android שהאפליקציה מכילה אמצעי בקרה למכשיר שצריכים להופיע באזור אמצעי הבקרה למכשיר בממשק המשתמש של Android.
ממשק ה-API ControlsProviderService מבוסס על ההנחה שאתם מכירים את הזרמים הריאקטיביים, כפי שהם מוגדרים בפרויקט 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 מספק methods ליצירת אמצעי הבקרה. כדי לאכלס את הכלי ליצירת תוספים, צריך לקבוע את המכשיר שרוצים לשלוט בו ואת אופן האינטראקציה של המשתמש איתו. כך עושים את זה:
- בוחרים את סוג המכשיר שאמצעי הבקרה מייצג. המחלקות
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() { // ... }
יצירת חשבונות של בעלי תוכן דיגיטלי עבור אמצעי הבקרה
אחרי שיוצרים את אמצעי הבקרה, צריך להקצות לו בעל תוכן דיגיטלי. המוציא לאור מודיע לממשק המשתמש של המערכת על קיומו של אמצעי הבקרה. ב-class ControlsProviderService יש שתי שיטות לבעלי תוכן דיגיטלי שצריך לבטל את ההגדרה שלהן בקוד האפליקציה:
-
createPublisherForAllAvailable: יוצרPublisherלכל אמצעי הבקרה שזמינים באפליקציה. משתמשים ב-Control.StatelessBuilderכדי ליצור אובייקטים שלControlבשביל בעל התוכן הדיגיטלי הזה. -
createPublisherFor: יוצרתPublisherלרשימה של אמצעי בקרה נתונים, שמזוהים באמצעות מזהי המחרוזת שלהם. כדי ליצור את אובייקטיםControlהאלה, צריך להשתמש ב-Control.StatefulBuilder, כי בעל האתר צריך להקצות מצב לכל אמצעי בקרה.
יצירת בעלי האפליקציה
כשאפליקציה מפרסמת אמצעי בקרה בממשק המשתמש של המערכת בפעם הראשונה, היא לא יודעת את המצב של כל אמצעי בקרה. קבלת המצב יכולה להיות פעולה שלוקחת זמן רב, שכוללת הרבה קפיצות ברשת של ספק המכשיר. משתמשים בשיטה createPublisherForAllAvailable כדי לפרסם את אמצעי הבקרה הזמינים במערכת. בשיטה הזו נעשה שימוש במחלקה Control.StatelessBuilder builder, כי הסטטוס של כל אמצעי בקרה לא ידוע.
אחרי שהלחצנים מופיעים בממשק המשתמש של 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) }
מפעילים את האפליקציה, ניגשים לתפריט אמצעי הבקרה של המכשיר ורואים את אמצעי הבקרה של התאורה והתרמוסטט.