Çoklu Ekran İletişim API'sı

Çoklu Ekran İletişimi API'si, bir arabadaki farklı bir yolcu bölgesinde çalışan aynı uygulamayla (aynı paket adı) iletişim kurmak için AAOS'teki ayrıcalıklı bir sistem uygulaması tarafından kullanılabilir. Bu sayfada, API'nin nasıl entegre edileceği açıklanmaktadır. Daha fazla bilgi edinmek için CarOccupantZoneManager.OccupantZoneInfo'ya da göz atabilirsiniz.

Yolcu bölgesi

Kullanıcı bölgesi kavramı, kullanıcıyı bir dizi ekranla eşler. Her kullanıcı bölgesinde DISPLAY_TYPE_MAIN türüne sahip bir ekran bulunur. Kullanıcı bölgesi, grup ekranı gibi ek ekranlara da sahip 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 kullanıcı bölgeleri ve kullanıcılar aynı SoC'de çalışır. Comms API üç bileşenden oluşur:

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

  • Discovery API, istemcinin araçtaki diğer yolcu bölgelerinin durumlarını ve bu yolcu bölgelerinde bulunan eş istemcileri izlemesine olanak tanır. Connection API'yi kullanmadan önce Discovery API'yi kullanın.

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

Bağlantı için Discovery API ve Connection API gereklidir. Power Management API 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ında iletişim için tasarlanmış ve yalnızca farklı görünür kullanıcılar arasında iletişim için kullanılmıştır.

Entegrasyon kılavuzu

AbstractReceiverService'i uygulama

Payload almak için alıcı uygulamasının AbstractReceiverService içinde tanımlanan soyut yöntemleri UYGULAMASI GEREKİR. Örnek:

