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

يمكن استخدام واجهة برمجة التطبيقات Multi-Display Communications API من خلال تطبيق يحظى بامتيازات النظام في نظام التشغيل Android Automotive (AAOS) للتواصل مع التطبيق نفسه (اسم الحزمة نفسه) الذي يتم تشغيله في تطبيق مختلف منطقة الركاب في السيارة توضّح هذه الصفحة كيفية دمج واجهة برمجة التطبيقات. للتعلّم أكثر، يمكنك أيضًا أن ترى CarOccupantZoneManager.OccupantZoneInfo:

منطقة الإشغال

يربط مفهوم منطقة الإشغال المستخدم بمجموعة من الشاشات. على كل وتضم منطقة الإشغال شاشة عرض DISPLAY_TYPE_MAIN. وقد تحتوي منطقة الإقامة على شاشات إضافية، مثل شاشة عرض جماعية. يتم تخصيص مستخدم Android لكل منطقة يتواجد فيها النزلاء. لكل مستخدم حسابه الخاص والتطبيقات.

ضبط الأجهزة

لا تتوافق واجهة برمجة التطبيقات Comms API إلا مع شريحة SoC واحدة. في نموذج المنظومة على الرقاقة الفردي، والمستخدمين الذين يعملون على نفس المنظومة على الرقاقة (SoC). تتألّف واجهة برمجة التطبيقات Comms API من ثلاثة مكونات:

  • واجهة برمجة تطبيقات إدارة الطاقة تتيح للعميل إدارة إمكانات المعروضة في مناطق الإشغال.

  • Discovery API تسمح للعميل بمراقبة حالات النزلاء الآخرين في السيارة، ولمراقبة العملاء الآخرين في مناطق الركاب. استخدام Discovery API قبل استخدام Connection API.

  • واجهة برمجة تطبيقات الاتصال تسمح للعميل بالاتصال ببرنامج نظيره في منطقة شاغل أخرى وإرسال حمولة إلى العميل النظير.

يجب إدخال واجهة برمجة التطبيقات Discovery وConnection API لإجراء الاتصال. القوة 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 whenMyFindrServicehas 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);

(المرسِل) Discover

قبل الاتصال بعميل المستلم، يجب أن يكتشف العميل المرسل العميل المستلِم من خلال تسجيل 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 مؤقتًا ثم إرساله بمجرد تحديد نقطة نهاية المُستلِم المتوقعة تسجيلك.

(نقطة نهاية جهاز الاستقبال) التسجيل وإلغاء التسجيل

يجب أن يطلب تطبيق المُستلِم الاتصال بـ registerReceiver() لتسجيل المستلِم. والنقاط النهائية. وتتمثل حالة الاستخدام النموذجية في أن الجزء يحتاج إلى مستقبِل Payload، ولذلك يسجل نقطة نهاية المتلقي:

private final PayloadCallback mPayloadCallback = (senderZone, payload) -> {
    …
};

if (mOccupantConnectionManager != null) {
    mOccupantConnectionManager.registerReceiver("FragmentB",
                getActivity().getMainExecutor(), mPayloadCallback);
}

بعد إرسال AbstractReceiverService في العميل المستلِم Payload إلى نقطة نهاية المُستلِم، سيتم ربط PayloadCallback المرتبط تم استدعاء.

يمكن لتطبيق العميل تسجيل نقاط نهاية متعددة لأجهزة الاستقبال طالما سمات receiverEndpointId فريدة من نوعها في تطبيق العميل. receiverEndpointId سيتم استخدامه من قِبل AbstractReceiverService لتحديد المستلِم نقاط نهاية لإرسال الحمولة إليها. مثلاً:

  • يُحدِّد المُرسِل 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" الاتصال ببعضهما البعض ثم والحصول على الموافقة.