واجهة برمجة التطبيقات Multi-Display Communications Communications

يمكن لتطبيق مفوَّض للنظام في ‎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 whenMyReceiverServicehas received aPayloadfrom 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:

إرسال الحمولة

الشكل 1: أرسِل الحمولة.

(خدمة المُستلِم) استلام الحمولة وإرسالها

بعد أن يتلقّى التطبيق المستلِم 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 الخاصة بهم فريدة بين تطبيقات العملاء. سيستخدمAbstractReceiverServicereceiverEndpointId لتحديد نقاط نهاية المستلمين التي سيتم إرسال الحمولة إليها. مثلاً:

  • يحدِّد المُرسِل 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 عملية الاتصال.

مسار عملية الربط

الشكل 2: مسار الاتصال

تحديد المشاكل وحلّها

التحقّق من السجلات

للتحقّق من السجلات المقابلة:

  1. شغِّل هذا الأمر للتسجيل:

    adb shell setprop log.tag.CarRemoteDeviceService VERBOSE && adb shell setprop log.tag.CarOccupantConnectionService VERBOSE && adb logcat -s "AbstractReceiverService","CarOccupantConnectionManager","CarRemoteDeviceManager","CarRemoteDeviceService","CarOccupantConnectionService"
    
  2. لتفريغ الحالة الداخلية لجهازَي CarRemoteDeviceService و CarOccupantConnectionService:

    adb shell dumpsys car_service --services CarRemoteDeviceService && adb shell dumpsys car_service --services CarOccupantConnectionService
    

قيم فارغة لعنصرَي CarRemoteDeviceManager وCarOccupantConnectionManager

راجِع الأسباب الأساسية المحتمَلة التالية:

  1. تعطّلت خدمة السيارة. كما هو موضّح سابقًا، تتم عمدًا إعادة ضبط المشرفَين على null عند تعطُّل خدمة السيارة. عند إعادة تشغيل خدمة السيارة، يتم ضبط المديرَين على قيم غير فارغة.

  2. إما 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 طلب ربط بعضهما البعض ثم الحصول على موافقة.