ohmu
·
2026-10-15
1#include "fastfetch.h"
2#include "battery.h"
3#include "common/android/api.h"
4#include "common/android/binder.h"
5#include "common/android/dex.h"
6#include "common/debug.h"
7#include "common/io.h"
8#include "common/processing.h"
9#include "common/properties.h"
10#include "common/settings.h"
11#include "common/strutil.h"
12
13#include <dirent.h>
14#include <errno.h>
15#include <fcntl.h>
16#include <inttypes.h>
17#include <string.h>
18
19// Android exposes the battery through BatteryManager, which the NDK does not wrap and whose sysfs
20// backing is unreadable from an app UID -- /sys/class/power_supply is closed to untrusted_app
21// outright, directory and all. What does work without any permission is
22// android.os.IBatteryPropertiesRegistrar in system_server, over /dev/binder (see
23// common/android/binder.h).
24//
25// `dumpsys battery` is the other route, tried first for shell and root because it is the richer of
26// the two: a flat `key: value` list read by name, so it carries what neither the registrar nor a
27// property has -- the technology and the critical capacity level -- and neither a vendor that prints
28// extra keys in the middle nor a release that appends a field can break it. The binder reply is
29// positional and breaks silently instead. What it costs is a fork/exec, which is why it is only
30// reached where it can succeed; see the UID gate in ffDetectBattery.
31//
32// An app UID gets none of that, and not because Android withholds it. `ACTION_BATTERY_CHANGED` is a
33// sticky broadcast, and a sticky broadcast is read without a permission: it carries the technology,
34// and a cycle count since Android 14 -- the device's own BatteryManager declares `EXTRA_CYCLE_COUNT`,
35// spelled "android.os.extra.CYCLE_COUNT". What is out of reach is the call that fetches it. A sticky
36// broadcast is read by `registerReceiver`, the NDK does not wrap it, and a native binary has no Java
37// side to make that call from, so every route to it is a Java process. termux-api is one, and it is
38// not the answer: on the device this was written on it hung on six of six runs, each killed at 30
39// seconds having written nothing, after exec'ing `am broadcast` and waiting on a socket that has no
40// timeout of its own.
41//
42// What is left within reach is a pair of system properties BatteryService mirrors out of every update
43// it processes: `debug.tracing.plug_type` and `debug.tracing.battery_status`. A system property is
44// world readable, which is the whole point of them here -- the charger bits used to need `dumpsys
45// battery`, and so the shell UID, while the status used to cost a second transaction. Both are now
46// free, to any UID; see the two defines below.
47//
48// The temperature comes from neither service. The kernel publishes it as a thermal zone, and
49// /sys/class/thermal is readable from an app UID -- unlike /sys/class/power_supply, which is closed
50// outright, directory and all -- so it is read there whenever `temp` asks for it. The dump has a
51// `temperature` line of its own and that one wins where the dump runs; see `detectTemperature` below
52// for why the zone is looked up by name rather than addressed by number.
53//
54// The registrar answers nothing else. `BatteryProperty` has no temperature, no technology and no
55// cycle count, and manufacturer and model name are not battery data at all. Of the properties that
56// do exist, ids 7 to 12 -- the ones Android 15 added -- are behind BATTERY_STATS, and ids 1 to 6 are
57// not. The one reading the module wants that is not a property either, how long the charge will
58// last, comes from a different service; see the `batterystats` block below.
59
60// The reply layout is positional and is not negotiated with the service, so it only stays correct as
61// long as AOSP keeps the order it already has. It has, because `BatteryProperty` has only ever grown
62// at the end -- API 35 appended [string8 mValueString] to it -- so the field read below is still the
63// first one written.
64//
65// The transaction code is positional for the same reason, and it has *not* stayed: the interface
66// still declared registerListener / unregisterListener / getProperty up to Android 9, and Android 10
67// dropped the two listener methods, which moved `getProperty` from the third position to the first.
68// The number is therefore read out of the device's own jar rather than guessed from the API level,
69// because a vendor fork is free to insert a method ahead of it -- and a wrong code reaches a
70// different method, or none. See common/android/dex.h.
71//
72// Getting either wrong is quiet rather than loud: `ffBinderReadU64` returns 0 for a read past the
73// end of the reply, so a drift shows up as a wrong capacity, not as a failed call.
74#define FF_BATTERY_ANDROID_SERVICE "batteryproperties"
75#define FF_BATTERY_ANDROID_DESCRIPTOR "android.os.IBatteryPropertiesRegistrar"
76
77// IBatteryPropertiesRegistrar has always been part of framework.jar -- it is not one of the
78// frameworks that moved into an APEX -- so there is one path and no fallback. The method this module
79// calls is named by the constant the dex carries for it, which is what the lookup turns into whatever
80// number this build uses.
81#define FF_BATTERY_ANDROID_JAR "/system/framework/framework.jar"
82#define FF_BATTERY_ANDROID_STUB "Landroid/os/IBatteryPropertiesRegistrar$Stub;"
83#define FF_BATTERY_ANDROID_GET_PROPERTY "TRANSACTION_getProperty"
84
85// How long the charge will last is not a property of the pack: it is a forecast BatteryStatsService
86// makes from the discharge history, so it lives on a second service. That service is `batterystats`,
87// whose interface is com.android.internal.app.IBatteryStats, and it is also in framework.jar --
88// com.android.internal.app is not one of the namespaces that moved into an APEX -- so the same read
89// resolves this code as the one above.
90//
91// The interface has 120 methods and the dex cannot say which of them an app UID may call: neither
92// IBatteryStats nor its $Stub carries an `_enforcePermission` wrapper, so the wall is inside
93// BatteryStatsService, which lives in services.jar and is not available to read. It was therefore
94// measured, as uid 10478 on an Android 16 device. The three this module needs answer with no
95// permission at all -- and so do `getAllWakeLocks` (117) and `getAllWakeLocksEx` (118), which is why
96// the claim is not "only three of 120". Every read-only statistic getter tried (`getAwakeTimeBattery`,
97// `getAwakeTimePlugged`, and the cellular / wifi / gps / bluetooth ones) came back with
98// "Access denied, requires: android.permission.BATTERY_STATS", or an `anyOf` of that and
99// UPDATE_DEVICE_STATS.
100//
101// The code is read out of the jar for the same reason as `getProperty`, and the numbers make the
102// point better than the argument does: this method is transaction 19 on that Android 16 device, 22
103// on a Mi 10 (Android 14) and 20 on a Mi 9 (Android 10). A hardcoded 19 would reach a different
104// method on two of the three.
105#define FF_BATTERY_ANDROID_STATS_SERVICE "batterystats"
106#define FF_BATTERY_ANDROID_STATS_DESCRIPTOR "com.android.internal.app.IBatteryStats"
107#define FF_BATTERY_ANDROID_STATS_STUB "Lcom/android/internal/app/IBatteryStats$Stub;"
108#define FF_BATTERY_ANDROID_COMPUTE_BATTERY_TIME_REMAINING "TRANSACTION_computeBatteryTimeRemaining"
109
110// The temperature is a third source again, and not a service this time: the kernel publishes it as a
111// thermal zone, and the zone Android's own thermal HAL names `battery` is the one to read. The name
112// is what is matched -- not the number, and not a prefix -- for the reasons spelled out in
113// detectTemperature below.
114#define FF_BATTERY_ANDROID_THERMAL_DIR "/sys/class/thermal/"
115#define FF_BATTERY_ANDROID_THERMAL_ZONE_PREFIX "thermal_zone"
116#define FF_BATTERY_ANDROID_THERMAL_BATTERY_TYPE "battery"
117
118// BatteryManager.BATTERY_PROPERTY_*
119typedef enum FFBatteryAndroidProperty : uint32_t {
120 FF_BATTERY_ANDROID_PROPERTY_CAPACITY = 4,
121 FF_BATTERY_ANDROID_PROPERTY_STATUS = 6,
122} FFBatteryAndroidProperty;
123
124// BatteryManager.BATTERY_STATUS_*, which the health HAL's `BatteryStatus` mirrors value for value.
125// The module has something to say about only three of the five: NOT_CHARGING (4) and FULL (5) both
126// mean a charger is attached, and it answers nothing about the charger to put that on, while the dump
127// route maps UNKNOWN (1) as well, because there it is a different statement from the service not
128// answering at all. The two are named anyway, because the property route validates the whole range
129// before it trusts a value it did not get from the service.
130typedef enum FFBatteryAndroidStatus : uint32_t {
131 FF_BATTERY_ANDROID_STATUS_UNKNOWN = 1,
132 FF_BATTERY_ANDROID_STATUS_CHARGING = 2,
133 FF_BATTERY_ANDROID_STATUS_DISCHARGING = 3,
134 FF_BATTERY_ANDROID_STATUS_NOT_CHARGING = 4,
135 FF_BATTERY_ANDROID_STATUS_FULL = 5,
136} FFBatteryAndroidStatus;
137
138// The pair of system properties that stand in for two of the readings above. Both are published by
139// `BatteryService.processValuesLocked` -- the two strings sit next to each other in the constant pool
140// of classes.dex in this device's services.jar, which is what a pair of `SystemProperties.set` calls
141// looks like after the fact -- and neither is a `BatteryProperty`, so nothing about the registrar's
142// positional reply applies to them.
143//
144// debug.tracing.plug_type -- BatteryManager.BATTERY_PLUGGED_*: 1 AC, 2 USB, 4 wireless,
145// 8 dock, and 0 when nothing is attached. The dock bit has no
146// `FF_BATTERY_STATUS_*` counterpart and is masked off below.
147// debug.tracing.battery_status -- BatteryManager.BATTERY_STATUS_*, the value
148// `getProperty(BATTERY_PROPERTY_STATUS)` answers with as well:
149// 1 UNKNOWN, 2 CHARGING, 3 DISCHARGING, 4 NOT_CHARGING, 5 FULL.
150#define FF_BATTERY_ANDROID_PROP_PLUG_TYPE "debug.tracing.plug_type"
151#define FF_BATTERY_ANDROID_PROP_BATTERY_STATUS "debug.tracing.battery_status"
152
153// The encoding of plug_type, read out of the device's own framework.jar rather than assumed: the
154// values are bits of one field and not a sequence, so `value != 0` is not a test for "wired", and a
155// device reporting the dock would be told it is on neither AC nor USB nor wireless.
156typedef enum FFBatteryAndroidPlugged : uint32_t {
157 FF_BATTERY_ANDROID_PLUGGED_AC = 1,
158 FF_BATTERY_ANDROID_PLUGGED_USB = 2,
159 FF_BATTERY_ANDROID_PLUGGED_WIRELESS = 4,
160} FFBatteryAndroidPlugged;
161
162// BatteryCapacityLevel.BATTERY_CAPACITY_LEVEL_CRITICAL, the level the framework itself shuts the
163// device down on. This is the health HAL's numbering, not the one the older
164// `BatteryManager.BATTERY_CAPACITY_LEVEL_*` constants used, which had CRITICAL at 4. The rest of the
165// enum is skipped: -1 (UNSUPPORTED) and 0 (UNKNOWN) are what a HAL that does not implement the
166// property reports, and 2 to 5 only restate the percentage. Only the dump carries this -- it is not a
167// `BatteryProperty`, so the binder route never sees it.
168typedef enum FFBatteryAndroidCapacityLevel : int32_t {
169 FF_BATTERY_ANDROID_CAPACITY_LEVEL_CRITICAL = 1,
170} FFBatteryAndroidCapacityLevel;
171
172// BatteryProperty starts with [int64 mValueLong], behind the usual [exception code][return value]
173// [out-param non-null marker] prefix of an AIDL reply. API 35 appended [string8 mValueString] after
174// the long for the one string valued property (`BATTERY_PROPERTY_SERIAL_NUMBER`), which does not
175// move the long, so one offset covers every release.
176#define FF_BATTERY_ANDROID_VALUE_OFFSET 12
177#define FF_BATTERY_ANDROID_VALUE_UNSET 0x8000000000000000ULL // Long.MIN_VALUE, i.e. never filled in
178
179// A method that answers with a bare `long` writes [int32 exception][int64 value] and nothing else:
180// there is no out parameter, so no non-null marker sits in front of the value the way it does in a
181// `getProperty` reply. Twelve bytes in all on the device.
182#define FF_BATTERY_ANDROID_LONG_VALUE_OFFSET 4
183
184// One `getProperty` reply is [exception code][return value][value present][int64 mValueLong], and it
185// measures 24 bytes on the device: API 35 appended [string8 mValueString] to `BatteryProperty`, and a
186// reply carries a word for it even when it is empty. A dump of the first 32 covers every word.
187#define FF_BATTERY_ANDROID_DEBUG_DUMP_SIZE 32
188
189static void initResult(FFBatteryResult* battery) {
190 battery->temperature = FF_BATTERY_TEMP_UNSET;
191 battery->cycleCount = 0;
192 battery->timeRemaining = -1;
193 battery->capacity = 0;
194 battery->status = FF_BATTERY_STATUS_NONE;
195 ffStrbufInit(&battery->manufacturer);
196 ffStrbufInit(&battery->modelName);
197 ffStrbufInit(&battery->technology);
198 ffStrbufInit(&battery->serial);
199 ffStrbufInit(&battery->manufactureDate);
200}
201
202#ifndef NDEBUG
203// Every word of the reply is read at a fixed offset, so a reply whose fields do not line up is
204// otherwise undiagnosable: the bytes are the only evidence. Each line carries the words next to their
205// bytes, because what the reader is looking for -- the exception code, the return value, the long --
206// is an int, and the long straddles the last word of the first line and the first of the second.
207static void debugDumpReply(const uint8_t* data, size_t size) {
208 const size_t limit = size < FF_BATTERY_ANDROID_DEBUG_DUMP_SIZE ? size : FF_BATTERY_ANDROID_DEBUG_DUMP_SIZE;
209 for (size_t offset = 0; offset < limit; offset += 16) {
210 char hex[16 * 3 + 1];
211 size_t used = 0;
212 for (size_t i = 0; i < 16 && offset + i < limit; ++i) {
213 used += (size_t) snprintf(hex + used, sizeof(hex) - used, "%02x ", data[offset + i]);
214 }
215 FF_DEBUG(" +0x%02zx %-47s | %d %d %d %d", offset, hex,
216 ffBinderReadI32(data, size, offset), ffBinderReadI32(data, size, offset + 4),
217 ffBinderReadI32(data, size, offset + 8), ffBinderReadI32(data, size, offset + 12));
218 }
219}
220 #define FF_BATTERY_ANDROID_DEBUG_DUMP(data, size) debugDumpReply(data, size)
221#else
222 #define FF_BATTERY_ANDROID_DEBUG_DUMP(data, size) ((void) 0)
223#endif
224
225static const char* getProperty(FFBinder* binder, uint32_t handle, uint32_t transaction, uint32_t property, uint64_t* value) {
226 FF_DEBUG("Property %u goes out as transaction %u", property, transaction);
227
228 uint8_t parcelBuffer[128];
229 FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer));
230 ffBinderParcelPutInterfaceToken(&parcel, FF_BATTERY_ANDROID_DESCRIPTOR);
231 ffBinderParcelPutU32(&parcel, property);
232
233 uint8_t replyBuffer[128];
234 FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer));
235 const char* error = ffBinderTransact(binder, handle, transaction, 0, &parcel, &reply);
236 if (error != nullptr) {
237 FF_DEBUG("Property %u could not be transacted: %s", property, error);
238 return error;
239 }
240 if (ffBinderReplyIsStatus(&reply)) {
241 FF_DEBUG("Property %u came back as a status reply of %d instead of a parcel",
242 property, (int) ffBinderReadI32(reply.data, reply.size, 0));
243 return "Battery service rejected the request";
244 }
245
246 // The three words AIDL writes in front of the value, named here rather than read inline so that
247 // the trace below can print them: the exception code, the method's own `int` return value (which
248 // is what the service sets when it does not know the property) and the non-null marker of the out
249 // parameter. A marker of 0 means the value behind it was never written.
250 const int32_t exception = ffBinderReadI32(reply.data, reply.size, 0);
251 const int32_t returnValue = ffBinderReadI32(reply.data, reply.size, 4);
252 FF_DEBUG("Property %u: reply is %zu bytes, exception %d, return value %d, value present %d",
253 property, reply.size, exception, returnValue, ffBinderReadI32(reply.data, reply.size, 8));
254 if (exception != 0) {
255 return "Battery service raised an exception";
256 }
257 if (returnValue != 0) {
258 FF_DEBUG("Property %u is not one this HAL implements, so the words below are not a value:",
259 property);
260 FF_BATTERY_ANDROID_DEBUG_DUMP(reply.data, reply.size);
261 return "Battery service does not report this property";
262 }
263
264 const uint64_t result = ffBinderReadU64(reply.data, reply.size, FF_BATTERY_ANDROID_VALUE_OFFSET);
265 FF_DEBUG("Property %u is %" PRIu64 " (0x%" PRIx64 ") at +0x%02x",
266 property, result, result, FF_BATTERY_ANDROID_VALUE_OFFSET);
267 if (result == FF_BATTERY_ANDROID_VALUE_UNSET) {
268 FF_DEBUG("Property %u is Long.MIN_VALUE, which is what the service leaves behind when it never "
269 "fills the field in, so there is nothing to report:",
270 property);
271 FF_BATTERY_ANDROID_DEBUG_DUMP(reply.data, reply.size);
272 return "Battery service left this property empty";
273 }
274
275 *value = result;
276 return nullptr;
277}
278
279// Reads either of the two properties above as a number. Both are written by `Integer.toString`, so a
280// buffer whose text is not a number is the property saying nothing rather than a 0 -- 0 is a plug type,
281// and it is not a status.
282static bool readPropertyUInt(const char* name, uint32_t* value) {
283 FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
284 if (!ffSettingsGetAndroidProperty(name, &buffer)) {
285 FF_DEBUG("The \"%s\" property is not set, so nothing is read from it", name);
286 return false;
287 }
288
289 // `ffStrbufToUInt` answers the default for text that does not start with a digit, which is why the
290 // default is a value neither property can hold rather than a 0.
291 const uint64_t parsed = ffStrbufToUInt(&buffer, UINT64_MAX);
292 if (parsed == UINT64_MAX) {
293 FF_DEBUG("The \"%s\" property reads \"%s\", which is not a number", name, buffer.chars);
294 return false;
295 }
296
297 *value = (uint32_t) parsed;
298 return true;
299}
300
301// The charger type. This is what the property route has and the registrar does not: `getProperty`
302// answers nothing about the charger, so the three bits used to come from `dumpsys battery`, i.e. from
303// the shell UID and root alone. A device that does not publish the property leaves the bits clear,
304// which is also what an unplugged charger looks like -- the module cannot tell those apart, and neither
305// can the framework, whose sticky broadcast carries this same value as `EXTRA_PLUGGED`.
306static void fillPlugged(FFBatteryResult* battery) {
307 uint32_t plugged = 0;
308 if (!readPropertyUInt(FF_BATTERY_ANDROID_PROP_PLUG_TYPE, &plugged)) {
309 return;
310 }
311
312 // A mask rather than a comparison: the three are bits of one field, and it is the bits that have a
313 // `FF_BATTERY_STATUS_*` counterpart. BATTERY_PLUGGED_DOCK (8) is the one that does not, so a device
314 // on a dock reports no powered bit at all -- and `plugged != 0` would have called that "wired".
315 if (plugged & FF_BATTERY_ANDROID_PLUGGED_AC) {
316 battery->status |= FF_BATTERY_STATUS_AC_CONNECTED;
317 }
318 if (plugged & FF_BATTERY_ANDROID_PLUGGED_USB) {
319 battery->status |= FF_BATTERY_STATUS_USB_CONNECTED;
320 }
321 if (plugged & FF_BATTERY_ANDROID_PLUGGED_WIRELESS) {
322 battery->status |= FF_BATTERY_STATUS_WIRELESS_CONNECTED;
323 }
324
325 FF_DEBUG("The \"%s\" property is %u, which becomes status bits 0x%x (1 AC, 2 USB, 4 wireless; 0 is "
326 "what both an unplugged charger and a device that does not publish the property look like)",
327 FF_BATTERY_ANDROID_PROP_PLUG_TYPE, plugged, (unsigned) battery->status);
328}
329
330// The status, read the cheap way first. The property carries the same `mHealthInfo.batteryStatus` the
331// transaction would fetch, so the two can only differ inside the window between the health HAL reporting
332// a value and the service storing it, while what the property costs is a read of a shared page instead
333// of a round trip through /dev/binder -- a round trip measured at about 0.04 ms on the device this was
334// written on, and what it saves is a positional reply that has to be decoded as well.
335//
336// It stays a preference rather than a replacement: a property that is not set (an early boot, or a
337// build whose BatteryService does not publish it) or that does not carry one of the five values sends
338// the caller to the service, which is the one answer that cannot be stale.
339static bool detectStatusFromProperty(uint64_t* status) {
340 uint32_t value = 0;
341 if (!readPropertyUInt(FF_BATTERY_ANDROID_PROP_BATTERY_STATUS, &value)) {
342 return false;
343 }
344 if (value < FF_BATTERY_ANDROID_STATUS_UNKNOWN || value > FF_BATTERY_ANDROID_STATUS_FULL) {
345 FF_DEBUG("The \"%s\" property is %u, which is not a battery status, so the service is asked",
346 FF_BATTERY_ANDROID_PROP_BATTERY_STATUS, value);
347 return false;
348 }
349
350 *status = value;
351 return true;
352}
353
354// Calls a method that takes no argument and answers with one `long`, which is how
355// IBatteryStats.computeBatteryTimeRemaining() is declared. `transaction` comes out of the jar, and the
356// sentinel the reader leaves for a code it could not find is caught here, so that a build which does
357// not declare the method is a message rather than a transaction that means something else.
358//
359// A negative answer is not an error: it is how the interface spells "no estimate", so it is passed
360// back as it came and the caller decides what that is worth. The unit is milliseconds.
361static const char* getTimeRemaining(FFBinder* binder, uint32_t handle, int32_t transaction, int64_t* milliseconds) {
362 if (transaction == FF_DEX_STATIC_INT_UNRESOLVED) {
363 FF_DEBUG("computeBatteryTimeRemaining is unresolved in the jar (transaction %d)", (int) transaction);
364 return "The battery stats service does not declare computeBatteryTimeRemaining";
365 }
366
367 uint8_t parcelBuffer[128];
368 FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer));
369 ffBinderParcelPutInterfaceToken(&parcel, FF_BATTERY_ANDROID_STATS_DESCRIPTOR);
370
371 uint8_t replyBuffer[128];
372 FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer));
373 const char* error = ffBinderTransact(binder, handle, (uint32_t) transaction, 0, &parcel, &reply);
374 if (error != nullptr) {
375 FF_DEBUG("computeBatteryTimeRemaining could not be transacted: %s", error);
376 return error;
377 }
378 if (ffBinderReplyIsStatus(&reply)) {
379 FF_DEBUG("computeBatteryTimeRemaining came back as a status reply of %d instead of a parcel",
380 (int) ffBinderReadI32(reply.data, reply.size, 0));
381 return "The battery stats service rejected the request";
382 }
383
384 // The exception word is read first and the value only once it is known to be a value: a denied
385 // call answers [exception -1][string16 message], so the eight bytes at the value's offset are
386 // then the length and the first characters of the message rather than a number.
387 const int32_t exception = ffBinderReadI32(reply.data, reply.size, 0);
388 if (exception != 0) {
389 FF_DEBUG("computeBatteryTimeRemaining is transaction %d, reply %zu bytes, exception %d",
390 transaction, reply.size, exception);
391 return "The battery stats service raised an exception";
392 }
393
394 *milliseconds = (int64_t) ffBinderReadU64(reply.data, reply.size, FF_BATTERY_ANDROID_LONG_VALUE_OFFSET);
395 FF_DEBUG("computeBatteryTimeRemaining is transaction %d: reply is %zu bytes, value %" PRId64 " ms",
396 transaction, reply.size, *milliseconds);
397 return nullptr;
398}
399
400// Writes the time estimate onto the battery just reported. It is not one of the registrar's
401// properties -- it is a forecast BatteryStatsService makes -- so it comes from a second service and
402// is filled in here, after the capacity and the status are already in place.
403//
404// `binder` and `handle` are the caller's, already open on `batterystats`. Nothing here can fail the
405// module: an estimate that is not available is what `FFBatteryResult` already spells with -1.
406static void fillTimeRemaining(FFBinder* binder, uint32_t handle, int32_t transaction, FFlist* results) {
407 if (results->length == 0) {
408 FF_DEBUG("No time remaining is reported: no battery was reported to put it on");
409 return;
410 }
411
412 int64_t milliseconds = 0;
413 const char* error = getTimeRemaining(binder, handle, transaction, &milliseconds);
414 if (error != nullptr) {
415 FF_DEBUG("The time remaining could not be computed, so none is reported: %s", error);
416 return;
417 }
418 if (milliseconds <= 0) {
419 FF_DEBUG("The battery stats service answered %" PRId64 " ms, which is how it says it has no estimate",
420 milliseconds);
421 return;
422 }
423
424 // The interface answers in milliseconds and `FFBatteryResult` carries seconds, which is what the
425 // other platform modules fill in. An answer under a second truncates to 0, which the module reads
426 // as "unknown" as well, so there is nothing to guard against.
427 FF_LIST_GET(FFBatteryResult, *results, 0)->timeRemaining = (int32_t) (milliseconds / 1000);
428}
429
430// The battery temperature is neither a registrar property nor part of the health HAL: the kernel
431// publishes it as a thermal zone, and the zone Android's thermal HAL names `battery` is the one to
432// read. It is read from /sys/class/thermal, which an app UID may open -- the reason the module does
433// not simply read /sys/class/power_supply/battery/temp is that that directory is closed to it.
434//
435// The zone is *found*, not addressed. Its number is a property of the probe order rather than of the
436// hardware -- the battery is `thermal_zone67` on the device this was written on, and nothing promises
437// the next one -- so the directory is walked and each entry is matched on the `type` it reports.
438//
439// That match is an exact string, and has to stay one. A prefix would be cheaper and wrong: the same
440// directory holds hardware trip points (`cpu-hw-trip-0` reports 95000, a perfectly believable 95
441// degrees), current and battery levels (`pmih010x-ibat-lvl0`, 0 or a small integer) and raw registers
442// (`vbat`, in millivolts). None of those are a temperature, and every one of them would be printed as
443// one. Reading a different zone is not a smaller error than reading none.
444//
445// A device that names its battery zone something else therefore reports no temperature rather than a
446// wrong one, which is what FF_BATTERY_TEMP_UNSET already means.
447static double detectTemperature(void) {
448 FF_AUTO_CLOSE_DIR DIR* dirp = opendir(FF_BATTERY_ANDROID_THERMAL_DIR);
449 if (dirp == nullptr) {
450 FF_DEBUG("Opening \"%s\" failed, so no temperature is reported: %s", FF_BATTERY_ANDROID_THERMAL_DIR, strerror(errno));
451 return FF_BATTERY_TEMP_UNSET;
452 }
453
454 FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
455 struct dirent* entry;
456 while ((entry = readdir(dirp)) != nullptr) {
457 if (!ffStrStartsWith(entry->d_name, FF_BATTERY_ANDROID_THERMAL_ZONE_PREFIX)) {
458 continue;
459 }
460
461 FF_AUTO_CLOSE_FD int zone = openat(dirfd(dirp), entry->d_name, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
462 if (zone < 0) {
463 continue;
464 }
465
466 // The type file ends with a newline, which the comparison below would otherwise never survive.
467 if (!ffReadFileBufferRelative(zone, "type", &buffer)) {
468 continue;
469 }
470 ffStrbufTrimRightSpace(&buffer);
471 if (!ffStrbufEqualS(&buffer, FF_BATTERY_ANDROID_THERMAL_BATTERY_TYPE)) {
472 continue;
473 }
474
475 if (!ffReadFileBufferRelative(zone, "temp", &buffer)) {
476 FF_DEBUG("The battery zone \"%s\" has no readable temperature", entry->d_name);
477 return FF_BATTERY_TEMP_UNSET;
478 }
479
480 // sysfs thermal publishes millidegrees Celsius. That is the one thing every zone here agrees
481 // on, even the ones whose reading is not a temperature at all.
482 const double value = ffStrbufToDouble(&buffer, FF_BATTERY_TEMP_UNSET);
483 if (value == FF_BATTERY_TEMP_UNSET) {
484 FF_DEBUG("The battery zone \"%s\" reports \"%s\", which is not a number", entry->d_name, buffer.chars);
485 return FF_BATTERY_TEMP_UNSET;
486 }
487
488 FF_DEBUG("The battery zone is \"%s\", reporting %.2f degrees", entry->d_name, value / 1000.);
489 return value / 1000.;
490 }
491
492 FF_DEBUG("No zone in \"%s\" is named \"%s\", so no temperature is reported",
493 FF_BATTERY_ANDROID_THERMAL_DIR, FF_BATTERY_ANDROID_THERMAL_BATTERY_TYPE);
494 return FF_BATTERY_TEMP_UNSET;
495}
496
497// Temperature is opt-in on every platform, and on Android that is not only about the display: the
498// walk above touches a directory with a hundred-odd entries on the device this was written on, and
499// none of that is paid for unless `temp` asks for it.
500static void fillTemperature(FFBatteryOptions* options, FFlist* results) {
501 if (!options->temp) {
502 FF_DEBUG("Temperature is not enabled, so the thermal zones are not walked");
503 return;
504 }
505 if (results->length == 0) {
506 return;
507 }
508
509 const double temperature = detectTemperature();
510 if (temperature != FF_BATTERY_TEMP_UNSET) {
511 FF_LIST_GET(FFBatteryResult, *results, 0)->temperature = temperature;
512 }
513}
514
515const char* ffDetectBattery(FFBatteryOptions* options, FFlist* results) {
516 // Resolved before the binder is opened, so that a jar which does not declare the method says so
517 // rather than the module failing later with a message about the transport. Both codes are asked
518 // for in one call, which is cheaper than two: the reader walks the jar's dex entries in order and
519 // stops once every request has been answered, so a second request only costs the walk to the
520 // entry that defines the second class. That walk is not free here, because the two classes are
521 // not in the same entry -- `IBatteryPropertiesRegistrar$Stub` is in classes3.dex and
522 // `IBatteryStats$Stub` in classes5.dex -- and on the device this was measured on it added 1.3 ms
523 // to a module that had been 3.3 ms, while the service lookup and the transaction behind it stayed
524 // inside the noise. The reader reports a code it cannot find by leaving the sentinel in the
525 // result and says nothing more -- what a missing code means for this module is decided here, not
526 // there.
527 //
528 // The two are not equally important. `getProperty` is what the module is made of, so a build
529 // without it has nothing to report; `computeBatteryTimeRemaining` only adds the time estimate,
530 // and a build without it still has the capacity and the status, so its sentinel is carried on to
531 // where the estimate is asked for instead of failing the route here.
532 int32_t transaction = FF_DEX_STATIC_INT_UNRESOLVED;
533 int32_t timeRemainingTransaction = FF_DEX_STATIC_INT_UNRESOLVED;
534 const FFDexStaticIntRequest requests[] = {
535 { FF_BATTERY_ANDROID_STUB, FF_BATTERY_ANDROID_GET_PROPERTY, &transaction },
536 { FF_BATTERY_ANDROID_STATS_STUB, FF_BATTERY_ANDROID_COMPUTE_BATTERY_TIME_REMAINING, &timeRemainingTransaction },
537 };
538 const char* error = ffDexStaticInts(FF_BATTERY_ANDROID_JAR, requests, ARRAY_SIZE(requests));
539 if (error != nullptr) {
540 FF_DEBUG("Reading the transaction codes from \"%s\" failed: %s", FF_BATTERY_ANDROID_JAR, error);
541 return error;
542 }
543 if (transaction == FF_DEX_STATIC_INT_UNRESOLVED) {
544 FF_DEBUG("getProperty is unresolved in the jar (transaction %d)", (int) transaction);
545 return "The battery service does not declare getProperty";
546 }
547
548 [[gnu::cleanup(ffBinderClose)]] FFBinder binder = { .fd = -1 };
549 error = ffBinderOpen(&binder);
550 if (error != nullptr) {
551 FF_DEBUG("Opening /dev/binder failed: %s", error);
552 return error;
553 }
554
555 // Released on every path out of this function, including the ones below that return early.
556 [[gnu::cleanup(ffBinderServiceHandleRelease)]] FFBinderServiceHandle service = { .binder = &binder };
557 error = ffBinderLookupService(&binder, FF_BATTERY_ANDROID_SERVICE, FF_BINDER_SM_GET_SERVICE, &service.handle);
558 if (error != nullptr) {
559 FF_DEBUG("Looking up the \"%s\" service failed: %s", FF_BATTERY_ANDROID_SERVICE, error);
560 return error;
561 }
562 FF_DEBUG("The \"%s\" service is handle %u, getProperty is transaction %d", FF_BATTERY_ANDROID_SERVICE, service.handle, transaction);
563
564 uint64_t capacity = 0;
565 error = getProperty(&binder, service.handle, (uint32_t) transaction, FF_BATTERY_ANDROID_PROPERTY_CAPACITY, &capacity);
566 if (error != nullptr) {
567 FF_DEBUG("The capacity is what failed, so no battery is reported at all");
568 return error;
569 }
570
571 // The status is asked of the property first, because that is where the second transaction on the
572 // service is saved. The service is still asked when the property has nothing to say, and a failure
573 // there is still fatal: the capacity and the status are what this route is made of, and a battery
574 // whose status is unknown is not a battery this route can describe.
575 uint64_t status = 0;
576 if (detectStatusFromProperty(&status)) {
577 FF_DEBUG("The status came from the \"%s\" property and is %" PRIu64 ", so the service is not asked "
578 "for it",
579 FF_BATTERY_ANDROID_PROP_BATTERY_STATUS, status);
580 } else {
581 error = getProperty(&binder, service.handle, (uint32_t) transaction, FF_BATTERY_ANDROID_PROPERTY_STATUS, &status);
582 if (error != nullptr) {
583 FF_DEBUG("The status is what failed, so no battery is reported at all");
584 return error;
585 }
586 }
587
588 FFBatteryResult* battery = FF_LIST_ADD(FFBatteryResult, *results);
589 initResult(battery);
590 battery->capacity = (double) capacity;
591 if (status == FF_BATTERY_ANDROID_STATUS_CHARGING) {
592 battery->status |= FF_BATTERY_STATUS_CHARGING;
593 } else if (status == FF_BATTERY_ANDROID_STATUS_DISCHARGING) {
594 battery->status |= FF_BATTERY_STATUS_DISCHARGING;
595 }
596 // The charger type is the one reading this route has that the registrar does not, and it is a
597 // property as well: the three bits used to come from `dumpsys battery`, and so from the shell UID
598 // and root alone.
599 fillPlugged(battery);
600 // The temperature comes from outside this route entirely, so it is asked for here rather than
601 // inside it: nothing about the binder session above has any bearing on it, and a device without a
602 // zone by that name simply leaves the field at the sentinel.
603 fillTemperature(options, results);
604 // The time estimate is the one part of this route that is allowed to be missing: a jar without
605 // the code, a service that is not running and a service that answers -1 all leave the field at
606 // the -1 the module starts it with, which is how `FFBatteryResult` spells "unknown". It is asked
607 // for last for that reason -- a failure here cannot cost the capacity and the status that have
608 // already been read.
609 [[gnu::cleanup(ffBinderServiceHandleRelease)]] FFBinderServiceHandle stats = { .binder = &binder };
610 error = ffBinderLookupService(&binder, FF_BATTERY_ANDROID_STATS_SERVICE, FF_BINDER_SM_GET_SERVICE, &stats.handle);
611 if (error != nullptr) {
612 FF_DEBUG("Looking up the \"%s\" service failed, so no time remaining is reported: %s",
613 FF_BATTERY_ANDROID_STATS_SERVICE, error);
614 } else {
615 fillTimeRemaining(&binder, stats.handle, timeRemainingTransaction, results);
616 }
617
618 // `BatteryStatus` has five values and only two of them say something the powered bits do not, so
619 // the trace spells the mapping out: anything that is not 2 or 3 leaves the charging / discharging
620 // bits clear, which is what UNKNOWN (1), NOT_CHARGING (4) and FULL (5) all come to -- that a
621 // charger is attached is stated by the bits the plug_type property fills in, not by this one. The
622 // temperature is not repeated here: it was traced where it was read, and it is the field of the
623 // three that can be missing without anything being wrong.
624 FF_DEBUG("Reporting one battery: capacity %.0f%%, status %" PRIu64 " -> bits 0x%x "
625 "(2 charging, 3 discharging; 1/4/5 leave them clear), %d s remaining (-1 when there is no "
626 "estimate). The technology stays unset: it rides the sticky broadcast, which this route "
627 "does not read",
628 battery->capacity, status, (unsigned) battery->status, battery->timeRemaining);
629 return nullptr;
630}