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

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

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

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

אפשר להגדיר את מאפייני המערכת כממשקי API באמצעות קובצי SysPRO Description (.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;
}

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

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

Type C++‎ Java Rust
ערך בוליאני std::optional<bool> Optional<Boolean> bool
מספר שלם std::optional<std::int32_t> Optional<Integer> i32
ממשק משתמש 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>

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

# 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.

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

דוגמה לחלודה:

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);
    }
    …
}
…