API ارتباطات چند نمایشگر

رابط برنامه‌نویسی کاربردی ارتباطات چند نمایشگری (Multi-Display Communications API) می‌تواند توسط یک برنامه دارای امتیاز سیستمی در AAOS برای ارتباط با همان برنامه (با نام بسته یکسان) که در یک منطقه سرنشین متفاوت در یک خودرو اجرا می‌شود، استفاده شود. این صفحه نحوه ادغام API را شرح می‌دهد. برای کسب اطلاعات بیشتر، می‌توانید CarOccupantZoneManager.OccupantZoneInfo را نیز مشاهده کنید.

منطقه مسکونی

مفهوم منطقه اشغال ، کاربر را به مجموعه‌ای از نمایشگرها متصل می‌کند. هر منطقه اشغال دارای یک نمایشگر با نوع DISPLAY_TYPE_MAIN است. یک منطقه اشغال همچنین ممکن است نمایشگرهای اضافی مانند نمایشگر خوشه‌ای داشته باشد. به هر منطقه اشغال یک کاربر اندروید اختصاص داده می‌شود. هر کاربر حساب‌ها و برنامه‌های خاص خود را دارد.

پیکربندی سخت‌افزار

رابط برنامه‌نویسی کاربردی ارتباطات (Comms API) فقط از یک SoC واحد پشتیبانی می‌کند. در مدل SoC واحد، تمام مناطق مسکونی و کاربران بر روی یک SoC واحد اجرا می‌شوند. رابط برنامه‌نویسی کاربردی ارتباطات (Comms API) از سه جزء تشکیل شده است:

  • API مدیریت توان به کلاینت اجازه می‌دهد تا توان نمایشگرها را در مناطق مسکونی مدیریت کند.

  • رابط برنامه‌نویسی کاربردی اکتشاف (Discovery API) به کلاینت اجازه می‌دهد تا وضعیت سایر مناطق سرنشین در خودرو و کلاینت‌های همتا در آن مناطق سرنشین را رصد کند. قبل از استفاده از رابط برنامه‌نویسی کاربردی اتصال (Connection API)، از رابط برنامه‌نویسی کاربردی اکتشاف (Discovery API) استفاده کنید.

  • رابط برنامه‌نویسی کاربردی اتصال (Connection API) به کلاینت اجازه می‌دهد تا به کلاینت همتا (peer client) خود در منطقه اشغالی دیگر متصل شود و یک payload به کلاینت همتا ارسال کند.

برای اتصال، 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 پیاده‌سازی شده را در فایل مانیفست خود اعلام کند، یک فیلتر intent با اکشن 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"/>

هر یک از سه مجوز فوق، مجوزهای ممتاز هستند که باید توسط فایل‌های allowlist از قبل اعطا شوند. برای مثال، در اینجا فایل allowlist برنامه 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>

دریافت مدیران خودرو

برای استفاده از API، برنامه‌ی کلاینت باید یک CarServiceLifecycleListener ثبت کند تا مدیران خودرو (Car manager) مرتبط را دریافت کند:

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 نیز توصیه می‌شوند، در غیر این صورت ممکن است کاربر شگفت‌زده شود.

  • در حال حاضر (اندروید ۱۵)، FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED پیاده‌سازی نشده است. برنامه‌ی کلاینت می‌تواند آن را نادیده بگیرد.

  • در حال حاضر (اندروید ۱۵)، رابط برنامه‌نویسی کاربردی ارتباطات (Comms API) فقط از چندین کاربر در یک نمونه اندروید پشتیبانی می‌کند تا برنامه‌های همتا بتوانند کد نسخه طولانی ( FLAG_CLIENT_SAME_LONG_VERSION ) و امضای ( FLAG_CLIENT_SAME_SIGNATURE ) یکسانی داشته باشند. در نتیجه، برنامه‌ها نیازی به تأیید مطابقت دو مقدار ندارند.

برای تجربه کاربری بهتر، کلاینت فرستنده می‌تواند در صورت عدم تنظیم پرچم، یک رابط کاربری (UI) را نشان دهد. برای مثال، اگر 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 در برنامه گیرنده توسط سرویس car محدود می‌شود و AbstractReceiverService.onConnectionInitiated() فراخوانی می‌شود. همانطور که در درخواست اتصال (Sender) توضیح داده شد، 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 دریافتی دریافت می‌کند و آرایه بایت را در شیء داده مورد انتظار deserialize می‌کند. به عنوان مثال، اگر فرستنده بخواهد یک رشته hello به نقطه انتهایی گیرنده با شناسه FragmentB ارسال کند، می‌تواند از Proto Buffers برای تعریف نوع داده مانند این استفاده کند:

