אפליקציה עם הרשאות מערכת ב-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 צריכים לבקש חיבור אחד לשני ולקבל אישור.