mirror of
https://github.com/fastfetch-cli/fastfetch.git
synced 2026-10-28 07:44:06 +01:00
Display (Android): adds cmd display get-displays back
and updates changelog
This commit is contained in:
@@ -33,6 +33,7 @@ Bugfixes:
|
|||||||
* Fixed MAC address reporting `02:00:00:00:00:00` on macOS 27. (LocalIP, Apple)
|
* Fixed MAC address reporting `02:00:00:00:00:00` on macOS 27. (LocalIP, Apple)
|
||||||
* Fixed assertion failure when percentage exceeds 100. (Percent)
|
* Fixed assertion failure when percentage exceeds 100. (Percent)
|
||||||
* Fixed a non-existent command being reported as a generic child process error on Windows instead of `command not found`. (Command, Windows)
|
* Fixed a non-existent command being reported as a generic child process error on Windows instead of `command not found`. (Command, Windows)
|
||||||
|
* Fixed Android display detection on devices running Android 17. (Display, Android)
|
||||||
|
|
||||||
Logos:
|
Logos:
|
||||||
* Added filled Artix logo (#2631)
|
* Added filled Artix logo (#2631)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "common/debug.h"
|
#include "common/debug.h"
|
||||||
#include "common/settings.h"
|
#include "common/settings.h"
|
||||||
#include "common/strutil.h"
|
#include "common/strutil.h"
|
||||||
|
#include "common/processing.h"
|
||||||
#include "linux/displayserver_linux.h"
|
#include "linux/displayserver_linux.h"
|
||||||
|
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
@@ -18,13 +19,16 @@
|
|||||||
// process -- that fork was all of what this module cost: it took 16.0-26.5 ms on the test device,
|
// process -- that fork was all of what this module cost: it took 16.0-26.5 ms on the test device,
|
||||||
// where `cmd display get-displays` itself takes 9.2 ms and a bare fork 8.6 ms. `cmd` does not exist
|
// where `cmd display get-displays` itself takes 9.2 ms and a bare fork 8.6 ms. `cmd` does not exist
|
||||||
// before Android 13 either, and `dumpsys` is behind android.permission.DUMP, so on Android 11 and 12
|
// before Android 13 either, and `dumpsys` is behind android.permission.DUMP, so on Android 11 and 12
|
||||||
// an app UID had no display at all. Both are gone: `display` is the service they sat on,
|
// an app UID had no display at all. `dumpsys` is gone: `display` is the service it sat on,
|
||||||
// android.hardware.display.IDisplayManager, it lives in system_server, an app UID may call it, and
|
// android.hardware.display.IDisplayManager, it lives in system_server, an app UID may call it, and
|
||||||
// one `getDisplayInfo(0)` answers the whole `DisplayInfo` the dump prints, field for field. With the
|
// one `getDisplayInfo(0)` answers the whole `DisplayInfo` the dump prints, field for field. With the
|
||||||
// fork gone the module took 2.8-3.2 ms where `cmd` took 16.0-26.5 ms, and Android 11 -- the release
|
// fork gone the module took 2.8-3.2 ms where `cmd` took 16.0-26.5 ms, and Android 11 -- the release
|
||||||
// `cmd` could not serve at all -- has been read this way on a device. What is left is dominated by
|
// `cmd` could not serve at all -- has been read this way on a device. What is left is dominated by
|
||||||
// the dex read below, not by the transaction.
|
// the dex read below, not by the transaction.
|
||||||
//
|
//
|
||||||
|
// `cmd display get-displays` stays behind the walk as a fallback, for the case where it is the dex
|
||||||
|
// read that fails rather than the transaction -- see the `cmd` section below the walk.
|
||||||
|
//
|
||||||
// What it costs is the parse. The reply is a Java Parcelable and its field set moves with every
|
// What it costs is the parse. The reply is a Java Parcelable and its field set moves with every
|
||||||
// release and every vendor fork -- `DisplayInfo` writes 34 fields on Android 11, 42 on 13, 57 on 16
|
// release and every vendor fork -- `DisplayInfo` writes 34 fields on Android 11, 42 on 13, 57 on 16
|
||||||
// and 57 on 17 -- so neither a fixed offset nor a sequence written against one release survives. The
|
// and 57 on 17 -- so neither a fixed offset nor a sequence written against one release survives. The
|
||||||
@@ -37,9 +41,9 @@
|
|||||||
// both a `Ljava/lang/String;` on the wire and are not the same length. The type descriptor settles
|
// both a `Ljava/lang/String;` on the wire and are not the same length. The type descriptor settles
|
||||||
// the primitives and both array shapes, a handful of names settle the rest, and every nested
|
// the primitives and both array shapes, a handful of names settle the rest, and every nested
|
||||||
// Parcelable is anchored on its class name. A layout this does not recognise fails the route rather
|
// Parcelable is anchored on its class name. A layout this does not recognise fails the route rather
|
||||||
// than answering. There is no second route behind this one any more, so a build whose layout is not
|
// than answering, which is what the `cmd` fallback behind it is for: a build whose layout is not one
|
||||||
// one this parser reads reports no display at all, rather than a plausible number out of the middle of
|
// this parser reads reports nothing rather than a plausible number out of the middle of a different
|
||||||
// a different field.
|
// field, and `cmd` still has the record in text.
|
||||||
//
|
//
|
||||||
// Android 17 moved three of the things this parse hangs on, and all three are read the same way -- out
|
// Android 17 moved three of the things this parse hangs on, and all three are read the same way -- out
|
||||||
// of the running build's own jar, so nothing here is a table written against a release:
|
// of the running build's own jar, so nothing here is a table written against a release:
|
||||||
@@ -167,10 +171,10 @@ typedef struct FFDisplayAndroidLayout {
|
|||||||
// `DisplayAddress` -- either of the two shapes, since which one a build sends is what the name on the
|
// `DisplayAddress` -- either of the two shapes, since which one a build sends is what the name on the
|
||||||
// wire says -- a `DeviceProductInfo` and its date, the HDR capabilities and a mode.
|
// wire says -- a `DeviceProductInfo` and its date, the HDR capabilities and a mode.
|
||||||
//
|
//
|
||||||
// A jar that cannot be read at all fails too, and with nothing behind this route that leaves the
|
// A jar that cannot be read at all fails too, which is one of the two cases the `cmd` fallback
|
||||||
// module with no display to report. Which is the point of the dex being the source: a build this
|
// behind this route exists for. Which is the point of the dex being the source: a build this parser
|
||||||
// parser cannot read declines to answer, rather than reporting a plausible number read out of the
|
// cannot read declines to answer, rather than reporting a plausible number read out of the middle of
|
||||||
// middle of a different field.
|
// a different field.
|
||||||
static bool displayLayoutLoad(const char* jarPath, FFDisplayAndroidLayout* layout) {
|
static bool displayLayoutLoad(const char* jarPath, FFDisplayAndroidLayout* layout) {
|
||||||
const FFDexParcelRequest requests[] = {
|
const FFDexParcelRequest requests[] = {
|
||||||
{
|
{
|
||||||
@@ -1266,6 +1270,20 @@ static FFDisplayType displayType(int32_t type) {
|
|||||||
// do HDR at all. `isForceSdr` is the framework having turned every capability of this display off and
|
// do HDR at all. `isForceSdr` is the framework having turned every capability of this display off and
|
||||||
// `persist.sys.hdr_mode` is the vendor's switch, so `Supported` and `Enabled` stay two different
|
// `persist.sys.hdr_mode` is the vendor's switch, so `Supported` and `Enabled` stay two different
|
||||||
// questions: the list is what the panel can do, those two are whether anything is asking for it.
|
// questions: the list is what the panel can do, those two are whether anything is asking for it.
|
||||||
|
//
|
||||||
|
// A non-empty list does not mean the display does HDR. The list is the answer of the vendor's composer
|
||||||
|
// HAL, which SurfaceFlinger and DisplayManagerService pass through unchanged, and it is wrong on the
|
||||||
|
// test device (vivo PD2505, Android 17): `dumpsys display` reports `mSupportedHdrTypes=[1, 2, 3]` --
|
||||||
|
// Dolby Vision, HDR10 and HLG -- for a Samsung U32J59x that does not do HDR at all, and the same
|
||||||
|
// three types hang off every one of its forty-odd modes, down to `640x480@60`, which is a VESA DMT
|
||||||
|
// mode and not a CTA-861 one. An answer derived per mode from the sink's EDID HDR Static Metadata
|
||||||
|
// block would not look like that, and no Samsung monitor claims Dolby Vision. Nothing on the device
|
||||||
|
// contradicts it either: SurfaceFlinger dumps every mode of that display as `hdrOutputType=INVALID`
|
||||||
|
// where the built-in panel's are `SYSTEM`, its only color mode is `ColorMode::NATIVE`, and `cmd
|
||||||
|
// display get-reported-hdr-types 4` repeats the same `[1, 2, 3]`. The raw EDID would settle it and is
|
||||||
|
// out of reach: `/sys/class/drm/card0-DP-1/edid` exists but is denied by SELinux, and `adb root` is
|
||||||
|
// refused on a retail build. So the list is reported as it arrives -- it is the only answer the
|
||||||
|
// platform gives, and there is nothing here to cross check it against.
|
||||||
static FFDisplayHdrStatus displayHdrStatus(const FFDisplayAndroidInfo* info) {
|
static FFDisplayHdrStatus displayHdrStatus(const FFDisplayAndroidInfo* info) {
|
||||||
if (info->hdrTypeCount <= 0) {
|
if (info->hdrTypeCount <= 0) {
|
||||||
return FF_DISPLAY_HDR_STATUS_UNSUPPORTED;
|
return FF_DISPLAY_HDR_STATUS_UNSUPPORTED;
|
||||||
@@ -1278,8 +1296,8 @@ static FFDisplayHdrStatus displayHdrStatus(const FFDisplayAndroidInfo* info) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reads every enabled display over binder. Returns false without appending anything when the service,
|
// Reads every enabled display over binder. Returns false without appending anything when the service,
|
||||||
// the transaction or the layout is not what this parser was written for. There is no route behind
|
// the transaction or the layout is not what this parser was written for, which is also what hands the
|
||||||
// this one, so that is also how the module ends up reporting no display.
|
// module over to the `cmd` fallback.
|
||||||
static bool detectWithBinder(FFDisplayServerResult* ds) {
|
static bool detectWithBinder(FFDisplayServerResult* ds) {
|
||||||
// The write sequence first. It is what the whole parse is made of, and a build whose jar cannot be
|
// The write sequence first. It is what the whole parse is made of, and a build whose jar cannot be
|
||||||
// read has nothing to walk -- so failing here costs one file open rather than a round trip to a
|
// read has nothing to walk -- so failing here costs one file open rather than a round trip to a
|
||||||
@@ -1427,6 +1445,306 @@ static bool detectWithBinder(FFDisplayServerResult* ds) {
|
|||||||
return ds->displays.length > 0;
|
return ds->displays.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
// The `cmd` fallback
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// The route behind the walk, for a build whose `framework.jar` can not be read at all or whose parcel
|
||||||
|
// layout is not one the walk knows. `cmd display get-displays` prints the `DisplayInfo` of every
|
||||||
|
// display as text, one record per line, so the same fields are there to read with no dex and no
|
||||||
|
// parcel -- at the price of a fork, which is what the walk above exists to avoid.
|
||||||
|
//
|
||||||
|
// It needs no permission, which is what makes it usable for an app UID, but the subcommand was only
|
||||||
|
// added to `DisplayManagerShellCommand` in Android 13: Android 11 and 12 answer `Unknown command:
|
||||||
|
// get-displays` on stdout with exit code 255 and this route reports nothing. Those two releases are
|
||||||
|
// read by the walk instead, so the fallback only has to cover 13 and newer.
|
||||||
|
//
|
||||||
|
// The record layout has changed across releases, and every difference is accepted rather than version
|
||||||
|
// checked:
|
||||||
|
//
|
||||||
|
// * The mode list is printed as `modes [...]` up to Android 14 and as `supportedModes [...]` from
|
||||||
|
// Android 15 on, which also prints `appsSupportedModes [...]` right behind it. Both spellings are
|
||||||
|
// searched for.
|
||||||
|
// * `renderFrameRate` is printed from Android 15 on. Before that the active mode's fps is the only
|
||||||
|
// refresh rate the dump carries. The active mode's fps is preferred even where it exists, see
|
||||||
|
// the comment on `refreshRate` below.
|
||||||
|
// * `displayGroupId` is printed from Android 12 on, the physical dpi behind `density` from
|
||||||
|
// Android 11 on, and `isForceSdr` from Android 15 on.
|
||||||
|
//
|
||||||
|
// A record is one line and the `DisplayInfo{` inside it is what gets parsed, so the `Display id 0: `
|
||||||
|
// prefix the command prints in front of each record is stepped over by the same search that finds it.
|
||||||
|
// `marker` and `platformApi` are the two things a second command would differ in; only `cmd` is wired
|
||||||
|
// up, `dumpsys display` prints the same records behind `mBaseDisplayInfo=` and is not a route here.
|
||||||
|
static bool detectWithCommand(FFDisplayServerResult* ds, char* const argv[], const char* marker, const char* platformApi) {
|
||||||
|
FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();
|
||||||
|
FFProcessHandle handle;
|
||||||
|
// `cmd` forwards its stdin to the service over binder, and the kernel rejects the whole
|
||||||
|
// transaction when that fd is a terminal, which it is whenever fastfetch runs in a terminal.
|
||||||
|
// Detaching the child from our stdin is only needed here, so the low level API is called instead
|
||||||
|
// of `ffProcessAppendStdOut`.
|
||||||
|
if (ffProcessSpawn(argv, FF_PROCESS_OUTPUT_STDOUT_BIT, ffGetNullFD(), &handle) != nullptr) {
|
||||||
|
return false; // `/system/bin/cmd` is not there before Android 13
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ffProcessReadOutput(&handle, &buf) != nullptr || buf.length == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ffStrbufTrimRightSpace(&buf);
|
||||||
|
|
||||||
|
uint32_t index = 0;
|
||||||
|
while ((index = ffStrbufNextIndexS(&buf, index, marker)) < buf.length) {
|
||||||
|
index += strlen(marker);
|
||||||
|
|
||||||
|
uint32_t nextIndex = ffStrbufNextIndexC(&buf, index, '\n');
|
||||||
|
buf.chars[nextIndex] = '\0';
|
||||||
|
const char* info = buf.chars + index;
|
||||||
|
|
||||||
|
// 0: DisplayInfo{"Builtin display", displayId 0, ..., real 1440 x 3168, ..., mode 2,
|
||||||
|
// renderFrameRate 60.000004, ..., defaultMode 4, ..., supportedModes [{id=2,
|
||||||
|
// width=1440, height=3168, fps=60.000004, ...}], ..., hdrCapabilities
|
||||||
|
// HdrCapabilities{mSupportedHdrTypes=[1, 2, 3, 4], ...}, isForceSdr false, ...,
|
||||||
|
// rotation 0, ..., type INTERNAL, uniqueId "local:4630946557703207059", ...,
|
||||||
|
// density 560 (560.0 x 560.0) dpi, ..., deviceProductInfo DeviceProductInfo{...,
|
||||||
|
// manufactureDate=ManufactureDate{week=27, year=2006}, ...}, ...}
|
||||||
|
const char* field = strstr(info, "DisplayInfo{\"");
|
||||||
|
FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateA(64);
|
||||||
|
if (field) {
|
||||||
|
field += strlen("DisplayInfo{\"");
|
||||||
|
const char* nameEnd = strchr(field, '"');
|
||||||
|
if (nameEnd) {
|
||||||
|
ffStrbufAppendNS(&name, (uint32_t) (nameEnd - field), field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// `real` is the size the display currently uses, which is smaller than the panel's when the
|
||||||
|
// framework emulates a smaller display size
|
||||||
|
unsigned width = 0, height = 0;
|
||||||
|
if ((field = strstr(info, ", real ")) && sscanf(field, ", real %u x %u", &width, &height) < 2) {
|
||||||
|
width = height = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `renderFrameRate` is printed from Android 15 on. It is documented as "a divisor of the
|
||||||
|
// active mode refresh rate", so it is the rate the display is currently *rendering* at and
|
||||||
|
// can be lower than the mode it is set to. It is therefore only used for a record whose
|
||||||
|
// mode list can not be read, where a possibly divided rate still beats none.
|
||||||
|
double renderFrameRate = 0;
|
||||||
|
if ((field = strstr(info, ", renderFrameRate ")) && sscanf(field, ", renderFrameRate %lf", &renderFrameRate) < 1) {
|
||||||
|
renderFrameRate = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned activeMode = 0, defaultMode = 0;
|
||||||
|
if ((field = strstr(info, ", mode ")) && sscanf(field, ", mode %u", &activeMode) < 1) {
|
||||||
|
activeMode = 0;
|
||||||
|
}
|
||||||
|
if ((field = strstr(info, ", defaultMode ")) && sscanf(field, ", defaultMode %u", &defaultMode) < 1) {
|
||||||
|
defaultMode = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The modes are listed with their resolution in the natural orientation, which is also how
|
||||||
|
// the preferred values are reported on the other platforms. `defaultMode` is the mode the
|
||||||
|
// display itself prefers, so it carries the panel's native resolution.
|
||||||
|
//
|
||||||
|
// One entry is a `{...}`, and the three fields are searched for inside it rather than matched
|
||||||
|
// as a single prefix: Android 15 prints `parentModeId`, `sfModeId` and `flags` between `id`
|
||||||
|
// and `width`, where `sscanf("{id=%u, width=%u, ...")` stops after the id and reports one
|
||||||
|
// conversion -- measured on the test device, that is a preferred resolution of 0 x 0 and a
|
||||||
|
// rate that falls back to `renderFrameRate`. Everything up to the entry's own closing brace
|
||||||
|
// belongs to it, so a field past that brace is the next entry's and ends the walk.
|
||||||
|
uint32_t preferredWidth = 0, preferredHeight = 0;
|
||||||
|
double preferredRefreshRate = 0, activeModeRefreshRate = 0;
|
||||||
|
field = strstr(info, ", supportedModes [");
|
||||||
|
if (field == nullptr) {
|
||||||
|
field = strstr(info, ", modes ["); // Android 14 and older
|
||||||
|
}
|
||||||
|
while (field && (field = strstr(field, "{id="))) {
|
||||||
|
// {id=2, ..., width=1440, height=3168, fps=60.000004, ...
|
||||||
|
const char* end = strchr(field, '}');
|
||||||
|
const char* modeWidthField = strstr(field, ", width=");
|
||||||
|
const char* modeHeightField = strstr(field, ", height=");
|
||||||
|
const char* modeFpsField = strstr(field, ", fps=");
|
||||||
|
unsigned id = 0, modeWidth = 0, modeHeight = 0;
|
||||||
|
double fps = 0;
|
||||||
|
if (end == nullptr || modeWidthField == nullptr || modeWidthField > end ||
|
||||||
|
modeHeightField == nullptr || modeHeightField > end ||
|
||||||
|
modeFpsField == nullptr || modeFpsField > end) {
|
||||||
|
break; // Not a mode entry this parser can read
|
||||||
|
}
|
||||||
|
if (sscanf(field, "{id=%u", &id) < 1 ||
|
||||||
|
sscanf(modeWidthField, ", width=%u", &modeWidth) < 1 ||
|
||||||
|
sscanf(modeHeightField, ", height=%u", &modeHeight) < 1 ||
|
||||||
|
sscanf(modeFpsField, ", fps=%lf", &fps) < 1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (id == activeMode) {
|
||||||
|
activeModeRefreshRate = fps;
|
||||||
|
}
|
||||||
|
if (id == defaultMode) {
|
||||||
|
preferredWidth = modeWidth;
|
||||||
|
preferredHeight = modeHeight;
|
||||||
|
preferredRefreshRate = fps;
|
||||||
|
}
|
||||||
|
if (activeModeRefreshRate > 0 && preferredWidth > 0) {
|
||||||
|
break; // Both are in, and the same list is printed a second time from Android 15 on
|
||||||
|
}
|
||||||
|
++field;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The nominal rate of the active mode, which is what `Display.getRefreshRate()` reports
|
||||||
|
// (`refreshRateOverride` if it is set, the mode's own rate otherwise) and what every other
|
||||||
|
// platform reports. `renderFrameRate` is deliberately not preferred: it is a render
|
||||||
|
// cadence that follows the content rather than a property of the display, and it does not
|
||||||
|
// exist before Android 15, so using it would make the reported rate change with the
|
||||||
|
// Android version as well as with what is on screen.
|
||||||
|
double refreshRate = activeModeRefreshRate;
|
||||||
|
if (refreshRate <= 0) {
|
||||||
|
refreshRate = renderFrameRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned rotation = 0;
|
||||||
|
if ((field = strstr(info, ", rotation ")) && sscanf(field, ", rotation %u", &rotation) < 1) {
|
||||||
|
rotation = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
FFDisplayType type = FF_DISPLAY_TYPE_UNKNOWN;
|
||||||
|
if ((field = strstr(info, ", type "))) {
|
||||||
|
field += strlen(", type ");
|
||||||
|
if (ffStrStartsWith(field, "INTERNAL")) {
|
||||||
|
type = FF_DISPLAY_TYPE_BUILTIN;
|
||||||
|
} else if (ffStrStartsWith(field, "EXTERNAL") || ffStrStartsWith(field, "WIFI")) {
|
||||||
|
// A WIFI display is a wireless sink, which is as external as a wired one
|
||||||
|
type = FF_DISPLAY_TYPE_EXTERNAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned density = 0;
|
||||||
|
double physicalXDpi = 0, physicalYDpi = 0;
|
||||||
|
if ((field = strstr(info, ", density "))) {
|
||||||
|
// `density 640 (501.0411 x 509.28604) dpi`, the physical dpi is only printed since
|
||||||
|
// Android 11
|
||||||
|
if (sscanf(field, ", density %u (%lf x %lf) dpi", &density, &physicalXDpi, &physicalYDpi) < 1) {
|
||||||
|
density = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The physical dpi describes the panel itself and does not change with the logical display
|
||||||
|
// size, so the physical size has to be derived from the native resolution
|
||||||
|
uint32_t physicalWidth = 0, physicalHeight = 0;
|
||||||
|
if (physicalXDpi > 0) {
|
||||||
|
physicalWidth = (uint32_t) ((preferredWidth ? preferredWidth : width) * 25.4 / physicalXDpi + 0.5);
|
||||||
|
}
|
||||||
|
if (physicalYDpi > 0) {
|
||||||
|
physicalHeight = (uint32_t) ((preferredHeight ? preferredHeight : height) * 25.4 / physicalYDpi + 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// `uniqueId` identifies the display across reboots, e.g. `local:4630946557703207059` on a
|
||||||
|
// physical display and `virtual:...` on a virtual one
|
||||||
|
uint64_t id = 0;
|
||||||
|
if ((field = strstr(info, ", uniqueId \""))) {
|
||||||
|
field += strlen(", uniqueId \"");
|
||||||
|
const char* uniqueIdEnd = strchr(field, '"');
|
||||||
|
const char* digits = uniqueIdEnd ? memchr(field, ':', (size_t) (uniqueIdEnd - field)) : nullptr;
|
||||||
|
id = (uint64_t) strtoull(digits ? digits + 1 : field, nullptr, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t manufactureYear = 0, manufactureWeek = 0;
|
||||||
|
if ((field = strstr(info, ", deviceProductInfo "))) {
|
||||||
|
// `manufactureDate=ManufactureDate{week=27, year=2006}`, either field may be `null`
|
||||||
|
const char* date = strstr(field, "manufactureDate=ManufactureDate{");
|
||||||
|
unsigned year = 0, week = 0;
|
||||||
|
if (date && sscanf(date + strlen("manufactureDate=ManufactureDate{"), "week=%u, year=%u", &week, &year) == 2) {
|
||||||
|
manufactureYear = (uint16_t) year;
|
||||||
|
manufactureWeek = (uint16_t) week;
|
||||||
|
} else if ((field = strstr(field, ", modelYear=")) && sscanf(field, ", modelYear=%u", &year) == 1) {
|
||||||
|
// A display reports either the date of manufacture or the model year
|
||||||
|
manufactureYear = (uint16_t) year;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// `displayId` sits inside the record, not in front of it: the id in the prefix the command
|
||||||
|
// prints is what the marker search stepped over, and the field itself is what this reads
|
||||||
|
unsigned displayId = 0;
|
||||||
|
if ((field = strstr(info, ", displayId ")) && sscanf(field, ", displayId %u", &displayId) < 1) {
|
||||||
|
displayId = 0;
|
||||||
|
}
|
||||||
|
bool primary = displayId == 0; // Display 0 is the default one
|
||||||
|
|
||||||
|
// Android counts density in dpi with 160 as the 1x baseline, fastfetch uses 96
|
||||||
|
FFDisplayResult* display = ffdsAppendDisplay(ds,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
refreshRate,
|
||||||
|
density * 96 / 160,
|
||||||
|
preferredWidth,
|
||||||
|
preferredHeight,
|
||||||
|
preferredRefreshRate,
|
||||||
|
rotation,
|
||||||
|
&name,
|
||||||
|
type,
|
||||||
|
primary,
|
||||||
|
id,
|
||||||
|
physicalWidth,
|
||||||
|
physicalHeight,
|
||||||
|
platformApi);
|
||||||
|
if (display) {
|
||||||
|
display->manufactureYear = manufactureYear;
|
||||||
|
display->manufactureWeek = manufactureWeek;
|
||||||
|
|
||||||
|
// Reported for every display, not only for the built-in one: `hdrCapabilities` is a
|
||||||
|
// field of the `DisplayInfo` record itself, so it describes that display and nothing
|
||||||
|
// else, and the other platforms report HDR per display too (EDID on Linux, the
|
||||||
|
// advanced color info per target on Windows). An external display or a wireless sink
|
||||||
|
// carries the field as well.
|
||||||
|
//
|
||||||
|
// `hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[1, 2, 3, 4], ...}` is printed
|
||||||
|
// since Android 11, where an empty list means that the display can not do HDR at all.
|
||||||
|
// The two fallbacks below it are device wide vendor properties, which is the price of
|
||||||
|
// answering for a record that does not print the field.
|
||||||
|
//
|
||||||
|
// This is the same field and the same vendor answer the walk above reads, so the same
|
||||||
|
// caveat applies: a non-HDR display can arrive with a non-empty list, see
|
||||||
|
// displayHdrStatus.
|
||||||
|
FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
|
||||||
|
field = strstr(info, "hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[");
|
||||||
|
if (field) {
|
||||||
|
field += strlen("hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[");
|
||||||
|
display->hdrStatus = *field == ']' ? FF_DISPLAY_HDR_STATUS_UNSUPPORTED : FF_DISPLAY_HDR_STATUS_SUPPORTED;
|
||||||
|
} else if (ffSettingsGetAndroidProperty("ro.surface_flinger.has_HDR_display", &buffer)) {
|
||||||
|
display->hdrStatus = ffStrbufIgnCaseEqualS(&buffer, "true") ? FF_DISPLAY_HDR_STATUS_SUPPORTED : FF_DISPLAY_HDR_STATUS_UNSUPPORTED;
|
||||||
|
} else {
|
||||||
|
display->hdrStatus = FF_DISPLAY_HDR_STATUS_UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (display->hdrStatus == FF_DISPLAY_HDR_STATUS_SUPPORTED) {
|
||||||
|
// `persist.sys.hdr_mode` is non-zero while HDR is turned on, and `isForceSdr
|
||||||
|
// true` means that the framework disabled every HDR capability of this display.
|
||||||
|
// Note that `ffSettingsGetAndroidProperty` appends, so the value needs its own
|
||||||
|
// buffer.
|
||||||
|
FF_STRBUF_AUTO_DESTROY hdrMode = ffStrbufCreate();
|
||||||
|
if (ffSettingsGetAndroidProperty("persist.sys.hdr_mode", &hdrMode) &&
|
||||||
|
ffStrbufToUInt(&hdrMode, 0) > 0 &&
|
||||||
|
!strstr(info, ", isForceSdr true")) {
|
||||||
|
display->hdrStatus = FF_DISPLAY_HDR_STATUS_ENABLED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The last display of the dump is not followed by a newline, so the loop must not step
|
||||||
|
// past the end of the buffer (`ffStrbufNextIndexC` returns the length when it finds none)
|
||||||
|
index = nextIndex < buf.length ? nextIndex + 1 : buf.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A command that produced no `DisplayInfo` record has to be reported as a failure, so that the
|
||||||
|
// caller can tell "this build answers differently" from "this build answered nothing"
|
||||||
|
return ds->displays.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool detectWithCmd(FFDisplayServerResult* ds) {
|
||||||
|
return detectWithCommand(ds,
|
||||||
|
(char*[]) { "/system/bin/cmd", "display", "get-displays", nullptr },
|
||||||
|
"Display id ",
|
||||||
|
"cmd");
|
||||||
|
}
|
||||||
|
|
||||||
// Several vendors embed the UI name and its version in `ro.build.display.id` without any
|
// Several vendors embed the UI name and its version in `ro.build.display.id` without any
|
||||||
// separator, e.g. `MyOS12.0.14_A2121` or `RedMagicOS10.0.24_NX779J`.
|
// separator, e.g. `MyOS12.0.14_A2121` or `RedMagicOS10.0.24_NX779J`.
|
||||||
static bool detectDEFromDisplayId(FFDisplayServerResult* ds, const char* const* names, uint32_t count) {
|
static bool detectDEFromDisplayId(FFDisplayServerResult* ds, const char* const* names, uint32_t count) {
|
||||||
@@ -1768,7 +2086,12 @@ void ffConnectDisplayServerImpl(FFDisplayServerResult* ds) {
|
|||||||
ffStrbufSetStatic(&ds->wmPrettyName, "WindowManager"); // A system service managed by system_server
|
ffStrbufSetStatic(&ds->wmPrettyName, "WindowManager"); // A system service managed by system_server
|
||||||
ffStrbufSetStatic(&ds->wmProtocolName, FF_WM_PROTOCOL_SURFACEFLINGER);
|
ffStrbufSetStatic(&ds->wmProtocolName, FF_WM_PROTOCOL_SURFACEFLINGER);
|
||||||
|
|
||||||
detectWithBinder(ds);
|
// Binder comes first: it needs no permission, no child process, and it is the only route that
|
||||||
|
// answers for an app UID on Android 12 and older, where `cmd display get-displays` does not exist.
|
||||||
|
// `cmd` is behind it for the case the dex read is what fails, see detectWithCommand.
|
||||||
|
if (!detectWithBinder(ds)) {
|
||||||
|
detectWithCmd(ds);
|
||||||
|
}
|
||||||
|
|
||||||
detectDE(ds);
|
detectDE(ds);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user