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ç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 durumdaMyReceiverService
,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_SERVICE
izni, 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
veFLAG_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
veFLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
ek şartlar haline gelir. Daha iyi bir kullanıcı deneyimi içinFLAG_CLIENT_RUNNING
veFLAG_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:
(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ındareceiver_endpoint_id:FragmentB
değerini belirtir.Payload
alındığında alıcıdakiAbstractReceiverService
, Payload'uFragmentB
'e göndermek içinforwardPayload("FragmentB", payload)
'yi arar. - Gönderen,
Payload
alanındadata_type:VOLUME_CONTROL
değerini belirtir.Payload
alındığında alıcıdakiAbstractReceiverService
, bu türPayload
öğesininFragmentB
adresine gönderilmesi gerektiğini bilir veforwardPayload("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.
Sorun giderme
Günlükleri kontrol edin
İlgili günlükleri kontrol etmek için:
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"
CarRemoteDeviceService
veCarOccupantConnectionService
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:
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.CarRemoteDeviceService
veyaCarOccupantConnectionService
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
vecar_occupant_connection_service
öğelerini içermesi gerekenmDefaultEnabledFeaturesFromConfig
öğ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 birStateCallback
kaydetti.unregisterStateCallback()
BuCarRemoteDeviceManager
örneği tarafındanStateCallback
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.