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

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

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

برای افزودن این پشتیبانی، یک 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() {
        ...
    }
    

جاوا

    public class MyCustomJavaControlService extends 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() {
      ...
    }
    

جاوا

    public class MyCustomJavaControlService extends ControlsProviderService {
 
    private final int LIGHT_ID = 1337;
    private final String LIGHT_TITLE = "My fancy light";
    private final int LIGHT_TYPE = DeviceTypes.TYPE_LIGHT;
    private final int THERMOSTAT_ID = 1338;
    private final String THERMOSTAT_TITLE = "My fancy thermostat";
    private final int THERMOSTAT_TYPE = DeviceTypes.TYPE_THERMOSTAT;
 
    ...
    }
    

ایجاد ناشران برای کنترل‌ها

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

  • createPublisherForAllAvailable() : یک Publisher برای تمام کنترل‌های موجود در برنامه شما ایجاد می‌کند. از Control.StatelessBuilder() برای ساخت اشیاء Control برای این ناشر استفاده کنید.
  • createPublisherFor() : یک Publisher برای لیستی از کنترل‌های داده شده، همانطور که توسط شناسه‌های رشته‌ای آنها مشخص شده‌اند، ایجاد می‌کند. از Control.StatefulBuilder برای ساخت این اشیاء Control استفاده کنید، زیرا ناشر باید به هر کنترل یک وضعیت اختصاص دهد.

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

وقتی برنامه شما برای اولین بار کنترل‌ها را در رابط کاربری سیستم منتشر می‌کند، برنامه از وضعیت هر کنترل اطلاعی ندارد. دریافت وضعیت می‌تواند یک عملیات زمان‌بر باشد که شامل مراحل زیادی در شبکه ارائه‌دهنده دستگاه می‌شود. از متد 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 =
          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): Flow.Publisher {
           TODO()
        }
 
        override fun performControlAction(
            controlId: String,
            action: ControlAction,
            consumer: Consumer
        ) {
            TODO()
        }
    }
    

جاوا

    public class MyCustomJavaControlService extends ControlsProviderService {
 
        private final int LIGHT_ID = 1337;
        private final String LIGHT_TITLE = "My fancy light";
        private final int LIGHT_TYPE = DeviceTypes.TYPE_LIGHT;
        private final int THERMOSTAT_ID = 1338;
        private final String THERMOSTAT_TITLE = "My fancy thermostat";
        private final int THERMOSTAT_TYPE = DeviceTypes.TYPE_THERMOSTAT;
 
        private boolean toggleState = false;
        private float rangeState = 18f;
        private final Map<String, ReplayProcessor> controlFlows = new HashMap<>();
 
        @NonNull
        @Override
        public Flow.Publisher createPublisherForAllAvailable() {
            List controls = new ArrayList<>();
            controls.add(createStatelessControl(LIGHT_ID, LIGHT_TITLE, LIGHT_TYPE));
            controls.add(createStatelessControl(THERMOSTAT_ID, THERMOSTAT_TITLE, THERMOSTAT_TYPE));
            return FlowAdapters.toFlowPublisher(Flowable.fromIterable(controls));
        }
 
        @NonNull
        @Override
        public Flow.Publisher createPublisherFor(@NonNull List controlIds) {
            ReplayProcessor updatePublisher = ReplayProcessor.create();
 
            controlIds.forEach(control -> {
                controlFlows.put(control, updatePublisher);
                updatePublisher.onNext(createLight());
                updatePublisher.onNext(createThermostat());
            });
 
            return FlowAdapters.toFlowPublisher(updatePublisher);
        }
    }
    

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

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

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

تصویری که منوی سیستم حاوی کنترل نور و ترموستات را نشان می‌دهد
شکل ۵. کنترل‌های نور و ترموستات برای اضافه کردن

حالا، متد createPublisherFor() را پیاده‌سازی کنید و موارد زیر را به Service خود اضافه کنید:

کاتلین

    private val job = SupervisorJob()
    private val scope = CoroutineScope(Dispatchers.IO + job)
    private val controlFlows = mutableMapOf<String, MutableSharedFlow>()
 
    private var toggleState = false
    private var rangeState = 18f
 
    override fun createPublisherFor(controlIds: List): Flow.Publisher {
        val flow = MutableSharedFlow(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  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()
    }
 
    

جاوا

    @NonNull
    @Override
    public Flow.Publisher createPublisherFor(@NonNull List controlIds) {
        ReplayProcessor updatePublisher = ReplayProcessor.create();
 
        controlIds.forEach(control -> {
            controlFlows.put(control, updatePublisher);
            updatePublisher.onNext(createLight());
            updatePublisher.onNext(createThermostat());
        });
 
        return FlowAdapters.toFlowPublisher(updatePublisher);
    }
 
    private Control createStatelessControl(int id, String title, int type) {
        Intent intent = new Intent(this, MainActivity.class)
                .putExtra(EXTRA_MESSAGE, title)
                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        PendingIntent action = PendingIntent.getActivity(
                this,
                id,
                intent,
                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
        );
 
        return new Control.StatelessBuilder(id + "", action)
                .setTitle(title)
                .setDeviceType(type)
                .build();
    }
 
    private Control createLight() {
        return createStatefulControl(
                LIGHT_ID,
                LIGHT_TITLE,
                LIGHT_TYPE,
                toggleState,
                new ToggleTemplate(
                        LIGHT_ID + "",
                        new ControlButton(
                                toggleState,
                                String.valueOf(toggleState).toUpperCase(Locale.getDefault())
                        )
                )
        );
    }
 
    private Control createThermostat() {
        return createStatefulControl(
                THERMOSTAT_ID,
                THERMOSTAT_TITLE,
                THERMOSTAT_TYPE,
                rangeState,
                new RangeTemplate(
                        THERMOSTAT_ID + "",
                        15f,
                        25f,
                        rangeState,
                        0.1f,
                        "%1.1f"
                )
        );
    }
 
    private  Control createStatefulControl(int id, String title, int type, T state, ControlTemplate template) {
        Intent intent = new Intent(this, MainActivity.class)
                .putExtra(EXTRA_MESSAGE, "$title $state")
                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        PendingIntent action = PendingIntent.getActivity(
                this,
                id,
                intent,
                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
        );
 
        return new Control.StatefulBuilder(id + "", action)
                .setTitle(title)
                .setDeviceType(type)
                .setStatus(Control.STATUS_OK)
                .setControlTemplate(template)
                .build();
    }
    

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

متد createPublisherFor() از کوروتین‌ها و جریان‌های کاتلین برای برآورده کردن API مورد نیاز Reactive Streams با انجام موارد زیر استفاده می‌کند:

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

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

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

برای تکمیل مثال، موارد زیر را به Service خود اضافه کنید:

کاتلین

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

جاوا

    @Override
    public void performControlAction(@NonNull String controlId, @NonNull ControlAction action, @NonNull Consumer consumer) {
        ReplayProcessor processor = controlFlows.get(controlId);
        if (processor == null) return;
 
        if (controlId.equals(LIGHT_ID + "")) {
            consumer.accept(ControlAction.RESPONSE_OK);
            if (action instanceof BooleanAction) toggleState = ((BooleanAction) action).getNewState();
            processor.onNext(createLight());
        }
        if (controlId.equals(THERMOSTAT_ID + "")) {
            consumer.accept(ControlAction.RESPONSE_OK);
            if (action instanceof FloatAction) rangeState = ((FloatAction) action).getNewValue()
            processor.onNext(createThermostat());
        }
    }
    

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

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