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

Bạn có thể dùng Multi-Display Communications API (API Giao tiếp trên nhiều màn hình) trong AAOS để giao tiếp với cùng một ứng dụng (cùng tên gói) đang chạy ở khu vực của những người khác trong xe. 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.

Khu vực của người dùng

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

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

  • Discovery API (API Khám phá) 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 đó. Hãy sử dụng Discovery API trước khi sử dụng Connection API.

  • Connection API (API Kết nối) cho phép ứng dụng kết nối với ứng dụng ngang hàng trong một khu vực khác của người dùng 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. Power management API là không bắt buộc.

Comms API không hỗ trợ giao tiếp giữa các ứng dụng khác nhau. Thay vào đó, API này được thiết kế chỉ để giao tiếp giữa các ứng dụng có cùng tên gói và được dùng chỉ để giao tiếp giữa những người dùng khác nhau có thể nhìn thấy 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 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 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 tương ứng của ứng dụng nhận (nếu có). Để nhận các điểm cuối đã đăng ký của ứng dụng nhận, hãy gọi getAllReceiverEndpoints(). Để chuyển tiếp Payload đến một điểm cuối nhất định của ứng dụng nhận, 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 ứng dụng nhận được đăng ký, mà MyReceiverService đượ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ó hành động 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 Payload đến dịch vụ đó.

Khai báo quyền

Ứng dụng 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ố 3 quyền ở trên đều là quyền đặc biệt, 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>

Nhận trình quản lý ô tô

Để sử dụng API, ứng dụng PHẢI đăng ký 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 dụng gửi) Khám phá

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

Bạn nên yêu cầu ứng dụng gửi kết nối với ứng dụng nhận chỉ khi tất cả cờ của ứng dụng nhận đượ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ị một giao diện người dùng để được người dùng phê duyệt 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 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 có thể bỏ qua cờ 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 thực thể 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 2 giá trị này giống nhau.

Để 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 thị một giao diện người dùng nếu một cờ không được đặt. Ví dụ: nếu FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED không được đặt, thì ứng dụng gửi có thể hiển thị một thông báo nhanh hoặc một hộp thoại để nhắc người dùng mở khoá màn hình của khu vực của người dùng nhận.

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

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 dụng gửi có thể tiếp tục gửi Payload đến các ứng dụng nhận đã kết nối.

(Ứng dụng gửi) Yêu cầu kết nối

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

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

(Ứng dụng gửi) Gửi tải trọng

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

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

Hình 1 minh hoạ luồng 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 ứng dụng nhận nhận được Payload, AbstractReceiverService.onPayloadReceived() của ứng dụng đó sẽ được gọi. 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 triển khai. Trong phương thức này, ứng dụng CÓ THỂ chuyển tiếp Payload đến(các) điểm cuối tương ứng của ứng dụng nhận hoặc lưu Payload vào bộ nhớ đệm rồi gửi khi điểm cuối dự kiến của ứng dụng nhận được đăng ký.

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

Ứng dụng nhận NÊN 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à một Đoạn mã cần nhận Payload, vì vậy, đoạn mã này sẽ đăng ký một điểm cuối của ứng dụng nhận:

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

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

Sau khi AbstractReceiverService trong ứng dụng 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 CÓ THỂ đăng ký nhiều điểm cuối của ứng dụng nhận miễn là receiverEndpointId của các điểm cuối đó là duy nhất trong ứng dụng. receiverEndpointId sẽ được AbstractReceiverService sử dụng để quyết định(các) điểm cuối của ứng dụng nhận mà Payload sẽ được gửi đến. Ví dụ:

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

(Ứng dụng gửi) Chấm dứt kết nối

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

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

Sau khi ngắt kết nối, ứng dụng gửi không còn có thể gửi Payload đến ứng dụng nhận nữa.

Luồng 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. Luồng 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

CarRemoteDeviceManager và CarOccupantConnectionManager có giá trị null

Hãy xem các nguyên nhân gốc có thể gây ra vấn đề này:

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

  2. CarRemoteDeviceService hoặc CarOccupantConnectionService không được bật. Để xác định xem một trong 2 dịch vụ này 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, 2 dịch vụ này 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 2 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ô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 có thể kiểm tra thông báo trong ngoại lệ và ngăn xếp sự cố để giải quyết vấn đề. Sau đây là các ví dụ về việc sử dụng sai API:

  • registerStateCallback() Ứng dụng này đã đăng ký một StateCallback.
  • unregisterStateCallback() Không có StateCallback nào được thực thể CarRemoteDeviceManager này đăng ký.
  • registerReceiver() receiverEndpointId đã được đăng ký.
  • unregisterReceiver() receiverEndpointId chưa được đăng ký.
  • requestConnection() Đã tồn tại 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ý để huỷ.
  • sendPayload() Không có kết nối đã thiết lập.
  • disconnect() Không có kết nối đã thiết lập.

Ứng dụng 1 có thể gửi Payload đến ứng dụng 2, nhưng không thể gửi theo chiều ngược lại

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