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

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

منطقه ساکن

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

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

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

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

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

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

برای اتصال به Discovery API و Connection API نیاز است. 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 when MyReceiverService has received a Payload from the sender client. MyReceiverService` می تواند این روش را لغو کند:

  • در صورت وجود، Payload را به نقطه پایانی گیرنده مربوطه ارسال کنید. برای دریافت نقاط پایانی گیرنده ثبت شده، getAllReceiverEndpoints() فراخوانی کنید. برای ارسال Payload به نقطه پایانی گیرنده معین، forwardPayload() را فراخوانی کنید.

یا،

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

AbstractReceiverService را اعلام کنید

برنامه گیرنده باید AbstractReceiverService پیاده سازی شده را در فایل مانیفست خود اعلام کند، یک فیلتر قصد با action 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>

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

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

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

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

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

ارسال بار

شکل 1. ارسال بار.

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

هنگامی که برنامه گیرنده Payload را دریافت کرد، AbstractReceiverService.onPayloadReceived() آن فراخوانی می شود. همانطور که در Send the 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 را به آن ارسال کند. به عنوان مثال:

  • فرستنده receiver_endpoint_id:FragmentB در Payload مشخص می کند. هنگام دریافت 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 به گیرنده ارسال کند.

جریان اتصال

یک جریان اتصال در شکل 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>
      

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

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

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

Client1 می تواند Payload را به client2 ارسال کند، اما نه برعکس

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