Ç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 when
MyReceiverServicehas received a
Payloadfrom 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çingetAllReceiverEndpoints()
numaralı telefonu arayın.Payload
'ü belirli bir alıcı uç noktasına yönlendirmek içinforwardPayload()
VEYA
Payload
öğesini önbelleğe alın ve beklenen alıcı uç noktası kaydedildiğinde gönderin. Bu durumdaMyReceiverService
,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
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 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:
(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ındareceiver_endpoint_id:FragmentB
değerini belirtir.Payload
aldığında alıcıdakiAbstractReceiverService
,FragmentB
'a yük göndermek içinforwardPayload("FragmentB", payload)
'yi çağırır. - Gönderen,
Payload
alanındadata_type:VOLUME_CONTROL
değerini belirtir. AlıcıdakiAbstractReceiverService
,Payload
aldığında bu türPayload
'lerinFragmentB
'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 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.
Sorun giderme
Günlükleri kontrol etme
İlgili günlükleri kontrol etmek için:
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"
CarRemoteDeviceService
veCarOccupantConnectionService
'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:
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.CarRemoteDeviceService
veyaCarOccupantConnectionService
etkin değil. Hangisinin etkinleştirilip etkinleştirilmediğini belirlemek için:adb shell dumpsys car_service --services CarFeatureController
car_remote_device_service
vecar_occupant_connection_service
içerenmDefaultEnabledFeaturesFromConfig
öğ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 birStateCallback
kaydettirdi.unregisterStateCallback()
BuCarRemoteDeviceManager
örneği tarafından kaydedilenStateCallback
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.