Multi-Display Communications API

אפליקציה עם הרשאות מערכת ב-AAOS יכולה להשתמש ב-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, אפליקציית המקלט חייבת להטמיע את ה-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() יופעל. כמו שמוסבר בבקשת החיבור(השולח),‏ 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)

איור 1. שולחים את מטען הנתונים.

(שירות המקבל) קבלת המטען הייעודי והעברתו

אחרי שאפליקציית הנמען מקבלת את 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 כדי להחליט לאילו נקודות קצה של מקלט לשלוח את המטען הייעודי(Payload).AbstractReceiverService לדוגמה:

  • השולח מציין 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 מציג את תהליך החיבור.

תהליך ההתחברות

איור 2. תהליך החיבור.

פתרון בעיות

בדיקת היומנים

כדי לבדוק את היומנים המתאימים:

  1. מריצים את הפקודה הבאה לרישום ביומן:

    adb shell setprop log.tag.CarRemoteDeviceService VERBOSE && adb shell setprop log.tag.CarOccupantConnectionService VERBOSE && adb logcat -s "AbstractReceiverService","CarOccupantConnectionManager","CarRemoteDeviceManager","CarRemoteDeviceService","CarOccupantConnectionService"
  2. כדי להציג את המצב הפנימי של CarRemoteDeviceService ושל CarOccupantConnectionService:

    adb shell dumpsys car_service --services CarRemoteDeviceService && adb shell dumpsys car_service --services CarOccupantConnectionService

‫Null CarRemoteDeviceManager ו-CarOccupantConnectionManager

אלה כמה מהסיבות האפשריות:

  1. שירות הרכב קרס. כפי שמוצג באיור שלמעלה, שני המנהלים מאופסים בכוונה ל-null כששירות המכוניות קורס. כשמפעילים מחדש את שירות המכוניות, שני המנהלים מוגדרים לערכים שאינם null.

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