في الإصدار 11 من نظام التشغيل Android والإصدارات الأحدث، تتيح ميزة "عناصر التحكّم في الأجهزة" ضمن "الوصول السريع" للمستخدم عرض الأجهزة الخارجية، مثل المصابيح والترموستات والكاميرات، والتحكّم فيها بسرعة من خلال تفاعلات المستخدم خلال ثلاث تفاعلات من أحد مشغّلات التطبيقات التلقائية. يختار المصنّع الأصلي للجهاز مشغّل التطبيقات الذي يستخدمه. يمكن لمجمّعي الأجهزة، مثل Google Home، وتطبيقات المورّدين الخارجيين، توفير الأجهزة لعرضها في هذه المساحة. توضّح لك هذه الصفحة كيفية عرض عناصر التحكّم في الأجهزة في هذه المساحة وربطها بتطبيق التحكّم.
لإضافة هذا الخيار، أنشئ ControlsProviderService
وأدخِله في البيان. أنشئ
عناصر التحكّم التي يتيحها تطبيقك استنادًا إلى أنواع عناصر التحكّم المحدّدة مسبقًا، ثم أنشئ
ناشرِين لهذه عناصر التحكّم.
واجهة المستخدم
يتم عرض الأجهزة ضمن عناصر التحكّم بالأجهزة كتطبيقات مصغّرة مستندة إلى نماذج. تتوفّر خمسة تطبيقات مصغّرة للتحكّم في الأجهزة، كما هو موضّح في الشكل التالي:
|
|
|
|
|
يؤدي النقر مع الاستمرار على تطبيق مصغّر إلى نقلك إلى التطبيق للتحكّم فيه بشكل أفضل. يمكنك تخصيص الرمز واللون في كل تطبيق مصغّر، ولكن للحصول على أفضل تجربة للمستخدم، استخدِم الرمز واللون التلقائيَين إذا كانت المجموعة التلقائية تتطابق مع الجهاز.
إنشاء الخدمة
يوضّح هذا القسم كيفية إنشاء
ControlsProviderService
.
تُعلم هذه الخدمة واجهة مستخدم نظام Android بأنّ تطبيقك يحتوي على عناصر تحكّم في الجهاز
يجب عرضها في منطقة عناصر التحكّم في الجهاز في واجهة مستخدم Android.
تفترض واجهة برمجة التطبيقات ControlsProviderService
معرفة بـ "البثّ التفاعلي"، على النحو المُحدَّد في مشروع "البثّ التفاعلي" على 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
builder عندما تكون حالة عنصر التحكّم غير معروفة. - استخدِم فئة ملف برمجي سازندة
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
builder، لأنّ حالة كلّ عنصر تحكّم
غير معروفة.
بعد ظهور عناصر التحكّم في واجهة مستخدم Android، يمكن للمستخدم اختيار عناصر التحكّم المفضّلة.
لاستخدام وظائف Kotlin المتكرّرة لإنشاء ControlsProviderService
، أضِف ملفًا جديدًا
للاعتماد إلى build.gradle
:
رائع
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()
على تنفيذ ملفعّل
لما يجب أن يفعله تطبيقك: التواصل مع جهازك ل retrieving its status، وعرض هذه الحالة على النظام.
تستخدِم الطريقة createPublisherFor()
وظائف Kotlin المتعدّدة المهام وعمليات Flow لتلبية متطلبات
واجهة برمجة التطبيقات Reactive Streams API المطلوبة من خلال تنفيذ ما يلي:
- لإنشاء
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()); } }
شغِّل التطبيق، وانتقِل إلى قائمة عناصر التحكّم في الجهاز، واطّلِع على عناصر التحكّم في الإضاءة والثرموستات.