אפליקציה בעלת הרשאות מערכת יכולה להשתמש ב-Multi-Display Communications API AAOS לתקשורת עם אותה אפליקציה (אותו שם חבילה) שפועלת במכשיר אחר אזור הנוסעים ברכב. בדף הזה נסביר איך לשלב את ה-API. למידה תוכלו לראות גם CarOccupantZoneManager.OccupantZoneInfo
אזור תפוסה
המושג אזור אורחים ממפה את המשתמש לקבוצה של תצוגות. כל אחד אזור התפוסה כולל תצוגה עם סוג DISPLAY_TYPE_MAIN. באזור המיועד לנוסעים יכולים להיות גם תצוגות נוספות, כמו אשכול. לכל אזור תפוסה מוקצה משתמש Android. לכל משתמש יש חשבון משלו ואפליקציות.
תצורת החומרה
ה-Comms API תומך רק ב-SoC יחיד. במודל SoC יחיד, כל הדיירים אזורים ומשתמשים פועלים באותו SoC. ה-Comms API מורכב משלושה רכיבים:
Power management API מאפשר ללקוח לנהל את העוצמה של מוצגת באזורי התפוסה.
Discovery API מאפשר ללקוח לעקוב אחר המצבים של דייר אחר באזורים ברכב, וכדי לעקוב אחר לקוחות עמיתים שנמצאים באזורי התושבים האלה. כדאי להשתמש ב-Discovery API לפני שמשתמשים ב-Connection API.
Connection API מאפשר ללקוח להתחבר ללקוח השכנה שלו אזור תפוסה אחר וכדי לשלוח מטען ייעודי (payload) ללקוח העמית.
כדי להתחבר, נדרשים ממשק ה-API של Discovery וה-Connection API. הכוח Management API הוא אופציונלי.
Comms API לא תומך בתקשורת בין אפליקציות שונות. במקום זאת, היא מיועדת רק לתקשורת בין אפליקציות שיש להן שם חבילה זהה ומשמשים רק לתקשורת בין משתמשים גלויים שונים.
מדריך הטמעה
הטמעת AbstractReceiverService
כדי לקבל את Payload
, האפליקציה המקבלת חייבת להטמיע את השיטות המופשטות
הוגדרה ב-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
יכול לעקוף את השיטה הזו כדי להפעיל
פעילות הרשאות, ולהתקשר אל acceptConnection()
או rejectConnection()
על התוצאה. אחרת, MyReceiverService
יוכל פשוט להתקשר
acceptConnection()
."
onPayloadReceived() is invoked when
MyReceiverServicehas received a
Payloadfrom the sender client.
MyReceiverService' יש אפשרות לשנות זאת
ל:
- מעבירים את
Payload
לנקודות הקצה המתאימות של המקבל, אם יש כאלה. שפת תרגום מקבלים את נקודות הקצה של המקבל הרשום, קוראים לפונקציהgetAllReceiverEndpoints()
. שפת תרגום העברה שלPayload
לנקודת קצה מסוימת של המקבל, קוראים לפונקציה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
מבטיחה שרק ה-framework יכול להיות מקושר לשירות הזה. אם השירות הזה
לא דורשת את ההרשאה, יכול להיות שאפליקציה אחרת תוכל לקשר לקובץ הזה
שירות ולשלוח אליו 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()
יופעלו. בתור
כפי שמוסבר בקישור של בקשת הקישור(השולח),
onConnectionInitiated()
היא שיטה מופשטת וחובה ליישם אותה
אפליקציית לקוח.
כשהנמען מאשר את בקשת החיבור, הסטטוס של השולח
תתבצע הפעלה של ConnectionRequestCallback.onConnected()
ולאחר מכן החיבור
נוצר.
(שולח) שליחת המטען הייעודי (payload)
לאחר יצירת החיבור, השולח יכול לשלוח את 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
, מבצעת פעולת deserialize של מערך הבייטים לאובייקט הנתונים המצופה.
לדוגמה, אם השולח רוצה לשלוח מחרוזת hello
למקבל התשלום
של נקודת הקצה עם המזהה FragmentB
, היא יכולה להשתמש במאגרי Proto Buffers כדי להגדיר סוג נתונים
כך:
message MyData {
required string receiver_endpoint_id = 1;
required string data = 2;
}
איור 1 ממחיש את התהליך של Payload
:
(שירות המקבל) קבלה ושליחה של המטען הייעודי (Payload)
לאחר שאפליקציית המקבל מקבלת את Payload
,
תתבצע הפעלה של AbstractReceiverService.onPayloadReceived()
. כפי שמוסבר ב
המטען הייעודי (payload), onPayloadReceived()
הוא
מופשטת וחובה להטמיע אותה באפליקציית הלקוח. בשיטה הזאת, הפונקציה
הלקוח יכול להעביר את Payload
לנקודות הקצה המתאימות של המקבל, או
לשמור במטמון את Payload
ואז לשלוח אותו כשנקודת הקצה הצפויה של המקבל
רשום.
(נקודת קצה של המקבל) רישום וביטול הרישום
אפליקציית המקבל צריכה לבצע קריאה למספר registerReceiver()
כדי לרשום את המקבל
נקודות קצה (endpoints). תרחיש לדוגמה טיפוסי הוא ש-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)
כדי לשלוח את המטען הייעודי (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"
כדי להשליך את המצב הפנימי של
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()
לא נוצר חיבור.
Client1 יכול לשלוח מטען ייעודי (payload) ל-Client2, אבל לא להיפך
החיבור הוא דרך אחת בתכנון. כדי ליצור חיבור דו-כיווני,
client1
ו-client2
חייבים לבקש חיבור זה לזה ואז
לקבל אישור.