הטמעה של מאפייני המערכת כממשקי API

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

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

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

מגדירים מאפייני מערכת כ-APIs באמצעות קובצי תיאור של 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;
}

קובץ אחד של תיאור Syspro מכיל הודעת מאפיינים (property), שמתארת קבוצת מאפיינים. המשמעות של השדות היא כזאת.

שדה משמעות
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
הערה: לא ניתן להשתמש בהרשאת הגישה ReadWrite בנכסים עם הקידומת ro..
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 ב-setters.
legacy_prop_name (אופציונלי, נכסי Readonly בלבד) השם הקודם של נכס המערכת הבסיסי. כשקוראים ל-getter, ה-API של ה-getter מנסה לקרוא את prop_name ומשתמש ב-legacy_prop_name אם prop_name לא קיים. משתמשים ב-legacy_prop_name כשמבטלים את השימוש בנכס קיים ומעבירים אותו לנכס חדש.

כל סוג של נכס ממופה לסוגי הנכסים הבאים ב-C++‎, ב-Java וב-Rust.

סוג C++‎ Java Rust
בוליאני std::optional<bool> Optional<Boolean> bool
מספר שלם std::optional<std::int32_t> Optional<Integer> i32
UInt std::optional<std::uint32_t> Optional<Integer> u32
ארוך std::optional<std::int64_t> Optional<Long> i64
אולונג std::optional<std::uint64_t> Optional<Long> u64
כפול std::optional<double> Optional<Double> f64
מחרוזת std::optional<std::string> Optional<String> String
Enum std::optional<{api_name}_values> Optional<{api_name}_values> {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 באמצעות קובצי תיאור של Syspro. 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: ["PlatformProperties"],
}

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

שימו לב שהשם של ספריית Rust נוצר על ידי המרת השם sysprop_library לאותיות קטנות, החלפה של . ו-- ב-_, ולאחר מכן הוספת lib והוספה של _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);
    }
    …
}
…