Most properties are a number in an area. Then somebody needs to express a set of seat positions, or a diagnostic frame, or a command with a result — and the simple model starts to strain.
Vector types#
The easy case. INT32_VEC, INT64_VEC and FLOAT_VEC carry several values of
one type in a single property.
{
.prop = toInt(VendorProperty::TYRE_PRESSURES), // FLOAT_VEC | GLOBAL
.access = VehiclePropertyAccess::READ,
.changeMode = VehiclePropertyChangeMode::ON_CHANGE,
.configArray = {4}, // convention: element count
}
// value.floatValues = { fl, fr, rl, rr }Vectors are appropriate when the elements are the same kind of thing and always change together. Four tyre pressures qualify. "Speed and gear and door state" does not — that is three properties wearing a trench coat.
Element order is a convention, not a contract
Nothing in the type system says index 0 is front-left. Document it in
configArray or configString, and write it in the property registry. Every
consumer must agree out of band, and they will not, unless you make them.
MIXED#
MIXED lets one property carry several typed arrays at once — some ints, a
float, a string. It is the escape hatch, and configArray describes the layout.
{
.prop = toInt(VendorProperty::CHARGE_SESSION),
.access = VehiclePropertyAccess::READ,
.changeMode = VehiclePropertyChangeMode::ON_CHANGE,
// configArray convention for MIXED:
// [0] does the value include a String?
// [1] number of Boolean values
// [2] number of Integer values
// [3] number of Integer arrays
// [4] number of Long values ... and so on
.configArray = {1, 1, 2, 0, 1, 0, 1, 0, 0},
}val value = properties.getProperty(Array<Any>::class.java, CHARGE_SESSION, 0)
val raw = value?.value as? Array<*> ?: return
val stationId = raw[0] as String
val isActive = raw[1] as Boolean
val kwhDelivered = raw[2] as Int
val secondsElapsed = raw[3] as Int
val startedAtMs = raw[4] as LongThat app code is fragile by construction. Index four is a Long because a
comment in a header said so.
Prefer several typed properties over one MIXED
MIXED moves the schema out of the type system and into prose. Three separate
INT32/STRING properties are more verbose and vastly easier to evolve, test
and debug. Reach for MIXED only when the values are genuinely atomic — they
must be read as one consistent snapshot or not at all.
configArray in general#
configArray is an untyped int[] whose meaning is per-property. AOSP uses it
for real things — supported enum values, element counts, MIXED layout — and every
use is a convention documented elsewhere.
For vendor properties, pick a convention, write it in one place, and generate both the HAL config and the client parser from it. Hand-maintaining both ends of an untyped array across two organisations is how field defects happen.
Backported properties#
The BACKPORTED group (0x30000000) exists for a specific situation: a system
property defined in a newer platform release, needed on an older one.
You cannot use the SYSTEM ID — it does not exist in your build's enum, and CTS
would object. You do not want a VENDOR ID either, because when you eventually
upgrade you would have to migrate every consumer.
BACKPORTED says "this is the system property with this ID, implemented early".
It keeps the ID stable so the upgrade is a group-bit change rather than a
redesign.
// On the older platform, before the system property exists
const val EV_CHARGE_STATE_BACKPORTED = 0x3000_0000.toInt() or AREA_GLOBAL or TYPE_INT32 or 0x0A11
// After upgrading to a platform that defines it
// VehiclePropertyIds.EV_CHARGE_STATE (SYSTEM group)Static properties#
changeMode = STATIC means the value never changes after boot: VIN, seat count,
fuel capacity, supported feature bitmasks. The framework may cache it forever.
{
.prop = toInt(VehicleProperty::INFO_VIN), // STRING | GLOBAL
.access = VehiclePropertyAccess::READ,
.changeMode = VehiclePropertyChangeMode::STATIC,
}Declaring something STATIC that actually changes is a nasty defect: the value
is read once at boot and the stale copy is served for the rest of the ignition
cycle. If it can change — even rarely, even only after a service action — it is
ON_CHANGE.
When to stop using properties#
The property model is state. It is not RPC, not streaming, and not bulk transfer. Signs you have outgrown it:
- You need a request/response with correlation — a command that returns a result for that request. A "command" property plus a "result" property loses the correlation as soon as two callers overlap.
- You are moving kilobytes per event. Properties are for values, not payloads.
- Your
configArrayhas grown a nested layout description. - You are encoding a protocol into
byteValues.
At that point the right answer is usually a separate vendor AIDL service alongside the VHAL, with its own interface, its own permission and its own versioning — not a more elaborate property.
Two interfaces is a legitimate design
Nothing requires all vehicle interaction to go through the VHAL. Vehicle state belongs in properties, because the framework's caching, subscription and permission machinery is built for it. Everything else can be a service you define, and it will be easier to evolve.
Next#
Into the framework, where these properties become APIs.

