ממשק API לתקשורת בין מסכים

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

איור 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.

תהליך החיבור

איור 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() לא נוצר חיבור.

Client1 יכול לשלוח מטען ייעודי (payload) ל-Client2, אבל לא להיפך

החיבור הוא דרך אחת בתכנון. כדי ליצור חיבור דו-כיווני, client1 ו-client2 חייבים לבקש חיבור זה לזה ואז לקבל אישור.