ב-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()
:
Kotlin
class MyCustomControlService : ControlsProviderService() { ... }
Java
public class MyCustomJavaControlService extends 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
כשלא ידוע מה המצב של אמצעי הבקרה. - משתמשים במחלקת ה-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
האלה, צריך להשתמש ב-Control.StatefulBuilder
, כי בעל האתר צריך להקצות מצב לכל אמצעי בקרה.
יצירת בעל התוכן הדיגיטלי
כשהאפליקציה מפרסמת בפעם הראשונה אמצעי בקרה בממשק המשתמש של המערכת, היא לא יודעת את המצב של כל אמצעי בקרה. קבלת המצב יכולה להיות פעולה שלוקחת זמן רב, שכוללת הרבה קפיצות ברשת של ספק המכשיר. משתמשים ב-method createPublisherForAllAvailable()
כדי לפרסם את אמצעי הבקרה הזמינים במערכת. בשיטה הזו נעשה שימוש במחלקת ה-builder Control.StatelessBuilder
, כי המצב של כל אמצעי בקרה לא ידוע.
אחרי שהאמצעים להגדרה מופיעים בממשק המשתמש של Android , המשתמש יכול לבחור את האמצעים המועדפים.
כדי להשתמש בשגרות המשנה של 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()
:
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:

כשלוחצים על אמצעי בקרה במכשיר, עוברים למסך שני שבו אפשר לבחור את האפליקציה. אחרי שבוחרים את האפליקציה, אפשר לראות איך קטע הקוד הקודם יוצר תפריט מערכת מותאם אישית שבו מוצגים אמצעי הבקרה החדשים, כמו שמוצג באיור 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()
מכילה הטמעה מזויפת של מה שהאפליקציה צריכה לעשות: לתקשר עם המכשיר כדי לאחזר את הסטטוס שלו, ולשדר את הסטטוס הזה למערכת.
ה-method createPublisherFor()
משתמשת ב-coroutines וב-flows של Kotlin כדי לעמוד בדרישות של Reactive Streams API. לשם כך היא מבצעת את הפעולות הבאות:
- יוצר
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()); } }
מפעילים את האפליקציה, ניגשים לתפריט אמצעי בקרה למכשירים ורואים את אמצעי הבקרה של התאורה והתרמוסטט.
