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

מאפייני המערכת מספקים דרך נוחה לשתף מידע, בדרך כלל הגדרות, בכל המערכת. כל מחיצה יכולה להשתמש במאפייני מערכת משלה באופן פנימי. בעיה יכולה להתרחש כשניגשים לנכסים במחיצות שונות, כמו /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
הערה: יכול להיות שלא ניתן להשתמש בגישה 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, ה-getter API מנסה לקרוא את 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
ספירה std::optional<{api_name}_values> Optional<{api_name}_values> {ApiName}Values
T List 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);
    }
    
}