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

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

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

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

שדה משמעות
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
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>

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