في نظام التشغيل Android 11 والإصدارات الأحدث، تتيح ميزة "الوصول السريع إلى عناصر التحكّم في الأجهزة" للمستخدم عرض الأجهزة الخارجية والتحكّم فيها بسرعة، مثل المصابيح وأجهزة الترموستات والكاميرات، وذلك من خلال عنصر تحكّم للمستخدم ضمن ثلاث تفاعلات من مشغّل التطبيقات التلقائي. يختار مصنّع الجهاز مشغّل التطبيقات الذي يستخدمه. يمكن لمجمّعات الأجهزة، مثل Google Home، وتطبيقات البائعين الخارجيين توفير أجهزة للعرض في هذه المساحة. توضّح لك هذه الصفحة كيفية عرض عناصر التحكّم في الأجهزة في هذه المساحة وربطها بتطبيق التحكّم.
لإضافة هذه الميزة، أنشئ ControlsProviderService
وحدِّده. أنشئ عناصر التحكّم التي يتيحها تطبيقك استنادًا إلى أنواع عناصر التحكّم المحدّدة مسبقًا، ثم أنشئ ناشرين لعناصر التحكّم هذه.
واجهة المستخدم
يتم عرض الأجهزة ضمن عناصر التحكّم بالأجهزة كتطبيقات مصغّرة مستندة إلى نماذج. تتوفّر خمسة أدوات للتحكّم في الأجهزة، كما هو موضّح في الشكل التالي:
![]() |
![]() |
![]() |
![]() |
![]() |
من خلال النقر مع الاستمرار على تطبيق مصغّر، يمكنك الانتقال إلى التطبيق للتحكّم فيه بشكل أفضل. يمكنك تخصيص الرمز واللون في كل أداة، ولكن للحصول على أفضل تجربة للمستخدم، استخدِم الرمز واللون التلقائيين إذا كانت المجموعة التلقائية تتطابق مع الجهاز.

إنشاء الخدمة
يوضّح هذا القسم كيفية إنشاء
ControlsProviderService
.
تُعلم هذه الخدمة واجهة مستخدم نظام التشغيل Android بأنّ تطبيقك يتضمّن عناصر تحكّم في الأجهزة
يجب عرضها في منطقة عناصر التحكّم في الأجهزة ضمن واجهة مستخدم Android.
تفترض واجهة برمجة التطبيقات ControlsProviderService
معرفة بمصادر البيانات التفاعلية، كما هو محدّد في مشروع Reactive Streams على GitHub
وتم تنفيذه في واجهات Java 9 Flow.
تم تصميم واجهة برمجة التطبيقات استنادًا إلى المفاهيم التالية:
- الناشر: تطبيقك هو الناشر.
- المشترك: واجهة مستخدم النظام هي المشترك ويمكنها طلب عدد من عناصر التحكّم من الناشر.
- الاشتراك: هو الإطار الزمني الذي يمكن للناشر خلاله إرسال إشعارات إلى واجهة مستخدم النظام. يمكن للناشر أو المشترك إغلاق هذه النافذة.
تعريف الخدمة
يجب أن يحدّد تطبيقك خدمة، مثل 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()
:
Kotlin
class MyCustomControlService : ControlsProviderService() { ... }
Java
public class MyCustomJavaControlService extends ControlsProviderService { ... }
اختيار نوع عنصر التحكّم الصحيح
توفّر واجهة برمجة التطبيقات طرق إنشاء لإنشاء عناصر التحكّم. لملء أداة الإنشاء، حدِّد الجهاز الذي تريد التحكّم فيه وطريقة تفاعل المستخدم معه. اتّبِع الخطوات التالية:
- اختَر نوع الجهاز الذي يمثّله عنصر التحكّم. فئة
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
:
Kotlin
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() { ... }
Java
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
المنشئ، لأنّ حالة كل عنصر تحكّم غير معروفة.
بعد ظهور عناصر التحكّم في واجهة مستخدم Android، يمكن للمستخدم اختيار عناصر التحكّم المفضّلة.
لاستخدام كوروتينات Kotlin لإنشاء ControlsProviderService
، أضِف تبعية جديدة إلى build.gradle
:
Groovy
dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk9:1.6.4" }
Kotlin
dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk9:1.6.4") }
بعد مزامنة ملفات Gradle، أضِف المقتطف التالي إلى Service
لتنفيذ createPublisherForAllAvailable()
:
Kotlin
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() } }
Java
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
:
Kotlin
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() }
Java
@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()
كوروتينات وFlows في Kotlin لتلبية متطلبات واجهة برمجة التطبيقات Reactive Streams من خلال تنفيذ ما يلي:
- تنشئ هذه الدالة
Flow
. - تنتظر لمدة ثانية واحدة.
- تنشئ حالة المصباح الذكي وتنقلها.
- ينتظر لمدة ثانية أخرى.
- تنشئ حالة الترموستات وترسلها.
التعامل مع الإجراءات
تشير الطريقة performControlAction()
إلى وقت تفاعل المستخدم مع عنصر تحكّم منشور. يحدّد نوع ControlAction
المُرسَل الإجراء.
نفِّذ الإجراء المناسب لعنصر التحكّم المحدّد، ثم عدِّل حالة الجهاز في واجهة مستخدم Android.
لإكمال المثال، أضِف ما يلي إلى Service
:
Kotlin
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) }
Java
@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()); } }
شغِّل التطبيق، وافتح قائمة عناصر التحكّم في الأجهزة، واطّلِع على عناصر التحكّم في الإضاءة والترموستات.
