Multi-Display Communications API, AAOS'teki ayrıcalıklı bir sistem uygulaması tarafından, bir arabanın farklı bir yolcu bölgesinde çalışan aynı uygulamayla (aynı paket adı) 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'ya da göz atabilirsiniz.
Yolcu bölgesi
Kullanıcı bölgesi kavramı, bir kullanıcıyı bir dizi ekrana eşler. Her yaşam alanı bölgesinde DISPLAY_TYPE_MAIN türünde bir ekran bulunur. 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 bölgedeki benzer istemciye bağlanmasına ve bu istemciye yük göndermesine olanak tanır.
Bağlantı için Discovery API ve Connection API gerekir. 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ı oluşturmak için kullanıcı onayı gerekiyorsa MyReceiverService, izin etkinliği başlatmak için bu yöntemi geçersiz kılabilir ve sonuca bağlı olarak acceptConnection() veya rejectConnection() yöntemini çağırabilir. Aksi takdirde, MyReceiverService yalnızca acceptConnection()'ı arayabilir.
onPayloadReceived(), gönderen istemciden MyReceiverService'a Payload geldiğinde çağrılır. MyReceiverService Bu yöntemi geçersiz kılarak şunları yapabilirsiniz:
- Varsa
Payloadöğesini ilgili alıcı uç noktalarına yönlendirin. Kayıtlı alıcı uç noktalarını almak içingetAllReceiverEndpoints()işlevini çağırın.Payloadöğesini belirli bir alıcı uç noktasına yönlendirmek içinforwardPayload()işlevini çağırın.
VEYA
Payloadöğesini önbelleğe alın ve beklenen alıcı uç noktası kaydedildiğinde gönderin. Bu işlem içinMyReceiverService,onReceiverRegistered()üzerinden bilgilendirilir.
AbstractReceiverService'i tanımlayın
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 eklemeli ve android.car.occupantconnection.permission.BIND_RECEIVER_SERVICE iznini istemelidir:
<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ğlanabilmesini sağlar. Bu hizmet için izin gerekmiyorsa farklı bir uygulama bu hizmete bağlanabilir ve doğrudan Payload gönderebilir.
İzin tanımlama
İ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 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ş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 bulunduğu bölge 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_READYveFLAG_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_ONveFLAG_OCCUPANT_ZONE_SCREEN_UNLOCKEDek şartlar haline gelir. Daha iyi bir kullanıcı deneyimi içinFLAG_CLIENT_RUNNINGveFLAG_CLIENT_IN_FOREGROUNDda önerilir. Aksi takdirde kullanıcı şaşırabilir.Şu an için (Android 15)
FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKEDuygulanmamaktadı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 eşleştiğini 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, kullanıcıya alıcı yolcu bölgesinin ekranını kilidini açmasını istemek için bir pop-up 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 gönder
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 UYGULANMALIDIR.
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ö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, 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 veri türünü şu şekilde tanımlayabilir:
message MyData {
required string receiver_endpoint_id = 1;
required string data = 2;
}
Şekil 1'de Payload akışı gösterilmektedir:
(Alıcı hizmeti) Yükü alma ve gönderme
Alıcı uygulama 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ı) Kayıt ve kayıt iptali
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 değerleri istemci uygulaması içinde 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,
Payloadalanındareceiver_endpoint_id:FragmentBdeğerini belirtir.Payloadalındığında alıcıdakiAbstractReceiverService, yüküFragmentB'e göndermek içinforwardPayload("FragmentB", payload)'yi çağırır. - Gönderen,
Payloadalanındadata_type:VOLUME_CONTROLdeğerini belirtir.Payloadalındığında alıcıdakiAbstractReceiverService, bu türPayload'nınFragmentB'ye gönderilmesi gerektiğini bilir veforwardPayload("FragmentB", payload)'ü çağırır.
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.unregisterReceiver("FragmentB");
}
(Gönderen) Bağlantıyı sonlandırma
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.
Sorun giderme
Günlükleri kontrol edin
İlgili günlükleri kontrol etmek için:
Günlük kaydı 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"CarRemoteDeviceServiceveCarOccupantConnectionServicecihazları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 nedenleri inceleyin:
Araç hizmeti kilitlendi. Daha önce gösterildiği gibi, araç hizmeti kilitlendiğinde iki yönetici de kasıtlı olarak
nullolacak şekilde sıfırlanır. Araba hizmeti yeniden başlatıldığında iki yönetici, boş olmayan değerlere ayarlanır.CarRemoteDeviceServiceveyaCarOccupantConnectionServiceetkin 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 CarFeatureControllermDefaultEnabledFeaturesFromConfigöğesini bulun. Bu öğecar_remote_device_servicevecar_occupant_connection_serviceöğelerini içermelidir. Ö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 oluştu
İ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 kötüye kullanımına örnekler:
registerStateCallback()Bu müşteri zaten birStateCallbackkaydettirmiş.unregisterStateCallback()BuStateCallbackörneği tarafındanCarRemoteDeviceManagerkaydedilmedi.registerReceiver()receiverEndpointIdzaten kayıtlı.unregisterReceiver()receiverEndpointIdkaydedilmemiş.requestConnection()Bekleyen veya kurulmuş bir bağlantı zaten var.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.