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

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

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

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

رابط کاربری

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

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

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

ابزاره پانل دما در حالت باز
شکل ۳. ابزاره پانل دما را باز کن.

ایجاد سرویس

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

ControlsProviderService API assumes familiarity with reactive streams, as defined in the Reactive Streams GitHub project and implemented in the Java 9 Flow interfaces. این API براساس مفاهیم زیر ساخته شده است:

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

اعلام سرویس

برنامه شما باید سرویسی (مانند 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 برای نشان دادن کنترلی استفاده می‌شود که قابلیت لمس را ارائه می‌دهد اما وضعیت آن قابل‌تعیین نیست، مثل کنترل ازراه‌دور تلویزیون IR. می‌توانید از این الگو برای تعریف روال یا ماکرو استفاده کنید که مجموعه‌ای از تغییرات کنترل و وضعیت است.

با این اطلاعات، می‌توانید کنترل را ایجاد کنید:

  • وقتی وضعیت کنترل نامشخص است، از کلاس سازنده 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"
}

کاتلین

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()
    }
}

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

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

با تک‌ضرب زدن روی کنترل‌های دستگاه، به صفحه دوم پیمایش می‌کنید که در آنجا می‌توانید برنامه‌تان را انتخاب کنید. پس‌از انتخاب برنامه‌تان، می‌بینید که قطعه کد قبلی چگونه منو سیستم سفارشی‌ای ایجاد می‌کند که کنترل‌های جدیدتان را نشان می‌دهد، همان‌طور که در شکل ۵ نشان داده شده است:

منوی سیستم که کنترل چراغ و دماپا را نشان می‌دهد
شکل ۵. کنترل‌های چراغ و دماپا برای افزودن.

اکنون، روش 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 برای برآورده کردن «میانای برنامه‌سازی کاربردی جریان‌های واکنشی» موردنیاز با انجام کارهای زیر استفاده می‌کند:

  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)
}

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

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