Multi-Display Communications API

Multi-Display Communications API, AAOS'teki ayrıcalıklı bir sistem uygulaması tarafından, bir arabadaki farklı bir yolcu bölgesinde çalışan aynı uygulama (aynı paket adı) ile iletişim kurmak için kullanılabilir. Bu sayfada, API'nin nasıl entegre edileceği açıklanmaktadır. Daha fazla bilgi edinmek için CarOccupantZoneManager.OccupantZoneInfo dokümanına da bakabilirsiniz.

Yolcu bölgesi

Kullanıcı bölgesi kavramı, bir kullanıcıyı bir dizi ekrana eşler. Her yaşam alanı, DISPLAY_TYPE_MAIN türünde bir ekrana sahiptir. Bir yolcu bölgesinde, küme ekranı gibi ek ekranlar da olabilir. Her yolcu bölgesine bir Android kullanıcısı atanır. Her kullanıcının kendi hesapları ve uygulamaları vardır.

Donanım yapılandırması

Comms API yalnızca tek bir SoC'yi destekler. Tek SoC modelinde, tüm yolcu bölgeleri ve kullanıcılar aynı SoC'de çalışır. Comms API üç bileşenden oluşur:

  • Güç yönetimi API'si, istemcinin yolcu bölgelerindeki ekranların gücünü yönetmesine olanak tanır.

  • Discovery API, istemcinin arabadaki diğer yolcu bölgelerinin durumunu ve bu yolcu bölgelerindeki benzer istemcileri izlemesine olanak tanır. Connection API'yi kullanmadan önce Discovery API'yi kullanın.

  • Connection API, istemcinin başka bir işgalci bölgesindeki benzer istemciye bağlanmasına ve benzer istemciye bir yük göndermesine olanak tanır.

Bağlantı için Discovery API ve Connection API gereklidir. Güç yönetimi API'si isteğe bağlıdır.

Comms API, farklı uygulamalar arasındaki iletişimi desteklemez. Bunun yerine, yalnızca aynı paket adına sahip uygulamalar arasındaki iletişim için tasarlanmıştır ve yalnızca farklı görünür kullanıcılar arasındaki iletişim için kullanılır.

Entegrasyon kılavuzu

AbstractReceiverService'i uygulama

Payload almak için alıcı uygulama, AbstractReceiverService içinde tanımlanan soyut yöntemleri uygulamalıdır. Örneğin:

public class MyReceiverService extends AbstractReceiverService {

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

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

Gönderen istemci bu alıcı istemciye bağlantı isteğinde bulunduğunda onConnectionInitiated() çağrılır. Bağlantının kurulması için kullanıcı onayı gerekiyorsa MyReceiverService, izin etkinliğini başlatmak için bu yöntemi geçersiz kılabilir ve sonuca bağlı olarak acceptConnection() veya rejectConnection()'i çağırabilir. Aksi takdirde, MyReceiverService yalnızca acceptConnection()arayabilir.

onPayloadReceived(), gönderen istemciden MyReceiverService bir Payload aldığında çağrılır. MyReceiverService Bu yöntemi aşağıdaki amaçlarla geçersiz kılabilir:

  • Payload öğesini varsa ilgili alıcı uç noktalarına yönlendirin. Kayıtlı alıcı uç noktalarını almak için getAllReceiverEndpoints() işlevini çağırın. Payload öğesini belirli bir alıcı uç noktasına yönlendirmek için forwardPayload() işlevini çağırın.

VEYA

  • Payload öğesini önbelleğe alın ve beklenen alıcı uç noktası kaydedildiğinde gönderin. Bu durumda MyReceiverService, onReceiverRegistered() üzerinden bilgilendirilir.

AbstractReceiverService'i bildirin

Alıcı uygulama, uygulanan AbstractReceiverService hizmetini manifest dosyasında BEYAN ETMELİ, bu hizmet için android.car.intent.action.RECEIVER_SERVICE işlemiyle bir intent filtresi EKLEMELİ ve android.car.occupantconnection.permission.BIND_RECEIVER_SERVICE iznini GEREKTİRMELİDİR:

<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_SERVICEizni, bu hizmete yalnızca çerçevenin bağlanmasını sağlar. Bu hizmet için izin gerekmiyorsa farklı bir uygulama bu hizmete bağlanabilir ve doğrudan Payload gönderebilir.

İzin beyan etme

İstemci uygulaması, izinleri manifest dosyasında BEYAN ETMELİDİR.

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

Yukarıdaki üç iznin her biri ayrıcalıklı izinlerdir ve izin verilenler listesi dosyaları tarafından önceden verilmelidir. Örneğin, MultiDisplayTest uygulamasının izin verilenler listesi dosyası aşağıda verilmiştir:

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

Araç yöneticileri edinme

API'yi kullanmak için istemci uygulamasının, ilişkili Car yöneticilerini almak üzere CarServiceLifecycleListener kaydetmesi ZORUNLUDUR:

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

(Gönderen) Keşfetme

Gönderen istemci, alıcı istemciye bağlanmadan önce CarRemoteDeviceManager.StateCallback kaydederek alıcı istemciyi KEŞFETMELİDİR:

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

Gönderen, alıcıya bağlantı isteğinde bulunmadan önce alıcının işgalci bölgesi ve alıcı uygulamasının tüm işaretlerinin ayarlandığından EMİN OLMALIDIR. Aksi takdirde hatalar oluşabilir. Örneğin:

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

Gönderenin, alıcının tüm işaretleri ayarlandığında alıcıya bağlantı isteği göndermesini öneririz. Ancak istisnalar vardır:

