mirror of
https://github.com/fastfetch-cli/fastfetch.git
synced 2026-11-08 22:20:17 +01:00
Display (Android): adds richer information and adds detectWithGetprop back
This commit is contained in:
@@ -7,66 +7,95 @@
|
||||
|
||||
#include <math.h>
|
||||
|
||||
static bool checkHdrStatus(FFDisplayResult* display) {
|
||||
static bool detectWithGetprop(FFDisplayServerResult* ds) {
|
||||
// Only for MiUI
|
||||
FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
|
||||
|
||||
if (ffSettingsGetAndroidProperty("ro.surface_flinger.has_HDR_display", &buffer)) {
|
||||
if (ffStrbufIgnCaseEqualS(&buffer, "true")) {
|
||||
display->hdrStatus = FF_DISPLAY_HDR_STATUS_SUPPORTED;
|
||||
|
||||
if (ffSettingsGetAndroidProperty("persist.sys.hdr_mode", &buffer) &&
|
||||
ffStrbufToUInt(&buffer, 0) > 0) {
|
||||
display->hdrStatus = FF_DISPLAY_HDR_STATUS_ENABLED;
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
display->hdrStatus = FF_DISPLAY_HDR_STATUS_UNSUPPORTED;
|
||||
return true;
|
||||
}
|
||||
if (ffSettingsGetAndroidProperty("persist.sys.miui_resolution", &buffer) &&
|
||||
ffStrbufContainC(&buffer, ',')) {
|
||||
// 1440,3200,560 => width,height,densityDpi
|
||||
uint32_t width = (uint32_t) ffStrbufToUInt(&buffer, 0);
|
||||
ffStrbufSubstrAfterFirstC(&buffer, ',');
|
||||
uint32_t height = (uint32_t) ffStrbufToUInt(&buffer, 0);
|
||||
ffStrbufSubstrAfterFirstC(&buffer, ',');
|
||||
uint32_t dpi = (uint32_t) ffStrbufToUInt(&buffer, 0) * 96 / 160;
|
||||
FFDisplayResult* display = ffdsAppendDisplay(ds,
|
||||
width,
|
||||
height,
|
||||
0,
|
||||
dpi,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
nullptr,
|
||||
FF_DISPLAY_TYPE_BUILTIN,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
"getprop");
|
||||
return !!display;
|
||||
}
|
||||
|
||||
display->hdrStatus = FF_DISPLAY_HDR_STATUS_UNKNOWN;
|
||||
return false;
|
||||
}
|
||||
|
||||
static void detectWithCmd(FFDisplayServerResult* ds) {
|
||||
// Unlike `dumpsys`, the shell command interface of the same service is not permission gated
|
||||
|
||||
// `cmd display get-displays` and `dumpsys display` print the same thing -- the `DisplayInfo` of every
|
||||
// display, one per line -- so one parser covers both and only the command and the marker in front of
|
||||
// each record differ:
|
||||
//
|
||||
// * `cmd display get-displays` 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.
|
||||
// * `dumpsys display` covers every release, including the ones that predate `get-displays`, but it
|
||||
// is gated behind `android.permission.DUMP`, so it only answers for `adb shell` and root.
|
||||
//
|
||||
// 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: `
|
||||
// of the one command and the `mBaseDisplayInfo=` of the other are both skipped by the same code.
|
||||
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((char*[]) {
|
||||
"/system/bin/cmd",
|
||||
"display",
|
||||
"get-displays",
|
||||
nullptr,
|
||||
},
|
||||
false,
|
||||
ffGetNullFD(),
|
||||
&handle) != nullptr) {
|
||||
return; // The shell command interface is not available on every Android version
|
||||
// Detaching the child from our stdin is only needed here, so the low level API is called instead
|
||||
// of `ffProcessAppendStdOut`.
|
||||
if (ffProcessSpawn(argv, false, ffGetNullFD(), &handle) != nullptr) {
|
||||
return false; // Neither command is available on every Android version
|
||||
}
|
||||
|
||||
if (ffProcessReadOutput(&handle, &buf) != nullptr || buf.length == 0) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
ffStrbufTrimRightSpace(&buf);
|
||||
|
||||
uint32_t index = 0;
|
||||
while ((index = ffStrbufNextIndexS(&buf, index, "Display id ")) < buf.length) {
|
||||
index += strlen("Display id ");
|
||||
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, ..., supportedModes [{id=2, width=1440, height=3168,
|
||||
// fps=60.000004, ...}], ..., type INTERNAL, ..., density 560 (560.0 x 560.0) dpi, ...}
|
||||
// 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) {
|
||||
@@ -77,36 +106,74 @@ static void detectWithCmd(FFDisplayServerResult* ds) {
|
||||
}
|
||||
}
|
||||
|
||||
// `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;
|
||||
}
|
||||
|
||||
double refreshRate = 0;
|
||||
if ((field = strstr(info, ", renderFrameRate ")) && sscanf(field, ", renderFrameRate %lf", &refreshRate) < 1) {
|
||||
refreshRate = 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;
|
||||
}
|
||||
if (refreshRate <= 0) {
|
||||
// `renderFrameRate` is only printed since Android 11. Older builds expose the active mode
|
||||
// only, so its refresh rate has to be looked up in the list of supported modes.
|
||||
unsigned activeMode = 0;
|
||||
field = strstr(info, ", mode ");
|
||||
if (field && sscanf(field, ", mode %u", &activeMode) >= 1) {
|
||||
field = strstr(info, "supportedModes [");
|
||||
while (field && (field = strstr(field, "{id="))) {
|
||||
// {id=2, width=1440, height=3168, fps=60.000004, ...
|
||||
unsigned id = 0;
|
||||
double fps = 0;
|
||||
if (sscanf(field, "{id=%u, width=%*u, height=%*u, fps=%lf", &id, &fps) < 2) {
|
||||
break;
|
||||
}
|
||||
if (id == activeMode) {
|
||||
refreshRate = fps;
|
||||
break;
|
||||
}
|
||||
++field;
|
||||
}
|
||||
|
||||
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.
|
||||
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, ...
|
||||
unsigned id = 0, modeWidth = 0, modeHeight = 0;
|
||||
double fps = 0;
|
||||
if (sscanf(field, "{id=%u, width=%u, height=%u, fps=%lf", &id, &modeWidth, &modeHeight, &fps) < 4) {
|
||||
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;
|
||||
@@ -114,18 +181,63 @@ static void detectWithCmd(FFDisplayServerResult* ds) {
|
||||
field += strlen(", type ");
|
||||
if (ffStrStartsWith(field, "INTERNAL")) {
|
||||
type = FF_DISPLAY_TYPE_BUILTIN;
|
||||
} else if (ffStrStartsWith(field, "EXTERNAL")) {
|
||||
} 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;
|
||||
if ((field = strstr(info, ", density ")) && sscanf(field, ", density %u", &density) < 1) {
|
||||
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: `cmd` prints the id again in its
|
||||
// own prefix, but `dumpsys` prints only `mBaseDisplayInfo=`
|
||||
unsigned displayId = 0;
|
||||
bool primary = sscanf(info, "%u", &displayId) >= 1 && displayId == 0; // Display 0 is the default one
|
||||
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,
|
||||
@@ -133,23 +245,79 @@ static void detectWithCmd(FFDisplayServerResult* ds) {
|
||||
height,
|
||||
refreshRate,
|
||||
density * 96 / 160,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
preferredWidth,
|
||||
preferredHeight,
|
||||
preferredRefreshRate,
|
||||
rotation,
|
||||
&name,
|
||||
type,
|
||||
primary,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
"cmd");
|
||||
id,
|
||||
physicalWidth,
|
||||
physicalHeight,
|
||||
platformApi);
|
||||
if (display) {
|
||||
display->hdrStatus = checkHdrStatus(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.
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
index = nextIndex + 1;
|
||||
// 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: the caller
|
||||
// keys off this value to decide whether to try the other command, and on Android 12 and older
|
||||
// `dumpsys` is the only one that can still yield a display.
|
||||
return ds->displays.length > 0;
|
||||
}
|
||||
|
||||
static bool detectWithCmd(FFDisplayServerResult* ds) {
|
||||
return detectWithCommand(ds,
|
||||
(char*[]) { "/system/bin/cmd", "display", "get-displays", nullptr },
|
||||
"Display id ",
|
||||
"cmd");
|
||||
}
|
||||
|
||||
static bool detectWithDumpsys(FFDisplayServerResult* ds) {
|
||||
return detectWithCommand(ds,
|
||||
(char*[]) { "/system/bin/dumpsys", "display", nullptr },
|
||||
"mBaseDisplayInfo=",
|
||||
"dumpsys");
|
||||
}
|
||||
|
||||
// Several vendors embed the UI name and its version in `ro.build.display.id` without any
|
||||
@@ -493,7 +661,12 @@ void ffConnectDisplayServerImpl(FFDisplayServerResult* ds) {
|
||||
ffStrbufSetStatic(&ds->wmPrettyName, "WindowManager"); // A system service managed by system_server
|
||||
ffStrbufSetStatic(&ds->wmProtocolName, FF_WM_PROTOCOL_SURFACEFLINGER);
|
||||
|
||||
detectWithCmd(ds);
|
||||
// `cmd` comes first because it needs no permission and therefore also works for an app UID.
|
||||
// `dumpsys` is the only route that answers on Android 12 and older, and only for `adb shell` and
|
||||
// root, and `getprop` is MiUI specific and the last resort.
|
||||
if (!detectWithCmd(ds) && !detectWithDumpsys(ds)) {
|
||||
detectWithGetprop(ds);
|
||||
}
|
||||
|
||||
detectDE(ds);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user