يمكن لتطبيق يتمتع بامتيازات النظام في AAOS استخدام واجهة برمجة التطبيقات Multi-Display Communications API للتواصل مع التطبيق نفسه (اسم الحزمة نفسه) الذي يتم تشغيله في منطقة ركاب مختلفة في السيارة. توضّح هذه الصفحة كيفية دمج واجهة برمجة التطبيقات. لمزيد من المعلومات، يمكنك أيضًا الاطّلاع على CarOccupantZoneManager.OccupantZoneInfo.
منطقة الإشغال
يربط مفهوم منطقة الإشغال المستخدم بمجموعة من الشاشات. تحتوي كل منطقة مخصّصة للركاب على شاشة عرض من النوع DISPLAY_TYPE_MAIN. قد تحتوي منطقة الإشغال أيضًا على شاشات إضافية، مثل شاشة مجمّعة. يتم تعيين مستخدم Android لكل منطقة إشغال. ويكون لكل مستخدم حساباته وتطبيقاته الخاصة.
إعدادات الأجهزة
تتيح واجهة Comms API استخدام منظومة واحدة على الرقاقة (SoC) فقط. في نموذج المنظومة الواحدة على الرقاقة، يتم تشغيل جميع مناطق الركاب والمستخدمين على المنظومة نفسها. تتألف واجهة Comms API من ثلاثة مكوّنات:
تسمح واجهة برمجة تطبيقات إدارة الطاقة للعميل بإدارة طاقة الشاشات في مناطق الركاب.
تتيح واجهة برمجة التطبيقات Discovery API للعميل مراقبة حالات مناطق الركاب الأخرى في السيارة ومراقبة العملاء المشابهين في مناطق الركاب هذه. استخدِم Discovery API قبل استخدام Connection API.
تسمح واجهة برمجة التطبيقات Connection API للعميل بالاتصال بالعميل النظير في منطقة أخرى يشغلها شخص آخر وإرسال حمولة إلى العميل النظير.
يجب استخدام واجهة برمجة التطبيقات Discovery API وواجهة برمجة التطبيقات Connection 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()
عندما يتلقّى MyReceiverService
Payload
من جهاز العميل المُرسِل. يمكن 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>
الحصول على "مدراء Car"
لاستخدام واجهة برمجة التطبيقات، يجب أن يسجّل تطبيق العميل 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
، وإلا قد يتفاجأ المستخدم.في الوقت الحالي (الإصدار 15 من نظام التشغيل Android)، لم يتم تنفيذ
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
مؤقتًا ثم إرسالها بعد تسجيل نقطة نهاية المستلِم المتوقّعة.
(نقطة نهاية الجهاز المتلقّي) التسجيل وإلغاء التسجيل
يجب أن يستدعي تطبيق المستقبِل 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
قيمتا Null لكلّ من 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()
لم يتم إنشاء اتصال.
يمكن لـ Client1 إرسال حمولة إلى Client2، ولكن ليس العكس
الاتصال أحادي الاتجاه بحكم التصميم. لإنشاء اتصال ثنائي الاتجاه، يجب أن يطلب كل من
client1
وclient2
إنشاء اتصال مع الآخر، ثم الحصول على الموافقة.