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

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

החל מגרסת Android 10, מאפייני המערכת שהגישה אליהם בין מחיצות מתבצעת בסכמה בקובצי תיאור של 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;
}

קובץ אחד של תיאור 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
הערה: לא ניתן להשתמש בנכסים עם הקידומת ro. גישה ל-ReadWrite.
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++‎ 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
ULong 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.

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

דוגמה ל-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);
    }
    …
}
…