שליטה במכשירים חיצוניים

במכשירי Android 11 ואילך, התכונה 'פקדי מכשירים בגישה מהירה' מאפשר למשתמש לצפות במהירות במכשירים חיצוניים כגון נורות, ולשלוט בהם תרמוסטטים, ומצלמות במחיר נמוך לכל משתמש, בתוך שלוש אינטראקציות מרכז האפליקציות המוגדר כברירת מחדל. יצרן המכשיר (OEM) בוחר את מרכז האפליקציות שבו הוא משתמש. שירותי צבירת מכשירים – למשל, Google Home – ואפליקציות של ספקים של צד שלישי יכולים לספק מכשירים להצגה במרחב הזה. בדף הזה נסביר איך להציג את אמצעי הבקרה של המכשיר במרחב הזה ולקשר אותם לאפליקציית הבקרה.

איור 1. מרחב בקרה במכשיר בממשק המשתמש של Android.

כדי להוסיף את התמיכה הזו, יוצרים ומצהירים על ControlsProviderService. יוצרים את אמצעי הבקרה שהאפליקציה תומכת בהם על סמך סוגי אמצעי בקרה מוגדרים מראש, ואז יוצרים בעלי תוכן דיגיטלי לאמצעי הבקרה האלה.

ממשק משתמש

המכשירים מוצגים בקטע פקדי מכשירים כווידג'טים לפי תבנית. חמישה ווידג'טים של בקרת מכשירים זמינים, כפי שמוצג באיור הבא:

החלפת מצב הווידג'ט
מתג
החלפת מצב באמצעות ווידג'ט של פס מחליק
אפשר להחליף מצב באמצעות פס הזזה
ווידג'ט טווח
טווח (לא ניתן להפעיל או להשבית)
ווידג'ט להחלפת מצב ללא מצב
מתג ללא שמירת מצב
הווידג'ט של חלונית הטמפרטורה (סגור)
חלונית הטמפרטורה (סגורה)
איור 2. אוסף של ווידג'טים בתבנית.

לחיצה ארוכה על ווידג'ט תעביר אתכם לאפליקציה כדי לקבל שליטה מעמיקה יותר. אפשר להתאים אישית את הסמל והצבע של כל ווידג'ט, אבל כדי ליהנות מחוויית המשתמש הטובה ביותר, מומלץ להשתמש בסמל ובצבע שמוגדרים כברירת מחדל אם הם תואמים למכשיר.

תמונה שמוצג בה הווידג'ט של חלונית הטמפרטורה (פתוח)
איור 3. פתיחת הווידג'ט של חלונית הטמפרטורה פתוחה.

יצירת השירות

בקטע הזה נסביר איך ליצור ControlsProviderService השירות הזה מציין לממשק המשתמש של מערכת Android שהאפליקציה שלך מכילה פקדי מכשירים חייבים להופיע באזור בקרת מכשירים בממשק המשתמש של Android.

ה-API של ControlsProviderService מבוסס על ההנחה שיש לכם ניסיון בשידורים תגובתיים, כמו מוגדר ב-Reactive Streams GitHub פרויקט ויושמו בתהליך של 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 מספק שיטות ל-builder כדי ליצור את אמצעי הבקרה. כדי לאכלס את Builder, בחירת המכשיר שבו תרצו לשלוט והאופן שבו המשתמש מקיים אינטראקציה איתו. מבצעים את השלבים הבאים:

  1. בוחרים את סוג המכשיר שהפקד מייצג. הכיתה DeviceTypes היא ספירה של כל המכשירים הנתמכים. הסוג משמש לקביעת הסמלים והצבעים של המכשיר בממשק המשתמש.
  2. קובעים את השם שמוצג למשתמש, את מיקום המכשיר – לדוגמה, מטבח – ואלמנטים טקסטואליים אחרים בממשק המשתמש שמשויכים לאמצעי הבקרה.
  3. בחירת התבנית המתאימה ביותר לתמיכה באינטראקציה של המשתמשים. לפקדים מוקצה ControlTemplate מהאפליקציה. התבנית הזו מציגה למשתמש ישירות את מצב הבקרה ואת שיטות הקלט הזמינות – כלומר את ControlAction. בטבלה הבאה מפורטות כמה מהתבניות הזמינות והפעולות שהן תומכות בהן:
