يمكن أن يستخدم تطبيق مفوَّض للنظام في واجهة برمجة التطبيقات ("متعدد الشاشات") في AAOS للتواصل مع التطبيق نفسه (اسم الحزمة نفسه) الذي يعمل في منطقة مختلفة لركاب السيارة. توضّح هذه الصفحة كيفية دمج واجهة برمجة التطبيقات. للاطّلاع على معلومات إضافية، يمكنك أيضًا الاطّلاع على 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()
عندما يتلقّى 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>
الحصول على مدراء السيارات
لاستخدام واجهة برمجة التطبيقات، يجب أن يسجِّل تطبيق العميل 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
طريقة مجردة ويجب أن ينفّذها تطبيق العميل. في هذه الطريقة، يمكن ل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
قيم فارغة لعنصرَي 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
طلب ربط بعضهما البعض ثم
الحصول على موافقة.