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

במכשירי Android 11 ואילך, התכונה 'פקדי מכשירים בגישה מהירה' מאפשר למשתמש לצפות במהירות במכשירים חיצוניים כגון נורות, ולשלוט בהם תרמוסטטים ומצלמות במחיר נוח למשתמש, בתוך שלוש אינטראקציות מרכז האפליקציות המוגדר כברירת מחדל. ה-OEM של המכשיר בוחר את מרכז האפליקציות שבו הוא ישתמש. Device (מכשיר) אגרגטורים - לדוגמה, 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 משמש לציון בקר שמספק יכולת מגע, אבל המצב שלו לא ניתן לקבוע, למשל שלט רחוק לטלוויזיה IR. אפשר להשתמש להגדרה של תרחיש או מאקרו, שהם מאגר של בקרה ושינויים במצב המוצר.

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

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

מגניב

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:

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

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

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

השיטה createPublisherFor() משתמשת בקורוטינים של Kotlin וזורמים כדי לספק את ה-API הנדרש של Reactive Streams:

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

ביצוע פעולות

ה-method 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());
        }
    }
    

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

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