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

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

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

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

رابط کاربری

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

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

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

تصویری که ویجت پانل دما را نشان می دهد (باز)
شکل 3. باز کردن ویجت پانل دمای باز.

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

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

ControlsProviderService API آشنایی با جریان های واکنشی را فرض می کند، همانطور که در پروژه Reactive Streams GitHub تعریف شده و در رابط های Java 9 Flow پیاده سازی شده است. 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() {
        ...
    }
    

جاوا

    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 برای نشان دادن کنترلی استفاده می‌شود که قابلیت لمس را فراهم می‌کند، اما وضعیت آن را نمی‌توان تعیین کرد، مانند کنترل تلویزیون 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() {
      ...
    }
    

جاوا

    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 استفاده می کند، زیرا وضعیت هر کنترل ناشناخته است.

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

برای استفاده از کوروتین های 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 =
          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);
        }
    }
    

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

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

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

تصویری که منوی سیستم حاوی کنترل نور و ترموستات را نشان می دهد
شکل 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() از کوروتین‌ها و جریان‌های Kotlin برای برآوردن API جریان‌های واکنشی مورد نیاز با انجام کارهای زیر استفاده می‌کند:

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

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

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