Multi-Display Communications API

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

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

תהליך החיבור

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