הטמעת מאפייני מערכת כ-API

מאפייני מערכת (sysprops) מספקים דרך נוחה לשתף מידע, בדרך כלל הגדרות, בכל המערכת. כל מחיצה יכולה להשתמש במאפייני המערכת שלה באופן פנימי. בעיה יכולה להתרחש כשניגשים למאפיינים במחיצות שונות, כמו /vendor שניגשת למאפיינים שהוגדרו ב-/system. מאז Android 8.0, אפשר לשדרג חלק מהמחיצות, כמו /system, אבל /vendor נשארת ללא שינוי. מאפייני המערכת הם רק מילון גלובלי של זוגות מחרוזות של מפתח-ערך ללא סכימה, ולכן קשה לייצב את המאפיינים. יכול להיות שהמחיצה /system תשנה או תסיר מאפיינים שהמחיצה /vendor תלויה בהם בלי הודעה מוקדמת.

ב-Android מגרסה 10 ואילך, מאפייני המערכת שאליהם ניגשים במחיצות מתוכננים בקובצי תיאור של מאפייני המערכת, וממשקי API לגישה למאפיינים נוצרים כפונקציות קונקרטיות עבור C++‎ ו-Rust, וכמחלקות עבור Java. ממשקי ה-API האלה נוחים יותר לשימוש כי לא צריך מחרוזות קסם (כמו ro.build.date) כדי לגשת אליהם, וכי הם יכולים להיות מוקלדים באופן סטטי. יציבות ממשק ה-ABI נבדקת גם במשך זמן של תהליך build, וה-build נכשל אם מתבצעים שינויים לא תואמים. הבדיקה הזו פועלת כממשקים מוגדרים במפורש בין מחיצות. ממשקי ה-API האלה יכולים גם לספק עקביות בין Rust,‏ Java ו-C++‎.

הגדרת מאפייני מערכת כממשקי API

מגדירים מאפייני מערכת כממשקי API באמצעות קובצי תיאור של Sysprop ‏ (.sysprop), שמשתמשים ב-TextFormat של protobuf, עם הסכימה הבאה:

// File: sysprop.proto

syntax = "proto3";

package sysprop;

enum Access {
  Readonly = 0;
  Writeonce = 1;
  ReadWrite = 2;
}

enum Owner {
  Platform = 0;
  Vendor = 1;
  Odm = 2;
}

enum Scope {
  Public = 0;
  Internal = 2;
}

enum Type {
  Boolean = 0;
  Integer = 1;
  Long = 2;
  Double = 3;
  String = 4;
  Enum = 5;
  UInt = 6;
  ULong = 7;

  BooleanList = 20;
  IntegerList = 21;
  LongList = 22;
  DoubleList = 23;
  StringList = 24;
  EnumList = 25;
  UIntList = 26;
  ULongList = 27;
}

message Property {
  string api_name = 1;
  Type type = 2;
  Access access = 3;
  Scope scope = 4;
  string prop_name = 5;
  string enum_values = 6;
  bool integer_as_bool = 7;
  string legacy_prop_name = 8;
}

message Properties {
  Owner owner = 1;
  string module = 2;
  repeated Property prop = 3;
}

קובץ תיאור של מאפיין מערכת מכיל הודעת מאפיינים שמתארת קבוצה של מאפיינים. המשמעות של השדות שלו היא כדלקמן:

שדה משמעות
owner מגדירים את המחיצה שכוללת את המאפיינים: platform,‏ vendor או odm.
module משמש ליצירת מרחב שמות (C++) או מחלקה סטטית סופית (Java) שבהם ממוקמים ממשקי API שנוצרו. לדוגמה: com.android.sysprop.BuildProperties הוא מרחב השמות com::android::sysprop::BuildProperties ב-C++‎, והוא המחלקה BuildProperties בחבילה ב-com.android.sysprop ב-Java.
prop רשימת נכסים.

המשמעויות של שדות ההודעה Property הן:

שדה משמעות
api_name השם של ה-API שנוצר.
type סוג הנכס.
access Readonly: יוצר רק API של getter
Writeonce, ReadWrite: יוצר ממשקי API של getter ו-setter
scope Internal: רק הבעלים יכולים לגשת. ‫
Public: כולם יכולים לגשת, למעט מודולי NDK.
prop_name השם של מאפיין המערכת הבסיסי, לדוגמה ro.build.date.
enum_values (Enum, ‏ EnumList בלבד) מחרוזת מופרדת באמצעות קו אנכי (|) שמורכבת מערכים אפשריים של טיפוסים בני מנייה (enum). לדוגמה, value1|value2.
integer_as_bool (Boolean, ‏ BooleanList בלבד) הגדרת שימוש ב-0 וב-1 במקום ב-false וב-true.
legacy_prop_name (אופציונלי, רק מאפייני Readonly) השם מדור קודם של מאפיין המערכת הבסיסי. כשקוראים לשיטת ה-getter, ה-API של ה-getter מנסה לקרוא את prop_name ומשתמש ב-legacy_prop_name אם prop_name לא קיים. משתמשים ב-legacy_prop_name כשמוציאים משימוש מאפיין קיים ועוברים למאפיין חדש.

כל סוג של מאפיין ממופה לסוגים הבאים ב-C++,‏ Java ו-Rust:

