אפליקציה עם הרשאות מערכת ב-AAOS יכולה להשתמש ב-Multi-Display Communications API כדי לתקשר עם אותה אפליקציה (אותו שם חבילה) שפועלת באזור נוסעים אחר ברכב. בדף הזה מוסבר איך לשלב את ה-API. מידע נוסף זמין גם במאמר CarOccupantZoneManager.OccupantZoneInfo.
אזור הנוסעים
המושג אזור תפוסה ממפה משתמש לקבוצה של מסכים. לכל אזור תפוס יש מסך עם הסוג DISPLAY_TYPE_MAIN. יכול להיות שבאזור הנוסעים יהיו גם מסכים נוספים, כמו מסך לוח המחוונים. לכל אזור של דייר מוקצה משתמש Android. לכל משתמש יש חשבונות ואפליקציות משלו.
הגדרת החומרה
Comms API תומך רק במערכת SoC אחת. במודל של מערכת SoC אחת, כל האזורים והמשתמשים של הדיירים פועלים באותה מערכת SoC. Comms API מורכב משלושה רכיבים:
Power management API מאפשר ללקוח לנהל את ההפעלה של המסכים באזורים שבהם נמצאים הנוסעים.
Discovery API מאפשר ללקוח לעקוב אחרי המצבים של אזורי נוסעים אחרים ברכב, ולעקוב אחרי לקוחות עמיתים באזורי הנוסעים האלה. לפני שמשתמשים ב-Connection API, צריך להשתמש ב-Discovery API.
Connection API מאפשר ללקוח להתחבר ללקוח עמית באזור דיירים אחר ולשלוח מטען ללקוח העמית.
כדי להתחבר, צריך להשתמש ב-Discovery API וב-Connection API. ה-Power management API הוא אופציונלי.
Comms API לא תומך בתקשורת בין אפליקציות שונות. במקום זאת, היא מיועדת רק לתקשורת בין אפליקציות עם אותו שם חבילה, ומשמשת רק לתקשורת בין משתמשים שונים שגלויים זה לזה.
מדריך שילוב
הטמעה של AbstractReceiverService
כדי לקבל את Payload
, אפליקציית המקבל חייבת להטמיע את ה-methods המופשטות שמוגדרות ב-AbstractReceiverService
. לדוגמה:
public class MyReceiverService extends AbstractReceiverService {
@Override
public void onConnectionInitiated(@NonNull OccupantZoneInfo senderZone) {
}
@Override
public void onPayloadReceived(@NonNull OccupantZoneInfo senderZone,
@NonNull Payload payload) {
}
}
onConnectionInitiated()
מופעל כשלקוח השולח מבקש חיבור ללקוח המקבל. אם נדרש אישור מהמשתמש כדי ליצור את החיבור, MyReceiverService
can יכולה לבטל את ה-method הזו כדי להפעיל פעילות של הרשאה, ולקרוא ל-acceptConnection()
או ל-rejectConnection()
בהתאם לתוצאה. אחרת, MyReceiverService
יכול פשוט להתקשר אל
acceptConnection()
.
onPayloadReceived()
מופעל כש-MyReceiverService
מקבל Payload
מלקוח השולח. MyReceiverService
can לבטל את השיטה הזו כדי:
- מעבירים את
Payload
לנקודות הקצה המתאימות של השרתים המקבלים, אם יש כאלה. כדי לקבל את נקודות הקצה של המקלט הרשום, מתקשרים אלgetAllReceiverEndpoints()
. כדי להעביר אתPayload
לנקודת קצה (endpoint) של מקלט נתון, מתקשרים אלforwardPayload()
או,
- שמירת
Payload
במטמון ושליחתו כשהנקודה הסופית של המקבל הרצוי רשומה, וMyReceiverService
מקבלת על כך הודעה דרךonReceiverRegistered()
הצהרה על AbstractReceiverService
אפליקציית המקבל חייבת להצהיר על AbstractReceiverService
שהוטמעה בקובץ המניפסט שלה, להוסיף מסנן Intent עם פעולה android.car.intent.action.RECEIVER_SERVICE
לשירות הזה ולדרוש את ההרשאה android.car.occupantconnection.permission.BIND_RECEIVER_SERVICE
:
<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
מוודאת שרק המסגרת יכולה לבצע כריכה לשירות הזה. אם השירות הזה לא דורש את ההרשאה, אפליקציה אחרת תוכל להתחבר לשירות הזה ולשלוח אליו Payload
ישירות.
הצהרה על הרשאה
אפליקציית הלקוח חייבת להצהיר על ההרשאות בקובץ המניפסט שלה.
<!-- 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"/>
כל אחת משלוש ההרשאות שלמעלה היא הרשאה מיוחדת, שחובה להעניק מראש באמצעות קבצים של רשימת ההיתרים. לדוגמה, הנה קובץ הרשאות של אפליקציית MultiDisplayTest
:
// 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>
איך מקבלים גישה לכלי לניהול רכבים
כדי להשתמש ב-API, אפליקציית הלקוח חייבת לרשום CarServiceLifecycleListener
כדי לקבל את מנהלי הרכב המשויכים:
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);
(הצד השולח) Discover
לפני שמתחברים ללקוח המקבל, לקוח השולח צריך לגלות את לקוח המקבל על ידי רישום של CarRemoteDeviceManager.StateCallback
:
// 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);
}
לפני שמבקשים להתחבר למקבל, השולח צריך לוודא שכל הדגלים של אזור הדייר של המקבל ושל אפליקציית המקבל מוגדרים. אחרת, יכולות להתרחש שגיאות. לדוגמה:
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;
}
מומלץ שהשולח יבקש חיבור לנמען רק אם כל הדגלים של הנמען מוגדרים. עם זאת, יש מקרים חריגים:
FLAG_OCCUPANT_ZONE_CONNECTION_READY
ו-FLAG_CLIENT_INSTALLED
הן הדרישות המינימליות שצריך כדי ליצור חיבור.אם אפליקציית המקבל צריכה להציג ממשק משתמש כדי לקבל אישור מהמשתמש לחיבור,
FLAG_OCCUPANT_ZONE_POWER_ON
ו-FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
הופכים לדרישות נוספות. כדי לשפר את חוויית המשתמש, מומלץ גם להשתמש ב-FLAG_CLIENT_RUNNING
וב-FLAG_CLIENT_IN_FOREGROUND
, אחרת המשתמש עלול להיות מופתע.בשלב הזה (Android 15), התכונה
FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
לא מוטמעת. אפליקציית הלקוח יכולה פשוט להתעלם ממנה.בשלב הזה (Android 15), ה-Comms API תומך רק בכמה משתמשים באותו מופע של Android, כך שאפליקציות עמיתות יכולות להשתמש באותו קוד גרסה ארוך (
FLAG_CLIENT_SAME_LONG_VERSION
) ובאותה חתימה (FLAG_CLIENT_SAME_SIGNATURE
). כתוצאה מכך, האפליקציות לא צריכות לוודא שהערכים זהים.
כדי לשפר את חוויית המשתמש, לקוח השולח יכול להציג ממשק משתמש אם לא מוגדר דגל. לדוגמה, אם FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
לא מוגדר, השולח יכול להציג הודעה קופצת או תיבת דו-שיח כדי לבקש מהמשתמש לבטל את הנעילה של המסך באזור של הנמען.
כשהשולח כבר לא צריך לגלות את הנמענים (לדוגמה, כשהוא מוצא את כל הנמענים ומקים חיבורים או כשהוא הופך ללא פעיל), הוא יכול להפסיק את הגילוי.
if (mRemoteDeviceManager != null) {
mRemoteDeviceManager.unregisterStateCallback();
}
כשמפסיקים את האיתור, החיבורים הקיימים לא מושפעים. השולח יכול להמשיך לשלוח Payload
למקלטים המחוברים.
(השולח) שליחת בקשת חיבור
כשכל הדגלים של המקבל מוגדרים, השולח יכול לבקש להתחבר למקבל:
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);
}
(שירות המקבל) אישור החיבור
אחרי שהשולח מבקש להתחבר למקבל, AbstractReceiverService
באפליקציה של המקבל יקבל קישור לשירות הרכב, ו-AbstractReceiverService.onConnectionInitiated()
יופעל. כמו שמוסבר ב(Sender) Request Connection,
onConnectionInitiated()
היא שיטה מופשטת וחובה להטמיע אותה באפליקציית הלקוח.
כשהמקבל מאשר את בקשת החיבור, הפונקציה ConnectionRequestCallback.onConnected()
של השולח מופעלת, ואז החיבור נוצר.
(השולח) שליחת מטען הנתונים
אחרי שהחיבור נוצר, השולח יכול לשלוח Payload
לנמען:
if (mOccupantConnectionManager != null) {
Payload payload = ...;
try {
mOccupantConnectionManager.sendPayload(receiverZone, payload);
} catch (CarOccupantConnectionManager.PayloadTransferException e) {
Log.e(TAG, "Failed to send Payload to " + receiverZone);
}
}
השולח יכול להוסיף אובייקט Binder
או מערך בייטים Payload
. אם השולח צריך לשלוח סוגי נתונים אחרים, הוא חייב לבצע סריאליזציה של הנתונים למערך בייטים, להשתמש במערך הבייטים כדי ליצור אובייקט Payload
ולשלוח את Payload
. לאחר מכן, לקוח המקבל מקבל את מערך הבייטים מה-Payload
שהתקבל, ומבטל את הסריאליזציה של מערך הבייטים לאובייקט הנתונים הצפוי.
לדוגמה, אם השולח רוצה לשלוח מחרוזת hello
לנקודת הקצה של המקבל עם המזהה FragmentB
, הוא יכול להשתמש ב-Proto Buffers כדי להגדיר סוג נתונים באופן הבא:
message MyData {
required string receiver_endpoint_id = 1;
required string data = 2;
}
איור 1 ממחיש את התהליך של Payload
:
(שירות המקבל) קבלת המטען הייעודי (payload) ושליחתו
אחרי שאפליקציית הנמען מקבלת את Payload
, הפונקציה AbstractReceiverService.onPayloadReceived()
שלה מופעלת. כפי שמוסבר בקטע שליחת מטען הייעודי, onPayloadReceived()
היא שיטה מופשטת וחובה להטמיע אותה באפליקציית הלקוח. בשיטה הזו, הלקוח יכול להעביר את Payload
לנקודות הקצה המתאימות של הנמען, או לשמור את Payload
במטמון ואז לשלוח אותו ברגע שנקודת הקצה הצפויה של הנמען נרשמת.
(נקודת קצה של מקלט) הרשמה וביטול הרשמה
אפליקציית המקבל צריכה לקרוא ל-registerReceiver()
כדי לרשום את נקודות הקצה של המקבל. תרחיש שימוש טיפוסי הוא שצריך לקבל Fragment Payload
, ולכן הוא רושם נקודת קצה של מקלט:
private final PayloadCallback mPayloadCallback = (senderZone, payload) -> {
…
};
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.registerReceiver("FragmentB",
getActivity().getMainExecutor(), mPayloadCallback);
}
אחרי ש-AbstractReceiverService
בלקוח המקבל ישלח את Payload
לנקודת הקצה של המקבל, יופעל PayloadCallback
המשויך.
אפליקציית הלקוח יכולה לרשום כמה נקודות קצה של מקלט, כל עוד ערכי receiverEndpointId
שלהן ייחודיים באפליקציית הלקוח. ערך receiverEndpointId
ישמש את AbstractReceiverService
כדי להחליט לאילו נקודות קצה של מקלט לשלוח את המטען הייעודי(Payload). לדוגמה:
- השולח מציין
receiver_endpoint_id:FragmentB
בPayload
. כשמתקבלת ההודעהPayload
, הרכיבAbstractReceiverService
במקלט קורא ל-forwardPayload("FragmentB", payload)
כדי לשלוח את המטען ל-FragmentB
- השולח מציין
data_type:VOLUME_CONTROL
בPayload
. כשמתקבלPayload
, רכיבAbstractReceiverService
ברסיבר יודע שסוגPayload
הזה צריך להישלח אלFragmentB
, ולכן הוא קורא ל-forwardPayload("FragmentB", payload)
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.unregisterReceiver("FragmentB");
}
(השולח) סיום החיבור
אם השולח לא צריך יותר לשלוח Payload
לנמען (למשל, אם הוא הופך ללא פעיל), הוא צריך לנתק את החיבור.
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.disconnect(receiverZone);
}
אחרי הניתוק, השולח לא יכול יותר לשלוח Payload
לנמען.
תהליך החיבור
איור 2 מציג את תהליך החיבור.
פתרון בעיות
בדיקת היומנים
כדי לבדוק את היומנים המתאימים:
מריצים את הפקודה הבאה לרישום ביומן:
adb shell setprop log.tag.CarRemoteDeviceService VERBOSE && adb shell setprop log.tag.CarOccupantConnectionService VERBOSE && adb logcat -s "AbstractReceiverService","CarOccupantConnectionManager","CarRemoteDeviceManager","CarRemoteDeviceService","CarOccupantConnectionService"
כדי ליצור dump של המצב הפנימי של
CarRemoteDeviceService
ושלCarOccupantConnectionService
:adb shell dumpsys car_service --services CarRemoteDeviceService && adb shell dumpsys car_service --services CarOccupantConnectionService
Null CarRemoteDeviceManager ו-CarOccupantConnectionManager
אלה כמה סיבות אפשריות לבעיה:
שירות הרכב קרס. כפי שמוצג באיור שלמעלה, שתי ההגדרות של המנהלים מאופסות בכוונה ל-
null
כששירות הרכב קורס. כשמפעילים מחדש את שירות הרכב, שני המנהלים מוגדרים לערכים שאינם null.האפשרות
CarRemoteDeviceService
אוCarOccupantConnectionService
לא מופעלת. כדי לבדוק אם אחת מהן מופעלת, מריצים את הפקודה:adb shell dumpsys car_service --services CarFeatureController
מחפשים את
mDefaultEnabledFeaturesFromConfig
, שצריך להכיל אתcar_remote_device_service
ואתcar_occupant_connection_service
. לדוגמה: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]
כברירת מחדל, שני השירותים האלה מושבתים. אם המכשיר תומך במספר מסכים, חובה להוסיף את קובץ ההגדרה הזה. אפשר להפעיל את שני השירותים בקובץ הגדרה:
// 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
אם אפליקציית הלקוח לא משתמשת ב-API כמצופה, יכול להתרחש חריג. במקרה כזה, אפליקציית הלקוח יכולה לבדוק את ההודעה בחריגה ואת מחסנית הקריאות לקריסת התוכנה כדי לפתור את הבעיה. דוגמאות לשימוש לרעה ב-API:
registerStateCallback()
הלקוח הזה כבר רשםStateCallback
.unregisterStateCallback()
לא נרשםStateCallback
על ידי מופעCarRemoteDeviceManager
הזה.- הדומיין
registerReceiver()
receiverEndpointId
כבר רשום. unregisterReceiver()
receiverEndpointId
לא רשום.requestConnection()
כבר קיים חיבור בהמתנה או חיבור פעיל.cancelConnection()
אין בקשת חיבור בהמתנה לביטול.sendPayload()
אין חיבור פעיל.disconnect()
אין חיבור פעיל.
לקוח1 יכול לשלוח מטען ייעודי (Payload) ללקוח2, אבל לא להיפך
החיבור הוא חד-כיווני. כדי ליצור חיבור דו-כיווני, גם client1
וגם client2
צריכים לבקש חיבור אחד לשני ולקבל אישור.