Skip to content

Commit a4a2292

Browse files
committed
Add bindings for Thermal Manager
https://developer.android.com/ndk/reference/group/thermal `AThermal` allows querying the current thermal (throttling) status, as well as forecasts of future thermal statuses to allow applications to respond and mitigate possible throttling in the (near) future.
1 parent 7811b58 commit a4a2292

File tree

3 files changed

+311
-0
lines changed

3 files changed

+311
-0
lines changed

ndk/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Unreleased
22

33
- image_reader: Add `ImageReader::new_with_data_space()` constructor and `ImageReader::data_space()` getter from API level 34. (#474)
4+
- Add bindings Thermal (`AThermalManager`). (#481)
45

56
# 0.9.0 (2024-04-26)
67

ndk/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,6 @@ pub mod native_window;
2929
pub mod shared_memory;
3030
pub mod surface_texture;
3131
pub mod sync;
32+
pub mod thermal;
3233
pub mod trace;
3334
mod utils;

ndk/src/thermal.rs

+309
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
//! Bindings for [`AThermalManager`]
2+
//!
3+
//! Structures and functions to access thermal status and register/unregister thermal status
4+
//! listener in native code.
5+
//!
6+
//! [`AThermalManager`]: https://developer.android.com/ndk/reference/group/thermal#athermalmanager
7+
#![cfg(feature = "api-level-30")]
8+
9+
#[cfg(doc)]
10+
use std::io::ErrorKind;
11+
use std::{io::Result, os::raw::c_void, ptr::NonNull};
12+
13+
use num_enum::{FromPrimitive, IntoPrimitive};
14+
15+
use crate::utils::{abort_on_panic, status_to_io_result};
16+
17+
/// Thermal status used in function [`ThermalManager::current_thermal_status()`] and
18+
/// [`ThermalStatusCallback`].
19+
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, FromPrimitive, IntoPrimitive)]
20+
#[repr(i32)]
21+
#[doc(alias = "AThermalStatus")]
22+
#[non_exhaustive]
23+
pub enum ThermalStatus {
24+
/// Error in thermal status.
25+
// TODO: Move to a Result?
26+
#[doc(alias = "ATHERMAL_STATUS_ERROR")]
27+
Error = ffi::AThermalStatus::ATHERMAL_STATUS_ERROR.0,
28+
/// Not under throttling.
29+
#[doc(alias = "ATHERMAL_STATUS_NONE")]
30+
None = ffi::AThermalStatus::ATHERMAL_STATUS_NONE.0,
31+
/// Light throttling where UX is not impacted.
32+
#[doc(alias = "ATHERMAL_STATUS_LIGHT")]
33+
Light = ffi::AThermalStatus::ATHERMAL_STATUS_LIGHT.0,
34+
/// Moderate throttling where UX is not largely impacted.
35+
#[doc(alias = "ATHERMAL_STATUS_MODERATE")]
36+
Moderate = ffi::AThermalStatus::ATHERMAL_STATUS_MODERATE.0,
37+
/// Severe throttling where UX is largely impacted.
38+
#[doc(alias = "ATHERMAL_STATUS_SEVERE")]
39+
Severe = ffi::AThermalStatus::ATHERMAL_STATUS_SEVERE.0,
40+
/// Platform has done everything to reduce power.
41+
#[doc(alias = "ATHERMAL_STATUS_CRITICAL")]
42+
Critical = ffi::AThermalStatus::ATHERMAL_STATUS_CRITICAL.0,
43+
/// Key components in platform are shutting down due to thermal condition. Device
44+
/// functionalities will be limited.
45+
#[doc(alias = "ATHERMAL_STATUS_EMERGENCY")]
46+
Emergency = ffi::AThermalStatus::ATHERMAL_STATUS_EMERGENCY.0,
47+
/// Need shutdown immediately.
48+
#[doc(alias = "ATHERMAL_STATUS_SHUTDOWN")]
49+
Shutdown = ffi::AThermalStatus::ATHERMAL_STATUS_SHUTDOWN.0,
50+
51+
#[doc(hidden)]
52+
#[num_enum(catch_all)]
53+
__Unknown(i32),
54+
}
55+
56+
impl From<ffi::AThermalStatus> for ThermalStatus {
57+
fn from(value: ffi::AThermalStatus) -> Self {
58+
value.0.into()
59+
}
60+
}
61+
62+
/// Prototype of the function that is called when thermal status changes. It's passed the updated
63+
/// thermal status as parameter.
64+
#[doc(alias = "AThermal_StatusCallback")]
65+
// TODO: SendSync? What thread does this run on?
66+
pub type ThermalStatusCallback = Box<dyn FnMut(ThermalStatus)>;
67+
68+
/// Token returned by [`ThermalManager::register_thermal_status_listener()`] for a given
69+
/// [`ThermalStatusCallback`].
70+
///
71+
/// Pass this to [`ThermalManager::unregister_thermal_status_listener()`] when you no longer wish to
72+
/// receive the callback.
73+
#[derive(Debug)]
74+
#[must_use = "Without this token the callback can no longer be unregistered and will leak Boxes"]
75+
// TODO: SendSync if this can be (de)registered across threads (on different instances even)?
76+
pub struct ThermalStatusListenerToken {
77+
func: ffi::AThermal_StatusCallback,
78+
data: *mut ThermalStatusCallback,
79+
}
80+
81+
/// An opaque type representing a handle to a thermal manager. An instance of thermal manager must
82+
/// be acquired prior to using thermal status APIs. It will be freed automatically on [`drop()`]
83+
/// after use.
84+
///
85+
/// To use:
86+
/// - Create a new thermal manager instance by calling the [`ThermalManager::new()`] function.
87+
/// - Get current thermal status with [`ThermalManager::current_thermal_status()`].
88+
/// - Register a thermal status listener with [`ThermalManager::register_thermal_status_listener()`].
89+
/// - Unregister a thermal status listener with
90+
/// [`ThermalManager::unregister_thermal_status_listener()`].
91+
/// - Release the thermal manager instance with [`drop()`].
92+
#[derive(Debug)]
93+
#[doc(alias = "AThermalManager")]
94+
pub struct ThermalManager {
95+
ptr: NonNull<ffi::AThermalManager>,
96+
}
97+
98+
impl ThermalManager {
99+
/// Acquire an instance of the thermal manager.
100+
///
101+
/// Returns [`None`] on failure.
102+
#[doc(alias = "AThermal_acquireManager")]
103+
pub fn new() -> Option<Self> {
104+
NonNull::new(unsafe { ffi::AThermal_acquireManager() }).map(|ptr| Self { ptr })
105+
}
106+
107+
/// Gets the current thermal status.
108+
///
109+
/// Returns current thermal status, [`ThermalStatus::Error`] on failure.
110+
// TODO: Result?
111+
#[doc(alias = "AThermal_getCurrentThermalStatus")]
112+
pub fn current_thermal_status(&self) -> ThermalStatus {
113+
unsafe { ffi::AThermal_getCurrentThermalStatus(self.ptr.as_ptr()) }.into()
114+
}
115+
116+
/// Register the thermal status listener for thermal status change.
117+
///
118+
/// Will leak [`Box`]es unless [`ThermalManager::unregister_thermal_status_listener()`] is
119+
/// called.
120+
///
121+
/// # Returns
122+
/// - [`ErrorKind::InvalidInput`] if the listener and data pointer were previously added and not removed.
123+
/// - [`ErrorKind::PermissionDenied`] if the required permission is not held.
124+
/// - [`ErrorKind::BrokenPipe`] if communication with the system service has failed.
125+
#[doc(alias = "AThermal_registerThermalStatusListener")]
126+
pub fn register_thermal_status_listener(
127+
&self,
128+
callback: ThermalStatusCallback,
129+
) -> Result<ThermalStatusListenerToken> {
130+
let boxed = Box::new(callback);
131+
// This box is only freed when unregister() is called
132+
let data = Box::into_raw(boxed);
133+
134+
unsafe extern "C" fn thermal_status_callback(
135+
data: *mut c_void,
136+
status: ffi::AThermalStatus,
137+
) {
138+
abort_on_panic(|| {
139+
let func: *mut ThermalStatusCallback = data.cast();
140+
(*func)(status.into())
141+
})
142+
}
143+
144+
status_to_io_result(unsafe {
145+
ffi::AThermal_registerThermalStatusListener(
146+
self.ptr.as_ptr(),
147+
Some(thermal_status_callback),
148+
data.cast(),
149+
)
150+
})
151+
.map(|()| ThermalStatusListenerToken {
152+
func: Some(thermal_status_callback),
153+
data,
154+
})
155+
}
156+
157+
/// Unregister the thermal status listener previously resgistered.
158+
///
159+
/// # Returns
160+
/// - [`ErrorKind::InvalidInput`] if the listener and data pointer were not previously added.
161+
/// - [`ErrorKind::PermissionDenied`] if the required permission is not held.
162+
/// - [`ErrorKind::BrokenPipe`] if communication with the system service has failed.
163+
#[doc(alias = "AThermal_unregisterThermalStatusListener")]
164+
pub fn unregister_thermal_status_listener(
165+
&self,
166+
token: ThermalStatusListenerToken,
167+
) -> Result<()> {
168+
status_to_io_result(unsafe {
169+
ffi::AThermal_unregisterThermalStatusListener(
170+
self.ptr.as_ptr(),
171+
token.func,
172+
token.data.cast(),
173+
)
174+
})?;
175+
let _ = unsafe { Box::from_raw(token.data) };
176+
Ok(())
177+
}
178+
179+
/// Provides an estimate of how much thermal headroom the device currently has before hitting
180+
/// severe throttling.
181+
///
182+
/// Note that this only attempts to track the headroom of slow-moving sensors, such as the
183+
/// skin temperature sensor. This means that there is no benefit to calling this function more
184+
/// frequently than about once per second, and attempted to call significantly more frequently
185+
/// may result in the function returning [`f32::NAN`].
186+
///
187+
/// In addition, in order to be able to provide an accurate forecast, the system does not
188+
/// attempt to forecast until it has multiple temperature samples from which to extrapolate.
189+
/// This should only take a few seconds from the time of the first call, but during this time,
190+
/// no forecasting will occur, and the current headroom will be returned regardless of the value
191+
/// of `forecast_seconds`.
192+
///
193+
/// The value returned is a non-negative float that represents how much of the thermal envelope
194+
/// is in use (or is forecasted to be in use). A value of `1.0` indicates that the device is
195+
/// (or will be) throttled at [`ThermalStatus::Severe`]. Such throttling can affect the CPU,
196+
/// GPU, and other subsystems. Values may exceed `1.0`, but there is no implied mapping to
197+
/// specific thermal levels beyond that point. This means that values greater than `1.0` may
198+
/// correspond to [`ThermalStatus::Severe`], but may also represent heavier throttling.
199+
///
200+
/// A value of `0.0` corresponds to a fixed distance from `1.0`, but does not correspond to any
201+
/// particular thermal status or temperature. Values on `(0.0, 1.0]` may be expected to scale
202+
/// linearly with temperature, though temperature changes over time are typically not linear.
203+
/// Negative values will be clamped to `0.0` before returning.
204+
///
205+
/// `forecast_seconds` specifies how many seconds into the future to forecast. Given that device
206+
/// conditions may change at any time, forecasts from further in the
207+
/// future will likely be less accurate than forecasts in the near future.
208+
////
209+
/// # Returns
210+
/// A value greater than equal to `0.0`, where `1.0` indicates the SEVERE throttling threshold,
211+
/// as described above. Returns [`f32::NAN`] if the device does not support this functionality
212+
/// or if this function is called significantly faster than once per second.
213+
#[cfg(feature = "api-level-31")]
214+
#[doc(alias = "AThermal_getThermalHeadroom")]
215+
pub fn thermal_headroom(
216+
&self,
217+
// TODO: Duration, even though it has a granularity of seconds?
218+
forecast_seconds: i32,
219+
) -> f32 {
220+
unsafe { ffi::AThermal_getThermalHeadroom(self.ptr.as_ptr(), forecast_seconds) }
221+
}
222+
223+
/// Gets the thermal headroom thresholds for all available thermal status.
224+
///
225+
/// A thermal status will only exist in output if the device manufacturer has the corresponding
226+
/// threshold defined for at least one of its slow-moving skin temperature sensors. If it's
227+
/// set, one should also expect to get it from [`ThermalManager::current_thermal_status()`] or
228+
/// [`ThermalStatusCallback`].
229+
///
230+
/// The headroom threshold is used to interpret the possible thermal throttling status
231+
/// based on the headroom prediction. For example, if the headroom threshold for
232+
/// [`ThermalStatus::Light`] is `0.7`, and a headroom prediction in `10s` returns `0.75` (or
233+
/// [`ThermalManager::thermal_headroom(10)`] = `0.75`), one can expect that in `10` seconds the
234+
/// system could be in lightly throttled state if the workload remains the same. The app can
235+
/// consider taking actions according to the nearest throttling status the difference between
236+
/// the headroom and the threshold.
237+
///
238+
/// For new devices it's guaranteed to have a single sensor, but for older devices with
239+
/// multiple sensors reporting different threshold values, the minimum threshold is taken to
240+
/// be conservative on predictions. Thus, when reading real-time headroom, it's not guaranteed
241+
/// that a real-time value of `0.75` (or [`ThermalManager::thermal_headroom(0)`] = `0.75`)
242+
/// exceeding the threshold of `0.7` above will always come with lightly throttled state (or
243+
/// [`ThermalManager::current_thermal_status()`] = [`ThermalStatus::Light`]) but it can be lower
244+
/// (or [`ThermalManager::current_thermal_status()`] = [`ThermalStatus::None`]). While it's
245+
/// always guaranteed that the device won't be throttled heavier than the unmet threshold's
246+
/// state, so a real-time headroom of `0.75` will never come with [`ThermalStatus::Moderate`]
247+
/// but always lower, and `0.65` will never come with [`ThermalStatus::Light`] but
248+
/// [`ThermalStatus::None`].
249+
///
250+
/// The returned list of thresholds is cached on first successful query and owned by the thermal
251+
/// manager, which will not change between calls to this function. The caller should only need
252+
/// to free the manager with [`drop()`].
253+
///
254+
/// # Returns
255+
/// - [`ErrorKind::InvalidInput`] if outThresholds or size_t is nullptr, or *outThresholds is not nullptr.
256+
/// - [`ErrorKind::BrokenPipe`] if communication with the system service has failed.
257+
/// - [`ErrorKind::Unsupported`] if the feature is disabled by the current system.
258+
#[cfg(feature = "api-level-35")]
259+
#[doc(alias = "AThermal_getThermalHeadroomThresholds")]
260+
pub fn thermal_headroom_thresholds(
261+
&self,
262+
) -> Result<Option<impl ExactSizeIterator<Item = ThermalHeadroomThreshold> + '_>> {
263+
let mut out_thresholds = std::ptr::null();
264+
let mut out_size = 0;
265+
status_to_io_result(unsafe {
266+
ffi::AThermal_getThermalHeadroomThresholds(
267+
self.ptr.as_ptr(),
268+
&mut out_thresholds,
269+
&mut out_size,
270+
)
271+
})?;
272+
if out_thresholds.is_null() {
273+
return Ok(None);
274+
}
275+
Ok(Some(
276+
unsafe { std::slice::from_raw_parts(out_thresholds, out_size) }
277+
.iter()
278+
.map(|t| ThermalHeadroomThreshold {
279+
headroom: t.headroom,
280+
thermal_status: t.thermalStatus.into(),
281+
}),
282+
))
283+
}
284+
}
285+
286+
impl Drop for ThermalManager {
287+
/// Release the thermal manager pointer acquired via [`ThermalManager::new()`].
288+
#[doc(alias = "AThermal_releaseManager")]
289+
fn drop(&mut self) {
290+
unsafe { ffi::AThermal_releaseManager(self.ptr.as_ptr()) }
291+
}
292+
}
293+
294+
/// This struct defines an instance of headroom threshold value and its status.
295+
///
296+
/// The value should be monotonically non-decreasing as the thermal status increases. For
297+
/// [`ThermalStatus::Severe`], its headroom threshold is guaranteed to be `1.0`. For status below
298+
/// severe status, the value should be lower or equal to `1.0`, and for status above severe, the
299+
/// value should be larger or equal to `1.0`.
300+
///
301+
/// Also see [`ThermalManager::thermal_headroom()`] for explanation on headroom, and
302+
/// [`ThermalManager::thermal_headroom_thresholds()`] for how to use this.
303+
#[cfg(feature = "api-level-35")]
304+
#[derive(Clone, Copy, Debug, PartialEq)]
305+
#[doc(alias = "AThermalHeadroomThreshold")]
306+
pub struct ThermalHeadroomThreshold {
307+
headroom: f32,
308+
thermal_status: ThermalStatus,
309+
}

0 commit comments

Comments
 (0)