  • FLAG_OCCUPANT_ZONE_CONNECTION_READY ve FLAG_CLIENT_INSTALLED, bağlantı kurmak için gereken minimum koşullardır.

  • Alıcı uygulamanın bağlantı için kullanıcı onayı almak üzere bir kullanıcı arayüzü göstermesi gerekiyorsa FLAG_OCCUPANT_ZONE_POWER_ON ve FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED ek şartlar haline gelir. Daha iyi bir kullanıcı deneyimi için FLAG_CLIENT_RUNNING ve FLAG_CLIENT_IN_FOREGROUND da önerilir. Aksi takdirde kullanıcı şaşırabilir.

  • Şu an için (Android 15) FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED uygulanmamaktadır. İstemci uygulaması bunu yoksayabilir.

  • Şu anda (Android 15) Comms API, yalnızca aynı Android örneğindeki birden fazla kullanıcıyı desteklemektedir. Böylece, eş uygulamalar aynı uzun sürüm koduna (FLAG_CLIENT_SAME_LONG_VERSION) ve imzaya (FLAG_CLIENT_SAME_SIGNATURE) sahip olabilir. Sonuç olarak, uygulamaların iki değerin aynı olduğunu doğrulaması gerekmez.

Daha iyi bir kullanıcı deneyimi için gönderen istemcisi, bir işaret ayarlanmamışsa kullanıcı arayüzü gösterebilir. Örneğin, FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED ayarlanmamışsa gönderen, alıcı yolcu bölgesi ekranının kilidini açmasını istemek için kullanıcıya kısa mesaj veya iletişim kutusu gösterebilir.

Gönderenin artık alıcıları bulması gerekmediğinde (ör. tüm alıcıları bulup bağlantı kurduğunda veya etkinliğini kaybettiğinde) bulma işlemi DURDURULABİLİR.

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

Keşif durdurulduğunda mevcut bağlantılar etkilenmez. Gönderen, bağlı alıcılara Payload göndermeye devam edebilir.

(Gönderen) Bağlantı isteği

Alıcının tüm işaretleri ayarlandığında gönderen, alıcıya bağlantı isteği GÖNDEREBİLİR:

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

(Alıcı hizmeti) Bağlantıyı kabul etme

Gönderen, alıcıya bağlantı isteğinde bulunduğunda alıcı uygulamasındaki AbstractReceiverService, araç hizmeti tarafından bağlanır ve AbstractReceiverService.onConnectionInitiated() çağrılır. (Gönderen) Bağlantı İsteği bölümünde açıklandığı gibi, onConnectionInitiated() soyutlanmış bir yöntemdir ve istemci uygulaması tarafından uygulanmalıdır.

Alıcı bağlantı isteğini kabul ettiğinde gönderenin ConnectionRequestCallback.onConnected() işlevi çağrılır ve bağlantı kurulur.

(Gönderen) Yükü gönderir.

Bağlantı kurulduktan sonra gönderen, alıcıya Payload gönderebilir:

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

Gönderen, Binder nesnesi veya Payload içine bir bayt dizisi yerleştirebilir. Gönderenin başka veri türleri göndermesi gerekiyorsa verileri bir bayt dizisine seri hale getirmesi, Payload nesnesi oluşturmak için bayt dizisini kullanması ve Payload nesnesini göndermesi ZORUNLUDUR. Ardından, alıcı istemci, alınan Payload'dan bayt dizisini alır ve bayt dizisini beklenen veri nesnesine seri durumdan çıkarır. Örneğin, gönderen, kimliği FragmentB olan alıcı uç noktasına hello dizesini göndermek istiyorsa Proto Buffers'ı kullanarak şu şekilde bir veri türü tanımlayabilir:

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

Şekil 1'de Payload akışı gösterilmektedir:

Yükü gönderme

1. Şekil. Yükü gönderin.

(Alıcı hizmeti) Yükü alma ve gönderme

Alıcı uygulaması Payload aldıktan sonra AbstractReceiverService.onPayloadReceived() çağrılır. Yükü gönderme bölümünde açıklandığı gibi, onPayloadReceived() soyutlanmış bir yöntemdir ve istemci uygulaması tarafından uygulanmalıdır. Bu yöntemde istemci, Payload öğesini ilgili alıcı uç noktalarına iletebilir veya Payload öğesini önbelleğe alıp beklenen alıcı uç noktası kaydedildikten sonra gönderebilir.

(Alıcı uç noktası) Kaydolma ve kaydı silme

Alıcı uç noktalarını kaydetmek için alıcı uygulaması registerReceiver() işlevini ÇAĞIRMALIDIR. Tipik bir kullanım alanı, bir parçanın Payload alması gerektiğidir. Bu nedenle, bir alıcı uç noktası kaydeder:

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

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

Alıcı istemcisindeki AbstractReceiverService, Payload öğesini alıcı uç noktasına gönderdikten sonra ilişkili PayloadCallback çağrılır.

İstemci uygulaması, receiverEndpointId benzersiz olduğu sürece birden fazla alıcı uç noktası kaydedebilir. receiverEndpointId, AbstractReceiverService tarafından yükün hangi alıcı uç noktalarına gönderileceğine karar vermek için kullanılır. Örneğin:

