ohmu
·
2026-10-15
1#include "displayserver.h"
2#include "common/android/api.h"
3#include "common/android/binder.h"
4#include "common/android/dex.h"
5#include "common/arrutil.h"
6#include "common/debug.h"
7#include "common/settings.h"
8#include "common/strutil.h"
9#include "common/processing.h"
10#include "linux/displayserver_linux.h"
11
12#include <math.h>
13
14// ---------------------------------------------------------------------------------------------
15// The binder route
16// ---------------------------------------------------------------------------------------------
17//
18// `cmd display get-displays` and `dumpsys display` used to be the routes here, and both are a child
19// process -- that fork was all of what this module cost: it took 16.0-26.5 ms on the test device,
20// where `cmd display get-displays` itself takes 9.2 ms and a bare fork 8.6 ms. `cmd` does not exist
21// before Android 13 either, and `dumpsys` is behind android.permission.DUMP, so on Android 11 and 12
22// an app UID had no display at all. `dumpsys` is gone: `display` is the service it sat on,
23// android.hardware.display.IDisplayManager, it lives in system_server, an app UID may call it, and
24// one `getDisplayInfo(0)` answers the whole `DisplayInfo` the dump prints, field for field. With the
25// fork gone the module took 2.8-3.2 ms where `cmd` took 16.0-26.5 ms, and Android 11 -- the release
26// `cmd` could not serve at all -- has been read this way on a device. What is left is dominated by
27// the dex read below, not by the transaction.
28//
29// `cmd display get-displays` stays behind the walk as a fallback, for the case where it is the dex
30// read that fails rather than the transaction -- see the `cmd` section below the walk.
31//
32// What it costs is the parse. The reply is a Java Parcelable and its field set moves with every
33// release and every vendor fork -- `DisplayInfo` writes 34 fields on Android 11, 42 on 13, 57 on 16
34// and 57 on 17 -- so neither a fixed offset nor a sequence written against one release survives. The
35// sequence is read out of the device's own `framework.jar` instead, from the `iget` order of
36// `DisplayInfo.writeToParcel`, and the walk below follows it: a field the release does not write is
37// simply not in the sequence, which is what makes every one of them optional by construction. See
38// common/android/dex.h for how the sequence is recovered and what it does not say.
39//
40// What the sequence does not say is how each field is framed: `writeString` and `writeString8` are
41// both a `Ljava/lang/String;` on the wire and are not the same length. The type descriptor settles
42// the primitives and both array shapes, a handful of names settle the rest, and every nested
43// Parcelable is anchored on its class name. A layout this does not recognise fails the route rather
44// than answering, which is what the `cmd` fallback behind it is for: a build whose layout is not one
45// this parser reads reports nothing rather than a plausible number out of the middle of a different
46// field, and `cmd` still has the record in text.
47//
48// Android 17 moved three of the things this parse hangs on, and all three are read the same way -- out
49// of the running build's own jar, so nothing here is a table written against a release:
50//
51// * `address` arrived as a `DisplayAddress$StablePhysical` where every release up to 16 sent a
52// `DisplayAddress$Physical`, and the new class writes the port beside the id -- two fields where
53// the old one writes one. Both sequences are read and the class name on the wire picks between
54// them, so Android 11 to 17 are all read by the walk below.
55// * `frameRateVelocityMapping` is new and is a `writeTypedList`, a count and then one body per
56// element, each behind the marker the typed writes use and with no class name anywhere. The
57// element's own sequence says how long it is.
58// * `DeviceProductInfo.mEdidStructureMetadata` is new and is a `writeTypedObject`: the same marker
59// and the same absent class name, one body behind it.
60
61#define FF_DISPLAY_ANDROID_SERVICE "display"
62#define FF_DISPLAY_ANDROID_DESCRIPTOR "android.hardware.display.IDisplayManager"
63
64// The jar the reply's classes live in, and their dex type descriptors. All of them are asked for in
65// one lookup: resolving a descriptor is dominated by a pass over the dex type table, and one pass
66// answers every request whose class is named in it -- see common/android/dex.h.
67#define FF_DISPLAY_ANDROID_JAR "/system/framework/framework.jar"
68
69#define FF_DISPLAY_ANDROID_CLASS_INFO "Landroid/view/DisplayInfo;"
70#define FF_DISPLAY_ANDROID_CLASS_ADDRESS "Landroid/view/DisplayAddress$Physical;"
71#define FF_DISPLAY_ANDROID_CLASS_STABLE_ADDRESS "Landroid/view/DisplayAddress$StablePhysical;"
72#define FF_DISPLAY_ANDROID_CLASS_PRODUCT_INFO "Landroid/hardware/display/DeviceProductInfo;"
73#define FF_DISPLAY_ANDROID_CLASS_EDID_STRUCTURE "Landroid/hardware/display/DeviceProductInfo$EdidStructureMetadata;"
74#define FF_DISPLAY_ANDROID_CLASS_MANUFACTURE_DATE "Landroid/hardware/display/DeviceProductInfo$ManufactureDate;"
75#define FF_DISPLAY_ANDROID_CLASS_HDR_CAPABILITIES "Landroid/view/Display$HdrCapabilities;"
76#define FF_DISPLAY_ANDROID_CLASS_MODE "Landroid/view/Display$Mode;"
77#define FF_DISPLAY_ANDROID_CLASS_FRAME_RATE_CATEGORY_RATE "Landroid/view/FrameRateCategoryRate;"
78#define FF_DISPLAY_ANDROID_CLASS_VELOCITY_POINT "Landroid/view/FrameRateVelocityPoint;"
79#define FF_DISPLAY_ANDROID_CLASS_CUTOUT "Landroid/view/DisplayCutout;"
80
81// The Java class names `writeParcelable` writes in front of each nested body, which is the anchor the
82// walk hangs on. It writes the *runtime* class, so the abstract `DisplayAddress` a `DisplayInfo`
83// declares arrives as whichever subclass the framework put there -- its `$Physical` up to Android 16
84// and its `$StablePhysical` from 17 on, which is why one field needs two names.
85#define FF_DISPLAY_ANDROID_NAME_ADDRESS "android.view.DisplayAddress$Physical"
86#define FF_DISPLAY_ANDROID_NAME_STABLE_ADDRESS "android.view.DisplayAddress$StablePhysical"
87#define FF_DISPLAY_ANDROID_NAME_PRODUCT_INFO "android.hardware.display.DeviceProductInfo"
88#define FF_DISPLAY_ANDROID_NAME_MANUFACTURE_DATE "android.hardware.display.DeviceProductInfo$ManufactureDate"
89#define FF_DISPLAY_ANDROID_NAME_HDR_CAPABILITIES "android.view.Display$HdrCapabilities"
90#define FF_DISPLAY_ANDROID_NAME_FRAME_RATE_CATEGORY_RATE "android.view.FrameRateCategoryRate"
91
92// `IDisplayManager$Stub`, as the device's own dex declares it. `getDisplayInfo` is the first method
93// and has been since the interface was introduced -- it is an `@UnsupportedAppUsage` API and that
94// order has never moved -- and `getDisplayIds` is the second. They are not counted out of the AOSP
95// source: a vendor fork inserts methods freely, and the seven a fork added on the test device all sit
96// behind these two.
97#define FF_DISPLAY_ANDROID_TRANSACTION_GET_DISPLAY_INFO 1u
98#define FF_DISPLAY_ANDROID_TRANSACTION_GET_DISPLAY_IDS 2u
99
100// One display's reply is 2404 bytes on the test device, most of it the two mode lists, and it grows
101// with the display. A reply that does not fit is a failure rather than a truncation.
102#define FF_DISPLAY_ANDROID_REPLY_SIZE 8192
103#define FF_DISPLAY_ANDROID_PARCEL_SIZE 256
104
105// How many displays are read at most. The reply buffer holds one display, so this only bounds the id
106// list and the array the parsed records are held in until every one of them has parsed.
107#define FF_DISPLAY_ANDROID_MAX_DISPLAYS 8
108
109// How many fields each write sequence can hold. A sequence that does not fit is reported as empty
110// rather than truncated, which fails the route -- half a sequence would walk half a record. See
111// common/android/dex.h.
112#define FF_DISPLAY_ANDROID_INFO_FIELDS 64
113#define FF_DISPLAY_ANDROID_NESTED_FIELDS 8
114#define FF_DISPLAY_ANDROID_MODE_FIELDS 12
115
116// The two instance fields of `DisplayCutout` that lengthen its body, in the order the request writes
117// their results. `mCutoutPathParserInfo` arrived in Android 13 and adds nine writes, `mSideOverrides`
118// in 16 and adds one; Android 11 declares neither.
119typedef enum FFDisplayAndroidCutoutField : uint32_t {
120 FF_DISPLAY_ANDROID_CUTOUT_PATH_PARSER_INFO,
121 FF_DISPLAY_ANDROID_CUTOUT_SIDE_OVERRIDES,
122 FF_DISPLAY_ANDROID_CUTOUT_FIELD_COUNT,
123} FFDisplayAndroidCutoutField;
124
125static const char* const ffDisplayAndroidCutoutFields[FF_DISPLAY_ANDROID_CUTOUT_FIELD_COUNT] = {
126 "mCutoutPathParserInfo",
127 "mSideOverrides",
128};
129
130// The write sequence of every class the reply is made of, read out of the device's own jar. It is
131// what turns the parse from a sequence written against one release into a walk of the release that
132// is running.
133typedef struct FFDisplayAndroidLayout {
134 FFDexParcelField displayInfo[FF_DISPLAY_ANDROID_INFO_FIELDS];
135 uint32_t displayInfoCount;
136 FFDexParcelField address[FF_DISPLAY_ANDROID_NESTED_FIELDS];
137 uint32_t addressCount;
138 FFDexParcelField stableAddress[FF_DISPLAY_ANDROID_NESTED_FIELDS];
139 uint32_t stableAddressCount;
140 FFDexParcelField productInfo[FF_DISPLAY_ANDROID_NESTED_FIELDS];
141 uint32_t productInfoCount;
142 FFDexParcelField edidStructureMetadata[FF_DISPLAY_ANDROID_NESTED_FIELDS];
143 uint32_t edidStructureMetadataCount;
144 FFDexParcelField manufactureDate[FF_DISPLAY_ANDROID_NESTED_FIELDS];
145 uint32_t manufactureDateCount;
146 FFDexParcelField hdrCapabilities[FF_DISPLAY_ANDROID_NESTED_FIELDS];
147 uint32_t hdrCapabilitiesCount;
148 FFDexParcelField mode[FF_DISPLAY_ANDROID_MODE_FIELDS];
149 uint32_t modeCount;
150 FFDexParcelField frameRateCategoryRate[FF_DISPLAY_ANDROID_NESTED_FIELDS];
151 uint32_t frameRateCategoryRateCount;
152 FFDexParcelField velocityPoint[FF_DISPLAY_ANDROID_NESTED_FIELDS];
153 uint32_t velocityPointCount;
154
155 // `DisplayCutout` is the one class whose body is not field-driven -- `writeCutoutToParcel` reads
156 // everything through compiler-synthesised accessors, so there is no `iget` to walk. What says how
157 // long its body is, is which fields the class declares, and that is what is asked for here. They
158 // are one array rather than two members because the request writes them as a run.
159 bool cutoutFields[FF_DISPLAY_ANDROID_CUTOUT_FIELD_COUNT];
160} FFDisplayAndroidLayout;
161
162// Reads every sequence above, and `DisplayCutout`'s declared fields, out of the dex entries of
163// `jarPath` -- in one lookup, because the walk behind it is what a lookup costs and the class data it
164// reads along the way already holds everything asked for here.
165//
166// A class the jar does not declare leaves an empty sequence, and that is an answer rather than a
167// failure for every class a release added part way through: `FrameRateCategoryRate` arrived in
168// Android 15 and `StablePhysical`, `EdidStructureMetadata` and `FrameRateVelocityPoint` in 17, and a
169// jar that does not declare one of them does not write the field either, so the walk never asks.
170// What *is* required is the sequences a record cannot be walked without: `DisplayInfo` itself, a
171// `DisplayAddress` -- either of the two shapes, since which one a build sends is what the name on the
172// wire says -- a `DeviceProductInfo` and its date, the HDR capabilities and a mode.
173//
174// A jar that cannot be read at all fails too, which is one of the two cases the `cmd` fallback
175// behind this route exists for. Which is the point of the dex being the source: a build this parser
176// cannot read declines to answer, rather than reporting a plausible number read out of the middle of
177// a different field.
178static bool displayLayoutLoad(const char* jarPath, FFDisplayAndroidLayout* layout) {
179 const FFDexParcelRequest requests[] = {
180 {
181 .classDescriptor = FF_DISPLAY_ANDROID_CLASS_INFO,
182 .methodName = "writeToParcel",
183 .fields = layout->displayInfo,
184 .capacity = FF_DISPLAY_ANDROID_INFO_FIELDS,
185 .count = &layout->displayInfoCount,
186 },
187 {
188 .classDescriptor = FF_DISPLAY_ANDROID_CLASS_ADDRESS,
189 .methodName = "writeToParcel",
190 .fields = layout->address,
191 .capacity = FF_DISPLAY_ANDROID_NESTED_FIELDS,
192 .count = &layout->addressCount,
193 },
194 {
195 // Android 17's shape of the same field. A build that does not declare the class leaves the
196 // sequence empty, which is how the walk knows the field can only be the older one.
197 .classDescriptor = FF_DISPLAY_ANDROID_CLASS_STABLE_ADDRESS,
198 .methodName = "writeToParcel",
199 .fields = layout->stableAddress,
200 .capacity = FF_DISPLAY_ANDROID_NESTED_FIELDS,
201 .count = &layout->stableAddressCount,
202 },
203 {
204 .classDescriptor = FF_DISPLAY_ANDROID_CLASS_PRODUCT_INFO,
205 .methodName = "writeToParcel",
206 .fields = layout->productInfo,
207 .capacity = FF_DISPLAY_ANDROID_NESTED_FIELDS,
208 .count = &layout->productInfoCount,
209 },
210 {
211 // Android 17's addition to `DeviceProductInfo`. Same as above: an empty sequence means the
212 // release does not carry the field, and it is not in the product info walk either.
213 .classDescriptor = FF_DISPLAY_ANDROID_CLASS_EDID_STRUCTURE,
214 .methodName = "writeToParcel",
215 .fields = layout->edidStructureMetadata,
216 .capacity = FF_DISPLAY_ANDROID_NESTED_FIELDS,
217 .count = &layout->edidStructureMetadataCount,
218 },
219 {
220 .classDescriptor = FF_DISPLAY_ANDROID_CLASS_MANUFACTURE_DATE,
221 .methodName = "writeToParcel",
222 .fields = layout->manufactureDate,
223 .capacity = FF_DISPLAY_ANDROID_NESTED_FIELDS,
224 .count = &layout->manufactureDateCount,
225 },
226 {
227 .classDescriptor = FF_DISPLAY_ANDROID_CLASS_HDR_CAPABILITIES,
228 .methodName = "writeToParcel",
229 .fields = layout->hdrCapabilities,
230 .capacity = FF_DISPLAY_ANDROID_NESTED_FIELDS,
231 .count = &layout->hdrCapabilitiesCount,
232 },
233 {
234 .classDescriptor = FF_DISPLAY_ANDROID_CLASS_MODE,
235 .methodName = "writeToParcel",
236 .fields = layout->mode,
237 .capacity = FF_DISPLAY_ANDROID_MODE_FIELDS,
238 .count = &layout->modeCount,
239 },
240 {
241 .classDescriptor = FF_DISPLAY_ANDROID_CLASS_FRAME_RATE_CATEGORY_RATE,
242 .methodName = "writeToParcel",
243 .fields = layout->frameRateCategoryRate,
244 .capacity = FF_DISPLAY_ANDROID_NESTED_FIELDS,
245 .count = &layout->frameRateCategoryRateCount,
246 },
247 {
248 // Android 17's `frameRateVelocityMapping` is a list of these, and this is the sequence that
249 // says how long one element is.
250 .classDescriptor = FF_DISPLAY_ANDROID_CLASS_VELOCITY_POINT,
251 .methodName = "writeToParcel",
252 .fields = layout->velocityPoint,
253 .capacity = FF_DISPLAY_ANDROID_NESTED_FIELDS,
254 .count = &layout->velocityPointCount,
255 },
256 {
257 // The one class whose body is not field-driven. `writeCutoutToParcel` reads everything
258 // through compiler-synthesised accessors, so there is no `iget` to walk and no class name
259 // on the wire either -- what says how long its body is, is which fields the class
260 // declares. Asking here rather than by a call of its own is the point: the walk that
261 // answers the seven sequences above is already holding this class's field list, and a
262 // second call would repeat it from the mapping down for 7.0 ms of a 23.5 ms lookup.
263 .classDescriptor = FF_DISPLAY_ANDROID_CLASS_CUTOUT,
264 .instanceFieldNames = ffDisplayAndroidCutoutFields,
265 .instanceFieldResults = layout->cutoutFields,
266 .instanceFieldCount = ARRAY_SIZE(ffDisplayAndroidCutoutFields),
267 },
268 };
269
270 const char* error = ffDexParcelFields(jarPath, requests, ARRAY_SIZE(requests));
271 if (error != nullptr) {
272 FF_DEBUG("Reading the display layout out of %s failed: %s", jarPath, error);
273 return false;
274 }
275 if (layout->displayInfoCount == 0 || (layout->addressCount == 0 && layout->stableAddressCount == 0)
276 || layout->productInfoCount == 0 || layout->manufactureDateCount == 0 || layout->hdrCapabilitiesCount == 0
277 || layout->modeCount == 0) {
278 FF_DEBUG("The classes a display is made of are not all declared in %s", jarPath);
279 return false;
280 }
281
282 FF_DEBUG(
283 "Display layout: %u fields in DisplayInfo, %u in a mode, address %s, product info %u + edid %u, velocity point %u, cutout path parser info %s, side overrides %s",
284 layout->displayInfoCount, layout->modeCount,
285 layout->addressCount > 0 ? (layout->stableAddressCount > 0 ? "physical and stable" : "physical") : "stable",
286 layout->productInfoCount, layout->edidStructureMetadataCount, layout->velocityPointCount,
287 layout->cutoutFields[FF_DISPLAY_ANDROID_CUTOUT_PATH_PARSER_INFO] ? "yes" : "no",
288 layout->cutoutFields[FF_DISPLAY_ANDROID_CUTOUT_SIDE_OVERRIDES] ? "yes" : "no"
289 );
290 return true;
291}
292
293// `DisplayInfo.type`, i.e. `Display.TYPE_*`. A dump prints these as `INTERNAL` / `EXTERNAL` /
294// `WIFI`; the numbers are what the parcel carries.
295typedef enum FFDisplayAndroidType : int32_t {
296 FF_DISPLAY_ANDROID_TYPE_UNKNOWN = 0,
297 FF_DISPLAY_ANDROID_TYPE_INTERNAL = 1,
298 FF_DISPLAY_ANDROID_TYPE_EXTERNAL = 2,
299 FF_DISPLAY_ANDROID_TYPE_WIFI = 3,
300 FF_DISPLAY_ANDROID_TYPE_OVERLAY = 4,
301 FF_DISPLAY_ANDROID_TYPE_VIRTUAL = 5,
302} FFDisplayAndroidType;
303
304// The `Parcel.writeValue` tags `DeviceProductInfo` uses. The framework writes one of these in front
305// of the value; the numbers are its own.
306typedef enum FFDisplayAndroidValue : int32_t {
307 FF_DISPLAY_ANDROID_VALUE_NULL = -1,
308 FF_DISPLAY_ANDROID_VALUE_STRING = 0,
309 FF_DISPLAY_ANDROID_VALUE_INTEGER = 1,
310 FF_DISPLAY_ANDROID_VALUE_PARCELABLE = 4,
311} FFDisplayAndroidValue;
312
313// A cursor over one reply. Every read is bounds checked and every mismatch raises `failed` rather
314// than returning a value: once a field has been read at the wrong place nothing behind it can be
315// trusted, and the only safe answer left is to report nothing.
316typedef struct FFDisplayAndroidReader {
317 const uint8_t* data;
318 size_t size;
319 size_t offset;
320 bool failed;
321} FFDisplayAndroidReader;
322
323static bool readerSkip(FFDisplayAndroidReader* reader, size_t bytes) {
324 if (bytes > reader->size - reader->offset) {
325 reader->failed = true;
326 return false;
327 }
328 reader->offset += bytes;
329 return true;
330}
331
332static int32_t readerI32(FFDisplayAndroidReader* reader) {
333 if (reader->offset + sizeof(int32_t) > reader->size) {
334 reader->failed = true;
335 return 0;
336 }
337 int32_t value = 0;
338 memcpy(&value, reader->data + reader->offset, sizeof(int32_t));
339 reader->offset += sizeof(int32_t);
340 return value;
341}
342
343static uint32_t readerU32(FFDisplayAndroidReader* reader) {
344 return (uint32_t) readerI32(reader);
345}
346
347static uint64_t readerU64(FFDisplayAndroidReader* reader) {
348 if (reader->offset + sizeof(uint64_t) > reader->size) {
349 reader->failed = true;
350 return 0;
351 }
352 uint64_t value = 0;
353 memcpy(&value, reader->data + reader->offset, sizeof(uint64_t));
354 reader->offset += sizeof(uint64_t);
355 return value;
356}
357
358static float readerF32(FFDisplayAndroidReader* reader) {
359 const uint32_t raw = readerU32(reader);
360 float value = 0;
361 memcpy(&value, &raw, sizeof(value));
362 return value;
363}
364
365// A `writeBoolean`, which is an int32. Anything but 0 or 1 is a field read at the wrong place.
366static bool readerBool(FFDisplayAndroidReader* reader) {
367 const int32_t value = readerI32(reader);
368 if (value != 0 && value != 1) {
369 reader->failed = true;
370 return false;
371 }
372 return value == 1;
373}
374
375// Whether the string16 at `offset` holds `expected`, which is a class name. The length has to be the
376// name's own and every code unit has to spell it, so a body that happens to start with a plausible
377// length does not pass for one.
378static bool readerClassNameAt(const FFDisplayAndroidReader* reader, size_t offset, const char* expected) {
379 const size_t length = strlen(expected);
380 if (length > 128 || offset + sizeof(int32_t) + (length + 1) * 2 > reader->size) {
381 return false;
382 }
383 int32_t declared = 0;
384 memcpy(&declared, reader->data + offset, sizeof(int32_t));
385 if ((size_t) declared != length) {
386 return false;
387 }
388 const uint8_t* text = reader->data + offset + sizeof(int32_t);
389 for (size_t i = 0; i < length; ++i) {
390 if (text[i * 2] != (uint8_t) expected[i] || text[i * 2 + 1] != 0) {
391 return false;
392 }
393 }
394 return true;
395}
396
397// The class name in front of a Parcelable body, which `writeParcelable` writes and which is the
398// anchor this whole parser hangs on: a field that moved puts something else here, and a name that
399// does not decode is how the parser finds out instead of reading a length out of the middle of a
400// body.
401static bool readerClassName(FFDisplayAndroidReader* reader, const char* expected) {
402 if (!readerClassNameAt(reader, reader->offset, expected)) {
403 reader->failed = true;
404 return false;
405 }
406 return readerSkip(reader, sizeof(int32_t) + (((strlen(expected) + 1) * 2 + 3) & ~(size_t) 3));
407}
408
409// A `writeString`, which is UTF-16 despite the name: an int32 count of code units, the code units, a
410// NUL terminator and the padding behind it. A count of -1 is how the same field spells a null String
411// and is not an error; `result` may be null where the text is only in the way.
412static bool readerString16(FFDisplayAndroidReader* reader, FFstrbuf* result) {
413 const int32_t length = readerI32(reader);
414 if (reader->failed) {
415 return false;
416 }
417 if (length < 0) {
418 return true;
419 }
420 const size_t payload = ((size_t) length * 2 + 2 + 3) & ~(size_t) 3;
421 if (payload > reader->size - reader->offset) {
422 reader->failed = true;
423 return false;
424 }
425 if (result != nullptr) {
426 for (int32_t i = 0; i < length; ++i) {
427 uint32_t codePoint = (uint32_t) reader->data[reader->offset + (size_t) i * 2]
428 | ((uint32_t) reader->data[reader->offset + (size_t) i * 2 + 1] << 8);
429 if (codePoint >= 0xD800 && codePoint <= 0xDBFF && i + 1 < length) {
430 // Anything outside the basic plane arrives as a surrogate pair, and the low half is
431 // what turns it back into one code point. A high surrogate without one is malformed
432 // and is passed through as it stands rather than guessed at.
433 const uint32_t low = (uint32_t) reader->data[reader->offset + (size_t) (i + 1) * 2]
434 | ((uint32_t) reader->data[reader->offset + (size_t) (i + 1) * 2 + 1] << 8);
435 if (low >= 0xDC00 && low <= 0xDFFF) {
436 codePoint = 0x10000u + ((codePoint - 0xD800u) << 10) + (low - 0xDC00u);
437 ++i;
438 }
439 }
440 ffStrbufAppendUtf32CodePoint(result, codePoint);
441 }
442 }
443 reader->offset += payload;
444 return true;
445}
446
447// A `writeString8`, which is the one that really is UTF-8: an int32 *byte* count, that many bytes, a
448// NUL and the padding. This is what `DisplayInfo.name` and `DisplayInfo.uniqueId` use -- the built-in
449// display's name is 12 bytes of UTF-8 where the same text is 4 code units as a string16.
450static bool readerString8(FFDisplayAndroidReader* reader, FFstrbuf* result) {
451 const int32_t length = readerI32(reader);
452 if (reader->failed) {
453 return false;
454 }
455 if (length < 0) {
456 return true;
457 }
458 const size_t payload = ((size_t) length + 1 + 3) & ~(size_t) 3;
459 if (payload > reader->size - reader->offset) {
460 reader->failed = true;
461 return false;
462 }
463 if (result != nullptr) {
464 ffStrbufAppendNS(result, (uint32_t) length, (const char*) reader->data + reader->offset);
465 }
466 reader->offset += payload;
467 return true;
468}
469
470// An array of primitives: a count and the values. A count of -1 is a null array, which carries none.
471static bool readerSkipArray(FFDisplayAndroidReader* reader, size_t elementSize) {
472 const int32_t count = readerI32(reader);
473 if (reader->failed) {
474 return false;
475 }
476 if (count < 0) {
477 return true;
478 }
479 if (count > 1024) {
480 reader->failed = true;
481 return false;
482 }
483 return readerSkip(reader, (size_t) count * elementSize);
484}
485
486// A `writeValue` whose value is an Integer, or a null which reads as zero.
487static int32_t readerValueInteger(FFDisplayAndroidReader* reader) {
488 switch ((FFDisplayAndroidValue) readerI32(reader)) {
489 case FF_DISPLAY_ANDROID_VALUE_NULL:
490 return 0;
491 case FF_DISPLAY_ANDROID_VALUE_INTEGER:
492 return readerI32(reader);
493 default:
494 reader->failed = true;
495 return 0;
496 }
497}
498
499// A `writeValue` whose value is a String, an Integer or a null. Nothing here wants the value; the
500// shapes it does not know are what it refuses to guess a length for.
501static bool readerSkipValue(FFDisplayAndroidReader* reader) {
502 switch ((FFDisplayAndroidValue) readerI32(reader)) {
503 case FF_DISPLAY_ANDROID_VALUE_NULL:
504 return true;
505 case FF_DISPLAY_ANDROID_VALUE_STRING:
506 return readerString16(reader, nullptr);
507 case FF_DISPLAY_ANDROID_VALUE_INTEGER:
508 (void) readerI32(reader);
509 return !reader->failed;
510 default:
511 reader->failed = true;
512 return false;
513 }
514}
515
516// One field whose dex type descriptor settles how it is framed. `[I` and `[F` are the two array
517// shapes the record carries and report their element count; the rest report their value.
518typedef struct FFDisplayAndroidScalar {
519 int32_t i32;
520 float f32;
521 int32_t count;
522} FFDisplayAndroidScalar;
523
524// Reads one field whose descriptor settles its framing -- an `I`, an `F`, a `J`, a `Z`, an `[I` or
525// an `[F`. A descriptor with no rule here is a failure rather than a guess: `Ljava/lang/String;`
526// alone can be a `writeString` or a `writeString8`, the two are not the same length on the wire, and
527// stepping over it as if it were either would move every field behind it.
528static bool readerScalar(FFDisplayAndroidReader* reader, const char* type, FFDisplayAndroidScalar* value) {
529 *value = (FFDisplayAndroidScalar) {};
530 if (ffStrEquals(type, "I")) {
531 value->i32 = readerI32(reader);
532 return !reader->failed;
533 }
534 if (ffStrEquals(type, "F")) {
535 value->f32 = readerF32(reader);
536 return !reader->failed;
537 }
538 if (ffStrEquals(type, "J")) {
539 (void) readerU64(reader);
540 return !reader->failed;
541 }
542 if (ffStrEquals(type, "Z")) {
543 value->i32 = readerBool(reader) ? 1 : 0;
544 return !reader->failed;
545 }
546 if (ffStrEquals(type, "[I") || ffStrEquals(type, "[F")) {
547 const int32_t count = readerI32(reader);
548 if (reader->failed || count > 1024) {
549 reader->failed = true;
550 return false;
551 }
552 // A negative count is a null array and carries no elements: `writeIntArray(null)` and
553 // `writeFloatArray(null)` both write a bare -1 where the count would be. Android 11 has one
554 // of these in the record -- `DeviceProductInfo.mRelativeAddress` is an `[I` there and is null
555 // on the built-in display. `readerSkipArray` spells the same rule for the arrays it steps
556 // over, and a null is a value everywhere else in this walk too.
557 if (count < 0) {
558 return true;
559 }
560 value->count = count;
561 return readerSkip(reader, (size_t) count * (ffStrEquals(type, "[I") ? sizeof(int32_t) : sizeof(float)));
562 }
563 reader->failed = true;
564 return false;
565}
566
567// The class name in front of a nested body, when which class it is decides how long the body is.
568// `writeParcelable` writes the runtime class, so a field declared an abstract type arrives as
569// whichever subclass the framework put there: `DisplayInfo.address` is a `DisplayAddress`, whose body
570// is one field on `$Physical` and two on the `$StablePhysical` Android 17 added. The name is read
571// first and the index of the one it matched is reported, so the caller walks the sequence that
572// belongs to it -- and a name that matches none of them fails, because there is nothing else that
573// says where the body ends.
574static bool readerParcelableHeadAny(FFDisplayAndroidReader* reader, const char* const* names, uint32_t nameCount, uint32_t* index, bool* present) {
575 const int32_t length = readerI32(reader);
576 if (reader->failed) {
577 return false;
578 }
579 if (length < 0) {
580 *present = false;
581 return true;
582 }
583 reader->offset -= sizeof(int32_t);
584 *present = true;
585 for (uint32_t i = 0; i < nameCount; ++i) {
586 if (readerClassNameAt(reader, reader->offset, names[i])) {
587 *index = i;
588 return readerClassName(reader, names[i]);
589 }
590 }
591 reader->failed = true;
592 return false;
593}
594
595// The same head for the fields that carry one class only. A null field is a single -1 where the class
596// name would be, which is a value and not a failure.
597static bool readerParcelableHead(FFDisplayAndroidReader* reader, const char* expected, bool* present) {
598 uint32_t index = 0;
599 return readerParcelableHeadAny(reader, &expected, 1, &index, present);
600}
601
602// A `Rect` written by `writeTypedObject` or by `writeTypedArray`: the non-null marker the typed write
603// puts in front of the body and then four ints. A null is a marker and no ints behind it.
604static bool readerRect(FFDisplayAndroidReader* reader) {
605 const int32_t marker = readerI32(reader);
606 if (reader->failed) {
607 return false;
608 }
609 return marker < 0 || readerSkip(reader, 4 * sizeof(int32_t));
610}
611
612// A nested Parcelable written by `writeTypedObject`, walked out of the sequence the jar gives for its
613// own class. What is on the wire is a marker and the body, with no class name in front of it -- the
614// reader is handed the `Creator` to build the value with, so the parcel does not have to carry one,
615// and that is the whole difference between this and `writeParcelable`. The device's own
616// `Parcel.writeTypedObject` writes a zero marker for a null and a one before every body, so anything
617// else is a field read at the wrong place rather than a value.
618static bool readerTypedObject(FFDisplayAndroidReader* reader, const FFDexParcelField* fields, uint32_t count) {
619 const int32_t marker = readerI32(reader);
620 if (reader->failed) {
621 return false;
622 }
623 if (marker == 0) {
624 return true;
625 }
626 if (marker != 1) {
627 reader->failed = true;
628 return false;
629 }
630 for (uint32_t i = 0; i < count; ++i) {
631 FFDisplayAndroidScalar value = {};
632 if (!readerScalar(reader, fields[i].type, &value)) {
633 return false;
634 }
635 }
636 return true;
637}
638
639// A `writeTypedList`: a count and then one `writeTypedObject` per element, all of them the same class,
640// which is why one sequence answers the whole list. A count of -1 is a null list and carries none.
641static bool readerTypedObjectList(FFDisplayAndroidReader* reader, const FFDexParcelField* fields, uint32_t count) {
642 const int32_t size = readerI32(reader);
643 if (reader->failed) {
644 return false;
645 }
646 if (size < 0) {
647 return true;
648 }
649 // A non-empty list whose elements this parser has no sequence for is a failure rather than an empty
650 // walk: the elements are still on the wire, and stepping over none of them would put every field
651 // behind the list at the wrong place, which is worse than reporting nothing.
652 if (size > 64 || (size > 0 && count == 0)) {
653 reader->failed = true;
654 return false;
655 }
656 for (int32_t i = 0; i < size; ++i) {
657 if (!readerTypedObject(reader, fields, count)) {
658 return false;
659 }
660 }
661 return true;
662}
663
664// `writeValue(mManufactureDate)`, the only Parcelable a `writeValue` carries here, and the one place
665// the parcel does something the AOSP sources do not describe: on Android 13 and later the tag is
666// followed by the size of everything behind it -- the class name and the body -- before the class
667// name, which is why a plain `tag + class name` walk lands four bytes early; Android 11 has no size
668// at all. Neither layout is assumed -- a value that decodes as the class name has no size, anything
669// else is stepped over -- and where there is one the cursor is snapped to the end it promises, so a
670// body this parser reads differently can not move the fields behind it. The size counts from its own
671// end, not from its start.
672static bool readerManufactureDate(FFDisplayAndroidReader* reader, const FFDisplayAndroidLayout* layout, uint16_t* year, uint16_t* week) {
673 // `mManufactureDate` is `@Nullable`, so a display with no date writes a bare `VAL_NULL` and
674 // nothing behind it -- and a date is not what the display is being read for. A missing one is
675 // skipped rather than failed: everything the module needs is still in the record, and dropping
676 // the display over an absent date would answer nothing at all. Only a tag that is neither a
677 // Parcelable nor a null fails, because that is a framing this walk does not know how to step over.
678 const int32_t tag = readerI32(reader);
679 if (tag == FF_DISPLAY_ANDROID_VALUE_NULL) {
680 *year = 0;
681 *week = 0;
682 return true;
683 }
684 if (tag != FF_DISPLAY_ANDROID_VALUE_PARCELABLE) {
685 reader->failed = true;
686 return false;
687 }
688
689 size_t bodyEnd = 0;
690 if (!readerClassNameAt(reader, reader->offset, FF_DISPLAY_ANDROID_NAME_MANUFACTURE_DATE)) {
691 const int32_t declared = readerI32(reader);
692 if (declared <= 0) {
693 reader->failed = true;
694 return false;
695 }
696 bodyEnd = reader->offset + (size_t) declared;
697 }
698 if (!readerClassName(reader, FF_DISPLAY_ANDROID_NAME_MANUFACTURE_DATE)) {
699 return false;
700 }
701
702 // `writeValue(mWeek)` and `writeValue(mYear)`, each an Integer or a null. Nothing is reported
703 // unless both are there, so a class that stops writing one leaves the date at nothing rather
704 // than at half of it.
705 uint16_t weekValue = 0;
706 uint16_t yearValue = 0;
707 for (uint32_t i = 0; i < layout->manufactureDateCount; ++i) {
708 const int32_t value = readerValueInteger(reader);
709 if (reader->failed || value < 0 || value > 3000) {
710 reader->failed = true;
711 return false;
712 }
713 if (ffStrEquals(layout->manufactureDate[i].name, "mWeek")) {
714 if (value > 53) {
715 reader->failed = true;
716 return false;
717 }
718 weekValue = (uint16_t) value;
719 } else if (ffStrEquals(layout->manufactureDate[i].name, "mYear")) {
720 yearValue = (uint16_t) value;
721 }
722 }
723 *week = weekValue;
724 *year = yearValue;
725
726 if (bodyEnd > 0) {
727 if (bodyEnd < reader->offset || bodyEnd > reader->size) {
728 reader->failed = true;
729 return false;
730 }
731 reader->offset = bodyEnd;
732 }
733 return true;
734}
735
736// `deviceProductInfo`, which is where a display's manufacture date is carried. Nothing else in it is
737// used, but it sits in front of `name`, the density and the rotation, so it has to be walked whether
738// or not its values are wanted -- and it is null on some displays.
739//
740// Three of its fields are handed to `writeValue`, which frames them by their runtime type rather than
741// by their declared one: `mProductId` is declared a `String` and goes through `writeValue` where
742// `mName` next to it goes through `writeString`, so those are named rather than dispatched by type.
743// The connection type is `mRelativeAddress`, an int array, on Android 11 and `mConnectionToSinkType`,
744// an int, on 13 and later; the descriptor settles that one, which is what makes the two read the same
745// code. Android 17 added `mVideoInputType`, an int, which the descriptor settles the same way, and
746// `mEdidStructureMetadata`, a `writeTypedObject` -- six fields up to 16, eight from 17 on.
747static bool readerDeviceProductInfo(FFDisplayAndroidReader* reader, const FFDisplayAndroidLayout* layout, uint16_t* year, uint16_t* week) {
748 bool present = false;
749 if (!readerParcelableHead(reader, FF_DISPLAY_ANDROID_NAME_PRODUCT_INFO, &present)) {
750 return false;
751 }
752 if (!present) {
753 return true;
754 }
755
756 for (uint32_t i = 0; i < layout->productInfoCount; ++i) {
757 const FFDexParcelField* field = &layout->productInfo[i];
758 if (ffStrEquals(field->name, "mName") || ffStrEquals(field->name, "mManufacturerPnpId")) {
759 if (!readerString16(reader, nullptr)) {
760 return false;
761 }
762 } else if (ffStrEquals(field->name, "mProductId") || ffStrEquals(field->name, "mModelYear")) {
763 if (!readerSkipValue(reader)) {
764 return false;
765 }
766 } else if (ffStrEquals(field->name, "mManufactureDate")) {
767 if (!readerManufactureDate(reader, layout, year, week)) {
768 return false;
769 }
770 } else if (ffStrEquals(field->name, "mEdidStructureMetadata")) {
771 // Android 17. Named rather than left to the descriptor for the same reason the fields above
772 // are: `writeTypedObject` and `writeParcelable` are both a `L...;` on the wire and are not
773 // the same length, and this one is the typed write -- a marker, the body, no class name.
774 if (!readerTypedObject(reader, layout->edidStructureMetadata, layout->edidStructureMetadataCount)) {
775 return false;
776 }
777 } else {
778 FFDisplayAndroidScalar value = {};
779 if (!readerScalar(reader, field->type, &value)) {
780 return false;
781 }
782 }
783 }
784 return true;
785}
786
787// `address`, which is null on some displays and a `DisplayAddress` on others: its `$Physical` up to
788// Android 16, whose body is one `long`, and its `$StablePhysical` from 17 on, whose body is that
789// `long` and the port beside it. Which one is on the wire is what the class name says, and either
790// sequence comes out of the jar rather than being assumed, so a release that changes either body is
791// read rather than misread.
792static bool readerDisplayAddress(FFDisplayAndroidReader* reader, const FFDisplayAndroidLayout* layout) {
793 static const char* const names[] = {
794 FF_DISPLAY_ANDROID_NAME_ADDRESS,
795 FF_DISPLAY_ANDROID_NAME_STABLE_ADDRESS,
796 };
797 bool present = false;
798 uint32_t index = 0;
799 if (!readerParcelableHeadAny(reader, names, ARRAY_SIZE(names), &index, &present)) {
800 return false;
801 }
802 if (!present) {
803 return true;
804 }
805 const bool stable = index != 0;
806 const FFDexParcelField* fields = stable ? layout->stableAddress : layout->address;
807 const uint32_t count = stable ? layout->stableAddressCount : layout->addressCount;
808 for (uint32_t i = 0; i < count; ++i) {
809 FFDisplayAndroidScalar value = {};
810 if (!readerScalar(reader, fields[i].type, &value)) {
811 return false;
812 }
813 }
814 return true;
815}
816
817// A nested Parcelable whose values this module does not use, walked because the fields behind it sit
818// on the far side of it. `frameRateCategoryRate` is the only one of those left before `uniqueId`.
819static bool readerSkippedParcelable(FFDisplayAndroidReader* reader, const char* expected, const FFDexParcelField* fields, uint32_t count) {
820 bool present = false;
821 if (!readerParcelableHead(reader, expected, &present)) {
822 return false;
823 }
824 if (!present) {
825 return true;
826 }
827 for (uint32_t i = 0; i < count; ++i) {
828 FFDisplayAndroidScalar value = {};
829 if (!readerScalar(reader, fields[i].type, &value)) {
830 return false;
831 }
832 }
833 return true;
834}
835
836// `hdrCapabilities`, whose only value this module uses is how many HDR types the display supports --
837// an empty list is the display saying it can not do HDR at all.
838static bool readerHdrCapabilities(FFDisplayAndroidReader* reader, const FFDisplayAndroidLayout* layout, int32_t* hdrTypeCount) {
839 bool present = false;
840 if (!readerParcelableHead(reader, FF_DISPLAY_ANDROID_NAME_HDR_CAPABILITIES, &present)) {
841 return false;
842 }
843 if (!present) {
844 return true;
845 }
846 for (uint32_t i = 0; i < layout->hdrCapabilitiesCount; ++i) {
847 FFDisplayAndroidScalar value = {};
848 if (!readerScalar(reader, layout->hdrCapabilities[i].type, &value)) {
849 return false;
850 }
851 if (ffStrEquals(layout->hdrCapabilities[i].name, "mSupportedHdrTypes")) {
852 if (value.count > 16) {
853 reader->failed = true;
854 return false;
855 }
856 *hdrTypeCount = value.count;
857 }
858 }
859 return true;
860}
861
862// `displayCutout`, the largest nested structure in the record and the one that is easiest to get
863// wrong: it is serialized by `DisplayCutout$ParcelableWrapper.writeCutoutToParcel`, because
864// `DisplayCutout` has no `writeToParcel` of its own, and that method reads everything through
865// `-$$Nest$` accessors the compiler synthesised -- so there is no `iget` to walk and no class name on
866// the wire either, and the write sequence the jar gives for every other class says nothing here.
867//
868// What says how long the body is, is which fields the class declares. Android 11 declares three --
869// `mBounds`, `mSafeInsets`, `mWaterfallInsets` -- Android 13 adds `mCutoutPathParserInfo`, which is
870// nine more writes, and Android 16 adds `mSideOverrides`, which is one more. Both are read from the
871// jar rather than from a version number. None of it is reported; it is walked because `rotation` and
872// everything behind it sit on the far side of it.
873static bool readerDisplayCutout(FFDisplayAndroidReader* reader, const FFDisplayAndroidLayout* layout) {
874 const int32_t marker = readerI32(reader);
875 if (reader->failed) {
876 return false;
877 }
878 if (marker != 1) {
879 // 0 for a null cutout and -1 for `DisplayCutout.NO_CUTOUT`, which is a display with none at
880 // all. Anything else is a field read at the wrong place.
881 if (marker == 0 || marker == -1) {
882 return true;
883 }
884 reader->failed = true;
885 return false;
886 }
887
888 if (!readerRect(reader)) { // mSafeInsets
889 return false;
890 }
891 const int32_t bounds = readerI32(reader); // mBounds, a typed array of Rects
892 if (reader->failed || bounds < 0 || bounds > 64) {
893 reader->failed = true;
894 return false;
895 }
896 for (int32_t i = 0; i < bounds; ++i) {
897 if (!readerRect(reader)) {
898 return false;
899 }
900 }
901 if (!readerRect(reader)) { // mWaterfallInsets
902 return false;
903 }
904
905 const bool* cutout = layout->cutoutFields;
906 if (cutout[FF_DISPLAY_ANDROID_CUTOUT_PATH_PARSER_INFO]) {
907 // the display size the cutout was built for and then the physical one
908 if (!readerSkip(reader, 4 * sizeof(int32_t))) {
909 return false;
910 }
911 const float density = readerF32(reader);
912 if (reader->failed || !(density > 0) || density > 1000) {
913 reader->failed = true;
914 return false;
915 }
916 // the cutout spec, e.g. `M -45,0 L -45,142 L 45,142 L 45,0 Z`, and then the rotation, the
917 // scale and the physical pixel ratio, which are three plain four byte fields
918 if (!readerString16(reader, nullptr) || !readerSkip(reader, 3 * sizeof(int32_t))) {
919 return false;
920 }
921 }
922 if (cutout[FF_DISPLAY_ANDROID_CUTOUT_SIDE_OVERRIDES]) {
923 return readerSkipArray(reader, sizeof(int32_t)); // mSideOverrides
924 }
925 return true;
926}
927
928// One entry of `supportedModes`, which is `Display$Mode`.
929typedef struct FFDisplayAndroidMode {
930 int32_t id;
931 uint32_t width;
932 uint32_t height;
933 float refreshRate;
934} FFDisplayAndroidMode;
935
936// Walks a `Display$Mode[]`: a count and then that many bare bodies, with no non-null marker per
937// element, which is what `writeTypedArray` would put there -- the framework writes the length with
938// `writeInt` and then one `writeToParcel` per element. The two ids the record carries are looked up
939// as it goes, so the list does not have to be kept.
940//
941// The body is walked out of the jar's own sequence rather than read as a fixed run: `Display$Mode`
942// writes four fields on Android 11, five on 13 and eight on 16, and the fourth -- the one the
943// refresh rate is in -- is `mRefreshRate` on the first two and `mPeakRefreshRate` on the third.
944static bool readerModeList(FFDisplayAndroidReader* reader, const FFDisplayAndroidLayout* layout, int32_t activeId, int32_t defaultId, FFDisplayAndroidMode* active, FFDisplayAndroidMode* preferred) {
945 const int32_t count = readerI32(reader);
946 if (reader->failed || count < 0 || count > 64) {
947 reader->failed = true;
948 return false;
949 }
950 for (int32_t i = 0; i < count; ++i) {
951 FFDisplayAndroidMode mode = {};
952 for (uint32_t j = 0; j < layout->modeCount; ++j) {
953 const FFDexParcelField* field = &layout->mode[j];
954 FFDisplayAndroidScalar value = {};
955 if (!readerScalar(reader, field->type, &value)) {
956 return false;
957 }
958 if (ffStrEquals(field->name, "mModeId")) {
959 mode.id = value.i32;
960 } else if (ffStrEquals(field->name, "mWidth")) {
961 mode.width = (uint32_t) value.i32;
962 } else if (ffStrEquals(field->name, "mHeight")) {
963 mode.height = (uint32_t) value.i32;
964 } else if (ffStrEquals(field->name, "mRefreshRate") || ffStrEquals(field->name, "mPeakRefreshRate")) {
965 mode.refreshRate = value.f32;
966 }
967 }
968 if (reader->failed
969 || mode.id < 0 || mode.width == 0 || mode.width > 32768 || mode.height == 0 || mode.height > 32768
970 || !(mode.refreshRate > 0) || mode.refreshRate > 1000) {
971 reader->failed = true;
972 return false;
973 }
974 if (active != nullptr && mode.id == activeId) {
975 *active = mode;
976 }
977 if (preferred != nullptr && mode.id == defaultId) {
978 *preferred = mode;
979 }
980 }
981 return !reader->failed;
982}
983
984// One parsed record. It is held until every display has parsed, because a record this parser does not
985// recognise says the layout is not the one it was written for, and half an answer from a layout it
986// does not understand is worse than falling back.
987typedef struct FFDisplayAndroidInfo {
988 uint32_t width;
989 uint32_t height;
990 uint32_t preferredWidth;
991 uint32_t preferredHeight;
992 double preferredRefreshRate;
993 double refreshRate;
994 uint32_t density;
995 double physicalXDpi;
996 double physicalYDpi;
997 uint32_t rotation;
998 uint32_t displayId;
999 int32_t type;
1000 int32_t modeId;
1001 int32_t defaultModeId;
1002 int32_t hdrTypeCount;
1003 float renderFrameRate;
1004 bool forceSdr;
1005 uint16_t manufactureYear;
1006 uint16_t manufactureWeek;
1007 uint64_t id;
1008 FFstrbuf name;
1009} FFDisplayAndroidInfo;
1010
1011// One `DisplayInfo` walk in progress: the record being filled, the two modes the mode list is
1012// resolved against, and the id string -- which is not part of the record, it is only read to pull the
1013// display's id out of it, so it is held here rather than there.
1014typedef struct FFDisplayAndroidWalk {
1015 FFDisplayAndroidInfo* info;
1016 FFstrbuf* uniqueId;
1017 FFDisplayAndroidMode active;
1018 FFDisplayAndroidMode preferred;
1019} FFDisplayAndroidWalk;
1020
1021// `uniqueId` identifies the display across reboots, e.g. `local:4630946557703207059` for the panel
1022// and `virtual:...` for a virtual one. The prefix says what kind of display it is, and the number
1023// behind the colon is the id.
1024static uint64_t displayUniqueId(const char* uniqueId) {
1025 const char* colon = strchr(uniqueId, ':');
1026 return (uint64_t) strtoull(colon != nullptr ? colon + 1 : uniqueId, nullptr, 10);
1027}
1028
1029// One field of the `DisplayInfo` write sequence. The name says which value the field carries; the
1030// type descriptor says how it is framed, and settles that for every primitive and both array shapes.
1031//
1032// Six things the descriptor does not settle, and each is named:
1033//
1034// * `address`, whose runtime class decides how long its body is. See readerDisplayAddress.
1035// * `displayCutout`, which has no write sequence to follow at all. See readerDisplayCutout.
1036// * `deviceProductInfo`, whose `mEdidStructureMetadata` is a typed write inside a named write.
1037// * `frameRateCategoryRate`, a Parcelable whose values this module does not use.
1038// * `frameRateVelocityMapping`, a `writeTypedList` -- a count and then a body per element, all of
1039// them the class the jar's own sequence was read for. Android 17 added it in place of the
1040// `appsSupportedModes` that 16 wrote, which was an array of Parcelables written by a loop rather
1041// than by a `write*Array`, and which `supportedModes` is still the shape of.
1042// * The `String` fields. `DisplayInfo` writes its own through `writeString8` -- a byte count and
1043// UTF-8 -- while `DeviceProductInfo` writes its through `writeString`, which is a code-unit count
1044// and UTF-16; the descriptor is `Ljava/lang/String;` either way. Within one class the framing is
1045// the class's habit rather than the field's, so a `String` this does not know is read the way the
1046// ones it does are, which is what a field a later release adds gets.
1047//
1048// A *name* this does not know is stepped over by its type, which is what makes a field a later
1049// release added cost its bytes and nothing else -- `displayGroupId`, `renderFrameRate`,
1050// `hasArrSupport`, `supportedRefreshRates`, `userPreferredModeId`, `appsSupportedModes`, `isForceSdr`
1051// and `committedState` are all of them. A *type* this does not know is a failure: the descriptor is
1052// the only thing that says how long the field is, and a `String` alone can be two different lengths.
1053static bool displayInfoField(FFDisplayAndroidReader* reader, const FFDexParcelField* field, const FFDisplayAndroidLayout* layout, FFDisplayAndroidWalk* walk) {
1054 FFDisplayAndroidInfo* info = walk->info;
1055 const char* name = field->name;
1056
1057 if (ffStrEquals(name, "address")) {
1058 return readerDisplayAddress(reader, layout);
1059 }
1060 if (ffStrEquals(name, "deviceProductInfo")) {
1061 return readerDeviceProductInfo(reader, layout, &info->manufactureYear, &info->manufactureWeek);
1062 }
1063 if (ffStrEquals(name, "displayCutout")) {
1064 return readerDisplayCutout(reader, layout);
1065 }
1066 if (ffStrEquals(name, "hdrCapabilities")) {
1067 return readerHdrCapabilities(reader, layout, &info->hdrTypeCount);
1068 }
1069 if (ffStrEquals(name, "frameRateCategoryRate")) {
1070 return readerSkippedParcelable(reader, FF_DISPLAY_ANDROID_NAME_FRAME_RATE_CATEGORY_RATE, layout->frameRateCategoryRate, layout->frameRateCategoryRateCount);
1071 }
1072 if (ffStrEquals(name, "frameRateVelocityMapping")) {
1073 return readerTypedObjectList(reader, layout->velocityPoint, layout->velocityPointCount);
1074 }
1075 if (ffStrEquals(name, "supportedModes") || ffStrEquals(name, "appsSupportedModes")) {
1076 // The two ids the list is resolved against are written in front of it on every release seen.
1077 // One that moved behind it would leave the list resolved against nothing, so the walk says so
1078 // rather than reporting the mode of whatever id happens to sit at the sentinel.
1079 const bool wanted = ffStrEquals(name, "supportedModes");
1080 if (wanted && (info->modeId == INT32_MIN || info->defaultModeId == INT32_MIN)) {
1081 reader->failed = true;
1082 return false;
1083 }
1084 return readerModeList(reader, layout, info->modeId, info->defaultModeId,
1085 wanted ? &walk->active : nullptr, wanted ? &walk->preferred : nullptr);
1086 }
1087 if (ffStrEquals(field->type, "Ljava/lang/String;")) {
1088 if (ffStrEquals(name, "name")) {
1089 return readerString8(reader, &info->name);
1090 }
1091 if (ffStrEquals(name, "uniqueId")) {
1092 return readerString8(reader, walk->uniqueId);
1093 }
1094 return readerString8(reader, nullptr); // `ownerPackageName`, and anything a later release adds
1095 }
1096
1097 FFDisplayAndroidScalar value = {};
1098 if (!readerScalar(reader, field->type, &value)) {
1099 return false;
1100 }
1101 if (ffStrEquals(name, "type")) {
1102 info->type = value.i32;
1103 } else if (ffStrEquals(name, "displayId")) {
1104 info->displayId = (uint32_t) value.i32;
1105 } else if (ffStrEquals(name, "logicalWidth")) {
1106 info->width = (uint32_t) value.i32;
1107 } else if (ffStrEquals(name, "logicalHeight")) {
1108 info->height = (uint32_t) value.i32;
1109 } else if (ffStrEquals(name, "rotation")) {
1110 info->rotation = (uint32_t) value.i32;
1111 } else if (ffStrEquals(name, "modeId")) {
1112 info->modeId = value.i32;
1113 } else if (ffStrEquals(name, "defaultModeId")) {
1114 info->defaultModeId = value.i32;
1115 } else if (ffStrEquals(name, "renderFrameRate")) {
1116 if (!(value.f32 > 0) || value.f32 > 1000) {
1117 reader->failed = true;
1118 return false;
1119 }
1120 info->renderFrameRate = value.f32;
1121 } else if (ffStrEquals(name, "logicalDensityDpi")) {
1122 info->density = (uint32_t) value.i32;
1123 } else if (ffStrEquals(name, "physicalXDpi")) {
1124 info->physicalXDpi = value.f32;
1125 } else if (ffStrEquals(name, "physicalYDpi")) {
1126 info->physicalYDpi = value.f32;
1127 } else if (ffStrEquals(name, "isForceSdr")) {
1128 info->forceSdr = value.i32 != 0;
1129 }
1130 return true;
1131}
1132
1133// How much of a reply a failure prints. A record is 2404 bytes on the test device and most of it is
1134// the two mode lists, so this covers one whole reply -- and the cap is there only so that a reply
1135// which is not one cannot fill the log.
1136#define FF_DISPLAY_ANDROID_DEBUG_DUMP_SIZE 4096
1137
1138#ifndef NDEBUG
1139// The reply is read by shape, so a reply that fails leaves nothing behind to diagnose it with: the
1140// bytes are the only evidence. Each line carries the text beside the bytes, because what the walk
1141// hangs on is the class name in front of a nested body -- UTF-16, and the one thing that a length
1142// read out of the middle of a different field cannot be told from without looking at it.
1143static void debugDumpReply(const uint8_t* data, size_t size) {
1144 const size_t limit = size < FF_DISPLAY_ANDROID_DEBUG_DUMP_SIZE ? size : FF_DISPLAY_ANDROID_DEBUG_DUMP_SIZE;
1145 for (size_t offset = 0; offset < limit; offset += 16) {
1146 char hex[16 * 3 + 1];
1147 char text[16 + 1];
1148 size_t used = 0;
1149 for (size_t i = 0; i < 16 && offset + i < limit; ++i) {
1150 used += (size_t) snprintf(hex + used, sizeof(hex) - used, "%02x ", data[offset + i]);
1151 text[i] = data[offset + i] >= 32 && data[offset + i] < 127 ? (char) data[offset + i] : '.';
1152 text[i + 1] = '\0';
1153 }
1154 FF_DEBUG(" +0x%04zx %-47s | %s", offset, hex, text);
1155 }
1156}
1157 #define FF_DISPLAY_ANDROID_DEBUG_DUMP(data, size) debugDumpReply(data, size)
1158#else
1159 #define FF_DISPLAY_ANDROID_DEBUG_DUMP(data, size) ((void) 0)
1160#endif
1161
1162// One `getDisplayInfo` reply: the exception code the stub writes, the non-null marker
1163// `writeTypedObject` writes in front of the Parcelable, and the record itself -- walked in the order
1164// the device's own `DisplayInfo.writeToParcel` writes it, out of the sequence `displayLayoutLoad`
1165// read from that device's jar.
1166static bool displayInfoParse(const uint8_t* data, size_t size, const FFDisplayAndroidLayout* layout, FFDisplayAndroidInfo* info) {
1167 FFDisplayAndroidReader reader = { .data = data, .size = size, .offset = 0, .failed = false };
1168
1169 const int32_t exception = readerI32(&reader);
1170 if (exception != 0) {
1171 FF_DEBUG("getDisplayInfo raised exception %d", exception);
1172 return false;
1173 }
1174 if (readerI32(&reader) != 1) {
1175 FF_DEBUG("getDisplayInfo answered no display");
1176 FF_DISPLAY_ANDROID_DEBUG_DUMP(data, size);
1177 return false;
1178 }
1179
1180 // Sentinels, so a field the release does not write can not be mistaken for one that was read as
1181 // zero. Every one of these is in the sequence on every release seen and the check below is what
1182 // says so; the two that are not -- `renderFrameRate` and `isForceSdr` -- have a defined fallback
1183 // and are the only ones left out of it.
1184 info->type = -1;
1185 info->displayId = UINT32_MAX;
1186 info->width = 0;
1187 info->height = 0;
1188 info->rotation = UINT32_MAX;
1189 info->modeId = INT32_MIN;
1190 info->defaultModeId = INT32_MIN;
1191 info->hdrTypeCount = 0;
1192 info->renderFrameRate = 0;
1193 info->density = 0;
1194 info->physicalXDpi = 0;
1195 info->physicalYDpi = 0;
1196 info->forceSdr = false;
1197
1198 FF_STRBUF_AUTO_DESTROY uniqueId = ffStrbufCreate();
1199 FFDisplayAndroidWalk walk = { .info = info, .uniqueId = &uniqueId };
1200
1201 // `uniqueId` is the last field this module reads. Everything behind it -- `removeMode`, the
1202 // brightness, the corners and shapes, the throttling -- would have to be walked for nothing, so
1203 // the walk stops here rather than implementing four more nested shapes no value comes out of.
1204 bool complete = false;
1205 for (uint32_t i = 0; i < layout->displayInfoCount; ++i) {
1206 const FFDexParcelField* field = &layout->displayInfo[i];
1207 if (!displayInfoField(&reader, field, layout, &walk)) {
1208 // Which field, out of how many, what the jar says it is and how far the walk got before it
1209 // gave up -- followed by the reply itself, which is what says whether the layout moved or
1210 // this parser is reading a shape it was not written for.
1211 FF_DEBUG(
1212 "getDisplayInfo(%u) answered a %s (%s) this parser does not recognise -- field %u of %u, cursor at %zu of %zu",
1213 info->displayId, field->name, field->type, i + 1, layout->displayInfoCount, reader.offset, reader.size
1214 );
1215 FF_DISPLAY_ANDROID_DEBUG_DUMP(data, size);
1216 return false;
1217 }
1218 if (ffStrEquals(field->name, "uniqueId")) {
1219 complete = true;
1220 break;
1221 }
1222 }
1223 if (!complete) {
1224 FF_DEBUG("getDisplayInfo(%u) answered a record with no uniqueId", info->displayId);
1225 FF_DISPLAY_ANDROID_DEBUG_DUMP(data, size);
1226 return false;
1227 }
1228
1229 if (info->type < 0 || info->type > FF_DISPLAY_ANDROID_TYPE_VIRTUAL
1230 || info->displayId == UINT32_MAX
1231 || info->width == 0 || info->width > 65536 || info->height == 0 || info->height > 65536
1232 || info->rotation > 3
1233 || info->modeId < 0 || info->defaultModeId < -1
1234 || info->density > 4000
1235 || !(info->physicalXDpi >= 0) || info->physicalXDpi > 2000
1236 || !(info->physicalYDpi >= 0) || info->physicalYDpi > 2000
1237 // A display with no name is still a display -- the key falls back to a number -- so only a
1238 // name that could not have come from a string is a reason to drop the record.
1239 || info->name.length > 256
1240 || uniqueId.length > 256) {
1241 FF_DEBUG(
1242 "getDisplayInfo(%u) answered type %d, %u x %u, rotation %u, mode %d, density %u, %f x %f dpi",
1243 info->displayId, info->type, info->width, info->height, info->rotation, info->modeId, info->density,
1244 info->physicalXDpi, info->physicalYDpi
1245 );
1246 FF_DISPLAY_ANDROID_DEBUG_DUMP(data, size);
1247 return false;
1248 }
1249
1250 info->preferredWidth = walk.preferred.width;
1251 info->preferredHeight = walk.preferred.height;
1252 info->preferredRefreshRate = walk.preferred.refreshRate;
1253 // The nominal rate of the active mode, which is what `Display.getRefreshRate()` reports and what
1254 // every other platform reports. `renderFrameRate` is only used when the list has no entry for
1255 // `modeId`, because it is a render cadence that follows the content rather than a property of the
1256 // display -- and it is not written at all before Android 15. `refreshRateOverride` is deliberately
1257 // not consulted either: it is what an app asked the framework to hold the panel at, not a
1258 // property of the display.
1259 info->refreshRate = walk.active.refreshRate > 0 ? walk.active.refreshRate : info->renderFrameRate;
1260 info->id = displayUniqueId(uniqueId.chars);
1261 return true;
1262}
1263
1264static FFDisplayType displayType(int32_t type) {
1265 switch ((FFDisplayAndroidType) type) {
1266 case FF_DISPLAY_ANDROID_TYPE_INTERNAL:
1267 return FF_DISPLAY_TYPE_BUILTIN;
1268 case FF_DISPLAY_ANDROID_TYPE_EXTERNAL:
1269 case FF_DISPLAY_ANDROID_TYPE_WIFI:
1270 // A WIFI display is a wireless sink, which is as external as a wired one
1271 return FF_DISPLAY_TYPE_EXTERNAL;
1272 default:
1273 return FF_DISPLAY_TYPE_UNKNOWN;
1274 }
1275}
1276
1277// `hdrCapabilities.mSupportedHdrTypes` is the display's own answer and an empty list means it can not
1278// do HDR at all. `isForceSdr` is the framework having turned every capability of this display off and
1279// `persist.sys.hdr_mode` is the vendor's switch, so `Supported` and `Enabled` stay two different
1280// questions: the list is what the panel can do, those two are whether anything is asking for it.
1281//
1282// A non-empty list does not mean the display does HDR. The list is the answer of the vendor's composer
1283// HAL, which SurfaceFlinger and DisplayManagerService pass through unchanged, and it is wrong on the
1284// test device (vivo PD2505, Android 17): `dumpsys display` reports `mSupportedHdrTypes=[1, 2, 3]` --
1285// Dolby Vision, HDR10 and HLG -- for a Samsung U32J59x that does not do HDR at all, and the same
1286// three types hang off every one of its forty-odd modes, down to `640x480@60`, which is a VESA DMT
1287// mode and not a CTA-861 one. An answer derived per mode from the sink's EDID HDR Static Metadata
1288// block would not look like that, and no Samsung monitor claims Dolby Vision. Nothing on the device
1289// contradicts it either: SurfaceFlinger dumps every mode of that display as `hdrOutputType=INVALID`
1290// where the built-in panel's are `SYSTEM`, its only color mode is `ColorMode::NATIVE`, and `cmd
1291// display get-reported-hdr-types 4` repeats the same `[1, 2, 3]`. The raw EDID would settle it and is
1292// out of reach: `/sys/class/drm/card0-DP-1/edid` exists but is denied by SELinux, and `adb root` is
1293// refused on a retail build. So the list is reported as it arrives -- it is the only answer the
1294// platform gives, and there is nothing here to cross check it against.
1295static FFDisplayHdrStatus displayHdrStatus(const FFDisplayAndroidInfo* info) {
1296 if (info->hdrTypeCount <= 0) {
1297 return FF_DISPLAY_HDR_STATUS_UNSUPPORTED;
1298 }
1299 FF_STRBUF_AUTO_DESTROY hdrMode = ffStrbufCreate();
1300 if (!info->forceSdr && ffSettingsGetAndroidProperty("persist.sys.hdr_mode", &hdrMode) && ffStrbufToUInt(&hdrMode, 0) > 0) {
1301 return FF_DISPLAY_HDR_STATUS_ENABLED;
1302 }
1303 return FF_DISPLAY_HDR_STATUS_SUPPORTED;
1304}
1305
1306// Reads every enabled display over binder. Returns false without appending anything when the service,
1307// the transaction or the layout is not what this parser was written for, which is also what hands the
1308// module over to the `cmd` fallback.
1309static bool detectWithBinder(FFDisplayServerResult* ds) {
1310 // The write sequence first. It is what the whole parse is made of, and a build whose jar cannot be
1311 // read has nothing to walk -- so failing here costs one file open rather than a round trip to a
1312 // service whose answer could not be read anyway.
1313 FFDisplayAndroidLayout layout = {};
1314 if (!displayLayoutLoad(FF_DISPLAY_ANDROID_JAR, &layout)) {
1315 return false;
1316 }
1317
1318 [[gnu::cleanup(ffBinderClose)]] FFBinder binder = { .fd = -1 };
1319 const char* error = ffBinderOpen(&binder);
1320 if (error != nullptr) {
1321 FF_DEBUG("Reading the display over binder failed: %s", error);
1322 return false;
1323 }
1324
1325 // Released on every path out, including the early returns below.
1326 [[gnu::cleanup(ffBinderServiceHandleRelease)]] FFBinderServiceHandle service = { .binder = &binder };
1327 error = ffBinderLookupService(&binder, FF_DISPLAY_ANDROID_SERVICE, FF_BINDER_SM_GET_SERVICE, &service.handle);
1328 if (error != nullptr) {
1329 FF_DEBUG("Looking up the \"%s\" service failed: %s", FF_DISPLAY_ANDROID_SERVICE, error);
1330 return false;
1331 }
1332
1333 uint8_t parcelBuffer[FF_DISPLAY_ANDROID_PARCEL_SIZE];
1334 uint8_t replyBuffer[FF_DISPLAY_ANDROID_REPLY_SIZE];
1335
1336 // `getDisplayIds(false)` is the list of the enabled displays, which is the set a display dump
1337 // prints. It is a separate call rather than a loop over 0..n because the ids are not contiguous,
1338 // and it is twelve bytes.
1339 //
1340 // The argument is written unconditionally although the method takes none on Android 11: the
1341 // generated stub reads the parameters its own signature declares and leaves the rest of the
1342 // parcel alone, so the extra int is dropped on those releases.
1343 FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer));
1344 ffBinderParcelPutInterfaceToken(&parcel, FF_DISPLAY_ANDROID_DESCRIPTOR);
1345 ffBinderParcelPutI32(&parcel, 0); // includeDisabled
1346
1347 FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer));
1348 error = ffBinderTransact(&binder, service.handle, FF_DISPLAY_ANDROID_TRANSACTION_GET_DISPLAY_IDS, 0, &parcel, &reply);
1349 if (error != nullptr) {
1350 FF_DEBUG("getDisplayIds failed: %s", error);
1351 return false;
1352 }
1353 if (ffBinderReplyIsStatus(&reply)) {
1354 FF_DEBUG("getDisplayIds answered status %d instead of a parcel", (int) ffBinderReadI32(reply.data, reply.size, 0));
1355 return false;
1356 }
1357 const int32_t exception = ffBinderReadI32(reply.data, reply.size, 0);
1358 if (exception != 0) {
1359 FF_DEBUG("getDisplayIds raised exception %d", exception);
1360 return false;
1361 }
1362 const int32_t count = ffBinderReadI32(reply.data, reply.size, sizeof(int32_t));
1363 if (count <= 0 || count > FF_DISPLAY_ANDROID_MAX_DISPLAYS) {
1364 FF_DEBUG("getDisplayIds answered %d displays", count);
1365 FF_DISPLAY_ANDROID_DEBUG_DUMP(reply.data, reply.size);
1366 return false;
1367 }
1368
1369 int32_t displayIds[FF_DISPLAY_ANDROID_MAX_DISPLAYS] = {};
1370 for (int32_t i = 0; i < count; ++i) {
1371 displayIds[i] = ffBinderReadI32(reply.data, reply.size, (size_t) (2 + i) * sizeof(int32_t));
1372 if (displayIds[i] < 0) {
1373 FF_DEBUG("getDisplayIds answered display id %d", displayIds[i]);
1374 FF_DISPLAY_ANDROID_DEBUG_DUMP(reply.data, reply.size);
1375 return false;
1376 }
1377 }
1378
1379 // Every display has to parse before any of them is reported, so the records are held here until
1380 // the loop has been through all of them.
1381 FFDisplayAndroidInfo infos[FF_DISPLAY_ANDROID_MAX_DISPLAYS] = {};
1382 int32_t parsed = 0;
1383 for (; parsed < count; ++parsed) {
1384 FFDisplayAndroidInfo* info = &infos[parsed];
1385 ffStrbufInit(&info->name);
1386
1387 FFBinderParcel request = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer));
1388 ffBinderParcelPutInterfaceToken(&request, FF_DISPLAY_ANDROID_DESCRIPTOR);
1389 ffBinderParcelPutI32(&request, displayIds[parsed]);
1390
1391 reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer));
1392 error = ffBinderTransact(&binder, service.handle, FF_DISPLAY_ANDROID_TRANSACTION_GET_DISPLAY_INFO, 0, &request, &reply);
1393 if (error != nullptr) {
1394 FF_DEBUG("getDisplayInfo(%d) failed: %s", displayIds[parsed], error);
1395 break;
1396 }
1397 if (ffBinderReplyIsStatus(&reply)) {
1398 FF_DEBUG("getDisplayInfo(%d) answered status %d instead of a parcel", displayIds[parsed], (int) ffBinderReadI32(reply.data, reply.size, 0));
1399 break;
1400 }
1401 if (!displayInfoParse(reply.data, reply.size, &layout, info)) {
1402 FF_DEBUG("getDisplayInfo(%d) answered a record this parser does not recognise", displayIds[parsed]);
1403 break;
1404 }
1405 }
1406
1407 if (parsed != count) {
1408 for (int32_t i = 0; i <= parsed && i < count; ++i) {
1409 ffStrbufDestroy(&infos[i].name);
1410 }
1411 return false;
1412 }
1413
1414 for (int32_t i = 0; i < count; ++i) {
1415 FFDisplayAndroidInfo* info = &infos[i];
1416
1417 // The physical dpi describes the panel itself and does not change with the logical display
1418 // size, so the physical size has to be derived from the native resolution. Nothing in the
1419 // record carries it directly.
1420 uint32_t physicalWidth = 0, physicalHeight = 0;
1421 if (info->physicalXDpi > 0) {
1422 physicalWidth = (uint32_t) ((info->preferredWidth ? info->preferredWidth : info->width) * 25.4 / info->physicalXDpi + 0.5);
1423 }
1424 if (info->physicalYDpi > 0) {
1425 physicalHeight = (uint32_t) ((info->preferredHeight ? info->preferredHeight : info->height) * 25.4 / info->physicalYDpi + 0.5);
1426 }
1427
1428 FFDisplayResult* display = ffdsAppendDisplay(ds,
1429 info->width,
1430 info->height,
1431 info->refreshRate,
1432 info->density * 96 / 160,
1433 info->preferredWidth,
1434 info->preferredHeight,
1435 info->preferredRefreshRate,
1436 info->rotation,
1437 &info->name, // moved
1438 displayType(info->type),
1439 info->displayId == 0, // Display 0 is the default one
1440 info->id,
1441 physicalWidth,
1442 physicalHeight,
1443 "binder");
1444 if (display == nullptr) {
1445 // `ffdsAppendDisplay` moves `name` only when it keeps the display, so it is still ours
1446 ffStrbufDestroy(&info->name);
1447 continue;
1448 }
1449 display->manufactureYear = info->manufactureYear;
1450 display->manufactureWeek = info->manufactureWeek;
1451 display->hdrStatus = displayHdrStatus(info);
1452 }
1453 return ds->displays.length > 0;
1454}
1455
1456// ---------------------------------------------------------------------------------------------
1457// The `cmd` fallback
1458// ---------------------------------------------------------------------------------------------
1459//
1460// The route behind the walk, for a build whose `framework.jar` can not be read at all or whose parcel
1461// layout is not one the walk knows. `cmd display get-displays` prints the `DisplayInfo` of every
1462// display as text, one record per line, so the same fields are there to read with no dex and no
1463// parcel -- at the price of a fork, which is what the walk above exists to avoid.
1464//
1465// It needs no permission, which is what makes it usable for an app UID, but the subcommand was only
1466// added to `DisplayManagerShellCommand` in Android 13: Android 11 and 12 answer `Unknown command:
1467// get-displays` on stdout with exit code 255 and this route reports nothing. Those two releases are
1468// read by the walk instead, so the fallback only has to cover 13 and newer.
1469//
1470// The record layout has changed across releases, and every difference is accepted rather than version
1471// checked:
1472//
1473// * The mode list is printed as `modes [...]` up to Android 14 and as `supportedModes [...]` from
1474// Android 15 on, which also prints `appsSupportedModes [...]` right behind it. Both spellings are
1475// searched for.
1476// * `renderFrameRate` is printed from Android 15 on. Before that the active mode's fps is the only
1477// refresh rate the dump carries. The active mode's fps is preferred even where it exists, see
1478// the comment on `refreshRate` below.
1479// * `displayGroupId` is printed from Android 12 on, the physical dpi behind `density` from
1480// Android 11 on, and `isForceSdr` from Android 15 on.
1481//
1482// A record is one line and the `DisplayInfo{` inside it is what gets parsed, so the `Display id 0: `
1483// prefix the command prints in front of each record is stepped over by the same search that finds it.
1484// `marker` and `platformApi` are the two things a second command would differ in; only `cmd` is wired
1485// up, `dumpsys display` prints the same records behind `mBaseDisplayInfo=` and is not a route here.
1486static bool detectWithCommand(FFDisplayServerResult* ds, char* const argv[], const char* marker, const char* platformApi) {
1487 FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();
1488 FFProcessHandle handle;
1489 // `cmd` forwards its stdin to the service over binder, and the kernel rejects the whole
1490 // transaction when that fd is a terminal, which it is whenever fastfetch runs in a terminal.
1491 // Detaching the child from our stdin is only needed here, so the low level API is called instead
1492 // of `ffProcessAppendStdOut`.
1493 if (ffProcessSpawn(argv, FF_PROCESS_OUTPUT_STDOUT_BIT, ffGetNullFD(), &handle) != nullptr) {
1494 return false; // `/system/bin/cmd` is not there before Android 13
1495 }
1496
1497 if (ffProcessReadOutput(&handle, &buf) != nullptr || buf.length == 0) {
1498 return false;
1499 }
1500 ffStrbufTrimRightSpace(&buf);
1501
1502 uint32_t index = 0;
1503 while ((index = ffStrbufNextIndexS(&buf, index, marker)) < buf.length) {
1504 index += strlen(marker);
1505
1506 uint32_t nextIndex = ffStrbufNextIndexC(&buf, index, '\n');
1507 buf.chars[nextIndex] = '\0';
1508 const char* info = buf.chars + index;
1509
1510 // 0: DisplayInfo{"Builtin display", displayId 0, ..., real 1440 x 3168, ..., mode 2,
1511 // renderFrameRate 60.000004, ..., defaultMode 4, ..., supportedModes [{id=2,
1512 // width=1440, height=3168, fps=60.000004, ...}], ..., hdrCapabilities
1513 // HdrCapabilities{mSupportedHdrTypes=[1, 2, 3, 4], ...}, isForceSdr false, ...,
1514 // rotation 0, ..., type INTERNAL, uniqueId "local:4630946557703207059", ...,
1515 // density 560 (560.0 x 560.0) dpi, ..., deviceProductInfo DeviceProductInfo{...,
1516 // manufactureDate=ManufactureDate{week=27, year=2006}, ...}, ...}
1517 const char* field = strstr(info, "DisplayInfo{\"");
1518 FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateA(64);
1519 if (field) {
1520 field += strlen("DisplayInfo{\"");
1521 const char* nameEnd = strchr(field, '"');
1522 if (nameEnd) {
1523 ffStrbufAppendNS(&name, (uint32_t) (nameEnd - field), field);
1524 }
1525 }
1526
1527 // `real` is the size the display currently uses, which is smaller than the panel's when the
1528 // framework emulates a smaller display size
1529 unsigned width = 0, height = 0;
1530 if ((field = strstr(info, ", real ")) && sscanf(field, ", real %u x %u", &width, &height) < 2) {
1531 width = height = 0;
1532 }
1533
1534 // `renderFrameRate` is printed from Android 15 on. It is documented as "a divisor of the
1535 // active mode refresh rate", so it is the rate the display is currently *rendering* at and
1536 // can be lower than the mode it is set to. It is therefore only used for a record whose
1537 // mode list can not be read, where a possibly divided rate still beats none.
1538 double renderFrameRate = 0;
1539 if ((field = strstr(info, ", renderFrameRate ")) && sscanf(field, ", renderFrameRate %lf", &renderFrameRate) < 1) {
1540 renderFrameRate = 0;
1541 }
1542
1543 unsigned activeMode = 0, defaultMode = 0;
1544 if ((field = strstr(info, ", mode ")) && sscanf(field, ", mode %u", &activeMode) < 1) {
1545 activeMode = 0;
1546 }
1547 if ((field = strstr(info, ", defaultMode ")) && sscanf(field, ", defaultMode %u", &defaultMode) < 1) {
1548 defaultMode = 0;
1549 }
1550
1551 // The modes are listed with their resolution in the natural orientation, which is also how
1552 // the preferred values are reported on the other platforms. `defaultMode` is the mode the
1553 // display itself prefers, so it carries the panel's native resolution.
1554 //
1555 // One entry is a `{...}`, and the three fields are searched for inside it rather than matched
1556 // as a single prefix: Android 15 prints `parentModeId`, `sfModeId` and `flags` between `id`
1557 // and `width`, where `sscanf("{id=%u, width=%u, ...")` stops after the id and reports one
1558 // conversion -- measured on the test device, that is a preferred resolution of 0 x 0 and a
1559 // rate that falls back to `renderFrameRate`. Everything up to the entry's own closing brace
1560 // belongs to it, so a field past that brace is the next entry's and ends the walk.
1561 uint32_t preferredWidth = 0, preferredHeight = 0;
1562 double preferredRefreshRate = 0, activeModeRefreshRate = 0;
1563 field = strstr(info, ", supportedModes [");
1564 if (field == nullptr) {
1565 field = strstr(info, ", modes ["); // Android 14 and older
1566 }
1567 while (field && (field = strstr(field, "{id="))) {
1568 // {id=2, ..., width=1440, height=3168, fps=60.000004, ...
1569 const char* end = strchr(field, '}');
1570 const char* modeWidthField = strstr(field, ", width=");
1571 const char* modeHeightField = strstr(field, ", height=");
1572 const char* modeFpsField = strstr(field, ", fps=");
1573 unsigned id = 0, modeWidth = 0, modeHeight = 0;
1574 double fps = 0;
1575 if (end == nullptr || modeWidthField == nullptr || modeWidthField > end ||
1576 modeHeightField == nullptr || modeHeightField > end ||
1577 modeFpsField == nullptr || modeFpsField > end) {
1578 break; // Not a mode entry this parser can read
1579 }
1580 if (sscanf(field, "{id=%u", &id) < 1 ||
1581 sscanf(modeWidthField, ", width=%u", &modeWidth) < 1 ||
1582 sscanf(modeHeightField, ", height=%u", &modeHeight) < 1 ||
1583 sscanf(modeFpsField, ", fps=%lf", &fps) < 1) {
1584 break;
1585 }
1586 if (id == activeMode) {
1587 activeModeRefreshRate = fps;
1588 }
1589 if (id == defaultMode) {
1590 preferredWidth = modeWidth;
1591 preferredHeight = modeHeight;
1592 preferredRefreshRate = fps;
1593 }
1594 if (activeModeRefreshRate > 0 && preferredWidth > 0) {
1595 break; // Both are in, and the same list is printed a second time from Android 15 on
1596 }
1597 ++field;
1598 }
1599
1600 // The nominal rate of the active mode, which is what `Display.getRefreshRate()` reports
1601 // (`refreshRateOverride` if it is set, the mode's own rate otherwise) and what every other
1602 // platform reports. `renderFrameRate` is deliberately not preferred: it is a render
1603 // cadence that follows the content rather than a property of the display, and it does not
1604 // exist before Android 15, so using it would make the reported rate change with the
1605 // Android version as well as with what is on screen.
1606 double refreshRate = activeModeRefreshRate;
1607 if (refreshRate <= 0) {
1608 refreshRate = renderFrameRate;
1609 }
1610
1611 unsigned rotation = 0;
1612 if ((field = strstr(info, ", rotation ")) && sscanf(field, ", rotation %u", &rotation) < 1) {
1613 rotation = 0;
1614 }
1615
1616 FFDisplayType type = FF_DISPLAY_TYPE_UNKNOWN;
1617 if ((field = strstr(info, ", type "))) {
1618 field += strlen(", type ");
1619 if (ffStrStartsWith(field, "INTERNAL")) {
1620 type = FF_DISPLAY_TYPE_BUILTIN;
1621 } else if (ffStrStartsWith(field, "EXTERNAL") || ffStrStartsWith(field, "WIFI")) {
1622 // A WIFI display is a wireless sink, which is as external as a wired one
1623 type = FF_DISPLAY_TYPE_EXTERNAL;
1624 }
1625 }
1626
1627 unsigned density = 0;
1628 double physicalXDpi = 0, physicalYDpi = 0;
1629 if ((field = strstr(info, ", density "))) {
1630 // `density 640 (501.0411 x 509.28604) dpi`, the physical dpi is only printed since
1631 // Android 11
1632 if (sscanf(field, ", density %u (%lf x %lf) dpi", &density, &physicalXDpi, &physicalYDpi) < 1) {
1633 density = 0;
1634 }
1635 }
1636
1637 // The physical dpi describes the panel itself and does not change with the logical display
1638 // size, so the physical size has to be derived from the native resolution
1639 uint32_t physicalWidth = 0, physicalHeight = 0;
1640 if (physicalXDpi > 0) {
1641 physicalWidth = (uint32_t) ((preferredWidth ? preferredWidth : width) * 25.4 / physicalXDpi + 0.5);
1642 }
1643 if (physicalYDpi > 0) {
1644 physicalHeight = (uint32_t) ((preferredHeight ? preferredHeight : height) * 25.4 / physicalYDpi + 0.5);
1645 }
1646
1647 // `uniqueId` identifies the display across reboots, e.g. `local:4630946557703207059` on a
1648 // physical display and `virtual:...` on a virtual one
1649 uint64_t id = 0;
1650 if ((field = strstr(info, ", uniqueId \""))) {
1651 field += strlen(", uniqueId \"");
1652 const char* uniqueIdEnd = strchr(field, '"');
1653 const char* digits = uniqueIdEnd ? memchr(field, ':', (size_t) (uniqueIdEnd - field)) : nullptr;
1654 id = (uint64_t) strtoull(digits ? digits + 1 : field, nullptr, 10);
1655 }
1656
1657 uint16_t manufactureYear = 0, manufactureWeek = 0;
1658 if ((field = strstr(info, ", deviceProductInfo "))) {
1659 // `manufactureDate=ManufactureDate{week=27, year=2006}`, either field may be `null`
1660 const char* date = strstr(field, "manufactureDate=ManufactureDate{");
1661 unsigned year = 0, week = 0;
1662 if (date && sscanf(date + strlen("manufactureDate=ManufactureDate{"), "week=%u, year=%u", &week, &year) == 2) {
1663 manufactureYear = (uint16_t) year;
1664 manufactureWeek = (uint16_t) week;
1665 } else if ((field = strstr(field, ", modelYear=")) && sscanf(field, ", modelYear=%u", &year) == 1) {
1666 // A display reports either the date of manufacture or the model year
1667 manufactureYear = (uint16_t) year;
1668 }
1669 }
1670
1671 // `displayId` sits inside the record, not in front of it: the id in the prefix the command
1672 // prints is what the marker search stepped over, and the field itself is what this reads
1673 unsigned displayId = 0;
1674 if ((field = strstr(info, ", displayId ")) && sscanf(field, ", displayId %u", &displayId) < 1) {
1675 displayId = 0;
1676 }
1677 bool primary = displayId == 0; // Display 0 is the default one
1678
1679 // Android counts density in dpi with 160 as the 1x baseline, fastfetch uses 96
1680 FFDisplayResult* display = ffdsAppendDisplay(ds,
1681 width,
1682 height,
1683 refreshRate,
1684 density * 96 / 160,
1685 preferredWidth,
1686 preferredHeight,
1687 preferredRefreshRate,
1688 rotation,
1689 &name,
1690 type,
1691 primary,
1692 id,
1693 physicalWidth,
1694 physicalHeight,
1695 platformApi);
1696 if (display) {
1697 display->manufactureYear = manufactureYear;
1698 display->manufactureWeek = manufactureWeek;
1699
1700 // Reported for every display, not only for the built-in one: `hdrCapabilities` is a
1701 // field of the `DisplayInfo` record itself, so it describes that display and nothing
1702 // else, and the other platforms report HDR per display too (EDID on Linux, the
1703 // advanced color info per target on Windows). An external display or a wireless sink
1704 // carries the field as well.
1705 //
1706 // `hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[1, 2, 3, 4], ...}` is printed
1707 // since Android 11, where an empty list means that the display can not do HDR at all.
1708 // The two fallbacks below it are device wide vendor properties, which is the price of
1709 // answering for a record that does not print the field.
1710 //
1711 // This is the same field and the same vendor answer the walk above reads, so the same
1712 // caveat applies: a non-HDR display can arrive with a non-empty list, see
1713 // displayHdrStatus.
1714 FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
1715 field = strstr(info, "hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[");
1716 if (field) {
1717 field += strlen("hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[");
1718 display->hdrStatus = *field == ']' ? FF_DISPLAY_HDR_STATUS_UNSUPPORTED : FF_DISPLAY_HDR_STATUS_SUPPORTED;
1719 } else if (ffSettingsGetAndroidProperty("ro.surface_flinger.has_HDR_display", &buffer)) {
1720 display->hdrStatus = ffStrbufIgnCaseEqualS(&buffer, "true") ? FF_DISPLAY_HDR_STATUS_SUPPORTED : FF_DISPLAY_HDR_STATUS_UNSUPPORTED;
1721 } else {
1722 display->hdrStatus = FF_DISPLAY_HDR_STATUS_UNKNOWN;
1723 }
1724
1725 if (display->hdrStatus == FF_DISPLAY_HDR_STATUS_SUPPORTED) {
1726 // `persist.sys.hdr_mode` is non-zero while HDR is turned on, and `isForceSdr
1727 // true` means that the framework disabled every HDR capability of this display.
1728 // Note that `ffSettingsGetAndroidProperty` appends, so the value needs its own
1729 // buffer.
1730 FF_STRBUF_AUTO_DESTROY hdrMode = ffStrbufCreate();
1731 if (ffSettingsGetAndroidProperty("persist.sys.hdr_mode", &hdrMode) &&
1732 ffStrbufToUInt(&hdrMode, 0) > 0 &&
1733 !strstr(info, ", isForceSdr true")) {
1734 display->hdrStatus = FF_DISPLAY_HDR_STATUS_ENABLED;
1735 }
1736 }
1737 }
1738
1739 // The last display of the dump is not followed by a newline, so the loop must not step
1740 // past the end of the buffer (`ffStrbufNextIndexC` returns the length when it finds none)
1741 index = nextIndex < buf.length ? nextIndex + 1 : buf.length;
1742 }
1743
1744 // A command that produced no `DisplayInfo` record has to be reported as a failure, so that the
1745 // caller can tell "this build answers differently" from "this build answered nothing"
1746 return ds->displays.length > 0;
1747}
1748
1749static bool detectWithCmd(FFDisplayServerResult* ds) {
1750 return detectWithCommand(ds,
1751 (char*[]) { "/system/bin/cmd", "display", "get-displays", nullptr },
1752 "Display id ",
1753 "cmd");
1754}
1755
1756// Several vendors embed the UI name and its version in `ro.build.display.id` without any
1757// separator, e.g. `MyOS12.0.14_A2121` or `RedMagicOS10.0.24_NX779J`.
1758static bool detectDEFromDisplayId(FFDisplayServerResult* ds, const char* const* names, uint32_t count) {
1759 FF_STRBUF_AUTO_DESTROY displayId = ffStrbufCreate();
1760 if (!ffSettingsGetAndroidProperty("ro.build.display.id", &displayId)) {
1761 return false;
1762 }
1763
1764 for (uint32_t i = 0; i < count; i++) {
1765 uint32_t length = (uint32_t) strlen(names[i]);
1766 if (!ffStrbufStartsWithS(&displayId, names[i])) {
1767 continue;
1768 }
1769
1770 ffStrbufSubstrBeforeFirstC(&displayId, '_'); // Drop the model suffix
1771 if (displayId.length > length) {
1772 ffStrbufInsertNC(&displayId, length, 1, ' ');
1773 }
1774 ffStrbufSet(&ds->dePrettyName, &displayId);
1775 return true;
1776 }
1777
1778 return false;
1779}
1780
1781static bool detectDE(FFDisplayServerResult* ds) {
1782 FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
1783 FF_STRBUF_AUTO_DESTROY brand = ffStrbufCreate();
1784
1785 // `ffSettingsGetAndroidProperty` appends to the given buffer, so the brand has to be read
1786 // into its own buffer exactly once: reading it into a shared buffer repeatedly would
1787 // concatenate the values and break every comparison against it.
1788 ffSettingsGetAndroidProperty("ro.product.brand", &brand);
1789
1790 // vivo reports the marketing name and version in `ro.vivo.os.build.display.id`,
1791 // separated by an underscore (`Funtouch OS_10`, `OriginOS 5`), and the build number
1792 // in `ro.vivo.product.version`.
1793 if (ffSettingsGetAndroidProperty("ro.vivo.os.build.display.id", &ds->dePrettyName)) {
1794 ffStrbufReplaceAllC(&ds->dePrettyName, '_', ' ');
1795 if (ffSettingsGetAndroidProperty("ro.vivo.product.version", &buffer)) {
1796 ffStrbufAppendC(&ds->dePrettyName, ' ');
1797 ffStrbufAppend(&ds->dePrettyName, &buffer);
1798 }
1799 return true;
1800 }
1801
1802 // HarmonyOS 2.0 - 4.x is built on top of the Android framework and keeps reporting an
1803 // EMUI version, but it is HarmonyOS. `ro.build.ohos.devicetype` is set only by those
1804 // builds, and `hw_sc.build.platform.version` is the HarmonyOS version, while
1805 // `ro.build.version.emui` only carries the EMUI compatibility version
1806 // (HarmonyOS 2.0 == EMUI 12, 3.0 == 13, 4.0 == 14, 4.2 == 14.2, 4.3 == 15).
1807 if (ffSettingsGetAndroidProperty("ro.build.ohos.devicetype", &buffer)) {
1808 ffStrbufClear(&buffer);
1809 if (!ffSettingsGetAndroidProperty("hw_sc.build.platform.version", &buffer) &&
1810 ffSettingsGetAndroidProperty("ro.huawei.build.display.id", &buffer)) {
1811 // A few builds leave `hw_sc.build.platform.version` unset and only expose the
1812 // version through `ro.huawei.build.display.id`, e.g. `JKM-AL00 2.0.0.263(C00E260R4P3)`
1813 ffStrbufSubstrAfterFirstC(&buffer, ' ');
1814 ffStrbufSubstrBeforeFirstC(&buffer, '(');
1815 ffStrbufSubstrBeforeLastC(&buffer, '.');
1816 }
1817 if (buffer.length > 0) {
1818 ffStrbufSetF(&ds->dePrettyName, "HarmonyOS %s", buffer.chars);
1819 } else {
1820 ffStrbufSetStatic(&ds->dePrettyName, "HarmonyOS");
1821 }
1822 return true;
1823 }
1824
1825 // HarmonyOS NEXT (5.0 and newer) drops the Android framework. It is the only Huawei
1826 // family reporting `ro.build.display.id` as `System 104.5.0.001(60J9)`, and none of its
1827 // properties carries the marketing version, so it can only be named without one.
1828 if (ffStrbufIgnCaseEqualS(&brand, "HUAWEI") &&
1829 ffSettingsGetAndroidProperty("ro.build.display.id", &buffer) &&
1830 ffStrbufStartsWithS(&buffer, "System ")) {
1831 ffStrbufSetStatic(&ds->dePrettyName, "HarmonyOS NEXT");
1832 return true;
1833 }
1834
1835 // HONOR reports MagicOS / MagicUI in `ro.build.version.magic`. MagicUI 3.x stores a
1836 // bare version number (`3.0.1`) instead of a `MagicUI_x.y.z` string.
1837 if (ffSettingsGetAndroidProperty("ro.build.version.magic", &ds->dePrettyName)) {
1838 ffStrbufReplaceAllC(&ds->dePrettyName, '_', ' ');
1839 if (!ffStrbufStartsWithS(&ds->dePrettyName, "Magic")) {
1840 ffStrbufPrependS(&ds->dePrettyName, "MagicUI ");
1841 }
1842 return true;
1843 }
1844
1845 if (ffSettingsGetAndroidProperty("ro.build.version.emui", &ds->dePrettyName)) {
1846 ffStrbufReplaceAllC(&ds->dePrettyName, '_', ' ');
1847 return true;
1848 }
1849
1850 // Xiaomi HyperOS. `ro.mi.os.version.incremental` is `OS1.0.10.0.TLDCNXM`, while
1851 // `ro.build.version.incremental` still starts with `V816` on HyperOS 1.0, so it can
1852 // not be used to tell HyperOS and MIUI apart.
1853 if (ffSettingsGetAndroidProperty("ro.mi.os.version.name", &ds->dePrettyName)) {
1854 ffStrbufClear(&ds->dePrettyName);
1855 if (ffSettingsGetAndroidProperty("ro.mi.os.version.incremental", &ds->dePrettyName) &&
1856 ffStrbufStartsWithS(&ds->dePrettyName, "OS")) {
1857 ffStrbufSubstrAfter(&ds->dePrettyName, 1); // Drop the leading "OS"
1858 ffStrbufPrependS(&ds->dePrettyName, "HyperOS ");
1859 } else {
1860 ffStrbufSetStatic(&ds->dePrettyName, "HyperOS");
1861 }
1862 return true;
1863 }
1864
1865 // Black Shark runs JOYUI, a MIUI fork, and therefore also sets
1866 // `ro.miui.ui.version.name`. `ro.build.version.incremental` is `V11.0.4.0.JOYUI`.
1867 if (ffStrbufIgnCaseEqualS(&brand, "blackshark")) {
1868 ffStrbufClear(&ds->dePrettyName);
1869 if (ffSettingsGetAndroidProperty("ro.build.version.incremental", &ds->dePrettyName)) {
1870 ffStrbufSubstrBeforeLastC(&ds->dePrettyName, '.'); // Drop the trailing "JOYUI"
1871 if (ffStrbufStartsWithS(&ds->dePrettyName, "V")) {
1872 ffStrbufSubstrAfter(&ds->dePrettyName, 0);
1873 }
1874 ffStrbufPrependS(&ds->dePrettyName, "JOYUI ");
1875 } else {
1876 ffStrbufSetStatic(&ds->dePrettyName, "JOYUI");
1877 }
1878 return true;
1879 }
1880
1881 // MIUI. `ro.build.version.incremental` is `V14.0.1.0.TJJCNXM` on stable builds and a
1882 // bare release date (`21.11.17`) on beta builds.
1883 if (ffSettingsGetAndroidProperty("ro.miui.ui.version.name", &buffer)) {
1884 ffStrbufClear(&ds->dePrettyName);
1885 if (ffSettingsGetAndroidProperty("ro.build.version.incremental", &ds->dePrettyName)) {
1886 if (ffStrbufStartsWithS(&ds->dePrettyName, "V")) {
1887 ffStrbufSubstrAfter(&ds->dePrettyName, 0);
1888 }
1889 ffStrbufPrependS(&ds->dePrettyName, "MiUI ");
1890 } else {
1891 ffStrbufSetStatic(&ds->dePrettyName, "MiUI");
1892 }
1893 return true;
1894 }
1895
1896 // realme UI is a ColorOS fork and reports both `ro.build.version.realmeui` and
1897 // `ro.build.version.oplusrom`; the former is the realme UI version.
1898 if (ffSettingsGetAndroidProperty("ro.build.version.realmeui", &ds->dePrettyName)) {
1899 if (ffStrbufStartsWithS(&ds->dePrettyName, "V")) {
1900 ffStrbufSubstrAfter(&ds->dePrettyName, 0);
1901 }
1902 ffStrbufPrependS(&ds->dePrettyName, "realme UI ");
1903 return true;
1904 }
1905
1906 // ColorOS 12 and newer report `ro.build.version.oplusrom`, ColorOS 11 and older
1907 // report `ro.build.version.opporom`.
1908 if (ffSettingsGetAndroidProperty("ro.build.version.oplusrom", &ds->dePrettyName) ||
1909 ffSettingsGetAndroidProperty("ro.build.version.opporom", &ds->dePrettyName)) {
1910 if (ffStrbufStartsWithS(&ds->dePrettyName, "V")) {
1911 ffStrbufSubstrAfter(&ds->dePrettyName, 0);
1912 }
1913 ffStrbufPrependS(&ds->dePrettyName, "ColorOS ");
1914 return true;
1915 }
1916
1917 if (ffSettingsGetAndroidProperty("ro.oxygen.version", &ds->dePrettyName)) {
1918 ffStrbufPrependS(&ds->dePrettyName, "OxygenOS ");
1919 return true;
1920 }
1921
1922 // HydrogenOS is the Chinese counterpart of OxygenOS and reports `ro.rom.version`.
1923 if (ffStrbufEqualS(&brand, "OnePlus") &&
1924 ffSettingsGetAndroidProperty("ro.rom.version", &ds->dePrettyName)) {
1925 ffStrbufPrependS(&ds->dePrettyName, "H2OS ");
1926 return true;
1927 }
1928
1929 if (ffSettingsGetAndroidProperty("ro.build.version.oneui", &ds->dePrettyName)) {
1930 // [ro.build.version.oneui]: [50101] => One UI 5.1.1
1931 // Samsung encodes the version with a base-100 carry: `major * 10000 + minor * 100 + patch`
1932 uint32_t version = (uint32_t) ffStrbufToUInt(&ds->dePrettyName, 0);
1933 uint32_t major = version / 10000;
1934 uint32_t minor = version / 100 % 100;
1935 uint32_t patch = version % 100;
1936 if (major == 0) {
1937 // Unexpected format, show the raw value
1938 ffStrbufPrependS(&ds->dePrettyName, "OneUI ");
1939 } else if (patch > 0) {
1940 ffStrbufSetF(&ds->dePrettyName, "OneUI %u.%u.%u", major, minor, patch);
1941 } else {
1942 ffStrbufSetF(&ds->dePrettyName, "OneUI %u.%u", major, minor);
1943 }
1944 return true;
1945 }
1946
1947 // Flyme. `ro.build.display.id` is `Flyme 10.5.0.1A`, where the trailing `A` marks a
1948 // stable release. `ro.flyme.version.id` holds the same string, except on Flyme 12,
1949 // where it is an Android build id instead.
1950 if (ffStrbufIgnCaseEqualS(&brand, "meizu")) {
1951 if (!ffSettingsGetAndroidProperty("ro.build.display.id", &ds->dePrettyName) ||
1952 !ffStrbufStartsWithS(&ds->dePrettyName, "Flyme")) {
1953 ffStrbufSetStatic(&ds->dePrettyName, "Flyme");
1954 }
1955 return true;
1956 }
1957
1958 // SmartisanOS. `ro.smartisan.version` is `4.2.6-201808311713-user-511` or
1959 // `6.6.6.2_TNT-201904101033-user-oce`.
1960 if (ffSettingsGetAndroidProperty("ro.smartisan.version", &ds->dePrettyName)) {
1961 ffStrbufSubstrBeforeFirstC(&ds->dePrettyName, '_');
1962 ffStrbufSubstrBeforeFirstC(&ds->dePrettyName, '-');
1963 ffStrbufPrependS(&ds->dePrettyName, "SmartisanOS ");
1964 return true;
1965 }
1966
1967 // ZUI / ZUXOS. `ro.com.zui.version` is the internal ZUI version (`17.0` for
1968 // ZUXOS 1.1.10.138), while `ro.build.display.id` embeds the marketing name and
1969 // version: `TB321FU_CN_OPEN_USER_Q00011.0_V_ZUXOS_1.1.10.138_ST_250626`.
1970 if (ffSettingsGetAndroidProperty("ro.com.zui.version", &ds->dePrettyName)) {
1971 FF_STRBUF_AUTO_DESTROY displayId = ffStrbufCreate();
1972 const char* name = nullptr;
1973 if (ffSettingsGetAndroidProperty("ro.build.display.id", &displayId)) {
1974 if (ffStrbufSubstrAfterFirstS(&displayId, "_ZUXOS_")) {
1975 name = "ZUXOS";
1976 } else if (ffStrbufSubstrAfterFirstS(&displayId, "_ZUI_")) {
1977 name = "ZUI";
1978 }
1979 if (name) {
1980 ffStrbufSubstrBeforeFirstC(&displayId, '_'); // Drop the trailing "_ST_250626"
1981 ffStrbufSetF(&ds->dePrettyName, "%s %s", name, displayId.chars);
1982 return true;
1983 }
1984 }
1985 ffStrbufPrependS(&ds->dePrettyName, "ZUI "); // Moto builds only expose `ro.com.zui.version`
1986 return true;
1987 }
1988
1989 // 360 OS (QiKU) reports `ro.build.uiversion` as `360UI:V3.0`.
1990 if (ffSettingsGetAndroidProperty("ro.build.uiversion", &ds->dePrettyName)) {
1991 uint32_t index = ffStrbufFirstIndexC(&ds->dePrettyName, ':');
1992 if (index < ds->dePrettyName.length) {
1993 // `360UI:V3.0` -> `360UI 3.0`
1994 ffStrbufRemoveSubstr(&ds->dePrettyName, index, index + 1);
1995 if (ds->dePrettyName.chars[index] == 'V') {
1996 ffStrbufRemoveSubstr(&ds->dePrettyName, index, index + 1);
1997 }
1998 ffStrbufInsertNC(&ds->dePrettyName, index, 1, ' ');
1999 }
2000 return true;
2001 }
2002
2003 // LeEco EUI reports `ro.letv.release.version` as `6.0.030S`, where the trailing `S`
2004 // marks a stable release.
2005 if (ffSettingsGetAndroidProperty("ro.letv.release.version", &ds->dePrettyName)) {
2006 ffStrbufPrependS(&ds->dePrettyName, "EUI ");
2007 return true;
2008 }
2009
2010 // nubia / ZTE
2011 if (ffStrbufIgnCaseEqualS(&brand, "nubia") || ffStrbufIgnCaseEqualS(&brand, "zte")) {
2012 // ObricUI reports `ro.os.ota.version`, e.g.
2013 // `1.8.0.2-20260204-125753-RELEASE-user-pacific-b911`
2014 if (ffSettingsGetAndroidProperty("ro.os.ota.version", &ds->dePrettyName)) {
2015 ffStrbufSubstrBeforeFirstC(&ds->dePrettyName, '-');
2016 ffStrbufPrependS(&ds->dePrettyName, "ObricUI ");
2017 return true;
2018 }
2019
2020 // MyOS, NebulaAIOS and RedMagicOS embed the name and version in `ro.build.display.id`
2021 static const char* const displayIdNames[] = { "RedMagicOS", "NebulaAIOS", "MyOS" };
2022 if (detectDEFromDisplayId(ds, displayIdNames, ARRAY_SIZE(displayIdNames))) {
2023 return true;
2024 }
2025
2026 // The older nubiaUI stores them in `ro.build.nubia.rom.name` and `.code`
2027 if (ffSettingsGetAndroidProperty("ro.build.nubia.rom.name", &ds->dePrettyName)) {
2028 if (ffSettingsGetAndroidProperty("ro.build.nubia.rom.code", &buffer)) {
2029 if (ffStrbufStartsWithS(&buffer, "V")) {
2030 ffStrbufSubstrAfter(&buffer, 0); // `V1.0` -> `1.0`
2031 }
2032 ffStrbufAppendC(&ds->dePrettyName, ' ');
2033 ffStrbufAppend(&ds->dePrettyName, &buffer);
2034 }
2035 return true;
2036 }
2037
2038 // MiFavor is the UI of the pre-MyOS ZTE phones; `ro.build.MiFavor_version` is a
2039 // bare version number (`4.0`). Note that MyOS, NebulaAIOS and RedMagicOS reuse
2040 // this property for their own version, so they are checked first.
2041 if (ffSettingsGetAndroidProperty("ro.build.MiFavor_version", &ds->dePrettyName)) {
2042 ffStrbufPrependS(&ds->dePrettyName, "MiFavor ");
2043 return true;
2044 }
2045 }
2046
2047 // LineageOS and other AOSP-based distributions
2048 if (ffSettingsGetAndroidProperty("ro.lineage.build.version", &ds->dePrettyName)) {
2049 ffStrbufPrependS(&ds->dePrettyName, "LineageOS ");
2050 return true;
2051 }
2052 if (ffSettingsGetAndroidProperty("org.pixelexperience.version.display", &ds->dePrettyName)) {
2053 // PixelExperience_Plus_whyred-13.0-20230325-0421-OFFICIAL
2054 ffStrbufSubstrBeforeFirstC(&ds->dePrettyName, '-');
2055 ffStrbufSubstrBeforeLastC(&ds->dePrettyName, '_');
2056 ffStrbufReplaceAllC(&ds->dePrettyName, '_', ' ');
2057 return true;
2058 }
2059
2060 if (ffStrbufEqualS(&brand, "asus") &&
2061 ffSettingsGetAndroidProperty("ro.build.version.incremental", &ds->dePrettyName)) {
2062 return true;
2063 }
2064 if (ffSettingsGetAndroidProperty("ro.build.display.id", &ds->dePrettyName)) {
2065 // Google Pixel and other devices running native Android
2066 return true;
2067 }
2068
2069 return false;
2070}
2071
2072void ffConnectDisplayServerImpl(FFDisplayServerResult* ds) {
2073 // Keep the same display-server preference as Linux: Wayland provides the
2074 // richest output information, followed by XCB and XRandR.
2075 if (instance.config.general.dsForceDrm == FF_DS_FORCE_DRM_TYPE_FALSE) {
2076 ffdsConnectWayland(ds);
2077
2078 if (ds->displays.length == 0) {
2079 ffdsConnectXcbRandr(ds);
2080 }
2081
2082 if (ds->displays.length == 0) {
2083 ffdsConnectXrandr(ds);
2084 }
2085
2086 if (ds->displays.length > 0) {
2087 ffdsDetectWMDE(ds);
2088 return;
2089 }
2090 }
2091
2092 // https://source.android.com/docs/core/graphics/surfaceflinger-windowmanager
2093 ffStrbufSetStatic(&ds->wmProcessName, "system_server");
2094 ffStrbufSetStatic(&ds->wmPrettyName, "WindowManager"); // A system service managed by system_server
2095 ffStrbufSetStatic(&ds->wmProtocolName, FF_WM_PROTOCOL_SURFACEFLINGER);
2096
2097 // Binder comes first: it needs no permission, no child process, and it is the only route that
2098 // answers for an app UID on Android 12 and older, where `cmd display get-displays` does not exist.
2099 // `cmd` is behind it for the case the dex read is what fails, see detectWithCommand.
2100 if (!detectWithBinder(ds)) {
2101 detectWithCmd(ds);
2102 }
2103
2104 detectDE(ds);
2105}