תבנית פעולה תיאור
ControlTemplate.getNoTemplateObject() None האפליקציה עשויה להשתמש בהרשאה הזו כדי להעביר מידע על הבקרה, אבל המשתמש לא יכול לבצע אינטראקציה.
ToggleTemplate BooleanAction מייצג פקד שאפשר להעביר בין המצבים 'מופעל' ו'מושבת'. האובייקט BooleanAction מכיל שדה שמשתנה כדי לייצג את המצב החדש המבוקש כשהמשתמש מקיש על הבקרה.
RangeTemplate FloatAction מייצג ווידג'ט של פס הזזה עם ערכי מינימום, מקסימום ושלבים שצוינו. כשהמשתמש יוצר אינטראקציה עם פס ההזזה, שולחים לאפליקציה אובייקט FloatAction חדש עם הערך המעודכן.
ToggleRangeTemplate BooleanAction, FloatAction התבנית הזו היא שילוב של ToggleTemplate ו-RangeTemplate. הוא תומך באירועי מגע וגם בפס הזזה, למשל, לשליטה באורות שניתן לעמעום.
TemperatureControlTemplate ModeAction, BooleanAction, FloatAction בנוסף לאנקפסולציה של הפעולות הקודמות, התבנית הזו מאפשרת למשתמש להגדיר מצב, כמו חימום, קירור, חימום/קירור, חיסכון או השבתה.
StatelessTemplate CommandAction משמש לציון אמצעי בקרה עם יכולת מגע, אבל אי אפשר לקבוע את המצב שלו, כמו שלט רחוק של טלוויזיה עם אינפרה-אדום. אפשר להשתמש בתבנית הזו כדי להגדיר תרחיש או מאקרו, שהם צבירת שינויים של בקרה ומצב.

בעזרת המידע הזה תוכלו ליצור את אמצעי הבקרה:

לדוגמה, כדי לשלוט בנורה חכמה ובתרמוסטט, מוסיפים את הקבועים הבאים ל-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 האלה, כי בעל האתר צריך להקצות מצב כל שליטה.

יצירת בעל אפליקציה

בפעם הראשונה שהאפליקציה מפרסמת רכיבי בקרה בממשק המשתמש של המערכת, היא לא יודעת מה המצב של כל רכיב בקרה. אחזור המצב יכול להיות פעולה ממושכת שכוללת הרבה קפיצות ברשת של ספק המכשיר. משתמשים ב createPublisherForAllAvailable() לפרסום אמצעי הבקרה הזמינים במערכת. השיטה הזאת משתמשת מחלקת ה-builder Control.StatelessBuilder, כי המצב של כל אמצעי בקרה הוא לא ידועה.

אחרי שהפקדים מופיעים בממשק המשתמש של Android, המשתמש יכול לבחור את הפקדים המועדפים עליו.

כדי להשתמש בקורוטינים של 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<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:

תמונה שבה מוצג ממשק המשתמש של המערכת לפקדים במכשירים
איור 4. פקדי המכשירים בתפריט המערכת.

הקשה על Device controls (אמצעי בקרה במכשיר) תעביר אתכם למסך שני שבו תוכלו לבחור את האפליקציה. אחרי שתבחרו את האפליקציה, תוכלו לראות איך קטע הקוד הקודם יוצר תפריט מערכת מותאם אישית שבו מוצגים אמצעי הבקרה החדשים, כפי שמוצג באיור 5:

תמונה שמוצג בה תפריט המערכת עם פקד של תאורה ותרמוסטט
איור 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.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);
    }
 
    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();
    }
    

בדוגמה הזו, ה-method createPublisherFor() מכילה של מה שהאפליקציה צריכה לעשות: לתקשר עם המכשיר כדי לאחזר את הסטטוס שלו ולפלט אותו למערכת.

כדי לעמוד בדרישות של Reactive Streams API, ה-method‏ createPublisherFor() משתמש ב-coroutines וב-flows של Kotlin באופן הבא:

  1. יצירת Flow.
  2. תמתין שנייה אחת.
  3. יצירת המצב של הנורה החכמה והעברתו.
  4. תמתין עוד שנייה.
  5. יצירת המצב של התרמוסטט והעברתו.

טיפול בפעולות

השיטה 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 Consumer consumer) {
        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 (אמצעי הבקרה של המכשיר) ומציגים את אמצעי הבקרה של התאורה והתרמוסטט.

תמונה שמוצגת בה פקד של אור ותרמוסטט
איור 6. אמצעי בקרה של תאורה ותרמוסטט.