کنترل دستگاه های خارجی

در اندروید ۱۱ و بالاتر، ویژگی کنترل‌های دستگاه با دسترسی سریع (Quick Access Device Controls) به کاربران امکان می‌دهد تا به سرعت دستگاه‌های خارجی مانند چراغ‌ها، ترموستات‌ها و دوربین‌ها را از طریق یک لانچر پیش‌فرض و تنها با سه تعامل مشاهده و کنترل کنند. تولیدکننده اصلی دستگاه انتخاب می‌کند که از کدام لانچر استفاده شود. تجمیع‌کننده‌های دستگاه - به عنوان مثال، Google Home - و برنامه‌های فروشندگان شخص ثالث می‌توانند دستگاه‌هایی را برای نمایش در این فضا ارائه دهند. این صفحه به شما نشان می‌دهد که چگونه کنترل‌های دستگاه را در این فضا نمایش دهید و آنها را به برنامه کنترل خود پیوند دهید.

شکل ۱. فضای کنترل دستگاه در رابط کاربری اندروید.

برای افزودن این پشتیبانی، یک ControlsProviderService ایجاد و اعلان کنید. کنترل‌هایی را که برنامه شما پشتیبانی می‌کند بر اساس انواع کنترل‌های از پیش تعریف شده ایجاد کنید و سپس ناشرانی برای این کنترل‌ها ایجاد کنید.

رابط کاربری

دستگاه‌ها در بخش کنترل‌های دستگاه به صورت ویجت‌های قالب‌بندی‌شده نمایش داده می‌شوند. پنج ویجت کنترل دستگاه در دسترس هستند، همانطور که در شکل زیر نشان داده شده است:

ویجت را برای کنترل‌های دستگاه تغییر دهید
ضامن
با ویجت کشویی تغییر وضعیت دهید
با اسلایدر تغییر وضعیت دهید
ویجت اسلایدر محدوده برای کنترل‌های دستگاه
محدوده (قابل فعال یا غیرفعال کردن نیست)
ویجت کنترل ضامن بدون وضعیت
ضامن بدون تابعیت
ویجت پنل دما در حالت بسته
پنل دما (بسته)
شکل ۲. مجموعه‌ای از ویجت‌های قالب‌بندی‌شده.

لمس و نگه داشتن یک ویجت شما را برای کنترل عمیق‌تر به برنامه می‌برد. می‌توانید آیکون و رنگ هر ویجت را سفارشی کنید، اما برای بهترین تجربه کاربری، اگر تنظیمات پیش‌فرض با دستگاه شما مطابقت دارد، از آیکون و رنگ پیش‌فرض استفاده کنید.

ویجت پنل دما در حالت باز
شکل ۳. ویجت پنل دما را باز کنید.

سرویس را ایجاد کنید

این بخش نحوه ایجاد ControlsProviderService را نشان می‌دهد. این سرویس به رابط کاربری سیستم اندروید می‌گوید که برنامه شما شامل کنترل‌های دستگاه است که باید در ناحیه کنترل‌های دستگاه رابط کاربری اندروید نمایش داده شوند.

API ControlsProviderService آشنایی با جریان‌های واکنشی (reactive streams) را که در پروژه گیت‌هاب Reactive Streams تعریف و در رابط‌های Flow جاوا ۹ پیاده‌سازی شده‌اند، فرض می‌کند. این API بر اساس مفاهیم زیر بنا شده است:

  • ناشر: برنامه شما ناشر است.
  • مشترک: رابط کاربری سیستم، مشترک است و می‌تواند تعدادی کنترل از ناشر درخواست کند.
  • اشتراک: بازه زمانی که ناشر می‌تواند به‌روزرسانی‌ها را به رابط کاربری سیستم ارسال کند. ناشر یا مشترک می‌توانند این پنجره را ببندند.

اعلام سرویس

برنامه شما باید یک سرویس - مانند 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>

در مرحله بعد، یک فایل کاتلین جدید با نام MyCustomControlService.kt ایجاد کنید و آن را ControlsProviderService ارث‌بری کنید:

class MyCustomControlService : ControlsProviderService() {
    // ...
}

نوع کنترل صحیح را انتخاب کنید

API متدهای سازنده را برای ایجاد کنترل‌ها ارائه می‌دهد. برای پر کردن سازنده، دستگاهی را که می‌خواهید کنترل کنید و نحوه تعامل کاربر با آن را تعیین کنید. مراحل زیر را انجام دهید:

  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 Control.StatefulBuilder استفاده کنید، زیرا ناشر باید به هر کنترل یک وضعیت (state) اختصاص دهد.

ناشر را ایجاد کنید

وقتی برنامه شما برای اولین بار کنترل‌ها را در رابط کاربری سیستم منتشر می‌کند، برنامه از وضعیت هر کنترل اطلاعی ندارد. دریافت وضعیت می‌تواند یک عملیات زمان‌بر باشد که شامل مراحل زیادی در شبکه ارائه‌دهنده دستگاه می‌شود. از متد createPublisherForAllAvailable برای اعلام کنترل‌های موجود به سیستم استفاده کنید. این متد از کلاس سازنده Control.StatelessBuilder استفاده می‌کند، زیرا وضعیت هر کنترل ناشناخته است.

