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

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

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

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

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

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

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

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

لوحة درجة الحرارة في الحالة المفتوحة
الشكل 3 افتح لوحة درجة الحرارة.

إنشاء الخدمة

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

تفترض واجهة برمجة التطبيقات ControlsProviderService API أنّك على دراية بمفهوم التدفقات التفاعلية، كما هو موضّح في مشروع Reactive Streams على GitHub ومطبَّق في واجهات Java 9 Flow. تستند واجهة برمجة التطبيقات إلى المفاهيم التالية:

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

الإفصاح عن الخدمة

يجب أن يعرّف تطبيقك خدمة، مثل 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:

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 وظائف Kotlin coroutines وKotlin flows لتلبية متطلبات واجهة برمجة التطبيقات 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 عناصر التحكّم في الإضاءة والترموستات