Display (Android): detects via binder

This commit is contained in:
Carter Li
2026-10-10 22:57:48 +08:00
parent cc435d2b70
commit 7f9faeb720
4 changed files with 2078 additions and 161 deletions
+3
View File
@@ -12,6 +12,9 @@ Changes:
Features:
* Added Sound detection on Android. (Sound, Android)
* Added display detection on Android over binder, which needs neither a child process nor a permission.
* The module took 16.0-26.5 ms on the test device and now takes 1.3-1.5 ms. The `cmd` and `dumpsys` routes are unchanged and remain the fallback.
* It is also the only route that answers for an app UID on Android 12 and older, where `cmd display get-displays` does not exist and `dumpsys` is behind `android.permission.DUMP`. (Display, Android)
* Added battery temperature, remaining time and charger type (AC / USB / wireless) detection on Android. (Battery, Android)
* Performance improvements & internal cleanups
* Added Finnish language support. (General)
+777 -156
View File
File diff suppressed because it is too large Load Diff
+99
View File
@@ -62,3 +62,102 @@ typedef struct FFDexStaticIntRequest {
// caller cannot read out of the results: the jar itself, which is reported when it cannot be opened
// or does not hold a dex that can be read. nullptr means the lookup ran.
[[gnu::nonnull(1, 2), nodiscard]] const char* ffDexStaticInts(const char* jarPath, const FFDexStaticIntRequest* requests, uint32_t count);
// ---------------------------------------------------------------------------------------------
// Reading a Parcelable's wire layout
// ---------------------------------------------------------------------------------------------
//
// A Parcelable's `writeToParcel` puts its fields on the wire in the order the method reads them, and
// that order is the only thing that says where each one is. It is not in the class's declaration: a
// dex stores a class's instance fields sorted by field index, which is sorted by *name*, so the
// declaration order a reader would want is not recorded anywhere but the method body.
//
// Recovering it therefore means walking the bytecode. The compiler emits
//
// iget v0, p0, LDisplayInfo;->logicalWidth:I
// invoke-virtual {p1, v0}, Landroid/os/Parcel;->writeInt(I)V
//
// once per field, in write order, so the `iget` opcodes in a linear pass over the instruction grid
// are the sequence. Two things have to be got right for that pass to stay on the grid, and both are
// easy to get wrong in a way that still produces a plausible-looking list:
//
// * Instructions are 1, 2, 3, 4 or 5 code units wide, and the width is a property of the opcode.
// The table below is ART's `Instruction::SizeInCodeUnits`.
// * The payload pseudo-instructions a `switch` or a `fill-array-data` leaves in the stream have a
// low byte of zero and a high byte of 1, 2 or 3. They are not instructions, they sit behind every
// reachable instruction, and stepping into one desynchronises the grid -- so the walk stops at the
// first of them rather than trying to step over it.
//
// An array field is read twice: once for its `length`, which is the count that goes on the wire, and
// again inside the loop that writes the elements. A class cannot declare two instance fields under
// one name, so a repeat is always that reload, and the first occurrence is where the field is
// written. The reader de-duplicates by name for that reason.
//
// What is *not* recovered is how each field is framed -- `writeString` against `writeString8`,
// `writeParcelable` against `writeTypedObject`. That is a property of the method the field is handed
// to, not of the field, and two `String` fields of the same class can differ: `DisplayInfo.name` goes
// through `writeString8` while `DeviceProductInfo.mName` goes through `writeString`. Telling them
// apart would mean resolving every invoke target, so the type descriptor is reported instead and the
// caller frames the fields its descriptor settles -- an `I`, a `J`, an `[F` -- and names the few it
// does not. Measured over three releases' own `framework.jar`, that is four names per class.
//
// The point of asking the device rather than carrying a table is that the field set moves: `DisplayInfo`
// has 34 instance fields on Android 11, 42 on 13 and 59 on 16, and the fields that arrived in between
// (`displayGroupId`, `renderFrameRate`, `appsSupportedModes`, `committedState`, ...) are exactly the
// ones a parser written against one release reads out of the middle of another. Reading the sequence
// from the build that is running makes every one of them optional by construction.
// A field name and a type descriptor are ASCII and short -- the longest name in `DisplayInfo` is 28
// bytes and the longest descriptor `Landroid/hardware/display/DeviceProductInfo$ManufactureDate;` is
// 60. A longer one is a class this reader was not written for, and is reported as unresolved rather
// than truncated: half a name would silently match nothing, which is indistinguishable from the field
// not being there.
#define FF_DEX_PARCEL_FIELD_NAME_MAX 40
#define FF_DEX_PARCEL_FIELD_TYPE_MAX 72
// One field of the write sequence. `type` is the dex type descriptor, e.g. "I", "[F",
// "Ljava/lang/String;" or "Landroid/view/DisplayAddress;".
typedef struct FFDexParcelField {
char name[FF_DEX_PARCEL_FIELD_NAME_MAX];
char type[FF_DEX_PARCEL_FIELD_TYPE_MAX];
} FFDexParcelField;
// One `<classDescriptor>.<methodName>` write sequence, and where it is written.
typedef struct FFDexParcelRequest {
const char* classDescriptor;
const char* methodName;
FFDexParcelField* fields;
uint32_t capacity;
uint32_t* count; // how many fields the sequence holds, or 0 when the jar cannot answer
} FFDexParcelRequest;
// Resolves the write sequence of every request out of the dex entries of `jarPath`, walking
// `classes.dex`, `classes2.dex`, ... in order until one of them defines the class -- the same walk,
// and the same one pass over the type table, that ffDexStaticInts makes.
//
// Every request is written a count or 0. A class the jar does not declare, a method it does not
// declare, and a sequence longer than the caller's `capacity` all leave 0 behind, and none of them
// holds back the others: they are independent questions about the same pass. A count of 0 is not a
// failure but an answer -- this build does not write that class, so a caller reading it has nothing
// to walk. The return value is reserved for the jar itself, as in ffDexStaticInts.
[[gnu::nonnull(1, 2), nodiscard]] const char* ffDexParcelFields(const char* jarPath, const FFDexParcelRequest* requests, uint32_t count);
// Whether `<classDescriptor>` declares an instance field named `<fieldName>`.
//
// This is the question a Parcelable whose layout is not field-driven needs. `DisplayCutout` is one:
// its `writeCutoutToParcel` reads everything through `-$$Nest$` accessors the compiler synthesised,
// so there is no `iget` to walk and no class name on the wire either. What does say how long its body
// is, is which fields the class carries -- Android 11 declares three, Android 13 adds
// `mCutoutPathParserInfo` and Android 16 adds `mSideOverrides`, and each one adds a known run of
// writes behind the three both have. The declaration is a field set rather than an order, which is
// exactly what this answers.
typedef struct FFDexInstanceFieldRequest {
const char* classDescriptor;
const char* fieldName;
bool* result;
} FFDexInstanceFieldRequest;
// Writes `false` for every request the jar cannot answer, on every path out -- a class the jar does
// not declare and a field the class does not declare are the same answer here, because both mean the
// field is not in the parcel. The return value is reserved for the jar itself.
[[gnu::nonnull(1, 2), nodiscard]] const char* ffDexInstanceFields(const char* jarPath, const FFDexInstanceFieldRequest* requests, uint32_t count);
File diff suppressed because it is too large Load Diff