message MyData {
  required string receiver_endpoint_id = 1;
  required string data = 2;
}

شکل ۱ جریان Payload را نشان می‌دهد:

ارسال بار مفید

شکل ۱. ارسال Payload.

(سرویس گیرنده) دریافت و ارسال محموله

به محض اینکه برنامه گیرنده، Payload دریافت کند، متد AbstractReceiverService.onPayloadReceived() آن فراخوانی می‌شود. همانطور که در بخش ارسال payload توضیح داده شد، متد onPayloadReceived() یک متد انتزاعی است و باید توسط برنامه کلاینت پیاده‌سازی شود. در این متد، کلاینت می‌تواند Payload را به نقطه(های) انتهایی گیرنده مربوطه ارسال کند، یا Payload ذخیره کرده و سپس پس از ثبت نقطه انتهایی گیرنده مورد انتظار، آن را ارسال کند.

(نقطه پایانی گیرنده) ثبت و لغو ثبت

برنامه گیرنده باید تابع registerReceiver() را برای ثبت نقاط انتهایی گیرنده فراخوانی کند. یک مورد استفاده معمول این است که یک Fragment نیاز به دریافت Payload دارد، بنابراین یک نقطه انتهایی گیرنده را ثبت می‌کند:

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

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

به محض اینکه AbstractReceiverService در کلاینت گیرنده، Payload را به نقطه انتهایی گیرنده ارسال کند، PayloadCallback مرتبط فراخوانی خواهد شد.

برنامه‌ی کلاینت می‌تواند چندین نقطه‌ی پایانی گیرنده را ثبت کند، مادامی که شناسه‌های receiverEndpointId ) آنها در بین برنامه‌ی کلاینت منحصر به فرد باشد. receiverEndpointId ) توسط AbstractReceiverService برای تصمیم‌گیری در مورد اینکه کدام نقطه‌ی پایانی گیرنده باید Payload را ارسال کند، استفاده خواهد شد. برای مثال:

  • فرستنده، در قسمت Payload receiver_endpoint_id:FragmentB را مشخص می‌کند. هنگام دریافت Payload ، سرویس AbstractReceiverService در گیرنده، تابع forwardPayload("FragmentB", payload) را برای ارسال 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 به گیرنده ارسال کند.

جریان اتصال

جریان اتصال در شکل ۲ نشان داده شده است.

جریان اتصال

شکل ۲. جریان اتصال.

عیب‌یابی

لاگ‌ها را بررسی کنید

برای بررسی لاگ‌های مربوطه:

  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) تهی (Null)

این علل ریشه‌ای احتمالی را بررسی کنید:

  1. سرویس خودرو از کار افتاد. همانطور که قبلاً نشان داده شد، دو مدیر عمداً هنگام از کار افتادن سرویس خودرو به مقدار null تنظیم مجدد می‌شوند. وقتی سرویس خودرو مجدداً راه‌اندازی می‌شود، دو مدیر روی مقادیر غیر 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>
      

استثنا هنگام فراخوانی API

اگر برنامه‌ی کلاینت از API آنطور که در نظر گرفته شده استفاده نکند، ممکن است یک خطا رخ دهد. در این حالت، برنامه‌ی کلاینت می‌تواند پیام موجود در خطا و پشته‌ی خرابی را بررسی کند تا مشکل را حل کند. نمونه‌هایی از سوءاستفاده از API عبارتند از:

  • registerStateCallback() ‎ این کلاینت قبلاً یک StateCallback ثبت کرده است.
  • unregisterStateCallback() هیچ StateCallback توسط این نمونه CarRemoteDeviceManager ثبت نشده است.
  • registerReceiver() receiverEndpointId قبلاً ثبت شده است.
  • unregisterReceiver() receiverEndpointId ثبت نشده است.
  • requestConnection() ‎ یک اتصال در حال انتظار یا برقرار شده از قبل وجود دارد.
  • cancelConnection() ‎ هیچ اتصال در انتظاری برای لغو وجود ندارد.
  • sendPayload() ‎‏ اتصال برقرار نشد.
  • disconnect() ‎ هیچ اتصالی برقرار نشد.

کلاینت ۱ می‌تواند Payload را به کلاینت ۲ ارسال کند، اما برعکس آن امکان‌پذیر نیست.

این اتصال به صورت یک طرفه طراحی شده است. برای ایجاد اتصال دو طرفه، هر دو client1 و client2 باید درخواست اتصال به یکدیگر را داشته باشند و سپس تأییدیه را دریافت کنند.