وقتی کنترل‌ها در رابط کاربری اندروید ظاهر شدند، کاربران می‌توانند کنترل‌های مورد علاقه خود را انتخاب کنند.

برای استفاده از کوروتین‌های کاتلین برای ایجاد یک ControlsProviderService ، یک وابستگی جدید به build.gradle خود اضافه کنید:

گرووی

dependencies {
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk9:1.6.4"
}

کاتلین

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 نشان داده شده است، پیدا کنید:

رابط کاربری سیستم برای کنترل دستگاه
شکل ۴. کنترل‌های دستگاه در منوی سیستم.

با ضربه زدن روی کنترل‌های دستگاه ، به صفحه دوم هدایت می‌شوید که می‌توانید برنامه خود را در آنجا انتخاب کنید. پس از انتخاب برنامه، می‌بینید که چگونه قطعه کد قبلی یک منوی سیستم سفارشی ایجاد می‌کند که کنترل‌های جدید شما را نشان می‌دهد، همانطور که در شکل 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 از کوروتین‌ها و جریان‌های کاتلین برای برآورده کردن API مورد نیاز Reactive Streams با انجام موارد زیر استفاده می‌کند:

  1. Flow ایجاد می‌کند.
  2. یک ثانیه صبر می‌کند.
  3. حالت نور هوشمند را ایجاد و منتشر می‌کند.
  4. یک ثانیه دیگر صبر می‌کند.
  5. وضعیت ترموستات را ایجاد و منتشر می‌کند.

رسیدگی به اقدامات

متد performControlAction زمانی که کاربر با یک کنترل منتشر شده تعامل می‌کند، سیگنال می‌دهد. نوع ControlAction ارسالی، اقدام مورد نظر را تعیین می‌کند. اقدام مناسب را برای کنترل داده شده انجام دهید و سپس وضعیت دستگاه را در رابط کاربری اندروید به‌روزرسانی کنید.

برای تکمیل مثال، موارد زیر را به 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)
}

برنامه را اجرا کنید، به منوی کنترل‌های دستگاه دسترسی پیدا کنید و کنترل‌های نور و ترموستات خود را مشاهده کنید.

کنترل‌هایی که چراغ و ترموستات را نشان می‌دهند
شکل ۶. کنترل‌های نور و ترموستات.
،

در اندروید ۱۱ و بالاتر، ویژگی کنترل‌های دستگاه با دسترسی سریع (Quick Access Device Controls) به کاربران امکان می‌دهد تا به سرعت دستگاه‌های خارجی مانند چراغ‌ها، ترموستات‌ها و دوربین‌ها را از طریق یک لانچر پیش‌فرض و تنها با سه تعامل مشاهده و کنترل کنند. تولیدکننده اصلی دستگاه انتخاب می‌کند که از کدام لانچر استفاده شود. تجمیع‌کننده‌های دستگاه - به عنوان مثال، Google Home - و برنامه‌های فروشندگان شخص ثالث می‌توانند دستگاه‌هایی را برای نمایش در این فضا ارائه دهند. این صفحه به شما نشان می‌دهد که چگونه کنترل‌های دستگاه را در این فضا نمایش دهید و آنها را به برنامه کنترل خود پیوند دهید.

شکل ۱. فضای کنترل دستگاه در رابط کاربری اندروید.

برای افزودن این پشتیبانی، یک ControlsProviderService ایجاد و اعلان کنید. کنترل‌هایی را که برنامه شما پشتیبانی می‌کند بر اساس انواع کنترل‌های از پیش تعریف شده ایجاد کنید و سپس ناشرانی برای این کنترل‌ها ایجاد کنید.

رابط کاربری

دستگاه‌ها در بخش کنترل‌های دستگاه به صورت ویجت‌های قالب‌بندی‌شده نمایش داده می‌شوند. پنج ویجت کنترل دستگاه در دسترس هستند، همانطور که در شکل زیر نشان داده شده است:

ویجت را برای کنترل‌های دستگاه تغییر دهید
ضامن
با ویجت کشویی تغییر وضعیت دهید
با اسلایدر تغییر وضعیت دهید
ویجت اسلایدر محدوده برای کنترل‌های دستگاه
محدوده (قابل فعال یا غیرفعال کردن نیست)
ویجت کنترل ضامن بدون وضعیت
ضامن بدون تابعیت
ویجت پنل دما در حالت بسته
پنل دما (بسته)
شکل ۲. مجموعه‌ای از ویجت‌های قالب‌بندی‌شده.

لمس و نگه داشتن یک ویجت شما را برای کنترل عمیق‌تر به برنامه می‌برد. می‌توانید آیکون و رنگ هر ویجت را سفارشی کنید، اما برای بهترین تجربه کاربری، اگر تنظیمات پیش‌فرض با دستگاه شما مطابقت دارد، از آیکون و رنگ پیش‌فرض استفاده کنید.

