ממשק 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 מאפשר ללקוח לעקוב אחרי המצבים של אזורי נוסעים אחרים ברכב, וגם לעקוב אחרי לקוחות עמיתים באזורים האלה. מומלץ להשתמש ב-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 whenMyReceiverServicehas received aPayloadfrom 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)

איור 1. שולחים את עומס העבודה.

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

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

תהליך החיבור

איור 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 יכול לשלוח עומס שימושי ללקוח 2, אבל לא להפך

החיבור הוא חד-כיווני מטבעו. כדי ליצור חיבור דו-כיווני, גם client1 וגם client2 חייבים לבקש חיבור זה לזה ואז לקבל אישור.