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

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

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

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

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

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

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

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

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

إنشاء الخدمة

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

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

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

الإعلان عن الخدمة

يجب أن يذكر تطبيقك خدمة، مثل 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 عندما تكون حالة عنصر التحكّم غير معروفة.
  • استخدِم فئة أداة الإنشاء 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> 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();
    }
    

في هذا المثال، تحتوي طريقة createPublisherFor() على تنفيذ وهمي لما يجب أن يفعله تطبيقك: التواصل مع جهازك لاسترداد حالته، وإرسال هذه الحالة إلى النظام.

تستخدِم الطريقة 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());
        }
    }
    

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

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