التحكّم في الأجهزة الخارجية

في الإصدار 11 من نظام التشغيل Android والإصدارات الأحدث، تتيح ميزة "عناصر التحكّم في الأجهزة" ضمن "الوصول السريع" للمستخدم عرض الأجهزة الخارجية، مثل المصابيح والترموستات والكاميرات، والتحكّم فيها بسرعة من خلال تفاعلات المستخدم خلال ثلاث تفاعلات من أحد مشغّلات التطبيقات التلقائية. يختار المصنّع الأصلي للجهاز مشغّل التطبيقات الذي يستخدمه. يمكن لمجمّعي الأجهزة، مثل Google Home، وتطبيقات المورّدين الخارجيين، توفير الأجهزة لعرضها في هذه المساحة. توضّح لك هذه الصفحة كيفية عرض عناصر التحكم بالجهاز في هذه المساحة وربطها بتطبيق التحكّم.

الشكل 1. مساحة التحكّم في الجهاز في واجهة مستخدم Android

لإضافة هذا الخيار، أنشئ ControlsProviderService وأدخِل بياناته. إنشاء يتحكّم بها تطبيقك استنادًا إلى أنواع عناصر تحكُّم محدّدة مسبقًا، ثمّ للناشرين لعناصر التحكم هذه.

واجهة المستخدم

يتم عرض الأجهزة ضمن عناصر التحكم في الأجهزة كتطبيقات مصغّرة وفقًا لنموذج. خمسة تتوفر أدوات التحكم في الجهاز، كما هو موضح في الشكل التالي:

تبديل التطبيق المصغّر
تبديل
التبديل باستخدام تطبيق مصغّر شريط التمرير
التبديل باستخدام شريط التمرير
أداة النطاق
النطاق (لا يمكن تشغيله أو إيقافه)
تطبيق مصغّر بلا حالة
إيقاف/تفعيل ميزة "بدون حالة"
تطبيق مصغّر للوحة درجة الحرارة (مغلقة)
لوحة درجة الحرارة (مغلقة)
الشكل 2. مجموعة من التطبيقات المصغّرة المستندة إلى النماذج

اللمس & ويؤدي الضغط على تطبيق مصغّر إلى نقلك إلى التطبيق لمزيد من التحكم. يمكنك تخصيص الرمز واللون في كل تطبيق مصغّر، ولكن لتقديم أفضل تجربة للمستخدم، يمكنك استخدام الرمز واللون الافتراضيين إذا كانت المجموعة التلقائية مطابقة للجهاز.

صورة تعرض تطبيق مصغّر للوحة درجة الحرارة (مفتوح)
الشكل 3. تم فتح التطبيق المصغّر للوحة درجة الحرارة.

إنشاء الخدمة

يوضّح هذا القسم كيفية إنشاء ControlsProviderService. تُعلِم هذه الخدمة واجهة مستخدم نظام Android بأنّ تطبيقك يحتوي على عناصر تحكُّم في الجهاز. التي يجب أن تظهر في منطقة عناصر التحكّم في الجهاز ضمن واجهة مستخدم Android.

تفترض واجهة برمجة التطبيقات ControlsProviderService معرفة بـ "البثّ التفاعلي"، على النحو المُحدَّد في مشروع "البثّ التفاعلي" على GitHub والمُطبَّق في واجهات Java 9 Flow . تم إنشاء واجهة برمجة التطبيقات استنادًا إلى المفاهيم التالية:

  • الناشر: تطبيقك هو الناشر.
  • المشترِك: واجهة مستخدم النظام هي المشترك ويمكنها طلب رقم. لعناصر التحكم من الناشر.
  • الاشتراك: الإطار الزمني الذي يمكن للناشر خلاله إرسال التعديلات إلى واجهة مستخدم النظام يمكن للناشر أو المشترك إغلاق هذا القسم نافذة.

تقديم بيان عن الخدمة

يجب أن يعلن تطبيقك عن خدمة، مثل MyCustomControlService، في بيان التطبيق.

يجب أن تتضمّن الخدمة فلتر أهداف 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 {
        ...
    }
    

اختيار نوع عنصر التحكّم الصحيح

توفّر واجهة برمجة التطبيقات طرقًا لإنشاء عناصر التحكّم. لملء حقل المستخدم، وتحديد الجهاز الذي تريد التحكم فيه وكيفية تفاعل المستخدم معها. نفِّذ الخطوات التالية:

  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 يُستخدَم للإشارة إلى عنصر تحكّم يتيح إمكانية اللمس ولكن لا يمكن تحديد حالته، مثل جهاز التحكّم عن بُعد في التلفزيون عبر الأشعة تحت الحمراء. يمكنك استخدام هذه الصفحة نموذج لتحديد سلسلة إجراءات أو وحدة ماكرو، وهي تجميع للتحكّم التغييرات والحالة.

باستخدام هذه المعلومات، يمكنك إنشاء عنصر التحكّم:

  • استخدِم فئة ملف تعريف الارتباط 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 هذه، لأنّ الناشر يجب أن يحدّد حالة لكل عنصر تحكّم.

إنشاء الناشر

عندما ينشر تطبيقك عناصر التحكّم لأول مرة على واجهة مستخدم النظام، لا يعرف التطبيق. حالة كل عنصر تحكم. قد تستغرق عملية الحصول على الحالة وقتًا طويلاً تتضمن العديد من القفزات في شبكة موفّر الجهاز. استخدِم الأسلوب createPublisherForAllAvailable() للإعلان عن عناصر التحكّم المتاحة للنظام. تستخدم هذه الطريقة دالة 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:

صورة تعرِض واجهة مستخدم النظام لعناصر التحكّم في الجهاز
الشكل 4. عناصر التحكّم بالأجهزة في قائمة النظام

يؤدي النقر على عناصر التحكّم في الجهاز إلى الانتقال إلى شاشة ثانية يمكنك فيها اختيار تطبيقك. بعد اختيار تطبيقك، سترى كيف ينشئ المقتطف السابق قائمة نظام مخصّصة تعرض عناصر التحكّم الجديدة، كما هو موضّح في الشكل 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();
    }
    

في هذا المثال، تحتوي الطريقة createPublisherFor() على تنفيذ ملفعّل لما يجب أن يفعله تطبيقك: التواصل مع جهازك ل retrieving its status، وعرض هذه الحالة على النظام.

تستخدم طريقة createPublisherFor() الكوروتينات بلغة Kotlin والتدفقات لتلبية احتياجات المستخدمين. واجهة برمجة التطبيقات Reactive Streams API المطلوبة لتنفيذ ما يلي:

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

شغِّل التطبيق، وانتقِل إلى قائمة عناصر التحكّم في الجهاز، واطّلِع على عناصر التحكّم في الإضاءة وال termostat.

صورة تعرض وحدة تحكّم في الترموستات والإضاءة
الشكل 6. عناصر التحكّم في الإضاءة والترموستات