From 873a881dab5f1acabab2f8cbdab8cd4d8bde27f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Fri, 18 Sep 2026 17:22:51 +0800 Subject: [PATCH] Battery (Android): rewrites impl via `/dev/binder` --- CHANGELOG.md | 3 + CMakeLists.txt | 1 + src/common/android/binder.c | 237 ++++++++++++++++++++++++ src/common/android/binder.h | 224 ++++++++++++++++++++++ src/detection/battery/battery_android.c | 169 +++++++++++------ 5 files changed, 578 insertions(+), 56 deletions(-) create mode 100644 src/common/android/binder.c create mode 100644 src/common/android/binder.h diff --git a/CHANGELOG.md b/CHANGELOG.md index a85292783..713ee19ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,9 @@ Features: * This is mostly untested due to lack of available devices running these ROMs. Please report any issues you encounter. * Improved Camera detection on Android (Camera, Android) * The camera list is now read from the camera2 NDK instead of `termux-api CameraInfo`, so the Termux:API app is no longer required and no subprocess is spawned. +* Improved Battery detection on Android (Battery, Android) + * The charge level and charging state are now read from the battery properties service over `/dev/binder` instead of `termux-api BatteryStatus`, so the Termux:API app is no longer required and no subprocess is spawned. + * Battery temperature, cycle count, manufacturer, model name, serial number and manufacture date need the `BATTERY_STATS` permission, which an app cannot obtain, so they are no longer reported. * Improved Display detection on Android when fastfetch runs as an app rather than from `adb shell`, where `dumpsys display` is not permitted. (Display, Android) * The displays are now read through the shell command interface of the display service, which needs no permission. * Improved COSMIC detection (DE / WM, Linux) diff --git a/CMakeLists.txt b/CMakeLists.txt index fda52041c..018c7d2ba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -615,6 +615,7 @@ if(LINUX) ) elseif(ANDROID) list(APPEND LIBFASTFETCH_SRC + src/common/android/binder.c src/common/impl/dbus.c src/common/impl/io_unix.c src/common/impl/netif_linux.c diff --git a/src/common/android/binder.c b/src/common/android/binder.c new file mode 100644 index 000000000..6d07a9147 --- /dev/null +++ b/src/common/android/binder.c @@ -0,0 +1,237 @@ +#include "common/android/binder.h" + +#include +#include +#include + +const char* ffBinderOpen(FFBinder* binder) { + *binder = (FFBinder) { .fd = -1, .shared = nullptr, .sharedSize = 0, .protocolVersion = 0 }; + + const int fd = open(FF_BINDER_DEVICE, O_RDWR | O_CLOEXEC); + if (fd < 0) { + return "Failed to open " FF_BINDER_DEVICE; + } + + struct binder_version version = { .protocol_version = 0 }; + if (ioctl(fd, BINDER_VERSION, &version) != 0 || version.protocol_version != BINDER_CURRENT_PROTOCOL_VERSION) { + close(fd); + return "Unsupported binder protocol version"; + } + + // The region is read-only: the kernel writes reply payloads into it and hands out their + // addresses in BR_REPLY, so it has to stay mapped for as long as the handle is used. + void* shared = mmap(nullptr, FF_BINDER_SHARED_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, fd, 0); + if (shared == MAP_FAILED) { + close(fd); + return "Failed to mmap " FF_BINDER_DEVICE; + } + + *binder = (FFBinder) { + .fd = fd, + .shared = (uint8_t*) shared, + .sharedSize = FF_BINDER_SHARED_SIZE, + .protocolVersion = version.protocol_version, + }; + return nullptr; +} + +void ffBinderClose(FFBinder* binder) { + if (binder->shared != nullptr) { + munmap(binder->shared, binder->sharedSize); + } + if (binder->fd >= 0) { + close(binder->fd); + } + binder->fd = -1; + binder->shared = nullptr; + binder->sharedSize = 0; +} + +const char* ffBinderTransact(FFBinder* binder, uint32_t handle, uint32_t code, const FFBinderParcel* parcel, FFBinderReply* reply) { + reply->size = 0; + reply->code = 0; + reply->flags = 0; + reply->handleCount = 0; + + if (parcel->truncated) { + return "Binder parcel does not fit the caller buffer"; + } + + uint8_t writeBuffer[sizeof(uint32_t) + sizeof(struct binder_transaction_data)]; + const struct binder_transaction_data transaction = { + .target = { .handle = handle }, + .cookie = 0, + .code = code, + .flags = 0, // a synchronous call; TF_ONE_WAY is the only flag the kernel acts on here + .sender_pid = 0, + .sender_euid = 0, + .data_size = parcel->size, + .offsets_size = 0, + .data = { .ptr = { .buffer = (binder_uintptr_t) (uintptr_t) parcel->data, .offsets = 0 } }, + }; + const uint32_t transactionCommand = BC_TRANSACTION; + memcpy(writeBuffer, &transactionCommand, sizeof(uint32_t)); + memcpy(writeBuffer + sizeof(uint32_t), &transaction, sizeof(transaction)); + + // The read buffer only has to hold the command words and the binder_transaction_data that + // follows BR_REPLY; the payload itself lives in the mmap region. + uint8_t readBuffer[FF_BINDER_REPLY_BUFFER_SIZE]; + struct binder_write_read exchange = { + .write_size = sizeof(writeBuffer), + .write_consumed = 0, + .write_buffer = (binder_uintptr_t) (uintptr_t) writeBuffer, + .read_size = sizeof(readBuffer), + .read_consumed = 0, + .read_buffer = (binder_uintptr_t) (uintptr_t) readBuffer, + }; + if (ioctl(binder->fd, BINDER_WRITE_READ, &exchange) != 0) { + return "BINDER_WRITE_READ failed"; + } + + const uint8_t* cursor = readBuffer; + const uint8_t* end = readBuffer + (size_t) exchange.read_consumed; + while ((size_t) (end - cursor) >= sizeof(uint32_t)) { + uint32_t command = 0; + memcpy(&command, cursor, sizeof(uint32_t)); + cursor += sizeof(uint32_t); + + switch (command) { + case BR_NOOP: + case BR_TRANSACTION_COMPLETE: + case BR_SPAWN_LOOPER: + break; + + case BR_FAILED_REPLY: + return "Binder transaction failed"; + case BR_DEAD_REPLY: + return "Binder service is not running"; + + case BR_INCREFS: + case BR_ACQUIRE: + case BR_RELEASE: + case BR_DECREFS: + cursor += sizeof(struct binder_ptr_cookie); + break; + case BR_DEAD_BINDER: + case BR_CLEAR_DEATH_NOTIFICATION_DONE: + cursor += sizeof(binder_uintptr_t); + break; + case BR_ERROR: + cursor += sizeof(int32_t); + break; + + case BR_REPLY: { + if ((size_t) (end - cursor) < sizeof(struct binder_transaction_data)) { + return "Truncated binder reply"; + } + struct binder_transaction_data data; + memcpy(&data, cursor, sizeof(data)); + cursor += sizeof(data); + + reply->code = data.code; + reply->flags = data.flags; + + const uint8_t* payload = (const uint8_t*) (uintptr_t) data.data.ptr.buffer; + const size_t payloadSize = (size_t) data.data_size; + const uint8_t* offsets = (const uint8_t*) (uintptr_t) data.data.ptr.offsets; + const size_t offsetsSize = (size_t) data.offsets_size; + + const char* error = nullptr; + if (payloadSize > 0) { + if (payloadSize > reply->capacity) { + error = "Binder reply does not fit the caller buffer"; + } else { + memcpy(reply->data, payload, payloadSize); + reply->size = payloadSize; + } + } + + // Walk the offset table for the flat objects the service sent us and keep the + // strong handles: those are the ones a later transaction can address. + for (size_t offset = 0; offset + sizeof(binder_size_t) <= offsetsSize; offset += sizeof(binder_size_t)) { + binder_size_t objectOffset = 0; + memcpy(&objectOffset, offsets + offset, sizeof(objectOffset)); + if (objectOffset + sizeof(struct flat_binder_object) > payloadSize) { + continue; + } + struct flat_binder_object object; + memcpy(&object, payload + objectOffset, sizeof(object)); + if (object.hdr.type != BINDER_TYPE_HANDLE) { + continue; + } + if (reply->handleCount < FF_BINDER_MAX_HANDLES) { + reply->handles[reply->handleCount++] = object.handle; + } + } + + // The kernel gave us a new reference whose only strong reference belongs to the + // reply buffer, so it has to be acquired before that buffer is freed -- otherwise + // the handle silently degrades to a weak reference and every later transaction on + // it fails with BR_FAILED_REPLY. libbinder does the same in acquire_object(). + uint8_t tail[FF_BINDER_MAX_HANDLES * 4 * sizeof(uint32_t) + sizeof(uint32_t) + sizeof(binder_uintptr_t)]; + size_t tailSize = 0; + for (uint32_t i = 0; i < reply->handleCount; i++) { + const uint32_t acquire = BC_ACQUIRE; + const uint32_t increfs = BC_INCREFS; + memcpy(tail + tailSize, &acquire, sizeof(uint32_t)); + tailSize += sizeof(uint32_t); + memcpy(tail + tailSize, &reply->handles[i], sizeof(uint32_t)); + tailSize += sizeof(uint32_t); + memcpy(tail + tailSize, &increfs, sizeof(uint32_t)); + tailSize += sizeof(uint32_t); + memcpy(tail + tailSize, &reply->handles[i], sizeof(uint32_t)); + tailSize += sizeof(uint32_t); + } + const uint32_t freeCommand = BC_FREE_BUFFER; + const binder_uintptr_t freeBuffer = data.data.ptr.buffer; + memcpy(tail + tailSize, &freeCommand, sizeof(uint32_t)); + tailSize += sizeof(uint32_t); + memcpy(tail + tailSize, &freeBuffer, sizeof(freeBuffer)); + tailSize += sizeof(freeBuffer); + + struct binder_write_read release = { + .write_size = tailSize, + .write_consumed = 0, + .write_buffer = (binder_uintptr_t) (uintptr_t) tail, + .read_size = 0, + .read_consumed = 0, + .read_buffer = 0, + }; + ioctl(binder->fd, BINDER_WRITE_READ, &release); + + return error; + } + + default: + return "Unexpected binder command"; + } + } + + return "No reply from binder"; +} + +const char* ffBinderLookupService(FFBinder* binder, const char* name, uint32_t transactionCode, uint32_t* handle) { + uint8_t parcelBuffer[256]; + FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer)); + ffBinderParcelPutInterfaceToken(&parcel, FF_BINDER_SM_DESCRIPTOR); + ffBinderParcelPutString16(&parcel, name); + + uint8_t replyBuffer[256]; + FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer)); + const char* error = ffBinderTransact(binder, FF_BINDER_SERVICE_MANAGER_HANDLE, transactionCode, &parcel, &reply); + if (error != nullptr) { + return error; + } + if (ffBinderReplyIsStatus(&reply)) { + return "Service manager rejected the request"; + } + if (ffBinderReadI32(reply.data, reply.size, 0) != 0) { + return "Service manager raised an exception"; + } + if (reply.handleCount == 0) { + return "Service is not registered"; + } + + *handle = reply.handles[0]; + return nullptr; +} diff --git a/src/common/android/binder.h b/src/common/android/binder.h new file mode 100644 index 000000000..5ad0e1642 --- /dev/null +++ b/src/common/android/binder.h @@ -0,0 +1,224 @@ +#pragma once + +// Android exposes no public C API for the system services that carry the data fastfetch wants: the +// NDK has no BatteryManager / WifiManager, libbinder_ndk only speaks to AIDL HALs (it refuses to +// prepare a transaction for a binder that has no NDK class), and `/system/bin/service call` costs a +// fork/exec worth 15-20 ms. What is left is /dev/binder itself, which is all that `service` uses +// underneath. This header speaks that protocol directly: open() + mmap() + BINDER_WRITE_READ, no +// extra shared library, no child process, ~0.13 ms per call once a handle is cached. +// +// Three details are easy to get wrong. All three were confirmed against the device's own +// libbinder.so and with a ptrace trace of `service`, and getting any of them wrong makes the +// service answer BAD_TYPE (0x80000001) or BR_FAILED_REPLY (0x7211): +// +// * A request parcel must start with three int32 values -- strict mode policy, work source and +// 'SYST'. Parcel::markForBinder() writes nothing for kernel binder, but the receiving side's +// Parcel::enforceInterface() still compares the third word against its own header. +// * Parcel::writeString16() writes the length, the UTF-16 code units, a UTF-16 NUL terminator and +// then pads to four bytes. Skipping the terminator shifts every following field by four bytes. +// * A handle arriving in a reply is owned by that reply buffer. It has to be acquired +// (BC_ACQUIRE + BC_INCREFS) before BC_FREE_BUFFER; otherwise only a weak reference survives and +// every later transaction on it fails, because the kernel looks up a strong reference. +// +// See .workbuddy-ai/android-binder-raw-client.md for the full write-up and the tooling. + +#include "fastfetch.h" // IWYU pragma: keep + +// The structures and command words below are not defined here: bionic generates this header from +// bionic/libc/kernel/uapi/linux/android/binder.h and every NDK ships it, so it is the +// authoritative copy of the binder ABI -- struct binder_write_read, struct binder_transaction_data, +// struct flat_binder_object, BINDER_WRITE_READ / BINDER_VERSION, the BC_* and BR_* command words, +// TF_* and BINDER_TYPE_*. Using it beats transcribing it: a second copy can only be wrong, and if +// the header itself is wrong that is the NDK's problem to fix, not something fastfetch could have +// caught by keeping its own. +#include + +#include +#include + +#define FF_BINDER_DEVICE "/dev/binder" + +// The kernel allocates reply payloads out of the region handed to mmap(); 1 MiB is what libbinder +// asks for. Replies themselves are small -- `listServices` on this device is the largest at a few +// hundred bytes -- but the region also carries the per-process buffer pool. +#define FF_BINDER_SHARED_SIZE (1024 * 1024) + +// A command word plus the binder_transaction_data that follows it. The payload never lands here: +// it lives in the mmap region, and BR_REPLY only hands over a pointer to it. +#define FF_BINDER_REPLY_BUFFER_SIZE 1024 + +// How many handles a reply may transfer to us. Replies that carry more than this still succeed, but +// the surplus handles are left unacquired and will fail on first use. +#define FF_BINDER_MAX_HANDLES 4 + +// The three int32 values Parcel::writeInterfaceToken() puts in front of the descriptor, and that +// Parcel::enforceInterface() checks the last of. These come from libbinder rather than from the +// kernel, which is why they are spelled out here. +#define FF_BINDER_STRICT_MODE_PENALTY_GATHER 0x80000000u // (1 << 31), Parcel.cpp +#define FF_BINDER_UNSET_WORK_SOURCE 0xffffffffu // IPCThreadState::kUnsetWorkSource +#define FF_BINDER_HEADER 0x53595354u // B_PACK_CHARS('S', 'Y', 'S', 'T') + +typedef struct FFBinder { + int fd; + uint8_t* shared; + size_t sharedSize; + int32_t protocolVersion; +} FFBinder; + +// Opens /dev/binder, checks the protocol version and maps the shared region. Returns nullptr on +// success, a static message otherwise; the device is left closed on failure. +[[gnu::nonnull(1), nodiscard]] const char* ffBinderOpen(FFBinder* binder); + +// Usable as a cleanup attribute. Safe on a zeroed or already closed FFBinder. +[[gnu::nonnull(1)]] void ffBinderClose(FFBinder* binder); + +// --------------------------------------------------------------------------------------------- +// Parcel writing. The caller owns the buffer; anything that does not fit is dropped and recorded +// in `truncated`, which ffBinderTransact() then reports as an error. +// --------------------------------------------------------------------------------------------- + +typedef struct FFBinderParcel { + uint8_t* data; + size_t capacity; + size_t size; + bool truncated; +} FFBinderParcel; + +[[nodiscard]] static inline FFBinderParcel ffBinderParcelCreate(uint8_t* data, size_t capacity) { + return (FFBinderParcel) { .data = data, .capacity = capacity, .size = 0, .truncated = false }; +} + +[[gnu::nonnull(1), nodiscard]] static inline uint8_t* ffBinderParcelReserve(FFBinderParcel* parcel, size_t bytes) { + if (bytes > parcel->capacity - parcel->size) { + parcel->truncated = true; + return nullptr; + } + uint8_t* result = parcel->data + parcel->size; + parcel->size += bytes; + return result; +} + +[[gnu::nonnull(1)]] static inline void ffBinderParcelPutU32(FFBinderParcel* parcel, uint32_t value) { + uint8_t* dst = ffBinderParcelReserve(parcel, sizeof(uint32_t)); + if (dst != nullptr) { + memcpy(dst, &value, sizeof(uint32_t)); + } +} + +[[gnu::nonnull(1)]] static inline void ffBinderParcelPutI32(FFBinderParcel* parcel, int32_t value) { + uint8_t* dst = ffBinderParcelReserve(parcel, sizeof(int32_t)); + if (dst != nullptr) { + memcpy(dst, &value, sizeof(int32_t)); + } +} + +[[gnu::nonnull(1)]] static inline void ffBinderParcelPutU64(FFBinderParcel* parcel, uint64_t value) { + uint8_t* dst = ffBinderParcelReserve(parcel, sizeof(uint64_t)); + if (dst != nullptr) { + memcpy(dst, &value, sizeof(uint64_t)); + } +} + +// AIDL string16. The descriptors and service names fastfetch asks for are ASCII, so one byte is one +// UTF-16 code unit; anything else would have to be encoded properly here. +[[gnu::nonnull(1, 2)]] static inline void ffBinderParcelPutString16(FFBinderParcel* parcel, const char* value) { + const size_t length = strlen(value); + const size_t payload = sizeof(uint32_t) + (length + 1) * 2; // length, code units, NUL terminator + const size_t padded = (payload + 3) & ~(size_t) 3; + uint8_t* dst = ffBinderParcelReserve(parcel, padded); + if (dst == nullptr) { + return; + } + + memset(dst, 0, padded); + const uint32_t length32 = (uint32_t) length; + memcpy(dst, &length32, sizeof(uint32_t)); + for (size_t i = 0; i < length; i++) { + dst[sizeof(uint32_t) + i * 2] = (uint8_t) value[i]; + } + // The two bytes after the last code unit stay zero: that is the UTF-16 NUL terminator, and + // Parcel::readInplace() advances by (length + 1) * sizeof(char16_t) to account for it. +} + +[[gnu::nonnull(1, 2)]] static inline void ffBinderParcelPutInterfaceToken(FFBinderParcel* parcel, const char* descriptor) { + ffBinderParcelPutU32(parcel, FF_BINDER_STRICT_MODE_PENALTY_GATHER); + ffBinderParcelPutU32(parcel, FF_BINDER_UNSET_WORK_SOURCE); + ffBinderParcelPutU32(parcel, FF_BINDER_HEADER); + ffBinderParcelPutString16(parcel, descriptor); +} + +// --------------------------------------------------------------------------------------------- +// Transactions +// --------------------------------------------------------------------------------------------- + +typedef struct FFBinderReply { + uint8_t* data; // caller owned; receives the payload + size_t capacity; // ditto + size_t size; // payload length in bytes + uint32_t code; // transaction code the service echoed back + uint32_t flags; // TF_* of the reply + uint32_t handleCount; + uint32_t handles[FF_BINDER_MAX_HANDLES]; +} FFBinderReply; + +[[nodiscard]] static inline FFBinderReply ffBinderReplyCreate(uint8_t* data, size_t capacity) { + return (FFBinderReply) { .data = data, .capacity = capacity, .size = 0, .code = 0, .flags = 0, .handleCount = 0, .handles = {} }; +} + +// A reply carrying TF_STATUS_CODE is not an AIDL parcel at all: the first four bytes are the raw +// status_t that onTransact() returned (-74 being UNKNOWN_TRANSACTION, i.e. no such method). Check +// this before interpreting anything else. +[[gnu::nonnull(1), nodiscard]] static inline bool ffBinderReplyIsStatus(const FFBinderReply* reply) { + return (reply->flags & TF_STATUS_CODE) != 0; +} + +[[gnu::nonnull(1), nodiscard]] static inline uint32_t ffBinderReadU32(const uint8_t* data, size_t size, size_t offset) { + uint32_t value = 0; + if (offset + sizeof(uint32_t) <= size) { + memcpy(&value, data + offset, sizeof(uint32_t)); + } + return value; +} + +[[gnu::nonnull(1), nodiscard]] static inline int32_t ffBinderReadI32(const uint8_t* data, size_t size, size_t offset) { + int32_t value = 0; + if (offset + sizeof(int32_t) <= size) { + memcpy(&value, data + offset, sizeof(int32_t)); + } + return value; +} + +[[gnu::nonnull(1), nodiscard]] static inline uint64_t ffBinderReadU64(const uint8_t* data, size_t size, size_t offset) { + uint64_t value = 0; + if (offset + sizeof(uint64_t) <= size) { + memcpy(&value, data + offset, sizeof(uint64_t)); + } + return value; +} + +// Synchronous transaction. Returns nullptr on success, a static message otherwise. Handles the +// reply carries are acquired and added to `reply->handles` before the reply buffer is freed. +// +// `handle` 0 is the service manager; every other handle comes from ffBinderLookupService(). A +// service that never replies blocks this call forever -- none of the services fastfetch reads does, +// but there is no timeout here to fall back on. +[[gnu::nonnull(1, 4, 5), nodiscard]] const char* ffBinderTransact(FFBinder* binder, uint32_t handle, uint32_t code, const FFBinderParcel* parcel, FFBinderReply* reply); + +// --------------------------------------------------------------------------------------------- +// Service manager +// --------------------------------------------------------------------------------------------- + +#define FF_BINDER_SERVICE_MANAGER_HANDLE 0u +#define FF_BINDER_SM_DESCRIPTOR "android.os.IServiceManager" + +// Entry points of android.os.IServiceManager, as this device answers them. The numbers do not match +// the AOSP sources: checkService is 4 here, not 3. FF_BINDER_SM_GET_SERVICE returns a nullable +// IBinder and is the simplest reply to parse; FF_BINDER_SM_CHECK_SERVICE is what `service` itself +// calls and answers with a parcelable instead. +#define FF_BINDER_SM_GET_SERVICE 1u +#define FF_BINDER_SM_GET_SERVICE2 2u +#define FF_BINDER_SM_CHECK_SERVICE 4u +#define FF_BINDER_SM_LIST_SERVICES 6u + +// Resolves a service name to a handle through the service manager. Returns nullptr on success. +[[gnu::nonnull(1, 2, 4), nodiscard]] const char* ffBinderLookupService(FFBinder* binder, const char* name, uint32_t transactionCode, uint32_t* handle); diff --git a/src/detection/battery/battery_android.c b/src/detection/battery/battery_android.c index 325be96cb..37e938ef2 100644 --- a/src/detection/battery/battery_android.c +++ b/src/detection/battery/battery_android.c @@ -1,71 +1,133 @@ #include "fastfetch.h" #include "battery.h" -#include "common/strutil.h" +#include "common/androidApi.h" +#include "common/android/binder.h" #include "common/processing.h" #include "common/properties.h" -#define FF_TERMUX_API_PATH FASTFETCH_TARGET_DIR_ROOT "/libexec/termux-api" -#define FF_TERMUX_API_PARAM "BatteryStatus" +// Android exposes the battery through BatteryManager, which the NDK does not wrap and whose sysfs +// backing is unreadable from an app UID. What does work without any permission is +// android.os.IBatteryPropertiesRegistrar in system_server, reachable either over /dev/binder (see +// common/android/binder.h) or through a `dumpsys battery` subprocess. The binder route is the cheap +// one: ~0.3 ms for both properties, and no child process. +// +// The registrar only answers properties that do not need BATTERY_STATS, and that rules out +// everything the module would otherwise like to show. Manufacturer, model name, technology, serial +// number, manufacture date, temperature and cycle count all sit behind that permission. -static inline void wrapYyjsonFree(yyjson_doc** doc) { - assert(doc); - if (*doc) { - yyjson_doc_free(*doc); - } -} +// The transaction code and the reply layout are both positional, and neither is negotiated with the +// service, so each only stays correct as long as AOSP keeps the order it already has. The code has +// not: `IBatteryPropertiesRegistrar` still declared registerListener / unregisterListener / +// getProperty up to Android 9, and Android 10 dropped the two listener methods, which moved +// `getProperty` from the third position to the first. The layout has, because `BatteryProperty` has +// only ever grown at the end -- API 35 appended [string8 mValueString] to it -- so the field read +// below is still the first one written. +// +// Getting either wrong is quiet rather than loud: `ffBinderReadU64` returns 0 for a read past the +// end of the reply, so a drift shows up as a wrong capacity, not as a failed call. +#define FF_BATTERY_ANDROID_SERVICE "batteryproperties" +#define FF_BATTERY_ANDROID_DESCRIPTOR "android.os.IBatteryPropertiesRegistrar" -static const char* parseTermuxApi(FFBatteryOptions* options, FFlist* results) { - FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); +// BatteryManager.BATTERY_PROPERTY_* +#define FF_BATTERY_ANDROID_PROPERTY_CAPACITY 4u +#define FF_BATTERY_ANDROID_PROPERTY_STATUS 6u - if (ffProcessAppendStdOut(&buffer, (char* const[]) { FF_TERMUX_API_PATH, FF_TERMUX_API_PARAM, nullptr })) { - return "Starting `" FF_TERMUX_API_PATH " " FF_TERMUX_API_PARAM "` failed"; - } +// BatteryManager.BATTERY_STATUS_* +#define FF_BATTERY_ANDROID_STATUS_CHARGING 2u +#define FF_BATTERY_ANDROID_STATUS_DISCHARGING 3u - [[gnu::cleanup(wrapYyjsonFree)]] yyjson_doc* doc = yyjson_read_opts(buffer.chars, buffer.length, 0, nullptr, nullptr); - if (!doc) { - return "Failed to parse battery info"; - } +// BatteryProperty starts with [int64 mValueLong], behind the usual [exception code][return value] +// [out-param non-null marker] prefix of an AIDL reply. API 35 appended [string8 mValueString] after +// the long for the one string valued property (`BATTERY_PROPERTY_SERIAL_NUMBER`), which does not +// move the long, so one offset covers every release. +#define FF_BATTERY_ANDROID_VALUE_OFFSET 12 +#define FF_BATTERY_ANDROID_VALUE_UNSET 0x8000000000000000ULL // Long.MIN_VALUE, i.e. never filled in - yyjson_val* root = yyjson_doc_get_root(doc); - if (!yyjson_is_obj(root)) { - return "Battery info result is not a JSON object"; - } - - FFBatteryResult* battery = FF_LIST_ADD(FFBatteryResult, *results); +static void initResult(FFBatteryResult* battery) { battery->temperature = FF_BATTERY_TEMP_UNSET; battery->cycleCount = 0; battery->timeRemaining = -1; + battery->capacity = 0; battery->status = FF_BATTERY_STATUS_NONE; ffStrbufInit(&battery->manufacturer); ffStrbufInit(&battery->modelName); ffStrbufInit(&battery->technology); ffStrbufInit(&battery->serial); ffStrbufInit(&battery->manufactureDate); +} - battery->capacity = yyjson_get_num(yyjson_obj_get(root, "percentage")); - const char* acStatus = yyjson_get_str(yyjson_obj_get(root, "plugged")); - if (acStatus) { - if (ffStrEquals(acStatus, "PLUGGED_AC")) { - battery->status |= FF_BATTERY_STATUS_AC_CONNECTED; - } else if (ffStrEquals(acStatus, "PLUGGED_USB")) { - battery->status |= FF_BATTERY_STATUS_USB_CONNECTED; - } else if (ffStrEquals(acStatus, "PLUGGED_WIRELESS")) { - battery->status |= FF_BATTERY_STATUS_WIRELESS_CONNECTED; - } - } - const char* status = yyjson_get_str(yyjson_obj_get(root, "status")); - if (status) { - if (ffStrEquals(status, "CHARGING")) { - battery->status |= FF_BATTERY_STATUS_CHARGING; - } else if (ffStrEquals(status, "DISCHARGING")) { - battery->status |= FF_BATTERY_STATUS_DISCHARGING; - } +static const char* getProperty(FFBinder* binder, uint32_t handle, uint32_t property, uint64_t* value) { + // `getProperty` is the third method declared in `IBatteryPropertiesRegistrar` up to Android 9 and + // the first one from Android 10 on. Written as an `if` because clang rejects `__builtin_available` + // -- which is what FF_API_AT_LEAST expands to -- in any other position. + uint32_t transaction = 3u; + if (FF_API_AT_LEAST(29)) { + transaction = 1u; } - if (options->temp) { - battery->temperature = yyjson_get_num(yyjson_obj_get(root, "temperature")); + uint8_t parcelBuffer[128]; + FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer)); + ffBinderParcelPutInterfaceToken(&parcel, FF_BATTERY_ANDROID_DESCRIPTOR); + ffBinderParcelPutU32(&parcel, property); + + uint8_t replyBuffer[128]; + FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer)); + const char* error = ffBinderTransact(binder, handle, transaction, &parcel, &reply); + if (error != nullptr) { + return error; + } + if (ffBinderReplyIsStatus(&reply)) { + return "Battery service rejected the request"; + } + if (ffBinderReadI32(reply.data, reply.size, 0) != 0) { + return "Battery service raised an exception"; + } + if (ffBinderReadI32(reply.data, reply.size, 4) != 0) { + return "Battery service does not report this property"; } + const uint64_t result = ffBinderReadU64(reply.data, reply.size, FF_BATTERY_ANDROID_VALUE_OFFSET); + if (result == FF_BATTERY_ANDROID_VALUE_UNSET) { + return "Battery service left this property empty"; + } + + *value = result; + return nullptr; +} + +static const char* parseBinder(FFlist* results) { + [[gnu::cleanup(ffBinderClose)]] FFBinder binder = { .fd = -1 }; + const char* error = ffBinderOpen(&binder); + if (error != nullptr) { + return error; + } + + uint32_t handle = 0; + error = ffBinderLookupService(&binder, FF_BATTERY_ANDROID_SERVICE, FF_BINDER_SM_GET_SERVICE, &handle); + if (error != nullptr) { + return error; + } + + uint64_t capacity = 0; + error = getProperty(&binder, handle, FF_BATTERY_ANDROID_PROPERTY_CAPACITY, &capacity); + if (error != nullptr) { + return error; + } + + uint64_t status = 0; + error = getProperty(&binder, handle, FF_BATTERY_ANDROID_PROPERTY_STATUS, &status); + if (error != nullptr) { + return error; + } + + FFBatteryResult* battery = FF_LIST_ADD(FFBatteryResult, *results); + initResult(battery); + battery->capacity = (double) capacity; + if (status == FF_BATTERY_ANDROID_STATUS_CHARGING) { + battery->status |= FF_BATTERY_STATUS_CHARGING; + } else if (status == FF_BATTERY_ANDROID_STATUS_DISCHARGING) { + battery->status |= FF_BATTERY_STATUS_DISCHARGING; + } return nullptr; } @@ -93,16 +155,7 @@ static const char* parseDumpsys(FFBatteryOptions* options, FFlist* results) { ffStrbufClear(&temp); FFBatteryResult* battery = FF_LIST_ADD(FFBatteryResult, *results); - battery->temperature = FF_BATTERY_TEMP_UNSET; - battery->cycleCount = 0; - battery->timeRemaining = -1; - battery->capacity = 0; - battery->status = FF_BATTERY_STATUS_NONE; - ffStrbufInit(&battery->manufacturer); - ffStrbufInit(&battery->modelName); - ffStrbufInit(&battery->technology); - ffStrbufInit(&battery->serial); - ffStrbufInit(&battery->manufactureDate); + initResult(battery); if (ffParsePropLines(start, "AC powered: ", &temp) && ffStrbufEqualS(&temp, "true")) { battery->status |= FF_BATTERY_STATUS_AC_CONNECTED; @@ -152,8 +205,12 @@ static const char* parseDumpsys(FFBatteryOptions* options, FFlist* results) { } const char* ffDetectBattery(FFBatteryOptions* options, FFlist* results) { - const char* error = parseTermuxApi(options, results); - if (error && parseDumpsys(options, results) == nullptr) { + // The binder route needs no permission and costs ~0.3 ms. `dumpsys battery` is kept as a + // fallback for `adb shell` and rooted environments, where the binder route also works but the + // extra fields it reports are worth having. termux-api was dropped: it returns nothing on this + // device and can hang for minutes, the same reason its camera path was removed. + const char* error = parseBinder(results); + if (error != nullptr && parseDumpsys(options, results) == nullptr) { return nullptr; } return error;