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

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

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

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

رابط کاربری

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

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

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

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

ایجاد سرویس

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

‫ControlsProviderService API آشنایی با جاری‌سازی‌های واکنش‌گرا را، همان‌گونه که در پروژه GitHub «جاری‌سازی‌های واکنش‌گرا» تعریف شده است و در واسط‌های «جاری‌سازی جاوا ۹» پیاده‌سازی شده است، پیش‌فرض می‌گیرد. این «میانای برنامه‌سازی کاربردی» براساس مفاهیم زیر ساخته شده است:

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

اعلام سرویس

برنامه شما باید سرویسی مثل 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"
}

کاتلین

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

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

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