در اندروید ۱۱ و نسخههای جدیدتر، ویژگی «کنترلهای دسترسی سریع دستگاه» به کاربر اجازه میدهد تا به سرعت دستگاههای خارجی مانند چراغها، ترموستاتها و دوربینها را از طریق یک لانچر پیشفرض و تنها با سه تعامل مشاهده و کنترل کند. تولیدکننده اصلی دستگاه، لانچر مورد استفاده را انتخاب میکند. تجمیعکنندههای دستگاه - به عنوان مثال، گوگل هوم - و برنامههای فروشندگان شخص ثالث میتوانند دستگاههایی را برای نمایش در این فضا ارائه دهند. این صفحه به شما نشان میدهد که چگونه کنترلهای دستگاه را در این فضا نمایش دهید و آنها را به برنامه کنترل خود پیوند دهید.
برای افزودن این پشتیبانی، یک 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 متدهای سازنده را برای ایجاد کنترلها ارائه میدهد. برای پر کردن سازنده، دستگاهی را که میخواهید کنترل کنید و نحوه تعامل کاربر با آن را تعیین کنید. مراحل زیر را انجام دهید:
- نوع دستگاهی که کنترل نشان میدهد را انتخاب کنید. کلاس
DeviceTypesیک شمارش از تمام دستگاههای پشتیبانی شده است. این نوع برای تعیین آیکونها و رنگهای دستگاه در رابط کاربری استفاده میشود. - نام کاربری، مکان دستگاه - مثلاً آشپزخانه - و سایر عناصر متنی رابط کاربری مرتبط با کنترل را تعیین کنید.
- بهترین الگو را برای پشتیبانی از تعامل کاربر انتخاب کنید. کنترلها از برنامه یک
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.PublishercreatePublisherFor(@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 با انجام موارد زیر استفاده میکند:
-
Flowایجاد میکند. - یک ثانیه صبر میکند.
- حالت نور هوشمند را ایجاد و منتشر میکند.
- یک ثانیه دیگر صبر میکند.
- وضعیت ترموستات را ایجاد و منتشر میکند.
رسیدگی به اقدامات
متد 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 Consumerconsumer) { 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()); } }
برنامه را اجرا کنید، به منوی کنترلهای دستگاه دسترسی پیدا کنید و کنترلهای نور و ترموستات خود را مشاهده کنید.





