ב-Android 11 ואילך, התכונה 'אמצעי בקרה למכשירים' לגישה מהירה מאפשרת למשתמשים לראות במהירות מכשירים חיצוניים כמו מנורות, תרמוסטטים ומצלמות ולשלוט בהם תוך שלוש אינטראקציות מתוך מרכז האפליקציות שמוגדר כברירת מחדל. יצרן המכשיר (OEM) בוחר את מרכז האפליקציות שבו הוא משתמש. שירותי צבירת מכשירים – למשל, Google Home – ואפליקציות של ספקים של צד שלישי יכולים לספק מכשירים להצגה במרחב הזה. בדף הזה נסביר איך להציג את אמצעי הבקרה של המכשיר במרחב הזה ולקשר אותם לאפליקציית הבקרה.
כדי להוסיף את התמיכה הזו, יוצרים ומצהירים על ControlsProviderService
. יוצרים את אמצעי הבקרה שהאפליקציה תומכת בהם על סמך סוגי אמצעי בקרה מוגדרים מראש, ואז יוצרים בעלי תוכן דיגיטלי לאמצעי הבקרה האלה.
ממשק משתמש
המכשירים מוצגים בקטע פקדי מכשירים כווידג'טים לפי תבנית. יש חמישה ווידג'טים לבקרת מכשירים, כפי שמוצג באיור הבא:
|
|
|
|
|
לחיצה ארוכה על ווידג'ט תעביר אתכם לאפליקציה כדי לקבל שליטה מעמיקה יותר. אפשר להתאים אישית את הסמל והצבע של כל ווידג'ט, אבל כדי ליהנות מחוויית המשתמש הטובה ביותר, מומלץ להשתמש בסמל ובצבע שמוגדרים כברירת מחדל אם הם תואמים למכשיר.
יצירת השירות
בקטע הזה מוסבר איך יוצרים את ControlsProviderService
.
השירות הזה מודיע לממשק המשתמש של מערכת Android שהאפליקציה מכילה אמצעי בקרה למכשיר, שצריך להציג באזור אמצעי בקרה למכשיר בממשק המשתמש של Android.
ה-API של ControlsProviderService
מתבסס על ההנחה שאתם מכירים את הזרמים המגיבים (reactive streams), כפי שהם מוגדרים בפרויקט Reactive Streams ב-GitHub ושמוטמעים בממשקי Flow של Java 9.
ה-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()
:
Kotlin
class MyCustomControlService : ControlsProviderService() { ... }
Java
public class MyCustomJavaControlService extends ControlsProviderService { ... }
בחירת סוג אמצעי הבקרה הנכון
ה-API מספק שיטות build ליצירת אמצעי הבקרה. כדי לאכלס את ה-builder, צריך לקבוע איזה מכשיר רוצים לשלוט בו ואיך המשתמש יוצר איתו אינטראקציה. מבצעים את השלבים הבאים:
- בוחרים את סוג המכשיר שאותו מייצג אמצעי הבקרה. הכיתה
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
כשהמצב של אמצעי הבקרה לא ידוע. - משתמשים בכיתה ה-builder
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; ... }
יצירת חשבונות של בעלי תוכן דיגיטלי לצורך בקרה
אחרי שיוצרים את אמצעי הבקרה, צריך להקצות לו בעל תוכן דיגיטלי. בעל התוכן הדיגיטלי מודיע לממשק המשתמש של המערכת על קיומו של אמצעי הבקרה. לכיתה ControlsProviderService
יש שתי שיטות של בעלי תוכן דיגיטלי שצריך לשנות בקוד האפליקציה:
createPublisherForAllAvailable()
: יוצרתPublisher
לכל אמצעי הבקרה שזמינים באפליקציה. משתמשים ב-Control.StatelessBuilder()
כדי ליצור אובייקטים מסוגControl
לבעל האפליקציה הזה.createPublisherFor()
: יוצרתPublisher
לרשימה של אמצעי בקרה נתונים, כפי שזוהו לפי מזהי המחרוזות שלהם. משתמשים ב-Control.StatefulBuilder
כדי ליצור את אובייקטי ה-Control
האלה, כי בעל התוכן הדיגיטלי צריך להקצות מצב לכל אמצעי בקרה.
יצירת בעל התוכן הדיגיטלי
בפעם הראשונה שהאפליקציה מפרסמת רכיבי בקרה בממשק המשתמש של המערכת, היא לא יודעת מה המצב של כל רכיב בקרה. אחזור המצב יכול להיות פעולה ממושכת שכוללת הרבה קפיצות ברשת של ספק המכשיר. משתמשים ב-method createPublisherForAllAvailable()
כדי להציג למערכת את אמצעי הבקרה הזמינים. בשיטה הזו נעשה שימוש בכיתה ה-builder Control.StatelessBuilder
, כי המצב של כל אמצעי בקרה לא ידוע.
אחרי שהפקדים מופיעים בממשק המשתמש של Android , המשתמש יכול לבחור את הפקדים המועדפים עליו.
כדי להשתמש בשגרות המשך (coroutines) ב-Kotlin כדי ליצור 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> 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:
הקשה על Device controls (אמצעי בקרה במכשיר) תעביר אתכם למסך שני שבו תוכלו לבחור את האפליקציה. אחרי שתבחרו את האפליקציה, תוכלו לראות איך קטע הקוד הקודם יוצר תפריט מערכת מותאם אישית שבו מוצגים אמצעי הבקרה החדשים, כפי שמוצג באיור 5:
עכשיו מטמיעים את השיטה createPublisherFor()
ומוסיפים את הקוד הבא ל-Service
:
Kotlin
private val job = SupervisorJob() private val scope = CoroutineScope(Dispatchers.IO + job) private val controlFlows = mutableMapOf>() 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()
מכילה הטמעה מזויפת של מה שהאפליקציה צריכה לעשות: לתקשר עם המכשיר כדי לאחזר את הסטטוס שלו ולהעביר את הסטטוס הזה למערכת.
כדי לעמוד בדרישות של Reactive Streams API, ה-method createPublisherFor()
משתמש ב-coroutines וב-flows של Kotlin באופן הבא:
- יצירת
Flow
. - הפונקציה ממתינה שנייה אחת.
- יצירת המצב של הנורה החכמה והעברתו.
- המערכת ממתינה עוד שנייה.
- יצירת המצב של התרמוסטט והעברתו.
טיפול בפעולות
השיטה performControlAction()
מאותתת כשהמשתמש מבצע אינטראקציה עם אמצעי בקרה שפורסם. סוג ההודעה ControlAction
שנשלחת קובע את הפעולה.
מבצעים את הפעולה המתאימה לאמצעי הבקרה הנתון, ולאחר מכן מעדכנים את המצב של המכשיר בממשק המשתמש של 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()); } }
מריצים את האפליקציה, נכנסים לתפריט Device controls (אמצעי הבקרה של המכשיר) ורואים את אמצעי הבקרה של התאורה והתרמוסטט.