סוג ‫C++‎ (ניתן לאכלוס בערך null) Java (ניתן להגדרה כ-nullable) Rust (אופציונלי או ניתן לאיפוס)
בוליאני std::optional<bool> Optional<Boolean> Option<bool>
Integer std::optional<std::int32_t> Optional<Integer> Option<i32>
UInt std::optional<std::uint32_t> Optional<Integer> Option<u32>
Long std::optional<std::int64_t> Optional<Long> Option<i64>
ULong std::optional<std::uint64_t> Optional<Long> Option<u64>
כפול std::optional<double> Optional<Double> Option<f64>
String std::optional<std::string> Optional<String> Option<String>
Enum std::optional<{api\_name}\_values> Optional<{api\_name}\_values> Option<{ApiName}Values>
רשימת T std::vector<std::optional<T>> List<T> Vec<T>

דוגמה לקובץ תיאור של Sysprop שמגדיר שלוש מאפיינים:

# File: android/sysprop/PlatformProperties.sysprop

owner: Platform
module: "android.sysprop.PlatformProperties"
prop {
    api_name: "build_date"
    type: String
    prop_name: "ro.build.date"
    scope: Public
    access: Readonly
}
prop {
    api_name: "date_utc"
    type: Integer
    prop_name: "ro.build.date_utc"
    scope: Internal
    access: Readonly
}
prop {
    api_name: "device_status"
    type: Enum
    enum_values: "on|off|unknown"
    prop_name: "device.status"
    scope: Public
    access: ReadWrite
}

הגדרת ספריות של מאפייני מערכת

אפשר להגדיר מודולים של sysprop_library באמצעות קובצי תיאור של Sysprop. ‫sysprop_library משמש כ-API ל-C++‎, ל-Java ול-Rust. מערכת ה-build יוצרת באופן פנימי rust_library אחד, java_library אחד ו-cc_library אחד לכל מופע של sysprop_library.

// File: Android.bp
sysprop_library {
    name: "PlatformProperties",
    srcs: ["android/sysprop/PlatformProperties.sysprop"],
    property_owner: "Platform",
    vendor_available: true,
}

צריך לכלול קבצים של רשימות API במקור לבדיקות API. כדי לעשות זאת, יוצרים קובצי API וספרייה בשם api. ממקמים את הספרייה api באותה ספרייה שבה נמצא Android.bp. שמות הקבצים של ה-API הם <module_name>-current.txt ו-<module_name>-latest.txt. ‫<module_name>-current.txt מכיל את החתימות של ה-API של קודי המקור הנוכחיים, ו-<module_name>-latest.txt מכיל את החתימות העדכניות של ה-API שהוקפאו. מערכת ה-build בודקת אם בוצעו שינויים בממשקי ה-API על ידי השוואה בין קובצי ה-API האלה לבין קובצי API שנוצרו משך זמן של תהליך build. אם קובץ current.txt לא תואם לקודי המקור, מוצגת הודעת שגיאה והוראות לעדכון קובץ current.txt. דוגמה לארגון של ספרייה וקובץ:

├── api
│   ├── PlatformProperties-current.txt
│   └── PlatformProperties-latest.txt
└── Android.bp

מודולים של לקוחות ב-Rust, ב-Java וב-C++‎ יכולים לקשר ל-sysprop_library כדי להשתמש בממשקי API שנוצרו. מערכת ה-Build יוצרת קישורים מלקוחות לספריות שנוצרו ב-C++‎, ב-Java וב-Rust, וכך מאפשרת ללקוחות גישה לממשקי API שנוצרו.

java_library {
    name: "JavaClient",
    srcs: ["foo/bar.java"],
    libs: ["PlatformProperties"],
}

cc_binary {
    name: "cc_client",
    srcs: ["baz.cpp"],
    shared_libs: ["libPlatformProperties"],
}

rust_binary {
    name: "rust_client",
    srcs: ["src/main.rs"],
    rustlibs: ["libplatformproperties_rust"],
}

בדוגמה הקודמת, אפשר לגשת למאפיינים המוגדרים באופן הבא.

דוגמה ל-Rust:

use platformproperties::DeviceStatusValues;

fn foo() -> Result<(), Error> {
  // Read "ro.build.date_utc". default value is -1.
  let date_utc = platformproperties::date_utc()?.unwrap_or_else(-1);

  // set "device.status" to "unknown" if "ro.build.date" is not set.
  if platformproperties::build_date()?.is_none() {
    platformproperties::set_device_status(DeviceStatusValues::UNKNOWN);
  }

  
}

דוגמה ל-Java:

import android.sysprop.PlatformProperties;



static void foo() {
    
    // read "ro.build.date_utc". default value is -1
    Integer dateUtc = PlatformProperties.date_utc().orElse(-1);

    // set "device.status" to "unknown" if "ro.build.date" is not set
    if (!PlatformProperties.build_date().isPresent()) {
        PlatformProperties.device_status(
            PlatformProperties.device_status_values.UNKNOWN
        );
    }
    
}

דוגמה ל-C++‎:

#include <android/sysprop/PlatformProperties.sysprop.h>
using namespace android::sysprop;



void bar() {
    
    // read "ro.build.date". default value is "(unknown)"
    std::string build_date = PlatformProperties::build_date().value_or("(unknown)");

    // set "device.status" to "on" if it's "unknown" or not set
    using PlatformProperties::device_status_values;
    auto status = PlatformProperties::device_status();
    if (!status.has_value() || status.value() == device_status_values::UNKNOWN) {
        PlatformProperties::device_status(device_status_values::ON);
    }
    
}