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

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

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

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

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

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

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

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

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

إنشاء الخدمة

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

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

class MyCustomControlService : 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:

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() {
    // ...
}

إنشاء ناشرين لعناصر التحكّم

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

class MyCustomControlService : ControlsProviderService() {

    override fun createPublisherForAllAvailable(): Flow.Publisher<Control> =
        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<String>): Flow.Publisher<Control> {
        TODO()
    }

    override fun performControlAction(
        controlId: String,
        action: ControlAction,
        consumer: Consumer<Int>,
    ) {
        TODO()
    }
}

اسحب قائمة النظام للأسفل وابحث عن زر عناصر تحكُّم الجهاز، كما هو موضّح في الشكل 4:

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

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

قائمة النظام تعرض عنصر تحكّم في الإضاءة والترموستات
الشكل 5. عناصر التحكّم في الإضاءة والترموستات التي تريد إضافتها

الآن، نفِّذ الطريقة createPublisherFor، وأضِف ما يلي إلى Service:

private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.IO + job)
private val controlFlows = mutableMapOf<String, MutableSharedFlow<Control>>()

private var toggleState = false
private var rangeState = 18f

override fun createPublisherFor(controlIds: List<String>): Flow.Publisher<Control> {
    val flow = MutableSharedFlow<Control>(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 <T> 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()
}

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

تستخدم الطريقة createPublisherFor كوروتينات وFlow في Kotlin لتلبية متطلبات Reactive Streams API من خلال تنفيذ ما يلي:

  1. تنشئ هذه الدالة Flow.
  2. تنتظر هذه السمة لمدة ثانية واحدة.
  3. تنشئ حالة المصباح الذكي وتنقلها.
  4. ينتظر لمدة ثانية أخرى.
  5. تنشئ هذه السمة حالة الترموستات وتعرضها.

التعامل مع الإجراءات

تشير الطريقة performControlAction إلى الوقت الذي يتفاعل فيه المستخدم مع عنصر تحكّم منشور. يحدّد نوع ControlAction المُرسَل الإجراء. نفِّذ الإجراء المناسب لعنصر التحكّم المحدّد، ثم عدِّل حالة الجهاز في واجهة مستخدم Android.

لإكمال المثال، أضِف ما يلي إلى Service:

override fun performControlAction(
    controlId: String,
    action: ControlAction,
    consumer: Consumer<Int>,
) {
    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)
}

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

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