API truyền thông đa màn hình

Một ứng dụng đặc quyền của hệ thống có thể sử dụng API Truyền thông đa màn hình trong AAOS để giao tiếp với cùng một ứng dụng (cùng tên gói) chạy trong khu vực của người trên xe ô tô. Trang này mô tả cách tích hợp API này. Để tìm hiểu hơn, bạn cũng có thể xem CarOccupantZoneManager.OccupantZoneInfo.

Vùng có người

Khái niệm khu vực có người ở liên kết người dùng đến một nhóm màn hình. Một Khu vực của người cư trú có một màn hình cùng loại DISPLAY_TYPE_MAIN. Khu vực có người cũng có thể có thêm các màn hình khác, chẳng hạn như màn hình cụm đồng hồ. Mỗi khu vực cư trú được chỉ định một người dùng Android. Mỗi người dùng có tài khoản riêng của họ và ứng dụng.

Cấu hình phần cứng

Comms API chỉ hỗ trợ một SoC duy nhất. Trong mô hình SoC duy nhất, tất cả người cư trú và người dùng chạy trên cùng một SoC. Comms API bao gồm ba thành phần:

  • API quản lý nguồn cho phép ứng dụng quản lý sức mạnh của hiển thị trong khu vực có người ở.

  • API Khám phá cho phép ứng dụng theo dõi trạng thái của những người cư trú khác trong xe và để giám sát những khách hàng ngang hàng trong khu vực của những người ngồi đó. Sử dụng Discovery API trước khi sử dụng API Kết nối.

  • Connection API cho phép ứng dụng kết nối với ứng dụng ngang hàng trong vùng khác của người cư trú và gửi tải trọng đến ứng dụng ngang hàng.

Bạn cần có API Discovery và API Kết nối để kết nối. Sức mạnh Management API là không bắt buộc.

Comms API không hỗ trợ hoạt động giao tiếp giữa các ứng dụng. Thay vào đó, ứng dụng này chỉ được thiết kế cho hoạt động giao tiếp giữa các ứng dụng có cùng tên gói và chỉ dùng để giao tiếp giữa những người dùng hiển thị khác nhau.

Hướng dẫn tích hợp

Triển khai AbstractReceiverService

Để nhận Payload, ứng dụng nhận PHẢI triển khai các phương thức trừu tượng được xác định trong AbstractReceiverService. Ví dụ:

public class MyReceiverService extends AbstractReceiverService {

    @Override
    public void onConnectionInitiated(@NonNull OccupantZoneInfo senderZone) {
    }

    @Override
    public void onPayloadReceived(@NonNull OccupantZoneInfo senderZone,
            @NonNull Payload payload) {
    }
}

onConnectionInitiated() được gọi khi ứng dụng của người gửi yêu cầu kết nối với ứng dụng nhận này. Nếu cần người dùng xác nhận để thiết lập kết nối, MyReceiverService có thể ghi đè phương thức này để chạy một hoạt động cấp quyền và gọi acceptConnection() hoặc rejectConnection() dựa trên vào kết quả. Nếu không, MyReceiverService có thể chỉ cần gọi acceptConnection().

