diff --git a/CHANGELOG.md b/CHANGELOG.md index b7dc5c55b..a85292783 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,8 @@ Features: * This is mostly untested due to lack of available devices running these ROMs. Please report any issues you encounter. * Improved Camera detection on Android (Camera, Android) * The camera list is now read from the camera2 NDK instead of `termux-api CameraInfo`, so the Termux:API app is no longer required and no subprocess is spawned. +* Improved Display detection on Android when fastfetch runs as an app rather than from `adb shell`, where `dumpsys display` is not permitted. (Display, Android) + * The displays are now read through the shell command interface of the display service, which needs no permission. * Improved COSMIC detection (DE / WM, Linux) * The version is now read from the `COSMIC_VERSION` environment variable when it is set. * Improved accuracy and performance of process name detection in the Top module. (Top, macOS) diff --git a/src/common/impl/processing_linux.c b/src/common/impl/processing_linux.c index 5783f880f..604094df3 100644 --- a/src/common/impl/processing_linux.c +++ b/src/common/impl/processing_linux.c @@ -56,7 +56,7 @@ static inline int ffPipe2(int* fds, int flags) { } // Not thread-safe -const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFProcessHandle* outHandle) { +const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinFd, FFProcessHandle* outHandle) { int pipes[2]; if (ffPipe2(pipes, O_CLOEXEC) == -1) { return "pipe() failed"; @@ -77,6 +77,9 @@ const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFProcessHandle* posix_spawn_file_actions_init(&file_actions); posix_spawn_file_actions_adddup2(&file_actions, pipes[1], useStdErr ? STDERR_FILENO : STDOUT_FILENO); posix_spawn_file_actions_adddup2(&file_actions, nullFile, useStdErr ? STDOUT_FILENO : STDERR_FILENO); + if (ffIsValidNativeFD(stdinFd)) { + posix_spawn_file_actions_adddup2(&file_actions, stdinFd, STDIN_FILENO); + } static char* oldLang = nullptr; static int langIndex = -1; @@ -142,6 +145,9 @@ const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFProcessHandle* // Child process dup2(pipes[1], useStdErr ? STDERR_FILENO : STDOUT_FILENO); dup2(nullFile, useStdErr ? STDOUT_FILENO : STDERR_FILENO); + if (ffIsValidNativeFD(stdinFd)) { + dup2(stdinFd, STDIN_FILENO); + } putenv("LANG=C.UTF-8"); execvp(argv[0], argv); _exit(127); diff --git a/src/common/impl/processing_windows.c b/src/common/impl/processing_windows.c index e90195dfe..5ddcdc236 100644 --- a/src/common/impl/processing_windows.c +++ b/src/common/impl/processing_windows.c @@ -83,7 +83,7 @@ static wchar_t* createChildEnvironment(void) { return result; } -const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFProcessHandle* outHandle) { +const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinFd, FFProcessHandle* outHandle) { const int32_t timeout = instance.config.general.processingTimeout; wchar_t pipeName[32]; @@ -131,6 +131,9 @@ const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFProcessHandle* siStartInfo.hStdOutput = hChildPipeWrite; siStartInfo.hStdError = ffGetNullFD(); } + if (ffIsValidNativeFD(stdinFd)) { + siStartInfo.hStdInput = stdinFd; + } FF_AUTO_FREE wchar_t* cmdline = nullptr; { diff --git a/src/common/processing.h b/src/common/processing.h index 51105c8ba..b52070fdf 100644 --- a/src/common/processing.h +++ b/src/common/processing.h @@ -1,6 +1,7 @@ #pragma once #include "common/FFstrbuf.h" +#include "common/io.h" // FFNativeFD, ffGetNullFD #ifndef _WIN32 #include // pid_t @@ -16,12 +17,18 @@ typedef struct FFProcessHandle { #endif } FFProcessHandle; -const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFProcessHandle* outHandle); +// `stdinFd` is the fd the child process gets as its stdin. Pass `ffGetNullFD()` to detach it from +// our stdin, or `FF_PROCESS_INHERIT_STDIN` to inherit ours, which is what the helpers below do. +// Detaching matters on Android, where `/system/bin/cmd` forwards its stdin over binder to the +// service: the kernel rejects the whole transaction when that fd is a terminal, and the tool then +// only reports `Failure calling service : Failed transaction`. +#define FF_PROCESS_INHERIT_STDIN ((FFNativeFD) -1) +const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinFd, FFProcessHandle* outHandle); const char* ffProcessReadOutput(FFProcessHandle* handle, FFstrbuf* buffer); // Destroys handle internally static inline const char* ffProcessAppendStdOut(FFstrbuf* buffer, char* const argv[]) { FFProcessHandle handle; - const char* error = ffProcessSpawn(argv, false, &handle); + const char* error = ffProcessSpawn(argv, false, FF_PROCESS_INHERIT_STDIN, &handle); if (error) { return error; } @@ -35,7 +42,7 @@ static inline const char* ffProcessAppendStdOut(FFstrbuf* buffer, char* const ar static inline const char* ffProcessAppendStdErr(FFstrbuf* buffer, char* const argv[]) { FFProcessHandle handle; - const char* error = ffProcessSpawn(argv, true, &handle); + const char* error = ffProcessSpawn(argv, true, FF_PROCESS_INHERIT_STDIN, &handle); if (error) { return error; } diff --git a/src/detection/command/command.c b/src/detection/command/command.c index 5be3d8aea..ef0095a57 100644 --- a/src/detection/command/command.c +++ b/src/detection/command/command.c @@ -22,6 +22,7 @@ static const char* spawnProcess(FFCommandOptions* options, FFProcessHandle* hand nullptr } : (char* const[]) { options->shell.chars, options->text.chars, nullptr }, options->useStdErr, + FF_PROCESS_INHERIT_STDIN, handle); } diff --git a/src/detection/displayserver/displayserver_android.c b/src/detection/displayserver/displayserver_android.c index 6adb71c03..f3c4b5f59 100644 --- a/src/detection/displayserver/displayserver_android.c +++ b/src/detection/displayserver/displayserver_android.c @@ -1,6 +1,7 @@ #include "displayserver.h" #include "common/arrutil.h" #include "common/settings.h" +#include "common/strutil.h" #include "common/processing.h" #include "linux/displayserver_linux.h" @@ -29,113 +30,128 @@ static bool checkHdrStatus(FFDisplayResult* display) { return false; } -static void detectWithDumpsys(FFDisplayServerResult* ds) { +static void detectWithCmd(FFDisplayServerResult* ds) { + // Unlike `dumpsys`, the shell command interface of the same service is not permission gated + FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate(); - if (ffProcessAppendStdOut(&buf, (char*[]) { - "/system/bin/dumpsys", - "display", - nullptr, - }) != nullptr || - buf.length == 0) { - return; // Only works in `adb shell`, or when rooted + 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 } + if (ffProcessReadOutput(&handle, &buf) != nullptr || buf.length == 0) { + return; + } + ffStrbufTrimRightSpace(&buf); + uint32_t index = 0; - while ((index = ffStrbufNextIndexS(&buf, index, "DisplayDeviceInfo")) < buf.length) { - index += strlen("DisplayDeviceInfo"); + while ((index = ffStrbufNextIndexS(&buf, index, "Display id ")) < buf.length) { + index += strlen("Display id "); + uint32_t nextIndex = ffStrbufNextIndexC(&buf, index, '\n'); buf.chars[nextIndex] = '\0'; const char* info = buf.chars + index; - // {"Builtin display": uniqueId="local:4630947134992368259", 1440 x 3200, modeId 2, defaultModeId 1, supportedModes [{id=1, width=1440, height=3200, fps=60.000004, alternativeRefreshRates=[24.000002, 30.000002, 40.0, 120.00001, 120.00001, 120.00001, 120.00001, 120.00001]}, + // 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, ...} + const char* field = strstr(info, "DisplayInfo{\""); FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateA(64); - unsigned width = 0, height = 0, modeId = 0; - double refreshRate = 0; - // {"Builtin display": uniqueId="local:4630947134992368259", 1440 x 3200, modeId 2 - int res = sscanf(info, "{\"%63[^\"]\":%*s%u x %u, modeId%u", name.chars, &width, &height, &modeId); - if (res >= 3) { - if (res == 4) { - ++info; // skip first '{' - while ((info = strchr(info, '{'))) { - ++info; + if (field) { + field += strlen("DisplayInfo{\""); + const char* nameEnd = strchr(field, '"'); + if (nameEnd) { + ffStrbufAppendNS(&name, (uint32_t) (nameEnd - field), field); + } + } - unsigned id; - double fps; - // id=1, width=1440, height=3200, fps=60.000004, - if (sscanf(info, "id=%u, %*s%*s fps=%lf", &id, &fps) >= 2) { - if (id == modeId) { - refreshRate = fps; - break; - } - } else { + 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; + } + 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; } } + } - ffStrbufRecalculateLength(&name); - FFDisplayResult* display = ffdsAppendDisplay(ds, - (uint32_t) width, - (uint32_t) height, - refreshRate, - 0, - 0, - 0, - 0, - 0, - &name, - FF_DISPLAY_TYPE_UNKNOWN, - false, - 0, - 0, - 0, - "dumpsys"); - if (display) { - display->hdrStatus = checkHdrStatus(display); + 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")) { + type = FF_DISPLAY_TYPE_EXTERNAL; } } + unsigned density = 0; + if ((field = strstr(info, ", density ")) && sscanf(field, ", density %u", &density) < 1) { + density = 0; + } + + unsigned displayId = 0; + bool primary = sscanf(info, "%u", &displayId) >= 1 && 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, + 0, + 0, + 0, + 0, + &name, + type, + primary, + 0, + 0, + 0, + "cmd"); + if (display) { + display->hdrStatus = checkHdrStatus(display); + } + index = nextIndex + 1; } } -static bool detectWithGetprop(FFDisplayServerResult* ds) { - // Only for MiUI - FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); - - 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"); - if (display) { - display->hdrStatus = checkHdrStatus(display); - } - return !!display; - } - - return false; -} - // 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`. static bool detectDEFromDisplayId(FFDisplayServerResult* ds, const char* const* names, uint32_t count) { @@ -477,9 +493,7 @@ void ffConnectDisplayServerImpl(FFDisplayServerResult* ds) { ffStrbufSetStatic(&ds->wmPrettyName, "WindowManager"); // A system service managed by system_server ffStrbufSetStatic(&ds->wmProtocolName, FF_WM_PROTOCOL_SURFACEFLINGER); - if (!detectWithGetprop(ds)) { - detectWithDumpsys(ds); - } + detectWithCmd(ds); detectDE(ds); }