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

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

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

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

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

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

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

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

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

إنشاء الخدمة

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

تفترض واجهة برمجة تطبيقات ControlsProviderService الإلمام بساحة المشاركات التفاعلية، على النحو الموضّح في مشروع Reactive Streams على 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. عناصر التحكّم في الإضاءة والترموستات