diff --git a/CHANGELOG.md b/CHANGELOG.md index d18d3abb3..796a8e2fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,11 @@ Features: * Added CPU name and frequency detection support on SPARC. (CPU, Linux) * Added package detection support for CRUX. (Packages, Linux) * Exposed in custom format as `{crux}`. +* Improved Android ROM detection (DE, Android) + * Added support for HarmonyOS, HarmonyOS NEXT, Flyme, JOYUI, SmartisanOS, realme UI, HydrogenOS, ZUI, ZUXOS, MyOS, NebulaAIOS, ObricUI, MiFavor, LineageOS, PixelExperience, EUI and 360 UI. + * Added support for MagicUI 3.x, which stores a bare version number instead of a `MagicUI_x.y.z` string. + * Added Samsung OneUI support (#2541) + * This is mostly untested due to lack of available devices running these ROMs. Please report any issues you encounter. * 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/detection/displayserver/displayserver_android.c b/src/detection/displayserver/displayserver_android.c index 4e666eb1d..6adb71c03 100644 --- a/src/detection/displayserver/displayserver_android.c +++ b/src/detection/displayserver/displayserver_android.c @@ -1,4 +1,5 @@ #include "displayserver.h" +#include "common/arrutil.h" #include "common/settings.h" #include "common/processing.h" #include "linux/displayserver_linux.h" @@ -135,56 +136,316 @@ static bool detectWithGetprop(FFDisplayServerResult* ds) { return false; } -static bool detectDE(FFDisplayServerResult* ds) { - if (ffSettingsGetAndroidProperty("ro.vivo.os.build.display.id", &ds->dePrettyName)) // OriginOS 6 - { - ffStrbufAppendC(&ds->dePrettyName, ' '); - ffSettingsGetAndroidProperty("ro.vivo.product.version", &ds->dePrettyName); // PD2505D_xxx +// 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) { + FF_STRBUF_AUTO_DESTROY displayId = ffStrbufCreate(); + if (!ffSettingsGetAndroidProperty("ro.build.display.id", &displayId)) { + return false; + } + + for (uint32_t i = 0; i < count; i++) { + uint32_t length = (uint32_t) strlen(names[i]); + if (!ffStrbufStartsWithS(&displayId, names[i])) { + continue; + } + + ffStrbufSubstrBeforeFirstC(&displayId, '_'); // Drop the model suffix + if (displayId.length > length) { + ffStrbufInsertNC(&displayId, length, 1, ' '); + } + ffStrbufSet(&ds->dePrettyName, &displayId); return true; } - if (ffSettingsGetAndroidProperty("ro.build.version.magic", &ds->dePrettyName) || - ffSettingsGetAndroidProperty("ro.build.version.emui", &ds->dePrettyName)) { + + return false; +} + +static bool detectDE(FFDisplayServerResult* ds) { + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY brand = ffStrbufCreate(); + + // `ffSettingsGetAndroidProperty` appends to the given buffer, so the brand has to be read + // into its own buffer exactly once: reading it into a shared buffer repeatedly would + // concatenate the values and break every comparison against it. + ffSettingsGetAndroidProperty("ro.product.brand", &brand); + + // vivo reports the marketing name and version in `ro.vivo.os.build.display.id`, + // separated by an underscore (`Funtouch OS_10`, `OriginOS 5`), and the build number + // in `ro.vivo.product.version` (`PD2505D_A_9.16.42`). + if (ffSettingsGetAndroidProperty("ro.vivo.os.build.display.id", &ds->dePrettyName)) { + ffStrbufReplaceAllC(&ds->dePrettyName, '_', ' '); + if (ffSettingsGetAndroidProperty("ro.vivo.product.version", &buffer)) { + ffStrbufAppendC(&ds->dePrettyName, ' '); + ffStrbufAppend(&ds->dePrettyName, &buffer); + } + return true; + } + + // HarmonyOS 2.0 - 4.x is built on top of the Android framework and keeps reporting an + // EMUI version, but it is HarmonyOS. `ro.build.ohos.devicetype` is set only by those + // builds, and `hw_sc.build.platform.version` is the HarmonyOS version, while + // `ro.build.version.emui` only carries the EMUI compatibility version + // (HarmonyOS 2.0 == EMUI 12, 3.0 == 13, 4.0 == 14, 4.2 == 14.2, 4.3 == 15). + if (ffSettingsGetAndroidProperty("ro.build.ohos.devicetype", &buffer)) { + ffStrbufClear(&buffer); + if (!ffSettingsGetAndroidProperty("hw_sc.build.platform.version", &buffer) && + ffSettingsGetAndroidProperty("ro.huawei.build.display.id", &buffer)) { + // A few builds leave `hw_sc.build.platform.version` unset and only expose the + // version through `ro.huawei.build.display.id`, e.g. `JKM-AL00 2.0.0.263(C00E260R4P3)` + ffStrbufSubstrAfterFirstC(&buffer, ' '); + ffStrbufSubstrBeforeFirstC(&buffer, '('); + ffStrbufSubstrBeforeLastC(&buffer, '.'); + } + if (buffer.length > 0) { + ffStrbufSetF(&ds->dePrettyName, "HarmonyOS %s", buffer.chars); + } else { + ffStrbufSetStatic(&ds->dePrettyName, "HarmonyOS"); + } + return true; + } + + // HarmonyOS NEXT (5.0 and newer) drops the Android framework. It is the only Huawei + // family reporting `ro.build.display.id` as `System 104.5.0.001(60J9)`, and none of its + // properties carries the marketing version, so it can only be named without one. + if (ffStrbufIgnCaseEqualS(&brand, "HUAWEI") && + ffSettingsGetAndroidProperty("ro.build.display.id", &buffer) && + ffStrbufStartsWithS(&buffer, "System ")) { + ffStrbufSetStatic(&ds->dePrettyName, "HarmonyOS NEXT"); + return true; + } + + // HONOR reports MagicOS / MagicUI in `ro.build.version.magic`. MagicUI 3.x stores a + // bare version number (`3.0.1`) instead of a `MagicUI_x.y.z` string. + if (ffSettingsGetAndroidProperty("ro.build.version.magic", &ds->dePrettyName)) { + ffStrbufReplaceAllC(&ds->dePrettyName, '_', ' '); + if (!ffStrbufStartsWithS(&ds->dePrettyName, "Magic")) { + ffStrbufPrependS(&ds->dePrettyName, "MagicUI "); + } + return true; + } + + if (ffSettingsGetAndroidProperty("ro.build.version.emui", &ds->dePrettyName)) { ffStrbufReplaceAllC(&ds->dePrettyName, '_', ' '); return true; } + + // Xiaomi HyperOS. `ro.mi.os.version.incremental` is `OS1.0.10.0.TLDCNXM`, while + // `ro.build.version.incremental` still starts with `V816` on HyperOS 1.0, so it can + // not be used to tell HyperOS and MIUI apart. if (ffSettingsGetAndroidProperty("ro.mi.os.version.name", &ds->dePrettyName)) { - // MiUI like ffStrbufClear(&ds->dePrettyName); - ffSettingsGetAndroidProperty("ro.build.version.incremental", &ds->dePrettyName); // Detail version number - if (ffStrbufStartsWithS(&ds->dePrettyName, "OS")) { - ds->dePrettyName.chars[0] = 'S'; - ds->dePrettyName.chars[1] = ' '; - ffStrbufPrependS(&ds->dePrettyName, "HyperO"); - } else if (ffStrbufStartsWithS(&ds->dePrettyName, "V")) { - ds->dePrettyName.chars[0] = ' '; - ffStrbufPrependS(&ds->dePrettyName, "MiUI"); + if (ffSettingsGetAndroidProperty("ro.mi.os.version.incremental", &ds->dePrettyName) && + ffStrbufStartsWithS(&ds->dePrettyName, "OS")) { + ffStrbufSubstrAfter(&ds->dePrettyName, 1); // Drop the leading "OS" + ffStrbufPrependS(&ds->dePrettyName, "HyperOS "); + } else { + ffStrbufSetStatic(&ds->dePrettyName, "HyperOS"); + } + return true; + } + + // Black Shark runs JOYUI, a MIUI fork, and therefore also sets + // `ro.miui.ui.version.name`. `ro.build.version.incremental` is `V11.0.4.0.JOYUI`. + if (ffStrbufIgnCaseEqualS(&brand, "blackshark")) { + ffStrbufClear(&ds->dePrettyName); + if (ffSettingsGetAndroidProperty("ro.build.version.incremental", &ds->dePrettyName)) { + ffStrbufSubstrBeforeLastC(&ds->dePrettyName, '.'); // Drop the trailing "JOYUI" + if (ffStrbufStartsWithS(&ds->dePrettyName, "V")) { + ffStrbufSubstrAfter(&ds->dePrettyName, 0); + } + ffStrbufPrependS(&ds->dePrettyName, "JOYUI "); + } else { + ffStrbufSetStatic(&ds->dePrettyName, "JOYUI"); + } + return true; + } + + // MIUI. `ro.build.version.incremental` is `V14.0.1.0.TJJCNXM` on stable builds and a + // bare release date (`21.11.17`) on beta builds. + if (ffSettingsGetAndroidProperty("ro.miui.ui.version.name", &buffer)) { + ffStrbufClear(&ds->dePrettyName); + if (ffSettingsGetAndroidProperty("ro.build.version.incremental", &ds->dePrettyName)) { + if (ffStrbufStartsWithS(&ds->dePrettyName, "V")) { + ffStrbufSubstrAfter(&ds->dePrettyName, 0); + } + ffStrbufPrependS(&ds->dePrettyName, "MiUI "); } else { ffStrbufSetStatic(&ds->dePrettyName, "MiUI"); } return true; } - if (ffSettingsGetAndroidProperty("ro.build.version.oplusrom", &ds->dePrettyName)) { + + // realme UI is a ColorOS fork and reports both `ro.build.version.realmeui` and + // `ro.build.version.oplusrom`; the former is the realme UI version. + if (ffSettingsGetAndroidProperty("ro.build.version.realmeui", &ds->dePrettyName)) { if (ffStrbufStartsWithS(&ds->dePrettyName, "V")) { ffStrbufSubstrAfter(&ds->dePrettyName, 0); } - ffStrbufPrependS(&ds->dePrettyName, "ColorOS"); + ffStrbufPrependS(&ds->dePrettyName, "realme UI "); return true; } + + // ColorOS 12 and newer report `ro.build.version.oplusrom`, ColorOS 11 and older + // report `ro.build.version.opporom`. + if (ffSettingsGetAndroidProperty("ro.build.version.oplusrom", &ds->dePrettyName) || + ffSettingsGetAndroidProperty("ro.build.version.opporom", &ds->dePrettyName)) { + if (ffStrbufStartsWithS(&ds->dePrettyName, "V")) { + ffStrbufSubstrAfter(&ds->dePrettyName, 0); + } + ffStrbufPrependS(&ds->dePrettyName, "ColorOS "); + return true; + } + if (ffSettingsGetAndroidProperty("ro.oxygen.version", &ds->dePrettyName)) { - ffStrbufPrependS(&ds->dePrettyName, "OxygenOS"); + ffStrbufPrependS(&ds->dePrettyName, "OxygenOS "); return true; } - if (ffSettingsGetAndroidProperty("ro.product.brand", &ds->dePrettyName) && ffStrbufEqualS(&ds->dePrettyName, "asus")) { - ffStrbufClear(&ds->dePrettyName); - ffSettingsGetAndroidProperty("ro.build.version.incremental", &ds->dePrettyName); + + // HydrogenOS is the Chinese counterpart of OxygenOS and reports `ro.rom.version`. + if (ffStrbufEqualS(&brand, "OnePlus") && + ffSettingsGetAndroidProperty("ro.rom.version", &ds->dePrettyName)) { + ffStrbufPrependS(&ds->dePrettyName, "H2OS "); + return true; + } + + if (ffSettingsGetAndroidProperty("ro.build.version.oneui", &ds->dePrettyName)) { + // [ro.build.version.oneui]: [50101] => One UI 5.1.1 + // Samsung encodes the version with a base-100 carry: `major * 10000 + minor * 100 + patch` + uint32_t version = (uint32_t) ffStrbufToUInt(&ds->dePrettyName, 0); + uint32_t major = version / 10000; + uint32_t minor = version / 100 % 100; + uint32_t patch = version % 100; + if (major == 0) { + // Unexpected format, show the raw value + ffStrbufPrependS(&ds->dePrettyName, "OneUI "); + } else if (patch > 0) { + ffStrbufSetF(&ds->dePrettyName, "OneUI %u.%u.%u", major, minor, patch); + } else { + ffStrbufSetF(&ds->dePrettyName, "OneUI %u.%u", major, minor); + } + return true; + } + + // Flyme. `ro.build.display.id` is `Flyme 10.5.0.1A`, where the trailing `A` marks a + // stable release. `ro.flyme.version.id` holds the same string, except on Flyme 12, + // where it is an Android build id instead. + if (ffStrbufIgnCaseEqualS(&brand, "meizu")) { + if (!ffSettingsGetAndroidProperty("ro.build.display.id", &ds->dePrettyName) || + !ffStrbufStartsWithS(&ds->dePrettyName, "Flyme")) { + ffStrbufSetStatic(&ds->dePrettyName, "Flyme"); + } + return true; + } + + // SmartisanOS. `ro.smartisan.version` is `4.2.6-201808311713-user-511` or + // `6.6.6.2_TNT-201904101033-user-oce`. + if (ffSettingsGetAndroidProperty("ro.smartisan.version", &ds->dePrettyName)) { + ffStrbufSubstrBeforeFirstC(&ds->dePrettyName, '_'); + ffStrbufSubstrBeforeFirstC(&ds->dePrettyName, '-'); + ffStrbufPrependS(&ds->dePrettyName, "SmartisanOS "); + return true; + } + + // ZUI / ZUXOS. `ro.com.zui.version` is the internal ZUI version (`17.0` for + // ZUXOS 1.1.10.138), while `ro.build.display.id` embeds the marketing name and + // version: `TB321FU_CN_OPEN_USER_Q00011.0_V_ZUXOS_1.1.10.138_ST_250626`. + if (ffSettingsGetAndroidProperty("ro.com.zui.version", &ds->dePrettyName)) { + FF_STRBUF_AUTO_DESTROY displayId = ffStrbufCreate(); + const char* name = nullptr; + if (ffSettingsGetAndroidProperty("ro.build.display.id", &displayId)) { + if (ffStrbufSubstrAfterFirstS(&displayId, "_ZUXOS_")) { + name = "ZUXOS"; + } else if (ffStrbufSubstrAfterFirstS(&displayId, "_ZUI_")) { + name = "ZUI"; + } + if (name) { + ffStrbufSubstrBeforeFirstC(&displayId, '_'); // Drop the trailing "_ST_250626" + ffStrbufSetF(&ds->dePrettyName, "%s %s", name, displayId.chars); + return true; + } + } + ffStrbufPrependS(&ds->dePrettyName, "ZUI "); // Moto builds only expose `ro.com.zui.version` + return true; + } + + // 360 OS (QiKU) reports `ro.build.uiversion` as `360UI:V3.0`. + if (ffSettingsGetAndroidProperty("ro.build.uiversion", &ds->dePrettyName)) { + uint32_t index = ffStrbufFirstIndexC(&ds->dePrettyName, ':'); + if (index < ds->dePrettyName.length) { + // `360UI:V3.0` -> `360UI 3.0` + ffStrbufRemoveSubstr(&ds->dePrettyName, index, index + 1); + if (ds->dePrettyName.chars[index] == 'V') { + ffStrbufRemoveSubstr(&ds->dePrettyName, index, index + 1); + } + ffStrbufInsertNC(&ds->dePrettyName, index, 1, ' '); + } + return true; + } + + // LeEco EUI reports `ro.letv.release.version` as `6.0.030S`, where the trailing `S` + // marks a stable release. + if (ffSettingsGetAndroidProperty("ro.letv.release.version", &ds->dePrettyName)) { + ffStrbufPrependS(&ds->dePrettyName, "EUI "); + return true; + } + + // nubia / ZTE + if (ffStrbufIgnCaseEqualS(&brand, "nubia") || ffStrbufIgnCaseEqualS(&brand, "zte")) { + // ObricUI reports `ro.os.ota.version`, e.g. + // `1.8.0.2-20260204-125753-RELEASE-user-pacific-b911` + if (ffSettingsGetAndroidProperty("ro.os.ota.version", &ds->dePrettyName)) { + ffStrbufSubstrBeforeFirstC(&ds->dePrettyName, '-'); + ffStrbufPrependS(&ds->dePrettyName, "ObricUI "); + return true; + } + + // MyOS, NebulaAIOS and RedMagicOS embed the name and version in `ro.build.display.id` + static const char* const displayIdNames[] = { "RedMagicOS", "NebulaAIOS", "MyOS" }; + if (detectDEFromDisplayId(ds, displayIdNames, ARRAY_SIZE(displayIdNames))) { + return true; + } + + // The older nubiaUI stores them in `ro.build.nubia.rom.name` and `.code` + if (ffSettingsGetAndroidProperty("ro.build.nubia.rom.name", &ds->dePrettyName)) { + if (ffSettingsGetAndroidProperty("ro.build.nubia.rom.code", &buffer)) { + if (ffStrbufStartsWithS(&buffer, "V")) { + ffStrbufSubstrAfter(&buffer, 0); // `V1.0` -> `1.0` + } + ffStrbufAppendC(&ds->dePrettyName, ' '); + ffStrbufAppend(&ds->dePrettyName, &buffer); + } + return true; + } + + // MiFavor is the UI of the pre-MyOS ZTE phones; `ro.build.MiFavor_version` is a + // bare version number (`4.0`). Note that MyOS, NebulaAIOS and RedMagicOS reuse + // this property for their own version, so they are checked first. + if (ffSettingsGetAndroidProperty("ro.build.MiFavor_version", &ds->dePrettyName)) { + ffStrbufPrependS(&ds->dePrettyName, "MiFavor "); + return true; + } + } + + // LineageOS and other AOSP-based distributions + if (ffSettingsGetAndroidProperty("ro.lineage.build.version", &ds->dePrettyName)) { + ffStrbufPrependS(&ds->dePrettyName, "LineageOS "); + return true; + } + if (ffSettingsGetAndroidProperty("org.pixelexperience.version.display", &ds->dePrettyName)) { + // PixelExperience_Plus_whyred-13.0-20230325-0421-OFFICIAL + ffStrbufSubstrBeforeFirstC(&ds->dePrettyName, '-'); + ffStrbufSubstrBeforeLastC(&ds->dePrettyName, '_'); + ffStrbufReplaceAllC(&ds->dePrettyName, '_', ' '); + return true; + } + + if (ffStrbufEqualS(&brand, "asus") && + ffSettingsGetAndroidProperty("ro.build.version.incremental", &ds->dePrettyName)) { return true; } if (ffSettingsGetAndroidProperty("ro.build.display.id", &ds->dePrettyName)) { - if (ffStrbufStartsWithS(&ds->dePrettyName, "RedMagicOS")) { - ffStrbufInsertNC(&ds->dePrettyName, strlen("RedMagicOS"), 1, ' '); - } - - // Google Pixel uses native Android + // Google Pixel and other devices running native Android return true; }