public class MyReceiverService extends AbstractReceiverService {

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

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

onConnectionInitiated(), gönderen istemci bu alıcı istemciyle bağlantı isteğinde bulunduğunda çağrılır. Bağlantıyı kurmak için kullanıcı onayı gerekiyorsa MyReceiverService, izin etkinliği başlatmak için bu yöntemi geçersiz kılar ve sonuca göre acceptConnection() veya rejectConnection()'i çağırır. Aksi takdirde MyReceiverService acceptConnection()arayabilir.

onPayloadReceived()is invoked whenMyReceiverServicehas received aPayloadfrom the sender client.MyReceiverService` bu yöntemi üstlenebilir:

  • Payload'yi, varsa ilgili alıcı uç noktalarına yönlendirin. Kayıtlı alıcı uç noktalarını almak için getAllReceiverEndpoints() numaralı telefonu arayın. Payload'ü belirli bir alıcı uç noktasına yönlendirmek için forwardPayload()

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 tanımlama

Alıcı uygulama, uygulanan AbstractReceiverService'ü manifest dosyasında beyan etmeli, bu hizmet için android.car.intent.action.RECEIVER_SERVICE işlemi içeren bir intent filtresi eklemeli ve android.car.occupantconnection.permission.BIND_RECEIVER_SERVICE iznini zorunlu kılmalı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_SERVICE izni, yalnızca çerçevenin bu hizmete bağlanmasını sağlar. Bu hizmet izin gerektirmiyorsa farklı bir uygulama bu hizmete bağlanıp doğrudan Payload gönderebilir.

İzin beyanı

İ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 listesindeki dosyalar tarafından önceden verilmesi GEREKİR. Ö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öneticilerini alma

API'yi kullanmak için istemci uygulamasının, ilişkili araç yöneticilerini almak üzere bir CarServiceLifecycleListener kaydetmesi GEREKİR:

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şfet

Gönderen istemci, alıcı istemciye bağlanmadan önce CarRemoteDeviceManager.StateCallback kaydederek alıcı istemcisini KEŞFEDECEK:

// 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ıyla bağlantı isteğinde bulunmadan önce alıcı kullanıcı bölgesi ve alıcı uygulamasının tüm işaretlerinin ayarlandığından emin OLMALIDIR. Aksi takdirde hatalar oluşabilir. Örnek:

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ıya yalnızca alıcıyla ilgili tüm işaretler ayarlandığında bağlantı isteğinde bulunmasını öneririz. Bununla birlikte, istisnalar da 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 anda (Android 15) FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED uygulanmıyor. İstemci uygulaması bu durumu yoksayabilir.

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

Daha iyi bir kullanıcı deneyimi için gönderen istemcisi, işaret ayarlanmamışsa kullanıcı arayüzü GÖSTEREBİLİR. Örneğin, FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED ayarlanmazsa gönderen, kullanıcıdan alıcı yolcu bölgesinin ekranının kilidini açmasını isteyen bir pop-up veya iletişim kutusu gösterebilir.

Göndericinin artık alıcıları keşfetmesi gerekmediğinde (örneğin, tüm alıcıları ve kurulan bağlantıları bulduğunda veya etkin olmadığında) keşfi durdurabilir.

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

Alıcının tüm işaretleri ayarlandığında gönderen, alıcıyla bağlantı isteğinde bulunabilir:

    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ı istediğinde alıcı uygulamasındaki AbstractReceiverService, araç hizmetine bağlanır ve AbstractReceiverService.onConnectionInitiated() çağrılır. (Gönderen) Bağlantı İste bölümünde açıklandığı gibi, onConnectionInitiated() soyut bir yöntemdir ve istemci uygulaması tarafından uygulanmalıdır.

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

(Gönderen) Yükleyiciyi gönderin

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, Payload içine bir Binder nesnesi veya bayt dizisi yerleştirebilir. Gönderenin başka veri türleri göndermesi gerekiyorsa verileri bir bayt dizisine serileştirmeli, bayt dizisini kullanarak bir Payload nesnesi oluşturmalı ve Payload'yi göndermelidir. Ardından alıcı istemci, alınan Payload öğesinden bayt dizisini alır ve bayt dizisini beklenen veri nesnesine dönüştürür. Örneğin, gönderen FragmentB kimlikli alıcı uç noktasına bir hello dizesi göndermek istiyorsa Proto Buffers'ı kullanarak aşağıdaki gibi bir veri türü tanımlayabilir:

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

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

Yükü gönderme

Şekil 1. Yükleyiciyi gönderin.

(Alıcı hizmeti) Yükün alınması ve gönderilmesi

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

(Alıcı uç noktası) Kaydolma ve kaydı iptal etme

Alıcı uygulaması, alıcı uç noktalarını kaydetmek için registerReceiver()'yi ÇAĞIRMALIDIR. Tipik bir kullanım alanı, bir parçanın Payload alıcısına ihtiyacı olmasıdır. Bu nedenle, alıcı uç noktası kaydeder:

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

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

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

İstemci uygulaması, receiverEndpointId değerleri istemci uygulamasında 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. Örnek:

  • Gönderen, Payload alanında receiver_endpoint_id:FragmentB değerini belirtir. Payload aldığında alıcıdaki AbstractReceiverService, FragmentB'a yük göndermek için forwardPayload("FragmentB", payload)'yi çağırır.
  • Gönderen, Payload alanında data_type:VOLUME_CONTROL değerini belirtir. Alıcıdaki AbstractReceiverService, Payload aldığında bu tür Payload'lerin FragmentB'ye gönderilmesi gerektiğini bilir ve forwardPayload("FragmentB", payload)'ı çağırır.
if (mOccupantConnectionManager != null) {
    mOccupantConnectionManager.unregisterReceiver("FragmentB");
}

(Gönderen) Bağlantıyı sonlandırma

Gönderenin alıcıya Payload göndermesi gerekmediğinde (örneğin, etkin olmadığında) bağlantıyı SONLANDIRACAK.

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

Bağlantı kesildikten sonra gönderen, alıcıya 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 etme

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

  1. Günlük kaydı için şu 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'un dahili durumunu dökmek için:

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

Boş CarRemoteDeviceManager ve CarOccupantConnectionManager

Olası temel nedenleri inceleyin:

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

  2. CarRemoteDeviceService veya CarOccupantConnectionService etkin değil. Hangisinin etkinleştirilip etkinleştirilmediğini belirlemek için:

    adb shell dumpsys car_service --services CarFeatureController
    
    • car_remote_device_service ve car_occupant_connection_service içeren 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ŞTİRMENİZ GEREKİR. İ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'yi çağırırken istisna

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

  • registerStateCallback() Bu müşteri zaten bir StateCallback kaydettirdi.
  • unregisterStateCallback() Bu CarRemoteDeviceManager örneği tarafından kaydedilen StateCallback yok.
  • registerReceiver() receiverEndpointId zaten kayıtlı.
  • unregisterReceiver() receiverEndpointId kayıtlı değil.
  • requestConnection() Beklemedeki veya kurulmuş bir bağlantı zaten mevcut.
  • cancelConnection() İptal edilecek bekleyen bağlantı yok.
  • sendPayload() Bağlantı kurulmadı.
  • disconnect() Bağlantı kurulmadı.

İstemci1, istemci2'ye Yük gönderebilir ancak diğer yöne 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'ın birbirlerine bağlantı isteğinde bulunması ve ardından onay alması GEREKİR.