ویجت پنل دما در حالت باز
شکل ۳. ویجت پنل دما را باز کنید.

سرویس را ایجاد کنید

این بخش نحوه ایجاد ControlsProviderService را نشان می‌دهد. این سرویس به رابط کاربری سیستم اندروید می‌گوید که برنامه شما شامل کنترل‌های دستگاه است که باید در ناحیه کنترل‌های دستگاه رابط کاربری اندروید نمایش داده شوند.

API ControlsProviderService آشنایی با جریان‌های واکنشی (reactive streams) را که در پروژه گیت‌هاب Reactive Streams تعریف و در رابط‌های Flow جاوا ۹ پیاده‌سازی شده‌اند، فرض می‌کند. این API بر اساس مفاهیم زیر بنا شده است:

  • ناشر: برنامه شما ناشر است.
  • مشترک: رابط کاربری سیستم، مشترک است و می‌تواند تعدادی کنترل از ناشر درخواست کند.
  • اشتراک: بازه زمانی که ناشر می‌تواند به‌روزرسانی‌ها را به رابط کاربری سیستم ارسال کند. ناشر یا مشترک می‌توانند این پنجره را ببندند.

اعلام سرویس

برنامه شما باید یک سرویس - مانند 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>

در مرحله بعد، یک فایل کاتلین جدید با نام MyCustomControlService.kt ایجاد کنید و آن را ControlsProviderService ارث‌بری کنید:

class MyCustomControlService : ControlsProviderService() {
    // ...
}

نوع کنترل صحیح را انتخاب کنید

API متدهای سازنده را برای ایجاد کنترل‌ها ارائه می‌دهد. برای پر کردن سازنده، دستگاهی را که می‌خواهید کنترل کنید و نحوه تعامل کاربر با آن را تعیین کنید. مراحل زیر را انجام دهید:

  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 Control.StatefulBuilder استفاده کنید، زیرا ناشر باید به هر کنترل یک وضعیت (state) اختصاص دهد.

ناشر را ایجاد کنید

وقتی برنامه شما برای اولین بار کنترل‌ها را در رابط کاربری سیستم منتشر می‌کند، برنامه از وضعیت هر کنترل اطلاعی ندارد. دریافت وضعیت می‌تواند یک عملیات زمان‌بر باشد که شامل مراحل زیادی در شبکه ارائه‌دهنده دستگاه می‌شود. از متد createPublisherForAllAvailable برای اعلام کنترل‌های موجود به سیستم استفاده کنید. این متد از کلاس سازنده Control.StatelessBuilder استفاده می‌کند، زیرا وضعیت هر کنترل ناشناخته است.

وقتی کنترل‌ها در رابط کاربری اندروید ظاهر شدند، کاربران می‌توانند کنترل‌های مورد علاقه خود را انتخاب کنند.

برای استفاده از کوروتین‌های کاتلین برای ایجاد یک ControlsProviderService ، یک وابستگی جدید به build.gradle خود اضافه کنید:

گرووی

dependencies {
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk9:1.6.4"
}

کاتلین

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 نشان داده شده است، پیدا کنید:

رابط کاربری سیستم برای کنترل دستگاه
شکل ۴. کنترل‌های دستگاه در منوی سیستم.

با ضربه زدن روی کنترل‌های دستگاه ، به صفحه دوم هدایت می‌شوید که می‌توانید برنامه خود را در آنجا انتخاب کنید. پس از انتخاب برنامه، می‌بینید که چگونه قطعه کد قبلی یک منوی سیستم سفارشی ایجاد می‌کند که کنترل‌های جدید شما را نشان می‌دهد، همانطور که در شکل 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 از کوروتین‌ها و جریان‌های کاتلین برای برآورده کردن API مورد نیاز Reactive Streams با انجام موارد زیر استفاده می‌کند:

  1. Flow ایجاد می‌کند.
  2. یک ثانیه صبر می‌کند.
  3. حالت نور هوشمند را ایجاد و منتشر می‌کند.
  4. یک ثانیه دیگر صبر می‌کند.
  5. وضعیت ترموستات را ایجاد و منتشر می‌کند.

رسیدگی به اقدامات

متد performControlAction زمانی که کاربر با یک کنترل منتشر شده تعامل می‌کند، سیگنال می‌دهد. نوع ControlAction ارسالی، اقدام مورد نظر را تعیین می‌کند. اقدام مناسب را برای کنترل داده شده انجام دهید و سپس وضعیت دستگاه را در رابط کاربری اندروید به‌روزرسانی کنید.

برای تکمیل مثال، موارد زیر را به 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)
}

برنامه را اجرا کنید، به منوی کنترل‌های دستگاه دسترسی پیدا کنید و کنترل‌های نور و ترموستات خود را مشاهده کنید.

کنترل‌هایی که چراغ و ترموستات را نشان می‌دهند
شکل ۶. کنترل‌های نور و ترموستات.