From 378e7109c64999f9e1af50958342f4229e44f98c Mon Sep 17 00:00:00 2001 From: Marijn Suijten Date: Thu, 20 Feb 2025 14:56:48 +0100 Subject: [PATCH 1/2] Add bindings for querying and setting Android System Properties The Android System Property API might not be very useful for most apps even though it still provides a read-only view of all properties but no rights to set any. Since this is accessible via the NDK this crate should provide a clean and complete (including UB-free) wrapper implementation either way. --- ndk-sys/CHANGELOG.md | 1 + ndk-sys/src/ffi_aarch64.rs | 62 +++++++ ndk-sys/src/ffi_arm.rs | 62 +++++++ ndk-sys/src/ffi_i686.rs | 62 +++++++ ndk-sys/src/ffi_x86_64.rs | 62 +++++++ ndk-sys/wrapper.h | 2 + ndk/CHANGELOG.md | 1 + ndk/src/lib.rs | 1 + ndk/src/system_properties.rs | 326 +++++++++++++++++++++++++++++++++++ ndk/src/utils.rs | 4 +- 10 files changed, 581 insertions(+), 2 deletions(-) create mode 100644 ndk/src/system_properties.rs diff --git a/ndk-sys/CHANGELOG.md b/ndk-sys/CHANGELOG.md index 13c873de..29e13338 100644 --- a/ndk-sys/CHANGELOG.md +++ b/ndk-sys/CHANGELOG.md @@ -1,6 +1,7 @@ # Unreleased - Regenerate bindings with `bindgen 0.71.1`. (#487) +- Include API bindings from `sys/system_properties.h`. (#495) # 0.6.0 (2024-04-26) diff --git a/ndk-sys/src/ffi_aarch64.rs b/ndk-sys/src/ffi_aarch64.rs index 2adf9ef5..1018e0b3 100644 --- a/ndk-sys/src/ffi_aarch64.rs +++ b/ndk-sys/src/ffi_aarch64.rs @@ -1410,6 +1410,8 @@ pub const PROPERTY_VERSION: &[u8; 8] = b"version\0"; pub const PROPERTY_DESCRIPTION: &[u8; 12] = b"description\0"; pub const PROPERTY_ALGORITHMS: &[u8; 11] = b"algorithms\0"; pub const PROPERTY_DEVICE_UNIQUE_ID: &[u8; 15] = b"deviceUniqueId\0"; +pub const PROP_VALUE_MAX: u32 = 92; +pub const PROP_NAME_MAX: u32 = 32; extern "C" { pub fn android_get_application_target_sdk_version() -> ::std::os::raw::c_int; } @@ -21844,4 +21846,64 @@ extern "C" { extern "C" { pub fn AMediaMuxer_getTrackFormat(muxer: *mut AMediaMuxer, idx: usize) -> *mut AMediaFormat; } +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct prop_info { + _unused: [u8; 0], +} +extern "C" { + pub fn __system_property_set( + __name: *const ::std::os::raw::c_char, + __value: *const ::std::os::raw::c_char, + ) -> ::std::os::raw::c_int; +} +extern "C" { + pub fn __system_property_find(__name: *const ::std::os::raw::c_char) -> *const prop_info; +} +extern "C" { + pub fn __system_property_read_callback( + __pi: *const prop_info, + __callback: ::std::option::Option< + unsafe extern "C" fn( + __cookie: *mut ::std::os::raw::c_void, + __name: *const ::std::os::raw::c_char, + __value: *const ::std::os::raw::c_char, + __serial: u32, + ), + >, + __cookie: *mut ::std::os::raw::c_void, + ); +} +extern "C" { + pub fn __system_property_foreach( + __callback: ::std::option::Option< + unsafe extern "C" fn(__pi: *const prop_info, __cookie: *mut ::std::os::raw::c_void), + >, + __cookie: *mut ::std::os::raw::c_void, + ) -> ::std::os::raw::c_int; +} +extern "C" { + pub fn __system_property_wait( + __pi: *const prop_info, + __old_serial: u32, + __new_serial_ptr: *mut u32, + __relative_timeout: *const timespec, + ) -> bool; +} +extern "C" { + pub fn __system_property_read( + __pi: *const prop_info, + __name: *mut ::std::os::raw::c_char, + __value: *mut ::std::os::raw::c_char, + ) -> ::std::os::raw::c_int; +} +extern "C" { + pub fn __system_property_get( + __name: *const ::std::os::raw::c_char, + __value: *mut ::std::os::raw::c_char, + ) -> ::std::os::raw::c_int; +} +extern "C" { + pub fn __system_property_find_nth(__n: ::std::os::raw::c_uint) -> *const prop_info; +} pub type __uint128_t = u128; diff --git a/ndk-sys/src/ffi_arm.rs b/ndk-sys/src/ffi_arm.rs index 023d2674..9a25a2ab 100644 --- a/ndk-sys/src/ffi_arm.rs +++ b/ndk-sys/src/ffi_arm.rs @@ -1463,6 +1463,8 @@ pub const PROPERTY_VERSION: &[u8; 8] = b"version\0"; pub const PROPERTY_DESCRIPTION: &[u8; 12] = b"description\0"; pub const PROPERTY_ALGORITHMS: &[u8; 11] = b"algorithms\0"; pub const PROPERTY_DEVICE_UNIQUE_ID: &[u8; 15] = b"deviceUniqueId\0"; +pub const PROP_VALUE_MAX: u32 = 92; +pub const PROP_NAME_MAX: u32 = 32; extern "C" { pub fn android_get_application_target_sdk_version() -> ::std::os::raw::c_int; } @@ -22294,3 +22296,63 @@ extern "C" { extern "C" { pub fn AMediaMuxer_getTrackFormat(muxer: *mut AMediaMuxer, idx: usize) -> *mut AMediaFormat; } +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct prop_info { + _unused: [u8; 0], +} +extern "C" { + pub fn __system_property_set( + __name: *const ::std::os::raw::c_char, + __value: *const ::std::os::raw::c_char, + ) -> ::std::os::raw::c_int; +} +extern "C" { + pub fn __system_property_find(__name: *const ::std::os::raw::c_char) -> *const prop_info; +} +extern "C" { + pub fn __system_property_read_callback( + __pi: *const prop_info, + __callback: ::std::option::Option< + unsafe extern "C" fn( + __cookie: *mut ::std::os::raw::c_void, + __name: *const ::std::os::raw::c_char, + __value: *const ::std::os::raw::c_char, + __serial: u32, + ), + >, + __cookie: *mut ::std::os::raw::c_void, + ); +} +extern "C" { + pub fn __system_property_foreach( + __callback: ::std::option::Option< + unsafe extern "C" fn(__pi: *const prop_info, __cookie: *mut ::std::os::raw::c_void), + >, + __cookie: *mut ::std::os::raw::c_void, + ) -> ::std::os::raw::c_int; +} +extern "C" { + pub fn __system_property_wait( + __pi: *const prop_info, + __old_serial: u32, + __new_serial_ptr: *mut u32, + __relative_timeout: *const timespec, + ) -> bool; +} +extern "C" { + pub fn __system_property_read( + __pi: *const prop_info, + __name: *mut ::std::os::raw::c_char, + __value: *mut ::std::os::raw::c_char, + ) -> ::std::os::raw::c_int; +} +extern "C" { + pub fn __system_property_get( + __name: *const ::std::os::raw::c_char, + __value: *mut ::std::os::raw::c_char, + ) -> ::std::os::raw::c_int; +} +extern "C" { + pub fn __system_property_find_nth(__n: ::std::os::raw::c_uint) -> *const prop_info; +} diff --git a/ndk-sys/src/ffi_i686.rs b/ndk-sys/src/ffi_i686.rs index 752766c0..4778e290 100644 --- a/ndk-sys/src/ffi_i686.rs +++ b/ndk-sys/src/ffi_i686.rs @@ -1327,6 +1327,8 @@ pub const PROPERTY_VERSION: &[u8; 8] = b"version\0"; pub const PROPERTY_DESCRIPTION: &[u8; 12] = b"description\0"; pub const PROPERTY_ALGORITHMS: &[u8; 11] = b"algorithms\0"; pub const PROPERTY_DEVICE_UNIQUE_ID: &[u8; 15] = b"deviceUniqueId\0"; +pub const PROP_VALUE_MAX: u32 = 92; +pub const PROP_NAME_MAX: u32 = 32; extern "C" { pub fn android_get_application_target_sdk_version() -> ::std::os::raw::c_int; } @@ -23011,4 +23013,64 @@ extern "C" { extern "C" { pub fn AMediaMuxer_getTrackFormat(muxer: *mut AMediaMuxer, idx: usize) -> *mut AMediaFormat; } +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct prop_info { + _unused: [u8; 0], +} +extern "C" { + pub fn __system_property_set( + __name: *const ::std::os::raw::c_char, + __value: *const ::std::os::raw::c_char, + ) -> ::std::os::raw::c_int; +} +extern "C" { + pub fn __system_property_find(__name: *const ::std::os::raw::c_char) -> *const prop_info; +} +extern "C" { + pub fn __system_property_read_callback( + __pi: *const prop_info, + __callback: ::std::option::Option< + unsafe extern "C" fn( + __cookie: *mut ::std::os::raw::c_void, + __name: *const ::std::os::raw::c_char, + __value: *const ::std::os::raw::c_char, + __serial: u32, + ), + >, + __cookie: *mut ::std::os::raw::c_void, + ); +} +extern "C" { + pub fn __system_property_foreach( + __callback: ::std::option::Option< + unsafe extern "C" fn(__pi: *const prop_info, __cookie: *mut ::std::os::raw::c_void), + >, + __cookie: *mut ::std::os::raw::c_void, + ) -> ::std::os::raw::c_int; +} +extern "C" { + pub fn __system_property_wait( + __pi: *const prop_info, + __old_serial: u32, + __new_serial_ptr: *mut u32, + __relative_timeout: *const timespec, + ) -> bool; +} +extern "C" { + pub fn __system_property_read( + __pi: *const prop_info, + __name: *mut ::std::os::raw::c_char, + __value: *mut ::std::os::raw::c_char, + ) -> ::std::os::raw::c_int; +} +extern "C" { + pub fn __system_property_get( + __name: *const ::std::os::raw::c_char, + __value: *mut ::std::os::raw::c_char, + ) -> ::std::os::raw::c_int; +} +extern "C" { + pub fn __system_property_find_nth(__n: ::std::os::raw::c_uint) -> *const prop_info; +} pub type __builtin_va_list = *mut ::std::os::raw::c_char; diff --git a/ndk-sys/src/ffi_x86_64.rs b/ndk-sys/src/ffi_x86_64.rs index 0416eb1f..354ad270 100644 --- a/ndk-sys/src/ffi_x86_64.rs +++ b/ndk-sys/src/ffi_x86_64.rs @@ -1372,6 +1372,8 @@ pub const PROPERTY_VERSION: &[u8; 8] = b"version\0"; pub const PROPERTY_DESCRIPTION: &[u8; 12] = b"description\0"; pub const PROPERTY_ALGORITHMS: &[u8; 11] = b"algorithms\0"; pub const PROPERTY_DEVICE_UNIQUE_ID: &[u8; 15] = b"deviceUniqueId\0"; +pub const PROP_VALUE_MAX: u32 = 92; +pub const PROP_NAME_MAX: u32 = 32; extern "C" { pub fn android_get_application_target_sdk_version() -> ::std::os::raw::c_int; } @@ -23049,6 +23051,66 @@ extern "C" { extern "C" { pub fn AMediaMuxer_getTrackFormat(muxer: *mut AMediaMuxer, idx: usize) -> *mut AMediaFormat; } +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct prop_info { + _unused: [u8; 0], +} +extern "C" { + pub fn __system_property_set( + __name: *const ::std::os::raw::c_char, + __value: *const ::std::os::raw::c_char, + ) -> ::std::os::raw::c_int; +} +extern "C" { + pub fn __system_property_find(__name: *const ::std::os::raw::c_char) -> *const prop_info; +} +extern "C" { + pub fn __system_property_read_callback( + __pi: *const prop_info, + __callback: ::std::option::Option< + unsafe extern "C" fn( + __cookie: *mut ::std::os::raw::c_void, + __name: *const ::std::os::raw::c_char, + __value: *const ::std::os::raw::c_char, + __serial: u32, + ), + >, + __cookie: *mut ::std::os::raw::c_void, + ); +} +extern "C" { + pub fn __system_property_foreach( + __callback: ::std::option::Option< + unsafe extern "C" fn(__pi: *const prop_info, __cookie: *mut ::std::os::raw::c_void), + >, + __cookie: *mut ::std::os::raw::c_void, + ) -> ::std::os::raw::c_int; +} +extern "C" { + pub fn __system_property_wait( + __pi: *const prop_info, + __old_serial: u32, + __new_serial_ptr: *mut u32, + __relative_timeout: *const timespec, + ) -> bool; +} +extern "C" { + pub fn __system_property_read( + __pi: *const prop_info, + __name: *mut ::std::os::raw::c_char, + __value: *mut ::std::os::raw::c_char, + ) -> ::std::os::raw::c_int; +} +extern "C" { + pub fn __system_property_get( + __name: *const ::std::os::raw::c_char, + __value: *mut ::std::os::raw::c_char, + ) -> ::std::os::raw::c_int; +} +extern "C" { + pub fn __system_property_find_nth(__n: ::std::os::raw::c_uint) -> *const prop_info; +} pub type __builtin_va_list = [__va_list_tag; 1usize]; #[repr(C)] #[derive(Debug, Copy, Clone)] diff --git a/ndk-sys/wrapper.h b/ndk-sys/wrapper.h index efe16f1d..eb4e5492 100644 --- a/ndk-sys/wrapper.h +++ b/ndk-sys/wrapper.h @@ -89,3 +89,5 @@ #include #include #include + +#include diff --git a/ndk/CHANGELOG.md b/ndk/CHANGELOG.md index 4bc9a7e2..04778f34 100644 --- a/ndk/CHANGELOG.md +++ b/ndk/CHANGELOG.md @@ -1,6 +1,7 @@ # Unreleased - image_reader: Add `ImageReader::new_with_data_space()` constructor and `ImageReader::data_space()` getter from API level 34. (#474) +- Add bindings for querying and setting Android System Properties. (#495) # 0.9.0 (2024-04-26) diff --git a/ndk/src/lib.rs b/ndk/src/lib.rs index 81e2ef61..68b00b70 100644 --- a/ndk/src/lib.rs +++ b/ndk/src/lib.rs @@ -29,5 +29,6 @@ pub mod native_window; pub mod shared_memory; pub mod surface_texture; pub mod sync; +pub mod system_properties; pub mod trace; mod utils; diff --git a/ndk/src/system_properties.rs b/ndk/src/system_properties.rs new file mode 100644 index 00000000..df480ae7 --- /dev/null +++ b/ndk/src/system_properties.rs @@ -0,0 +1,326 @@ +//! Bindings for [System Properties] +//! +//! [System Properties]: https://source.android.com/docs/core/architecture/configuration/add-system-properties + +use std::{ + ffi::{c_char, c_void, CStr, CString, FromBytesWithNulError, FromVecWithNulError}, + fmt, + io::Error, + ptr::NonNull, + str::{FromStr, Utf8Error}, +}; +#[cfg(feature = "api-level-26")] +use std::{mem::MaybeUninit, num::NonZeroU32, time::Duration}; + +use thiserror::Error; + +use crate::utils::{abort_on_panic, status_to_io_result}; + +/// Possible failures returned by [`get_raw()`]. +#[derive(Debug, Error)] +pub enum GetRawError { + #[error("Property is missing or empty")] + MissingOrEmpty, + #[error(transparent)] + NulError(#[from] FromVecWithNulError), + #[error(transparent)] + Io(#[from] Error), +} + +/// Internal helper to deduplicate the implementation between [`get_raw()`] and +/// [`Property::read_raw()`]. +fn process_owned(get: impl FnOnce(*mut c_char) -> i32) -> Result { + // Pre-allocate a `Vec` which we can move to the user with the result + let mut value = Vec::with_capacity(ffi::PROP_VALUE_MAX as usize); + let ret = get(value.as_mut_ptr()); + match ret { + 0 => Err(GetRawError::MissingOrEmpty), + ..=-1 => Err(Error::from_raw_os_error(-ret).into()), + 1.. => { + // TODO: This "smart" implementation leaves the user with a 92-byte allocation since + // set_len() currently doesn't shrink. Any such operation would likely reallocate, + // making this have no advantage over the stack-local variant that allocates after the + // fact. + unsafe { value.set_len(ret as usize + 1) } + Ok(CString::from_vec_with_nul(value)?) + } + } +} + +/// Returns the property value as an owned [`CString`] with possibly invalid UTF-8 [but no interor +/// NULs]. The maximum length can be up to 92 ([`ffi::PROP_VALUE_MAX`]) including NUL terminator. +/// +/// [but no interor NULs]: GetRawError::NulError +/// +/// See [`get()`] for a more convenient API that validates this string for UTF-8 and directly parses +/// it into a [`FromStr`]-compatible type. +/// +/// # Deprecation +/// Deprecated since Android O (API level 26), use [`Property::find()`] with +/// [`Property::read_callback()`] instead which does not have a limit on `value` nor `name` length. +#[doc(alias = "__system_property_get")] +pub fn get_raw(name: &CStr) -> Result { + process_owned(|value| unsafe { ffi::__system_property_get(name.as_ptr(), value) }) +} + +/// Possible failures returned by [`get()`]. +#[allow(missing_debug_implementations)] // Our MSRV is too low for derive(Debug) to emit bounds on T. +#[derive(Error)] +pub enum GetError { + #[error("Property is missing or empty")] + MissingOrEmpty, + #[error(transparent)] + NulError(#[from] FromBytesWithNulError), + #[error("Property does not contain valid UTF-8")] + Utf8Error(#[from] Utf8Error), + #[error(transparent)] + Io(#[from] Error), + #[error(transparent)] + ParseError(T), +} + +/// Internal helper to deduplicate the implementation between [`get()`] and [`Property::read()`]. +fn process_parse(get: impl FnOnce(*mut c_char) -> i32) -> Result> { + let mut value = [0u8; ffi::PROP_VALUE_MAX as usize]; + let ret = get(value.as_mut_ptr()); + match ret { + 0 => Err(GetError::MissingOrEmpty), + ..=-1 => Err(Error::from_raw_os_error(-ret).into()), + 1.. => { + let c_str = CStr::from_bytes_with_nul(&value[..ret as usize + 1])?; + c_str.to_str()?.parse().map_err(GetError::ParseError) + } + } +} + +/// Returns the property value as a [`FromStr`]-parsed type from a source string of at most 92 +/// ([`ffi::PROP_VALUE_MAX`]) characters, including NUL terminator. +/// +/// # Implementation details +/// This is implemented without any up-front allocations like [`get_raw()`], but requires a trip +/// through [`CStr`] and [`str`] (for calling [`FromStr::from_str()`]) meaning the resulting +/// string has to be compliant with [`CStr`] ([no interior NULs]) and [`str`] ([valid UTF-8]). In +/// other words, parsing into a [`String`] will never contain interior NULs (and it is unknown and +/// unlikely whether the property API allows for this). +/// +/// [no interior NULs]: GetError::NulError +/// [valid UTF-8]: GetError::Utf8Error +/// +/// # Deprecation +/// Deprecated since Android O (API level 26), use [`Property::find()`] with +/// [`Property::read_callback()`] instead which does not have a limit on `value` nor `name` length. +#[doc(alias = "__system_property_get")] +pub fn get(name: &CStr) -> Result> { + process_parse(|value| unsafe { ffi::__system_property_get(name.as_ptr(), value) }) +} + +/// Sets system property `name` to `value`, creating the system property if it doesn't already +/// exist. +#[doc(alias = "__system_property_set")] +pub fn set(name: &CStr, value: &CStr) -> std::io::Result<()> { + let ret = unsafe { ffi::__system_property_set(name.as_ptr(), value.as_ptr()) }; + match ret { + 0 => Ok(()), + ..=-1 => Err(Error::from_raw_os_error(-ret)), + 1.. => panic!("Unexpected non-zero non-negative return value `{ret}`"), + } +} + +/// Modern abstraction to [find], cache, [read] and [wait] on properties. +/// +/// [find]: Property::find() +/// [read]: Property::read() +/// [wait]: Property::wait() +#[doc(alias = "prop_info")] +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +pub struct Property(NonNull); + +/// The name, value and serial of a property during [`Property::read_callback()`]. +#[cfg(feature = "api-level-26")] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct PropertyValue<'a> { + pub name: &'a CStr, + pub value: &'a CStr, + pub serial: u32, +} + +impl fmt::Debug for Property { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + #[cfg(feature = "api-level-26")] + { + let mut result = None; + self.read_callback(|pv: &PropertyValue<'_>| result = Some(pv.fmt(f))); + result.expect("Read callback never called") + } + #[cfg(not(feature = "api-level-26"))] + { + f.debug_struct("Property") + .field("value", &self.read_raw()) + .finish_non_exhaustive() + } + } +} + +impl Property { + /// Returns a [`Property`] corresponding to the system property `name`, or [`None`] if it + /// doesn't exist. Use [`Property::read()`] or [`Property::read_callback()`] to query the + /// current value. + /// + /// Property lookup is expensive, so it can be useful to cache the result of this function. + #[doc(alias = "__system_property_find")] + pub fn find(name: &CStr) -> Option { + let prop = unsafe { ffi::__system_property_find(name.as_ptr()) }; + // TODO: No lifetime information available for this pointer + NonNull::new(prop.cast_mut()).map(Self) + } + + /// Calls the `callback` for every system property with a [`Property`] handle. Use in + /// conjunction with [`Property::read_callback()`] to get its name and value. + /// + /// This method is for inspecting and debugging the property system, and not generally useful. + #[doc(alias = "__system_property_foreach")] + pub fn foreach(mut callback: F) -> std::io::Result<()> { + unsafe extern "C" fn ffi_callback( + pi: *const ffi::prop_info, + cookie: *mut c_void, + ) { + abort_on_panic(|| { + let callback = cookie as *mut F; + + // TODO: No lifetime information available for this pointer + (*callback)(Property(NonNull::new(pi.cast_mut()).unwrap())) + }) + } + + let ret = unsafe { + ffi::__system_property_foreach(Some(ffi_callback::), <*mut _>::cast(&mut callback)) + }; + + status_to_io_result(ret) + } + + /// Returns an owned [`CString`] with possibly invalid UTF-8 [but no interor NULs]. The maximum + /// length can be up to 92 ([`ffi::PROP_VALUE_MAX`]) including NUL terminator. + /// + /// [but no interor NULs]: GetRawError::NulError + /// + /// See [`Property::read()`] for a more convenient API that validates this string for UTF-8 and + /// directly parses it into a [`FromStr`]-compatible type. + /// + /// # Deprecation + /// Deprecated since Android O (API level 26), use [`Self::read_callback()`] instead which does + /// not have a limit on `value` nor `name` length. + #[doc(alias = "__system_property_read")] + pub fn read_raw(&self) -> Result { + process_owned(|value| unsafe { + // TODO: should we return the name of ffi::PROP_NAME_MAX? + ffi::__system_property_read(self.0.as_ptr(), std::ptr::null_mut(), value) + }) + } + + /// Returns the property value as a [`FromStr`]-parsed type from a source string of at most 92 + /// ([`ffi::PROP_VALUE_MAX`]) characters, including NUL terminator. + /// + /// # Implementation details + /// This is implemented without any up-front allocations like [`get_raw()`], but requires a trip + /// through [`CStr`] and [`str`] (for calling [`FromStr::from_str()`]) meaning the resulting + /// string has to be compliant with [`CStr`] ([no interior NULs]) and [`str`] ([valid UTF-8]). + /// In other words, parsing into a [`String`] will never contain interior NULs (and it is + /// unknown and unlikely whether the property API allows for this). + /// + /// [no interior NULs]: GetError::NulError + /// [valid UTF-8]: GetError::Utf8Error + /// + /// # Deprecation + /// Deprecated since Android O (API level 26), use [`Self::read_callback()`] instead which does + /// not have a limit on `value` nor `name` length. + #[doc(alias = "__system_property_read")] + pub fn read(&self) -> Result> { + process_parse(|value| unsafe { + // TODO: should we return the name of ffi::PROP_NAME_MAX? + ffi::__system_property_read(self.0.as_ptr(), std::ptr::null_mut(), value) + }) + } + + /// Calls `callback` with a consistent trio of `name`, `value` and `serial` number (stored in + /// [`PropertyValue`]) for this [`Property`]. + #[cfg(feature = "api-level-26")] + #[doc(alias = "__system_property_read_callback")] + pub fn read_callback)>(&self, callback: F) { + // Wrap the callback in a MaybeUninit so that ffi_callback() can "copy from" a pointer to it + // and consume the FnOnce, leaving the original callback "invalid" but inaccessible without + // unsafe. + let mut callback = MaybeUninit::new(callback); + unsafe extern "C" fn ffi_callback)>( + cookie: *mut c_void, + name: *const c_char, + value: *const c_char, + serial: u32, + ) { + abort_on_panic(|| { + let callback: F = std::ptr::read(cookie.cast()); + let name = CStr::from_ptr(name); + let value = CStr::from_ptr(value); + + callback(&PropertyValue { + name, + value, + serial, + }) + }) + } + + unsafe { + ffi::__system_property_read_callback( + self.0.as_ptr(), + Some(ffi_callback::), + callback.as_mut_ptr().cast(), + ) + } + } + + /// Waits for this specific system property to be updated past `old_serial`. Waits no longer + /// than `timeout`, or forever if `timeout` is [`None`]. + /// + /// Returns the new serial in [`Some`], or [`None`] on timeout. + #[cfg(feature = "api-level-26")] + #[doc(alias = "__system_property_wait")] + pub fn wait(&self, old_serial: Option, timeout: Option) -> Option { + wait_optional_prop(Some(self), old_serial, timeout) + } +} + +/// Internal helper to deduplicate the implementation between [`wait()`] and [`Property::wait()`]. +#[cfg(feature = "api-level-26")] +fn wait_optional_prop( + prop: Option<&Property>, + old_serial: Option, + timeout: Option, +) -> Option { + let mut new_serial = MaybeUninit::uninit(); + let timeout = timeout.map_or(std::ptr::null(), |t| &ffi::timespec { + tv_sec: t.as_secs() as i64, + tv_nsec: t.subsec_nanos() as i64, + }); + unsafe { + ffi::__system_property_wait( + prop.map_or(std::ptr::null(), |p| p.0.as_ptr()), + old_serial.map_or(0, NonZeroU32::get), + new_serial.as_mut_ptr(), + timeout, + ) + } + .then(|| unsafe { new_serial.assume_init() }) +} + +/// Waits for this specific system property to be updated past `old_serial`. Waits no longer than +/// `timeout`, or forever if `timeout` is [`None`]. +/// +/// Same as [`Property::wait()`], but for the global serial number. +/// +/// Returns the new serial in [`Some`], or [`None`] on timeout. +#[cfg(feature = "api-level-26")] +#[doc(alias = "__system_property_wait")] +pub fn wait(old_serial: Option, timeout: Option) -> Option { + wait_optional_prop(None, old_serial, timeout) +} diff --git a/ndk/src/utils.rs b/ndk/src/utils.rs index fe1ec5b6..a1bbb328 100644 --- a/ndk/src/utils.rs +++ b/ndk/src/utils.rs @@ -10,8 +10,8 @@ use std::io::{Error, Result}; pub(crate) fn status_to_io_result(status: i32) -> Result<()> { match status { 0 => Ok(()), - r if r < 0 => Err(Error::from_raw_os_error(-r)), - r => unreachable!("Status is positive integer {}", r), + ..=-1 => Err(Error::from_raw_os_error(-status)), + 1.. => unreachable!("Status is positive integer {status}"), } } From 8312a206ebe53c88aa95ce969f623f48f767b8cb Mon Sep 17 00:00:00 2001 From: Marijn Suijten Date: Thu, 25 Jul 2024 10:30:27 +0200 Subject: [PATCH 2/2] ndk: Add definitions and bindings for API levels `android/api-levels.h` from the NDK defines constants for all (NDK-supported) API levels and both a getter for the "current apps' target API level" as well as the API level of the device the app is running on. Create bindings to allow users to query this information at runtime from their Rust code as well. The equivalent of the NDK's `__ANDROID_API__` define are our `api-level-xx` crate features, used to restrict APIs (and inexistant linker symbols) at build-time. (As there is no full-fledged Android build system involved, users of the `ndk` crate are expected to keep this in sync with the minimum/target API level passed to their build tool of choice.) --- ndk-sys/CHANGELOG.md | 1 + ndk-sys/generate_bindings.sh | 1 + ndk-sys/src/ffi_aarch64.rs | 1 - ndk-sys/src/ffi_arm.rs | 1 - ndk-sys/src/ffi_i686.rs | 1 - ndk-sys/src/ffi_x86_64.rs | 1 - ndk/CHANGELOG.md | 1 + ndk/src/api_level.rs | 134 +++++++++++++++++++++++++++++++++++ ndk/src/lib.rs | 1 + ndk/src/system_properties.rs | 3 +- 10 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 ndk/src/api_level.rs diff --git a/ndk-sys/CHANGELOG.md b/ndk-sys/CHANGELOG.md index 29e13338..3719de63 100644 --- a/ndk-sys/CHANGELOG.md +++ b/ndk-sys/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased +- **Breaking:** Removed `__ANDROID_API__` constant as it is always defined to the value of `__ANDROID_API_FUTURE__`. (#479) - Regenerate bindings with `bindgen 0.71.1`. (#487) - Include API bindings from `sys/system_properties.h`. (#495) diff --git a/ndk-sys/generate_bindings.sh b/ndk-sys/generate_bindings.sh index 395be9c5..b5001732 100755 --- a/ndk-sys/generate_bindings.sh +++ b/ndk-sys/generate_bindings.sh @@ -31,6 +31,7 @@ while read ARCH && read TARGET ; do --blocklist-item 'C?_?JNIEnv' \ --blocklist-item '_?JavaVM' \ --blocklist-item '_?j\w+' \ + --blocklist-item '__ANDROID_API__' \ --newtype-enum '\w+_(result|status)_t' \ --newtype-enum 'ACameraDevice_request_template' \ --newtype-enum 'ADataSpace' \ diff --git a/ndk-sys/src/ffi_aarch64.rs b/ndk-sys/src/ffi_aarch64.rs index 1018e0b3..e316ed85 100644 --- a/ndk-sys/src/ffi_aarch64.rs +++ b/ndk-sys/src/ffi_aarch64.rs @@ -44,7 +44,6 @@ pub const __BIONIC__: u32 = 1; pub const __WORDSIZE: u32 = 64; pub const __bos_level: u32 = 0; pub const __ANDROID_API_FUTURE__: u32 = 10000; -pub const __ANDROID_API__: u32 = 10000; pub const __ANDROID_API_G__: u32 = 9; pub const __ANDROID_API_I__: u32 = 14; pub const __ANDROID_API_J__: u32 = 16; diff --git a/ndk-sys/src/ffi_arm.rs b/ndk-sys/src/ffi_arm.rs index 9a25a2ab..55b30afb 100644 --- a/ndk-sys/src/ffi_arm.rs +++ b/ndk-sys/src/ffi_arm.rs @@ -171,7 +171,6 @@ pub const __BIONIC__: u32 = 1; pub const __WORDSIZE: u32 = 32; pub const __bos_level: u32 = 0; pub const __ANDROID_API_FUTURE__: u32 = 10000; -pub const __ANDROID_API__: u32 = 10000; pub const __ANDROID_API_G__: u32 = 9; pub const __ANDROID_API_I__: u32 = 14; pub const __ANDROID_API_J__: u32 = 16; diff --git a/ndk-sys/src/ffi_i686.rs b/ndk-sys/src/ffi_i686.rs index 4778e290..8185b823 100644 --- a/ndk-sys/src/ffi_i686.rs +++ b/ndk-sys/src/ffi_i686.rs @@ -34,7 +34,6 @@ pub const __BIONIC__: u32 = 1; pub const __WORDSIZE: u32 = 32; pub const __bos_level: u32 = 0; pub const __ANDROID_API_FUTURE__: u32 = 10000; -pub const __ANDROID_API__: u32 = 10000; pub const __ANDROID_API_G__: u32 = 9; pub const __ANDROID_API_I__: u32 = 14; pub const __ANDROID_API_J__: u32 = 16; diff --git a/ndk-sys/src/ffi_x86_64.rs b/ndk-sys/src/ffi_x86_64.rs index 354ad270..55d24bbc 100644 --- a/ndk-sys/src/ffi_x86_64.rs +++ b/ndk-sys/src/ffi_x86_64.rs @@ -34,7 +34,6 @@ pub const __BIONIC__: u32 = 1; pub const __WORDSIZE: u32 = 64; pub const __bos_level: u32 = 0; pub const __ANDROID_API_FUTURE__: u32 = 10000; -pub const __ANDROID_API__: u32 = 10000; pub const __ANDROID_API_G__: u32 = 9; pub const __ANDROID_API_I__: u32 = 14; pub const __ANDROID_API_J__: u32 = 16; diff --git a/ndk/CHANGELOG.md b/ndk/CHANGELOG.md index 04778f34..83af0e4c 100644 --- a/ndk/CHANGELOG.md +++ b/ndk/CHANGELOG.md @@ -1,6 +1,7 @@ # Unreleased - image_reader: Add `ImageReader::new_with_data_space()` constructor and `ImageReader::data_space()` getter from API level 34. (#474) +- Add `api_level` module with definitions and bindings for Android API levels. (#479) - Add bindings for querying and setting Android System Properties. (#495) # 0.9.0 (2024-04-26) diff --git a/ndk/src/api_level.rs b/ndk/src/api_level.rs new file mode 100644 index 00000000..0699eddf --- /dev/null +++ b/ndk/src/api_level.rs @@ -0,0 +1,134 @@ +//! Bindings for [API levels] +//! +//! Defines functions and constants for working with Android API levels. +//! +//! [API levels]: https://developer.android.com/ndk/reference/group/apilevels + +use num_enum::{FromPrimitive, IntoPrimitive}; +use thiserror::Error; + +/// Android API levels, equivalent to the constants defined in `` and the Java +/// [`Build.VERSION_CODES`] constants. +/// +/// [`Build.VERSION_CODES`]: https://developer.android.com/reference/android/os/Build.VERSION_CODES +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, FromPrimitive, IntoPrimitive)] +#[repr(u32)] +#[non_exhaustive] +pub enum ApiLevel { + /// Magic version number for an Android OS build which has not yet turned into an official + /// release. + #[doc(alias = "__ANDROID_API_FUTURE__")] + Future = ffi::__ANDROID_API_FUTURE__, + + /// Names the Gingerbread API level (9) + #[doc(alias = "__ANDROID_API_G__")] + G = ffi::__ANDROID_API_G__, + /// Names the Ice-Cream Sandwich API level (14) + #[doc(alias = "__ANDROID_API_I__")] + I = ffi::__ANDROID_API_I__, + /// Names the Jellybean API level (16) + #[doc(alias = "__ANDROID_API_J__")] + J = ffi::__ANDROID_API_J__, + /// Names the Jellybean MR1 API level (17) + #[doc(alias = "__ANDROID_API_J_MR1__")] + JMr1 = ffi::__ANDROID_API_J_MR1__, + /// Names the Jellybean MR2 API level (18) + #[doc(alias = "__ANDROID_API_J_MR2__")] + JMr2 = ffi::__ANDROID_API_J_MR2__, + /// Names the KitKat API level (19) + #[doc(alias = "__ANDROID_API_K__")] + K = ffi::__ANDROID_API_K__, + /// Names the Lollipop API level (21) + #[doc(alias = "__ANDROID_API_L__")] + L = ffi::__ANDROID_API_L__, + /// Names the Lollipop MR1 API level (22) + #[doc(alias = "__ANDROID_API_L_MR1__")] + LMr1 = ffi::__ANDROID_API_L_MR1__, + /// Names the Marshmallow API level (23) + #[doc(alias = "__ANDROID_API_M__")] + M = ffi::__ANDROID_API_M__, + /// Names the Nougat API level (24) + #[doc(alias = "__ANDROID_API_N__")] + N = ffi::__ANDROID_API_N__, + /// Names the Nougat MR1 API level (25) + #[doc(alias = "__ANDROID_API_N_MR1__")] + NMr1 = ffi::__ANDROID_API_N_MR1__, + /// Names the Oreo API level (26) + #[doc(alias = "__ANDROID_API_O__")] + O = ffi::__ANDROID_API_O__, + /// Names the Oreo MR1 API level (27) + #[doc(alias = "__ANDROID_API_O_MR1__")] + OMr1 = ffi::__ANDROID_API_O_MR1__, + /// Names the Pie API level (28) + #[doc(alias = "__ANDROID_API_P__")] + P = ffi::__ANDROID_API_P__, + /// Names the Android 10 (aka "Q" or "Quince Tart") API level (29) + #[doc(alias = "__ANDROID_API_Q__")] + Q = ffi::__ANDROID_API_Q__, + /// Names the Android 11 (aka "R" or "Red Velvet Cake") API level (30) + #[doc(alias = "__ANDROID_API_R__")] + R = ffi::__ANDROID_API_R__, + /// Names the Android 12 (aka "S" or "Snowcone") API level (31) + #[doc(alias = "__ANDROID_API_S__")] + S = ffi::__ANDROID_API_S__, + /// Names the Android 13 (aka "T" or "Tiramisu") API level (33) + #[doc(alias = "__ANDROID_API_T__")] + T = ffi::__ANDROID_API_T__, + /// Names the Android 14 (aka "U" or "UpsideDownCake") API level (34) + #[doc(alias = "__ANDROID_API_U__")] + U = ffi::__ANDROID_API_U__, + /// Names the Android 15 (aka "V" or "VanillaIceCream") API level (35) + #[doc(alias = "__ANDROID_API_V__")] + V = ffi::__ANDROID_API_V__, + #[doc(hidden)] + #[num_enum(catch_all)] + __Unknown(u32), +} + +/// Returns the `targetSdkVersion` from `AndroidManifest.xml` of the caller, or [`ApiLevel::Future`] +/// if there is no known target SDK version (for code not running in the context of an app). +/// +/// See also [`device_api_level()`]. +#[cfg(feature = "api-level-24")] +#[doc(alias = "android_get_application_target_sdk_version")] +pub fn application_target_sdk_version() -> ApiLevel { + let version = unsafe { ffi::android_get_application_target_sdk_version() }; + u32::try_from(version) + // Docs suggest that it would only return `Future` + .expect("Unexpected sign bit in `application_target_sdk_version()`") + .into() +} + +/// Possible failures returned by [`device_api_level()`]. +#[derive(Debug, Error)] +pub enum DeviceApiLevelError { + #[cfg(not(feature = "api-level-29"))] + #[error("`__system_property_get(\"ro.build.version.sdk\")` failed")] + FallbackPropertyGetFailed(#[from] super::system_properties::GetError), + #[error("device_api_level() encountered a negative version code")] + TryFromIntError(#[from] std::num::TryFromIntError), +} + +/// Returns the API level of the device we're actually running on. +/// +/// The returned value is equivalent to the Java [`Build.VERSION.SDK_INT`] API. +/// +/// [`Build.VERSION.SDK_INT`]: https://developer.android.com/reference/android/os/Build.VERSION#SDK_INT +/// +/// Below `api-level-29` this falls back to reading the `"ro.build.version.sdk"` system property, +/// with the possibility to return more types of errors. +/// +/// See also [`application_target_sdk_version()`]. +#[doc(alias = "android_get_device_api_level")] +pub fn device_api_level() -> Result { + #[cfg(not(feature = "api-level-29"))] + let version = super::system_properties::get::(unsafe { + // TODO: Switch to C-string literal since MSRV 1.77 + std::ffi::CStr::from_bytes_with_nul_unchecked(b"ro.build.version.sdk\0") + })?; + + #[cfg(feature = "api-level-29")] + let version = unsafe { ffi::android_get_device_api_level() }; + + Ok(u32::try_from(version)?.into()) +} diff --git a/ndk/src/lib.rs b/ndk/src/lib.rs index 68b00b70..e6e8ffa2 100644 --- a/ndk/src/lib.rs +++ b/ndk/src/lib.rs @@ -11,6 +11,7 @@ )] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +pub mod api_level; pub mod asset; pub mod audio; pub mod bitmap; diff --git a/ndk/src/system_properties.rs b/ndk/src/system_properties.rs index df480ae7..19246ff2 100644 --- a/ndk/src/system_properties.rs +++ b/ndk/src/system_properties.rs @@ -64,8 +64,7 @@ pub fn get_raw(name: &CStr) -> Result { } /// Possible failures returned by [`get()`]. -#[allow(missing_debug_implementations)] // Our MSRV is too low for derive(Debug) to emit bounds on T. -#[derive(Error)] +#[derive(Debug, Error)] pub enum GetError { #[error("Property is missing or empty")] MissingOrEmpty,