master ohmu/fastfetch-openbsd / src / detection / battery / battery_android.c
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}