  • Gönderen, Payload alanında receiver_endpoint_id:FragmentB değerini belirtir. Payload alındığında alıcıdaki AbstractReceiverService, Payload'u FragmentB'e göndermek için forwardPayload("FragmentB", payload)'yi arar.
  • Gönderen, Payload alanında data_type:VOLUME_CONTROL değerini belirtir. Payload alındığında alıcıdaki AbstractReceiverService, bu tür Payload öğesinin FragmentB adresine gönderilmesi gerektiğini bilir ve forwardPayload("FragmentB", payload) işlevini çağırır.
if (mOccupantConnectionManager != null) {
    mOccupantConnectionManager.unregisterReceiver("FragmentB");
}

(Gönderen) Bağlantıyı sonlandırın

Gönderenin artık alıcıya Payload göndermesi gerekmediğinde (ör. etkinlik dışı hale geldiğinde) bağlantıyı sonlandırması GEREKİR.

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

Bağlantı kesildikten sonra gönderen, alıcıya artık Payload gönderemez.

Bağlantı akışı

Bağlantı akışı Şekil 2'de gösterilmektedir.

Bağlantı akışı

Şekil 2. Bağlantı akışı.

Sorun giderme

Günlükleri kontrol edin

İlgili günlükleri kontrol etmek için:

  1. Günlüğe kaydetme için bu komutu çalıştırın:

    adb shell setprop log.tag.CarRemoteDeviceService VERBOSE && adb shell setprop log.tag.CarOccupantConnectionService VERBOSE && adb logcat -s "AbstractReceiverService","CarOccupantConnectionManager","CarRemoteDeviceManager","CarRemoteDeviceService","CarOccupantConnectionService"
  2. CarRemoteDeviceService ve CarOccupantConnectionService cihazlarının dahili durumunu boşaltmak için:

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

Null CarRemoteDeviceManager ve CarOccupantConnectionManager

Olası temel nedenlere göz atın:

  1. Araç hizmeti kilitlendi. Daha önce gösterildiği gibi, araba hizmeti kilitlendiğinde iki yönetici kasıtlı olarak null olacak şekilde sıfırlanır. Araba hizmeti yeniden başlatıldığında iki yönetici, boş olmayan değerlere ayarlanır.

  2. CarRemoteDeviceService veya CarOccupantConnectionService etkin değil. Birinin veya diğerinin etkin olup olmadığını belirlemek için şu komutu çalıştırın:

    adb shell dumpsys car_service --services CarFeatureController
    • car_remote_device_service ve car_occupant_connection_service öğelerini içermesi gereken mDefaultEnabledFeaturesFromConfig öğesini bulun. Örneğin:

      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]
      
    • Bu iki hizmet varsayılan olarak devre dışıdır. Bir cihaz çoklu ekranı destekliyorsa bu yapılandırma dosyasını yerleştirmeniz ZORUNLUDUR. İki hizmeti bir yapılandırma dosyasında etkinleştirebilirsiniz:

      // 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 çağrılırken istisna

İstemci uygulaması API'yi amaçlandığı şekilde kullanmıyorsa bir istisna oluşabilir. Bu durumda, istemci uygulaması sorunu çözmek için istisnada ve kilitlenme yığınında mesajı kontrol edebilir. API'nin hatalı kullanımına örnek olarak aşağıdakiler verilebilir:

  • registerStateCallback() Bu müşteri zaten bir StateCallback kaydetti.
  • unregisterStateCallback() Bu CarRemoteDeviceManager örneği tarafından StateCallback kaydedilmedi.
  • registerReceiver() receiverEndpointId zaten kayıtlı.
  • unregisterReceiver() receiverEndpointId kaydedilmemiş.
  • requestConnection() Beklemede olan veya kurulmuş bir bağlantı zaten mevcut.
  • cancelConnection() İptal edilecek bekleyen bağlantı yok.
  • sendPayload() Bağlantı kurulmamış.
  • disconnect() Bağlantı kurulmamış.

İstemci1, istemci2'ye yük gönderebilir ancak istemci2, istemci1'e yük gönderemez.

Bağlantı, tasarım gereği tek yönlüdür. İki yönlü bağlantı kurmak için hem client1 hem de client2 BİRBİRİNE bağlantı isteği göndermeli ve ardından onay almalıdır.