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

Ứng dụng đặc quyền của hệ thống trong AAOS có thể sử dụng API giao tiếp nhiều màn hình để 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 ngồi 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 có người

Khái niệm vùng người ngồi liên kết người dùng với một nhóm màn hình. Mỗi vùng người ngồi có một màn hình với loại DISPLAY_TYPE_MAIN. Vùng người ngồi cũng có thể có các màn hình bổ sung, chẳng hạn như màn hình cụm đồng hồ. Mỗi vùng người ngồi đượ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 SoC đơn, tất cả các khu vực người ngồi 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:

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

  • API Khám phá cho phép ứng dụng theo dõi trạng thái của các khu vực dành cho người ngồi khác trong ô tô và theo dõi các ứng dụng ngang hàng trong các khu vực dành cho người ngồi đó. Sử dụng API Khám phá 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 một vùng người ngồi khác và gửi tải trọng đến ứng dụng ngang hàng.

Bạn cần có API Khám phá và API Kết nối để kết nối. API quản lý nguồn điện là không bắt buộc.

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

onPayloadReceived()is invoked whenMyReceiverServicehas received aPayloadfrom the sender client.MyReceiverService` có thể ghi đè phương thức này để:

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

HOẶC,

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

Khai báo AbstractReceiverService

Ứng dụng receiver 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 này 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 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, PHẢI được cấp trước bởi 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>

Tải trình quản lý Ứng dụng ô tô

Để sử dụng API, ứng dụng khách PHẢI đăng ký CarServiceLifecycleListener để nhận 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 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 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 người ngồi trên thiết bị nhận và ứng dụng nhận đều được đặt. Nếu không, có thể xảy ra lỗi. 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 chỉ nên yêu cầu người gửi 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ố 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 để 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 người dùng tốt hơn, 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 yêu cầu 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ộ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 hai giá trị này có thống nhất với 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 thị 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, thì người gửi có thể hiển thị thông báo ngắn hoặc hộp thoại để nhắc người dùng mở khoá màn hình của khu vực người ngồi trong xe nhận.

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

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

Khi tính năng khám phá bị dừng, các kết nối hiện có sẽ không bị ảnh hưởng. Trình gửi có thể tiếp tục gửi Payload đến các trình 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 trình nhận được đặt, trình gửi CÓ THỂ yêu cầu kết nối với trình 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 trình gửi yêu cầu kết nối với trình nhận, AbstractReceiverService trong ứng dụng nhận sẽ được liên kết với dịch vụ ô tô và AbstractReceiverService.onConnectionInitiated() sẽ được gọi. Như đã giải thích trong phần (Sender) Request Connection (Yêu cầu kết nối (Người gử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 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);
    }
}

Bên gửi có thể đặt một đối tượng Binder hoặc một mảng byte vào Payload. Nếu trình gửi cần gửi các loại dữ liệu khác, thì trình gửi PHẢI chuyển đổi tuần tự dữ liệu thành một mảng byte, sử dụng mảng byte để tạo đố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à chuyển đổi tuần tự mảng byte thành đối tượng dữ liệu dự kiến. Ví dụ: nếu trình gửi muốn gửi một Chuỗi hello đến điểm cuối của trình nhận có mã nhận dạng FragmentB, thì trình gửi 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ạ luồng Payload:

Gửi tải trọng

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

(Dịch vụ tiếp 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 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 trình nhận tương ứng hoặc lưu Payload vào bộ nhớ đệm rồi gửi sau khi đăng ký điểm cuối của trình nhận dự kiến.

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

Ứng dụng receiver (trình nhận) PHẢI gọi registerReceiver() để đăng ký các điểm cuối của trình nhận. Một trường hợp sử dụng thông thường là một Mảnh cần nhận Payload, vì vậy, mảnh này sẽ đăng ký một điểm cuối của trình 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 trình 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 trình nhận miễn là receiverEndpointId của chúng là duy nhất trong ứng dụng khách. receiverEndpointId sẽ được AbstractReceiverService sử dụng để quyết định(các) điểm cuối của trình nhận sẽ gửi Trọng tải đến. Ví dụ:

  • Người gửi chỉ định receiver_endpoint_id:FragmentB trong Payload. Khi nhận được Payload, AbstractReceiverService trong trình thu sẽ gọi forwardPayload("FragmentB", payload) để gửi Trọng tải đến FragmentB
  • Người gửi chỉ định data_type:VOLUME_CONTROL trong Payload. Khi nhận được Payload, AbstractReceiverService trong trình nhận biết rằng loại Payload này sẽ đượ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) Chấm dứt kết nối

Khi người gửi không cần gửi Payload đến người nhận nữa (ví dụ: người gửi không hoạt động), người gửi 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

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

Luồng 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 sau để 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
    

CarRemoteDeviceManager và CarOccupantConnectionManager rỗng

Hãy xem những 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 trình quản lý được đặt lại thành null khi dịch vụ xe gặp sự cố. Khi dịch vụ bảo dưỡng xe được khởi động lại, hai trình quản lý sẽ được đặt thành giá trị không rỗng.

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

    adb shell dumpsys car_service --services CarFeatureController
    
    • Tìm mDefaultEnabledFeaturesFromConfig, 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 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 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 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 sai API:

  • registerStateCallback() Ứng dụng khách này đã đăng ký một StateCallback.
  • unregisterStateCallback() Thực thể CarRemoteDeviceManager này chưa đăng ký StateCallback nào.
  • 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ý để 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.

Ứng dụng 1 có thể gửi Tải trọng đến ứng dụng 2, nhưng không thể làm ngược lại

Theo thiết kế, kết nối này là 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 mới được phê duyệt.