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

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

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

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

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

סוג ‫C++ (ניתן לאיפוס) Java (ניתן להגדרה כ-nullable) Rust (אופציונלי או ניתן להגדרה כ-nullable)
בוליאני std::optional<bool> Optional<Boolean> Option<bool>
מספר שלם 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>
Double 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);
    }
    
}