يمكن لتطبيق مفوَّض للنظام في AAOS استخدام واجهة برمجة التطبيقات Multi-Display Communications API للتواصل مع التطبيق نفسه (اسم الحزمة نفسه) الذي يتم تشغيله في منطقة occupant مختلفة في السيارة. توضّح هذه الصفحة كيفية دمج واجهة برمجة التطبيقات. لمزيد من المعلومات، يمكنك أيضًا الاطّلاع على CarOccupantZoneManager.OccupantZoneInfo.
منطقة الركاب
يربط مفهوم منطقة الأشخاص مستخدمًا بمجموعة من الشاشات. تحتوي كل منطقة انشغال مقعد على شاشة من النوع DISPLAY_TYPE_MAIN. قد تحتوي منطقة الركاب أيضًا على شاشات إضافية، مثل شاشة مجموعة. يتم تعيين مستخدم Android لكل منطقة ركاب. يكون لكل مستخدم حساباته وتطبيقاته الخاصة.
إعدادات الجهاز
لا تتوافق واجهة برمجة التطبيقات Comms API إلا مع نظام متكامل على الرقاقة (SoC) واحد. في طراز النظام المتكامل على الرقاقة الواحد، تعمل جميع مناطق المقيمين والمستخدمين على النظام المتكامل على الرقاقة نفسه. تتألف واجهة برمجة التطبيقات Comms API من ثلاثة مكوّنات:
تسمح Power management API للعميل بإدارة طاقة الشاشات في مناطق الركاب.
تسمح واجهة برمجة التطبيقات Discovery API للعميل بمراقبة حالات مناطق الركاب الأخرى في السيارة، ومراقبة التطبيقات المشابهة في مناطق الركاب هذه. استخدِم Discovery API قبل استخدام Connection API.
تسمح واجهة برمجة التطبيقات Connection API للعميل بالاتصال بعميل مماثل في منطقة مستخدم آخر وإرسال حمولة إلى العميل المماثل.
يجب توفّر Discovery API وConnection API لإجراء عملية الربط. إنّ واجهة برمجة التطبيقات Power management API اختيارية.
لا تتيح واجهة برمجة التطبيقات Comms API التواصل بين التطبيقات المختلفة. بدلاً من ذلك، تم تصميمه فقط للتواصل بين التطبيقات التي تحمل اسم الحزمة نفسه ويُستخدَم فقط للتواصل بين مستخدمين مختلفين يظهرون للجميع.
دليل الدمج
تنفيذ AbstractReceiverService
لتلقّي Payload
، يجب أن ينفذ التطبيق المستلِم الطرق المجردة
المحدّدة في AbstractReceiverService
. مثلاً:
public class MyReceiverService extends AbstractReceiverService {
@Override
public void onConnectionInitiated(@NonNull OccupantZoneInfo senderZone) {
}
@Override
public void onPayloadReceived(@NonNull OccupantZoneInfo senderZone,
@NonNull Payload payload) {
}
}
يتمّ استدعاء onConnectionInitiated()
عندما يطلب العميل المُرسِل
الاتصال بهذا العميل المستلِم. إذا كان تأكيد المستخدم مطلوبًا لإنشاء
الاتصال، يمكن لـ MyReceiverService
إلغاء هذه الطريقة لبدء
نشاط الإذن، واستدعاء acceptConnection()
أو rejectConnection()
استنادًا
إلى النتيجة. بخلاف ذلك، يمكن لـ "MyReceiverService
" الاتصال بـ "acceptConnection()
".
onPayloadReceived()is invoked when
MyReceiverServicehas received a
Payloadfrom the sender client.
MyReceiverService` يمكنه إلغاء
هذه الوسيلة لإجراء ما يلي:
- إعادة توجيه
Payload
إلى نقاط نهاية المستلِم المقابلة، إن توفّرت للحصول على نقاط نهاية جهاز الاستقبال المسجَّلة، اتصل بالرقمgetAllReceiverEndpoints()
. لتوجيهPayload
إلى نقطة نهاية مستلِم معيّنة، اتصل بالرقمforwardPayload()
.
أو
- تخزين
Payload
في ذاكرة التخزين المؤقت وإرساله عندما يتم تسجيل نقطة نهاية المستلِم المتوقّعة التي يتم إشعارMyReceiverService
بها من خلالonReceiverRegistered()
تعريف AbstractReceiverService
يجب أن يُعلِن التطبيق المستلِم عن AbstractReceiverService
المُطبَّقة في
ملف البيان، وأن يُضيف فلتر أهداف يتضمّن الإجراء
android.car.intent.action.RECEIVER_SERVICE
لهذه الخدمة، وأن يطلب إذن
android.car.occupantconnection.permission.BIND_RECEIVER_SERVICE
:
<service android:name=".MyReceiverService"
android:permission="android.car.occupantconnection.permission.BIND_RECEIVER_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.car.intent.action.RECEIVER_SERVICE" />
</intent-filter>
</service>
يضمن android.car.occupantconnection.permission.BIND_RECEIVER_SERVICE
الإذن
أنّه لا يمكن إلا للإطار العملي الربط بهذه الخدمة. إذا كانت هذه الخدمة
لا تتطلّب الإذن، قد يتمكّن تطبيق مختلف من الربط بهذه الخدمة
وإرسال Payload
إليها مباشرةً.
الإفصاح عن الإذن
يجب أن يعلن تطبيق العميل عن الأذونات في ملف البيان الخاص به.
<!-- This permission is needed for connection API -->
<uses-permission android:name="android.car.permission.MANAGE_OCCUPANT_CONNECTION"/>
<!-- This permission is needed for discovery API -->
<uses-permission android:name="android.car.permission.MANAGE_REMOTE_DEVICE"/>
<!-- This permission is needed if the client app calls CarRemoteDeviceManager#setOccupantZonePower() -->
<uses-permission android:name="android.car.permission.CAR_POWER"/>
كلّ من الأذونات الثلاثة أعلاه هي أذونات مميّزة، ويجب منحهم
مسبقًا من خلال ملفات القائمة المسموح بها. على سبيل المثال، في ما يلي ملف القائمة المسموح بها لتطبيق
MultiDisplayTest
:
// packages/services/Car/data/etc/com.google.android.car.multidisplaytest.xml
<permissions>
<privapp-permissions package="com.google.android.car.multidisplaytest">
… …
<permission name="android.car.permission.MANAGE_OCCUPANT_CONNECTION"/>
<permission name="android.car.permission.MANAGE_REMOTE_DEVICE"/>
<permission name="android.car.permission.CAR_POWER"/>
</privapp-permissions>
</permissions>
الحصول على مدراء السيارات
لاستخدام واجهة برمجة التطبيقات، يجب أن يسجِّل تطبيق العميل CarServiceLifecycleListener
لتلقّي مدراء السيارات المرتبطين:
private CarRemoteDeviceManager mRemoteDeviceManager;
private CarOccupantConnectionManager mOccupantConnectionManager;
private final Car.CarServiceLifecycleListener mCarServiceLifecycleListener = (car, ready) -> {
if (!ready) {
Log.w(TAG, "Car service crashed");
mRemoteDeviceManager = null;
mOccupantConnectionManager = null;
return;
}
mRemoteDeviceManager = car.getCarManager(CarRemoteDeviceManager.class);
mOccupantConnectionManager = car.getCarManager(CarOccupantConnectionManager.class);
};
Car.createCar(getContext(), /* handler= */ null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER,
mCarServiceLifecycleListener);
(المُرسِل) ميزة "اقتراحات"
قبل الاتصال بعميل المستلِم، يجب أن يكتشف العميل المُرسِل العميل المستلِم من خلال تسجيل CarRemoteDeviceManager.StateCallback
:
// The maps are accessed by the main thread only, so there is no multi-thread issue.
private final ArrayMap<OccupantZoneInfo, Integer> mOccupantZoneStateMap = new ArrayMap<>();
private final ArrayMap<OccupantZoneInfo, Integer> mAppStateMap = new ArrayMap<>();
private final StateCallback mStateCallback = new StateCallback() {
@Override
public void onOccupantZoneStateChanged(
@androidx.annotation.NonNull OccupantZoneInfo occupantZone,
int occupantZoneStates) {
mOccupantZoneStateMap.put(occupantZone, occupantZoneStates);
}
@Override
public void onAppStateChanged(
@androidx.annotation.NonNull OccupantZoneInfo occupantZone,
int appStates) {
mAppStateMap.put(occupantZone, appStates);
}
};
if (mRemoteDeviceManager != null) {
mRemoteDeviceManager.registerStateCallback(getActivity().getMainExecutor(),
mStateCallback);
}
قبل طلب الاتصال بجهاز الاستقبال، يجب أن يتأكّد المُرسِل من ضبط كل علامات منطقة مستخدم جهاز الاستقبال وتطبيق جهاز الاستقبال. وإلا، قد تحدث أخطاء. مثلاً:
private boolean canRequestConnectionToReceiver(OccupantZoneInfo receiverZone) {
Integer zoneState = mOccupantZoneStateMap.get(receiverZone);
if ((zoneState == null) || (zoneState.intValue() & (FLAG_OCCUPANT_ZONE_POWER_ON
// FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED is not implemented yet. Right now
// just ignore this flag.
// | FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
| FLAG_OCCUPANT_ZONE_CONNECTION_READY)) == 0) {
return false;
}
Integer appState = mAppStateMap.get(receiverZone);
if ((appState == null) ||
(appState.intValue() & (FLAG_CLIENT_INSTALLED
| FLAG_CLIENT_SAME_LONG_VERSION | FLAG_CLIENT_SAME_SIGNATURE
| FLAG_CLIENT_RUNNING | FLAG_CLIENT_IN_FOREGROUND)) == 0) {
return false;
}
return true;
}
ننصح المُرسِل بعدم طلب الاتصال بالمُستلِم إلا بعد ضبط جميع علامات المُستلِم. ومع ذلك، هناك استثناءات:
FLAG_OCCUPANT_ZONE_CONNECTION_READY
وFLAG_CLIENT_INSTALLED
هما الحدّ الأدنى من المتطلبات اللازمة لإنشاء اتصال.إذا كان تطبيق المُستلِم بحاجة إلى عرض واجهة مستخدم للحصول على موافقة المستخدم على الاتصال، يصبح
FLAG_OCCUPANT_ZONE_POWER_ON
وFLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
شرطَين إضافيَين. للحصول على تجربة أفضل للمستخدم، ننصحك أيضًا باستخدامFLAG_CLIENT_RUNNING
وFLAG_CLIENT_IN_FOREGROUND
، وإلا قد يتفاجأ المستخدم.في الوقت الحالي (Android 15)، لم يتم تنفيذ
FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
. ويمكن لتطبيق العميل تجاهلها.في الوقت الحالي (Android 15)، لا تتيح واجهة برمجة التطبيقات Comms API سوى استخدام مستخدمين متعدّدين على مثيل Android نفسه حتى تتمكّن التطبيقات المشابهة من الحصول على رمز الإصدار الطويل نفسه (
FLAG_CLIENT_SAME_LONG_VERSION
) والتوقيع نفسه (FLAG_CLIENT_SAME_SIGNATURE
). ونتيجةً لذلك، لا تحتاج التطبيقات إلى التحقّق من تطابق القيمتَين.
لتوفير تجربة أفضل للمستخدم، يمكن لبرنامج العميل المُرسِل عرض واجهة مستخدم إذا لم يتم تحديد علامة. على سبيل المثال، إذا لم يتم ضبط FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
، يمكن للمُرسِل
عرض إشعار عابر أو مربّع حوار لطلب فتح قفل شاشة
منطقة ركاب المستلِم.
عندما لا يحتاج المُرسِل إلى اكتشاف المستلِمين (على سبيل المثال، عندما يَعثر على جميع المستلِمين ويُنشئ اتصالات أو يصبح غير نشط)، يمكنه إيقاف عملية الاكتشاف.
if (mRemoteDeviceManager != null) {
mRemoteDeviceManager.unregisterStateCallback();
}
عند إيقاف ميزة "الاكتشاف"، لن تتأثر عمليات الربط الحالية. يمكن للمُرسِل
مواصلة إرسال Payload
إلى المستلِمين المرتبطين.
(المُرسِل) طلب الربط
عند ضبط جميع علامات جهاز الاستقبال، يمكن للمُرسِل طلب ربطه بجهاز الاستقبال:
private final ConnectionRequestCallback mRequestCallback = new ConnectionRequestCallback() {
@Override
public void onConnected(OccupantZoneInfo receiverZone) {
}
@Override
public void onFailed(OccupantZoneInfo receiverZone, int connectionError) {
}
@Override
public void onDisconnected(OccupantZoneInfo receiverZone) {
}
};
if (mOccupantConnectionManager != null && canRequestConnectionToReceiver(receiverZone)) {
mOccupantConnectionManager.requestConnection(receiverZone,
getActivity().getMainExecutor(), mRequestCallback);
}
(خدمة المستلِم) قبول عملية الربط
بعد أن يطلب المُرسِل الاتصال بالمُستلِم، سيتم ربط AbstractReceiverService
في تطبيق المُستلِم بخدمة السيارة،
وسيتم استدعاء AbstractReceiverService.onConnectionInitiated()
. كما هو موضح في (المُرسِل) طلب الاتصال،onConnectionInitiated()
هي طريقة مجردة ويجب أن ينفذها
تطبيق العميل.
عندما يقبل المستلِم طلب الربط، سيتمّ استدعاء
ConnectionRequestCallback.onConnected()
للمُرسِل، ثمّ سيتمّ تأسيس
العلاقة.
(المُرسِل) إرسال الحمولة
بعد إنشاء عملية الربط، يمكن للمُرسِل إرسال Payload
إلى
المُستلِم:
if (mOccupantConnectionManager != null) {
Payload payload = ...;
try {
mOccupantConnectionManager.sendPayload(receiverZone, payload);
} catch (CarOccupantConnectionManager.PayloadTransferException e) {
Log.e(TAG, "Failed to send Payload to " + receiverZone);
}
}
يمكن للمُرسِل وضع عنصر Binder
أو مصفوفة بايت في Payload
. إذا كان المُرسِل يحتاج إلى إرسال أنواع بيانات أخرى، يجب تسلسل البيانات في صفيف برمجي
، واستخدام صفيف البيانات لإنشاء عنصر Payload
وإرسالPayload
. بعد ذلك، يحصل العميل المستلِم على مصفوفة البايتات من الرسالة المستلَمة
Payload
، ويُعيد تسلسل مصفوفة البايتات إلى عنصر البيانات المتوقّع.
على سبيل المثال، إذا أراد المُرسِل إرسال سلسلة hello
إلى نقطة نهاية المُستلِم
بمعرّف FragmentB
، يمكنه استخدام Proto Buffers لتحديد نوع بيانات
على النحو التالي:
message MyData {
required string receiver_endpoint_id = 1;
required string data = 2;
}
يوضّح الشكل 1 عملية Payload
:
(خدمة المُستلِم) استلام الحمولة وإرسالها
بعد أن يتلقّى التطبيق المستلِم Payload
، سيتمّ استدعاء
AbstractReceiverService.onPayloadReceived()
. كما هو موضّح في إرسال الحمولة، onPayloadReceived()
هي Payload
طريقة مجردة ويجب أن ينفّذها تطبيق العميل. في هذه الطريقة، يمكن لPayload
العميل إعادة توجيه Payload
إلى نقاط نهاية المستلِم المقابلة، أو تخزين Payload
في ذاكرة التخزين المؤقت ثم إرساله بعد تسجيل نقطة نهاية المستلِم المتوقّعة.
(نقطة نهاية المُستلِم) التسجيل وإلغاء التسجيل
من المفترض أن يتصل تطبيق المستلِم بخدمة registerReceiver()
لتسجيل نقاط نهاية المستلِم. من حالات الاستخدام الشائعة أن يحتاج "الجزء" إلى تلقّي Payload
، لذلك
يسجِّل نقطة نهاية مستقبلة:
private final PayloadCallback mPayloadCallback = (senderZone, payload) -> {
…
};
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.registerReceiver("FragmentB",
getActivity().getMainExecutor(), mPayloadCallback);
}
بعد أن يُرسِل AbstractReceiverService
في برنامج استقبال الرسائل
Payload
إلى نقطة نهاية المستلِم، سيتم
استدعاء PayloadCallback
المرتبط.
يمكن لتطبيق العميل تسجيل نقاط نهاية متعددة للمستلمين طالما أنّ
receiverEndpointId
الخاصة بهم فريدة بين تطبيقات العملاء. سيستخدمAbstractReceiverService
receiverEndpointId
لتحديد نقاط نهاية
المستلمين التي سيتم إرسال الحمولة إليها. مثلاً:
- يحدِّد المُرسِل
receiver_endpoint_id:FragmentB
فيPayload
. عندتلقّيPayload
، يتصلAbstractReceiverService
في جهاز الاستقبالforwardPayload("FragmentB", payload)
لإرسال الحمولة إلىFragmentB
- يحدِّد المُرسِل
data_type:VOLUME_CONTROL
فيPayload
. عند تلقّيPayload
، يعلمAbstractReceiverService
في جهاز الاستقبال أنّه يجب إرسال هذا النوع منPayload
إلىFragmentB
، لذا يتصلforwardPayload("FragmentB", payload)
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.unregisterReceiver("FragmentB");
}
(المُرسِل) إنهاء الاتصال
بعد أن ينتهي المُرسِل من إرسال Payload
إلى المُستلِم (على سبيل المثال،
يصبح غير نشط)، من المفترض أن يُنهي الاتصال.
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.disconnect(receiverZone);
}
بعد إلغاء الربط، لن يتمكّن المُرسِل من إرسال Payload
إلى المستلِم.
مسار عملية الربط
يوضّح الشكل 2 عملية الاتصال.
تحديد المشاكل وحلّها
التحقّق من السجلات
للتحقّق من السجلات المقابلة:
شغِّل هذا الأمر للتسجيل:
adb shell setprop log.tag.CarRemoteDeviceService VERBOSE && adb shell setprop log.tag.CarOccupantConnectionService VERBOSE && adb logcat -s "AbstractReceiverService","CarOccupantConnectionManager","CarRemoteDeviceManager","CarRemoteDeviceService","CarOccupantConnectionService"
لتفريغ الحالة الداخلية لجهازَي
CarRemoteDeviceService
وCarOccupantConnectionService
:adb shell dumpsys car_service --services CarRemoteDeviceService && adb shell dumpsys car_service --services CarOccupantConnectionService
قيم فارغة لعنصرَي CarRemoteDeviceManager وCarOccupantConnectionManager
راجِع الأسباب الأساسية المحتمَلة التالية:
تعطّلت خدمة السيارة. كما هو موضّح سابقًا، تتم عمدًا إعادة ضبط المشرفَين على
null
عند تعطُّل خدمة السيارة. عند إعادة تشغيل خدمة السيارة، يتم ضبط المديرَين على قيم غير فارغة.إما
CarRemoteDeviceService
أوCarOccupantConnectionService
غير مفعَّل. لتحديد ما إذا كان أحدهما مفعّلاً أو الآخر، يمكنك تنفيذ:adb shell dumpsys car_service --services CarFeatureController
ابحث عن
mDefaultEnabledFeaturesFromConfig
الذي من المفترض أن يحتوي علىcar_remote_device_service
وcar_occupant_connection_service
. على سبيل المثال:mDefaultEnabledFeaturesFromConfig:[car_evs_service, car_navigation_service, car_occupant_connection_service, car_remote_device_service, car_telemetry_service, cluster_home_service, com.android.car.user.CarUserNoticeService, diagnostic, storage_monitoring, vehicle_map_service]
تكون هاتان الخدمتان غير مفعّلتَين تلقائيًا. عندما يتيح الجهاز استخدام شاشات متعددة، يجب دمج ملف الإعداد هذا. يمكنك تفعيل الخدماتتَين في ملف إعدادات:
// packages/services/Car/service/res/values/config.xml <string-array translatable="false" name="config_allowed_optional_car_features"> <item>car_occupant_connection_service</item> <item>car_remote_device_service</item> … … </string-array>
استثناء عند استدعاء واجهة برمجة التطبيقات
إذا لم يستخدم تطبيق العميل واجهة برمجة التطبيقات على النحو المقصود، قد يحدث استثناء. في هذه الحالة، يمكن لتطبيق العميل التحقّق من الرسالة في الاستثناء و تسلسل الأعطال لحلّ المشكلة. في ما يلي أمثلة على إساءة استخدام واجهة برمجة التطبيقات:
registerStateCallback()
سبق أن سجّل هذا العميلStateCallback
.unregisterStateCallback()
لم يتم تسجيل أيStateCallback
من خلال مثيلCarRemoteDeviceManager
هذا.- سبق أن تم تسجيل النطاق
registerReceiver()
receiverEndpointId
. unregisterReceiver()
receiverEndpointId
غير مسجَّل.requestConnection()
سبق أن تم إجراء عملية ربط في انتظار المراجعة أو تم إجراؤها.cancelConnection()
ما مِن عملية اتصال في انتظار المراجعة لإلغائها.sendPayload()
لم يتم إنشاء اتصال.disconnect()
لم يتم إنشاء اتصال.
يمكن للعميل 1 إرسال الحمولة إلى العميل 2، ولكن ليس العكس.
يتم الاتصال بطريقة واحدة بحكم التصميم. لإنشاء اتصال ثنائي الاتجاه، على كل من
client1
وclient2
طلب ربط بعضهما البعض ثم
الحصول على موافقة.