Multi-Display Communications API

يمكن لتطبيق يتمتع بامتيازات النظام في 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:

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

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

(خدمة الجهاز المتلقّي) استلام الحمولة وإرسالها

بعد أن يتلقّى تطبيق المستلِم 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.

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

الشكل 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

قيمتا Null لكلّ من 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() لم يتم إنشاء اتصال.

يمكن لـ Client1 إرسال حمولة إلى Client2، ولكن ليس العكس

الاتصال أحادي الاتجاه بحكم التصميم. لإنشاء اتصال ثنائي الاتجاه، يجب أن يطلب كل من client1 وclient2 إنشاء اتصال مع الآخر، ثم الحصول على الموافقة.