onPayloadReceived()is invoked whenMyReceiverServicehas received aPayloadfrom the sender client.MyReceiverService` có thể ghi đè tùy chọn này để:

  • Chuyển tiếp Payload đến(các) điểm cuối tương ứng của receiver, nếu có. Người nhận lấy các điểm cuối của receiver đã đăng ký, hãy gọi getAllReceiverEndpoints(). Người nhận chuyển tiếp Payload đến một điểm cuối của receiver cụ thể, hãy gọi forwardPayload()

HOẶC,

  • Lưu Payload vào bộ nhớ đệm và gửi khi điểm cuối dự kiến của receiver là đã đăng ký mà MyReceiverService sẽ được thông báo qua onReceiverRegistered()

Khai báo AbstractReceiverService

Ứng dụng nhận PHẢI khai báo AbstractReceiverService được triển khai trong tệp kê khai, hãy thêm bộ lọc ý định bằng thao tác android.car.intent.action.RECEIVER_SERVICE cho dịch vụ này và yêu cầu Quyền 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>

Quyền android.car.occupantconnection.permission.BIND_RECEIVER_SERVICE đảm bảo rằng chỉ khung mới có thể liên kết với dịch vụ này. Nếu dịch vụ này không yêu cầu quyền này, nên một ứng dụng khác có thể liên kết với và gửi trực tiếp Payload tới dịch vụ đó.

Khai báo quyền

Ứng dụng khách PHẢI khai báo các quyền trong tệp kê khai.

<!-- 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"/>

Mỗi quyền trong số ba quyền trên đều là quyền đặc quyền và PHẢI được cấp trước bằng các tệp danh sách cho phép. Ví dụ: đây là tệp danh sách cho phép của Ứng dụng 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>

Tải Trình quản lý ô tô

Để sử dụng API này, ứng dụng khách PHẢI đăng ký CarServiceLifecycleListener để xem Trình quản lý ô tô được liên kết:

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);

(Người gửi) Khám phá

Trước khi kết nối với ứng dụng của người nhận, ứng dụng của người gửi NÊN khám phá ứng dụng nhận bằng cách đăng ký một 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);
}

Trước khi yêu cầu kết nối với người nhận, người gửi PHẢI đảm bảo tất cả các cờ vùng nhận tín hiệu và ứng dụng nhận tín hiệu được đặt. Nếu không, lỗi có thể xảy ra. Ví dụ:

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;
}

Người gửi nên yêu cầu kết nối với người nhận chỉ khi tất cả cờ của trình thu nhận được đặt. Tuy nhiên, vẫn có những trường hợp ngoại lệ:

  • FLAG_OCCUPANT_ZONE_CONNECTION_READYFLAG_CLIENT_INSTALLED là các yêu cầu tối thiểu cần thiết để thiết lập kết nối.

  • Nếu ứng dụng nhận cần hiển thị giao diện người dùng để nhận được sự chấp thuận của người dùng kết nối, FLAG_OCCUPANT_ZONE_POWER_ONFLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED trở thành yêu cầu bổ sung. Đối với nâng cao trải nghiệm người dùng, FLAG_CLIENT_RUNNING và Bạn cũng nên dùng FLAG_CLIENT_IN_FOREGROUND, nếu không người dùng có thể ngạc nhiên.

  • Hiện tại (Android 15) chưa triển khai FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED. Ứng dụng khách có thể bỏ qua thông báo đó.

  • Hiện tại (Android 15), Comms API chỉ hỗ trợ nhiều người dùng trên cùng một phiên bản Phiên bản Android để các ứng dụng ngang hàng có thể có cùng một mã phiên bản dài (FLAG_CLIENT_SAME_LONG_VERSION) và chữ ký (FLAG_CLIENT_SAME_SIGNATURE). Do đó, các ứng dụng không cần xác minh rằng có hai giá trị đồng nhất.

Để cải thiện trải nghiệm người dùng, ứng dụng của người gửi CÓ THỂ hiển thị giao diện người dùng nếu cờ không thiết lập. Ví dụ: nếu bạn không đặt FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED thì người gửi có thể hiện một thông báo ngắn hoặc hộp thoại để nhắc người dùng mở khoá màn hình vùng nhận tín hiệu.

Khi người gửi không còn cần phải tìm người nhận (ví dụ: khi tìm tất cả các receiver và kết nối đã thiết lập hoặc trở nên không hoạt động), nó CAN dừng khám phá.

if (mRemoteDeviceManager != null) {
    mRemoteDeviceManager.unregisterStateCallback();
}

Khi tính năng khám phá bị ngừng, các kết nối hiện có sẽ không bị ảnh hưởng. Người gửi có thể tiếp tục gửi Payload đến các receiver đã kết nối.

(Người gửi) Yêu cầu kết nối

Khi tất cả cờ của trình nhận được đặt, người gửi CAN yêu cầu kết nối đến người nhận:

    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);
}

(Dịch vụ người nhận) Chấp nhận kết nối

Sau khi người gửi yêu cầu kết nối với người nhận, AbstractReceiverService trong ứng dụng nhận sẽ bị dịch vụ ô tô liên kết, và AbstractReceiverService.onConnectionInitiated() sẽ được gọi. Như đã giải thích trong (Người gửi) Yêu cầu kết nối, onConnectionInitiated() là một phương thức trừu tượng và PHẢI được triển khai bằng ứng dụng khách.

Khi người nhận chấp nhận yêu cầu kết nối, thông tin ConnectionRequestCallback.onConnected() sẽ được gọi, sau đó kết nối được thiết lập.

(Người gửi) Gửi tải trọng

Sau khi thiết lập kết nối, người gửi CÓ THỂ gửi Payload đến người nhận:

if (mOccupantConnectionManager != null) {
    Payload payload = ...;
    try {
        mOccupantConnectionManager.sendPayload(receiverZone, payload);
    } catch (CarOccupantConnectionManager.PayloadTransferException e) {
        Log.e(TAG, "Failed to send Payload to " + receiverZone);
    }
}

Người gửi có thể đặt một đối tượng Binder hoặc một mảng byte vào Payload. Nếu người gửi cần gửi các loại dữ liệu khác, nó PHẢI chuyển đổi tuần tự dữ liệu thành một byte mảng, sử dụng mảng byte để tạo đối tượng Payload và gửi đối tượng Payload. Sau đó, ứng dụng nhận sẽ lấy mảng byte từ giá trị nhận được Payload và giải tuần tự mảng byte vào đối tượng dữ liệu dự kiến. Ví dụ: nếu người gửi muốn gửi Chuỗi hello đến người nhận điểm cuối có ID FragmentB, thì điểm cuối này có thể sử dụng Vùng đệm Proto để xác định loại dữ liệu như sau:

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

Hình 1 minh hoạ quy trình Payload:

Gửi tải trọng

Hình 1. Gửi tải trọng.

(Dịch vụ bộ nhận) Nhận và gửi tải trọng

Sau khi ứng dụng nhận nhận được Payload, AbstractReceiverService.onPayloadReceived() sẽ được gọi. Như được giải thích trong Gửi tải trọng, onPayloadReceived() là một phương thức tóm tắt và PHẢI được ứng dụng khách triển khai. Trong phương thức này, ứng dụng CAN chuyển tiếp Payload đến(các) điểm cuối của bộ nhận tương ứng, hoặc lưu Payload vào bộ nhớ đệm, rồi gửi sau khi điểm cuối dự kiến của receiver là đã đăng ký.

(Điểm cuối của người nhận) Đăng ký và huỷ đăng ký

Ứng dụng nhận NÊN gọi registerReceiver() để đăng ký dịch vụ nhận điểm cuối. Một trường hợp sử dụng điển hình là Mảnh cần nhận Payload, vì vậy nó sẽ đăng ký một điểm cuối của receiver:

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

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

Sau khi AbstractReceiverService trong ứng dụng của dịch vụ nhận gửi Payload đến điểm cuối của receiver, thì PayloadCallback được liên kết sẽ là đã gọi.

Ứng dụng khách CÓ THỂ đăng ký nhiều điểm cuối của trình nhận, miễn là receiverEndpointId là duy nhất trong số các ứng dụng khách. receiverEndpointId sẽ được AbstractReceiverService dùng để quyết định receiver nào (các) điểm cuối để điều phối Tải trọng đến. Ví dụ:

  • Người gửi chỉ định receiver_endpoint_id:FragmentB trong Payload. Thời gian nhận Payload, AbstractReceiverService trong các lệnh gọi receiver forwardPayload("FragmentB", payload) để gửi Tải trọng đến FragmentB
  • Người gửi chỉ định data_type:VOLUME_CONTROL trong Payload. Thời gian khi nhận Payload, AbstractReceiverService trong receiver sẽ biết loại Payload này sẽ được gửi đến FragmentB, nên phương thức này sẽ gọi forwardPayload("FragmentB", payload)
if (mOccupantConnectionManager != null) {
    mOccupantConnectionManager.unregisterReceiver("FragmentB");
}

(Người gửi) Chấm dứt kết nối

Sau khi người gửi không cần gửi Payload cho người nhận nữa (ví dụ: nó sẽ bị vô hiệu hoá), thì nó PHẢI chấm dứt kết nối.

if (mOccupantConnectionManager != null) {
    mOccupantConnectionManager.disconnect(receiverZone);
}

Sau khi bị ngắt kết nối, người gửi không thể gửi Payload cho người nhận nữa.

Luồng kết nối

Quy trình kết nối được minh hoạ trong Hình 2.

Luồng kết nối

Hình 2. Quy trình kết nối.

Khắc phục sự cố

Kiểm tra nhật ký

Cách kiểm tra nhật ký tương ứng:

  1. Chạy lệnh này để ghi nhật ký:

    adb shell setprop log.tag.CarRemoteDeviceService VERBOSE && adb shell setprop log.tag.CarOccupantConnectionService VERBOSE && adb logcat -s "AbstractReceiverService","CarOccupantConnectionManager","CarRemoteDeviceManager","CarRemoteDeviceService","CarOccupantConnectionService"
    
  2. Để kết xuất trạng thái nội bộ của CarRemoteDeviceServiceCarOccupantConnectionService:

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

Giá trị rỗng CarRemoteDeviceManager và CarOccupantConnectionManager

Hãy xem các nguyên nhân gốc rễ có thể xảy ra sau đây:

  1. Dịch vụ xe gặp sự cố. Như minh hoạ trước đó, hai người quản lý là chủ động đặt lại thành null khi dịch vụ ô tô gặp sự cố. Trường hợp dịch vụ ô tô được khởi động lại, hai trình quản lý được đặt thành các giá trị khác rỗng.

  2. CarRemoteDeviceService hoặc CarOccupantConnectionService thì không bật. Để xác định xem một trong hai tuỳ chọn đã được bật hay chưa, hãy chạy mã:

    adb shell dumpsys car_service --services CarFeatureController
    
    • Tìm mDefaultEnabledFeaturesFromConfig, trong đó có chứa car_remote_device_servicecar_occupant_connection_service. Ví dụ:

      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]
      
    • Theo mặc định, hai dịch vụ này bị tắt. Khi có thiết bị hỗ trợ chế độ nhiều màn hình, bạn PHẢI phủ tệp cấu hình này. Bạn có thể bật hai dịch vụ trong một tệp cấu hình:

      // 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>
      

Trường hợp ngoại lệ khi gọi API

Nếu ứng dụng khách không sử dụng API như dự kiến, thì có thể xảy ra ngoại lệ. Trong trường hợp này, ứng dụng khách có thể kiểm tra thông báo trong trường hợp ngoại lệ và ngăn xếp sự cố để giải quyết vấn đề. Sau đây là một số ví dụ về việc sử dụng sai API:

  • registerStateCallback() Khách hàng này đã đăng ký một StateCallback.
  • unregisterStateCallback() Chưa có StateCallback nào được đăng ký bởi Thực thể CarRemoteDeviceManager.
  • registerReceiver() receiverEndpointId đã được đăng ký.
  • unregisterReceiver() receiverEndpointId chưa được đăng ký.
  • requestConnection() Đã có một kết nối đang chờ xử lý hoặc đã được thiết lập.
  • cancelConnection() Không có kết nối nào đang chờ xử lý để huỷ.
  • sendPayload() Chưa thiết lập kết nối nào.
  • disconnect() Chưa thiết lập kết nối nào.

Client1 có thể gửi Tải trọng đến client2, nhưng không thể gửi theo cách khác

Kết nối có một chiều theo thiết kế. Để thiết lập kết nối hai chiều, cả hai client1client2 PHẢI yêu cầu kết nối với nhau, sau đó được phê duyệt.