API giao tiếp nhiều màn hình

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

Vùng người ngồi trong xe

Khái niệm về vùng người dùng sẽ liên kết một người dùng với một nhóm màn hình. Mỗi vùng cư trú đều có một màn hình thuộc loại DISPLAY_TYPE_MAIN. Khu vực dành cho người ngồi trong xe cũng có thể có các màn hình khác, chẳng hạn như màn hình phân cụm. Mỗi vùng dành cho người ngồi trong xe được chỉ định một người dùng Android. Mỗi người dùng đều có tài khoản và ứng dụng riê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ả các vùng và người dùng của người cư ngụ đều chạy trên cùng một SoC. Comms API bao gồm 3 thành phần:

  • API quản lý nguồn cho phép ứng dụng quản lý nguồn của màn hình trong các vùng dành cho người ngồi.

  • Discovery API cho phép ứng dụng theo dõi trạng thái của các khu vực khác trong xe và theo dõi các ứng dụng ngang hàng trong những khu vực đó. Sử dụng Discovery API trước khi sử dụng Connection API.

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

Bạn cần có Discovery API và Connection API để kết nối. API Quản lý nguồn là không bắt buộc.

Comms API không hỗ trợ giao tiếp giữa các ứng dụng. Thay vào đó, nó chỉ được thiết kế để giao tiếp giữa các ứng dụng có cùng tên gói và chỉ được dùng để giao tiếp giữa những người dùng 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 gửi yêu cầu kết nối đến ứng dụng nhận này. Nếu cần có sự xác nhận của người dùng để thiết lập kết nối, thì 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 kết quả. Nếu không, MyReceiverService có thể chỉ cần gọi acceptConnection().

onPayloadReceived() được gọi khi MyReceiverService đã nhận được Payload từ ứng dụng gửi. MyReceiverService có thể ghi đè phương thức này để:

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

HOẶC,

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

Khai báo AbstractReceiverService

Ứng dụng nhận PHẢI khai báo AbstractReceiverService đã triển khai trong tệp kê khai, thêm bộ lọc ý định có 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, thì một ứng dụng khác có thể liên kết với dịch vụ này và gửi trực tiếp một Payload đến 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 của ứng dụng.

<!-- 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ố 3 quyền trên đều là quyền đặc biệt và PHẢI được cấp trước bằng các tệp danh sách cho phép. Ví dụ: sau đâ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>

Mời người quản lý xe

Để sử dụng API này, ứng dụng khách PHẢI đăng ký một CarServiceLifecycleListener để nhận các 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 nhận, ứng dụng gửi PHẢI phát hiện ứ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 thiết bị nhận, thiết bị gửi PHẢI đảm bảo rằng tất cả cờ của vùng cư trú của thiết bị nhận và ứng dụng của thiết bị nhận đề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 chỉ nên yêu cầu kết nối với người nhận khi tất cả các cờ của người nhận đều được đặt. Tuy nhiên, vẫn có một số 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ự phê duyệt của người dùng về kết nối, thì FLAG_OCCUPANT_ZONE_POWER_ONFLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED sẽ trở thành các yêu cầu bổ sung. Để mang lại trải nghiệm tốt hơn cho người dùng, bạn cũng nên sử dụng FLAG_CLIENT_RUNNINGFLAG_CLIENT_IN_FOREGROUND, nếu không người dùng có thể sẽ ngạc nhiên.

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

  • 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 Android để các ứng dụng ngang hàng có thể có cùng 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 hai giá trị này có giống nhau hay không.

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

Khi không cần tìm thấy các thiết bị nhận nữa (ví dụ: khi tìm thấy tất cả các thiết bị nhận và thiết lập kết nối hoặc trở nên không hoạt động), thiết bị gửi CÓ THỂ dừng quá trình tìm kiếm.

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

Khi quá trình khám phá dừng lại, 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 thiết bị nhận đã kết nối.

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

Khi tất cả cờ của thiết bị nhận được đặt, thiết bị gửi CÓ THỂ yêu cầu kết nối với thiết bị 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ụ 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 người nhận sẽ được liên kết bằng dịch vụ trên ô tô và AbstractReceiverService.onConnectionInitiated() sẽ được gọi. Như đã giải thích trong phần (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ởi ứng dụng khách.

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

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

Sau khi kết nối được thiết lập, người gửi CÓ THỂ gửi Payload cho 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 trong Payload. Nếu cần gửi các loại dữ liệu khác, người gửi PHẢI chuyển đổi dữ liệu thành một mảng byte, dùng mảng byte đó để tạo một đối tượng Payload và gửi Payload. Sau đó, ứng dụng nhận sẽ nhận được mảng byte từ Payload đã nhận và giải tuần tự mảng byte thành đối tượng dữ liệu dự kiến. Ví dụ: nếu người gửi muốn gửi một chuỗi hello đến điểm cuối của người nhận có mã nhận dạng FragmentB, thì người gửi có thể dùng Proto Buffers để xác định một kiểu 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ụ nhận) Nhận và gửi tải trọng

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

(Điểm cuối của đầu thu) Đăng ký và huỷ đăng ký

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

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

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

Sau khi AbstractReceiverService trong ứng dụng khách nhận gửi Payload đến điểm cuối của ứng dụng nhận, PayloadCallback được liên kết sẽ được gọi.

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

  • Người gửi chỉ định receiver_endpoint_id:FragmentB trong Payload. Khi nhận Payload, AbstractReceiverService trong receiver sẽ gọi forwardPayload("FragmentB", payload) để gửi Payload đến FragmentB
  • Người gửi chỉ định data_type:VOLUME_CONTROL trong Payload. Khi nhận được Payload, AbstractReceiverService trong bộ nhận sẽ biết rằng loại Payload này phải được gửi đến FragmentB, vì vậy, nó sẽ gọi forwardPayload("FragmentB", payload)
if (mOccupantConnectionManager != null) {
    mOccupantConnectionManager.unregisterReceiver("FragmentB");
}

(Người gửi) Ngắt kết nối

Khi người gửi không cần gửi Payload cho người nhận nữa (ví dụ: khi người nhận không hoạt động), người gửi NÊN chấm dứt kết nối.

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

Sau khi bạn huỷ kết nối, người gửi sẽ không thể gửi Payload cho người nhận nữa.

Quy trình kết nối

Luồng kết nối được minh hoạ trong Hình 2.

Quy trình 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. Cách 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

Null CarRemoteDeviceManager và CarOccupantConnectionManager

Hãy xem xét những nguyên nhân gốc rễ có thể gây ra vấn đề này:

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

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

    adb shell dumpsys car_service --services CarFeatureController
    • Tìm mDefaultEnabledFeaturesFromConfig, trong đó phải có 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 sẽ bị tắt. Khi một thiết bị hỗ trợ 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ụ này 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>
      

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 một trường hợp 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ề hành vi sử dụng API sai cách:

  • registerStateCallback() Ứng dụng khách này đã đăng ký một StateCallback.
  • unregisterStateCallback() Không có StateCallback nào được đăng ký bởi phiên bản CarRemoteDeviceManager này.
  • 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 đã thiết lập.
  • cancelConnection() Không có kết nối đang chờ xử lý nào để huỷ.
  • sendPayload() Không có kết nối nào được thiết lập.
  • disconnect() Không có kết nối nào được thiết lập.

Client1 có thể gửi Payload đến client2, nhưng không thể gửi theo chiều ngược lại

Theo thiết kế, kết nối này chỉ có một chiều. Để thiết lập kết nối hai chiều, cả client1client2 ĐỀU PHẢI yêu cầu kết nối với nhau rồi nhận được sự phê duyệt.