אפליקציה עם הרשאות מערכת ב-AAOS יכולה להשתמש ב-Multi-Display Communications API כדי לתקשר עם אותה אפליקציה (עם אותו שם חבילה) שפועלת באזור נוסעים אחר ברכב. בדף הזה נסביר איך לשלב את ה-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 מאפשר ללקוח להתחבר ללקוח המקביל שלו באזור אחר של דייר, ולשלוח לו עומס שימושי.
כדי לבצע את החיבור, נדרשים Discovery API ו-Connection API. ממשק ה-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
לנקודת קצה (endpoint) של נמען מסוים, צריך לבצע קריאה ל-forwardPayload()
OR
- שומרים את
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), ה-method
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);
}
}
השולח יכול להוסיף לאובייקט Payload
אובייקט Binder
או מערך בייטים. אם השולח צריך לשלוח סוגים אחרים של נתונים, הוא חייב לסדר את הנתונים במערך בייטים, להשתמש במערך הבייטים כדי ליצור אובייקט Payload
ולשלוח את ה-Payload
. לאחר מכן, לקוח המקבל מקבל את מערך הבייטים מ-Payload
שהתקבל, ומבצע דה-סריאליזציה של מערך הבייטים לאובייקט הנתונים הצפוי.
לדוגמה, אם השולח רוצה לשלוח מחרוזת hello
לנקודת הקצה של הנמען עם המזהה FragmentB
, הוא יכול להשתמש ב-Proto Buffers כדי להגדיר סוג נתונים כך:
message MyData {
required string receiver_endpoint_id = 1;
required string data = 2;
}
איור 1 מדגים את התהליך של Payload
:
(שירות מקלט) קבלה ושליחה של עומס העבודה
אחרי שאפליקציית הנמען תקבל את Payload
, ה-AbstractReceiverService.onPayloadReceived()
שלה יופעל. כפי שמוסבר בקטע שליחת עומס העבודה, onPayloadReceived()
הוא method מופשט שחובה להטמיע באפליקציית הלקוח. ב-method הזה, הלקוח יכול להעביר את Payload
לנקודות הקצה של הנמען המתאימות, או לשמור את Payload
במטמון ולשלוח אותו ברגע שנקודת הקצה הצפויה של הנמען תירשם.
(נקודת קצה של מקלט) רישום וביטול רישום
אפליקציית המקבל צריכה לבצע קריאה ל-registerReceiver()
כדי לרשום את נקודות הקצה של המקבל. תרחיש לדוגמה: יש צורך לקבל Payload
ב-Fragment, ולכן צריך לרשום נקודת קצה של מקלט:
private final PayloadCallback mPayloadCallback = (senderZone, payload) -> {
…
};
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.registerReceiver("FragmentB",
getActivity().getMainExecutor(), mPayloadCallback);
}
אחרי שה-AbstractReceiverService
בלקוח המקבל שולח את ה-Payload
לנקודת הקצה של המקבל, ה-PayloadCallback
המשויך יופעל.
אפליקציית הלקוח יכולה לרשום כמה נקודות קצה של נמענים, כל עוד הערכים של receiverEndpointId
ייחודיים באפליקציית הלקוח. ה-AbstractReceiverService
ישתמש ב-receiverEndpointId
כדי להחליט לאילו נקודות קצה של נמענים לשלוח את 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()
אין חיבור.
לקוח 1 יכול לשלוח עומס שימושי ללקוח 2, אבל לא להפך
החיבור הוא חד-כיווני מטבעו. כדי ליצור חיבור דו-כיווני, גם client1
וגם client2
חייבים לבקש חיבור זה לזה ואז לקבל אישור.