في 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:
أنيق
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() أنماط "كوروتين" في Kotlin وFlows لتلبية واجهة برمجة التطبيقات المطلوبة لـ 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()); } }
شغِّل التطبيق، وافتح قائمة عناصر التحكّم في الأجهزة ، واطّلِع على عناصر التحكّم في الإضاءة والترموستات.