Implementing System Properties as APIs

System properties provide a convenient way to share information, usually configurations, system-wide. Each partition can use its own system properties internally. A problem can happen when properties are accessed across partitions, such as /vendor accessing /system-defined properties. Since Android 8.0, some partitions, such as /system, can be upgraded, while /vendor is left unchanged. Because system properties are just a global dictionary of string key/value pairs with no schema, it's difficult to stabilize properties. The /system partition could change or remove properties that the /vendor partition depends on without any notice.

Starting with the Android 10 release, system properties accessed across partitions are schematized into Sysprop Description files, and APIs to access properties are generated as concrete functions for C++ and Rust, and classes for Java. These APIs are more convenient to use because no magic strings (such as ro.build.date) are needed for access, and because they can be statically typed. ABI stability is also checked at build time, and the build breaks if incompatible changes happen. This check acts as explicitly defined interfaces between partitions. These APIs can also provide consistency between Rust, Java and C++.

Defining system properties as APIs

Define system properties as APIs with Sysprop Description files (.sysprop), which use a TextFormat of protobuf, with the following schema:

// 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;
}

One Sysprop Description file contains one properties message, which describes a set of properties. The meaning of its fields are as follows.

Field Meaning
owner Set to the partition that owns the properties: Platform, Vendor, or Odm.
module Used to create a namespace (C++) or static final class (Java) in which generated APIs are placed. For example, com.android.sysprop.BuildProperties will be namespace com::android::sysprop::BuildProperties in C++, and the BuildProperties class in the package in com.android.sysprop in Java.
prop List of properties.

The meanings of the Property message fields are as follows.

Field Meaning
api_name The name of the generated API.
type The type of this property.
access Readonly: Generates getter API only

Writeonce, ReadWrite: Generates getter and setter APIs

Note: Properties with the prefix ro. may not use ReadWrite access.

scope Internal: Only the owner can access.

Public: Everyone can access, except for NDK modules.

prop_name The name of the underlying system property, for example ro.build.date.
enum_values (Enum, EnumList only) A bar(|)-separated string that consists of possible enum values. For example, value1|value2.
integer_as_bool (Boolean, BooleanList only) Make setters use 0 and 1 instead of false and true.
legacy_prop_name (optional, Readonly properties only) The legacy name of the underlying system property. When calling getter, the getter API tries to read prop_name and uses legacy_prop_name if prop_name doesn't exist. Use legacy_prop_name when deprecating an existing property and moving to a new property.

Each type of property maps to the following types in C++, Java and Rust.

Type C++ Java Rust
Boolean std::optional<bool> Optional<Boolean> bool
Integer std::optional<std::int32_t> Optional<Integer> i32
UInt std::optional<std::uint32_t> Optional<Integer> u32
Long std::optional<std::int64_t> Optional<Long> i64
ULong std::optional<std::uint64_t> Optional<Long> u64
Double std::optional<double> Optional<Double> f64
String std::optional<std::string> Optional<String> String
Enum std::optional<{api_name}_values> Optional<{api_name}_values> {ApiName}Values
T List std::vector<std::optional<T>> List<T> Vec<T>

Here's an example of a Sysprop Description file defining three properties:

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

Defining system properties libraries

You can now define sysprop_library modules with Sysprop Description files. sysprop_library serves as an API for C++, Java and Rust. The build system internally generates one rust_library, one java_library and one cc_library for each instance of sysprop_library.

// File: Android.bp
sysprop_library {
    name: "PlatformProperties",
    srcs: ["android/sysprop/PlatformProperties.sysprop"],
    property_owner: "Platform",
    vendor_available: true,
}

You must include API lists files in the source for API checks. To do this, create API files and an api directory. Put the api directory in the same directory as Android.bp. The API filenames are <module_name>-current.txt, <module_name>-latest.txt. <module_name>-current.txt holds the API signatures of current source codes, and <module_name>-latest.txt holds the latest frozen API signatures. The build system checks whether the APIs are changed by comparing these API files with generated API files at build time and emits an error message and instructions to update current.txt file if current.txt doesn't match with the source codes. Here's an example directory and file organization:

├── api
│   ├── PlatformProperties-current.txt
│   └── PlatformProperties-latest.txt
└── Android.bp

Rust, Java and C++ client modules can link against sysprop_library to use generated APIs. The build system creates links from clients to generated C++, Java and Rust libraries, thus giving clients access to generated APIs.

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"],
}

Note that the Rust library name is generated by converting the sysprop_library name to lowercase, replacing . and - with _, and then prepending lib and appending _rust.

In the above example, you could access defined properties as follows.

Rust example:

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 example:

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++ example:

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