feat(app): allow external apps to hide statusbar via manifest flags

- Add parseAppFlagsString() in AppManifestParsing.cpp to parse comma-separated flags (HideStatusBar, Hidden, None)
- Parse [app]flags in V1 and app.flags in V2 manifests
- Loader already supports HideStatusBar, GuiService hides statusbarWidget when set
- Enables fullscreen for ELF apps (e.g. BibleVerse, BookPlayer) without firmware recompilation of internal manifests
- Add tests for flag parsing (38 tests passing)
- Tested on es3c28p /dev/cu.usbmodem1101 @ 192.168.68.133 with HelloWorld app
This commit is contained in:
Adolfo Reyna
2026-07-19 19:25:03 -04:00
parent 84cd73e503
commit f94cc160cf
5 changed files with 110 additions and 0 deletions
@@ -14,6 +14,9 @@ bool isValidAppVersionName(const std::string& version);
bool isValidAppVersionCode(const std::string& version); bool isValidAppVersionCode(const std::string& version);
bool isValidName(const std::string& name); bool isValidName(const std::string& name);
/** Parses a comma-separated flags string (e.g. "HideStatusBar,Hidden") into appFlags bitmask. */
uint16_t parseAppFlagsString(const std::string& raw);
/** Parses a V1 (sectioned INI, e.g. "[app]versionName=...") manifest map. */ /** Parses a V1 (sectioned INI, e.g. "[app]versionName=...") manifest map. */
bool parseManifestV1(const std::map<std::string, std::string>& map, AppManifest& manifest); bool parseManifestV1(const std::map<std::string, std::string>& map, AppManifest& manifest);
@@ -56,6 +56,34 @@ bool isValidName(const std::string& name) {
}); });
} }
uint16_t parseAppFlagsString(const std::string& raw) {
uint16_t flags = AppManifest::Flags::None;
if (raw.empty()) {
return flags;
}
auto parts = string::split(raw, ",");
for (auto& part : parts) {
std::string trimmed = string::trim(part, " \t\r\n");
if (trimmed.empty()) {
continue;
}
std::string lower = string::lowercase(trimmed);
if (lower == "hidestatusbar" || lower == "hide_statusbar" || lower == "hide-statusbar" || lower == "hide_status_bar") {
flags |= AppManifest::Flags::HideStatusBar;
} else if (lower == "hidden") {
flags |= AppManifest::Flags::Hidden;
} else if (lower == "none" || lower == "0" || lower == "") {
// keep as none, no additional flag
} else {
LOG_W(TAG, "Unknown app flag \"%s\" - ignoring", trimmed.c_str());
}
}
return flags;
}
/** The V1 format's first line is always the literal "[manifest]" section header; V2 files are flat from the first line onward. */ /** The V1 format's first line is always the literal "[manifest]" section header; V2 files are flat from the first line onward. */
static bool detectIsV1Format(const std::string& filePath) { static bool detectIsV1Format(const std::string& filePath) {
std::string first_line; std::string first_line;
@@ -71,6 +71,12 @@ bool parseManifestV1(const std::map<std::string, std::string>& map, AppManifest&
return false; return false;
} }
// Optional: [app]flags - e.g. "HideStatusBar" or "HideStatusBar,Hidden"
auto flags_it = map.find("[app]flags");
if (flags_it != map.end()) {
manifest.appFlags = parseAppFlagsString(flags_it->second);
}
return true; return true;
} }
@@ -71,6 +71,12 @@ bool parseManifestV2(const std::map<std::string, std::string>& map, AppManifest&
return false; return false;
} }
// Optional: app.flags - e.g. "HideStatusBar" or "HideStatusBar,Hidden"
auto flags_it = map.find("app.flags");
if (flags_it != map.end()) {
manifest.appFlags = parseAppFlagsString(flags_it->second);
}
return true; return true;
} }
@@ -81,3 +81,70 @@ TEST_CASE("parseManifestV2() should fail when the app id is invalid") {
AppManifest manifest; AppManifest manifest;
CHECK_EQ(parseManifestV2(properties, manifest), false); CHECK_EQ(parseManifestV2(properties, manifest), false);
} }
TEST_CASE("parseAppFlagsString() should parse various flag combinations") {
CHECK_EQ(parseAppFlagsString(""), 0);
CHECK_EQ(parseAppFlagsString("None"), 0);
CHECK_EQ(parseAppFlagsString("HideStatusBar"), AppManifest::Flags::HideStatusBar);
CHECK_EQ(parseAppFlagsString("hidden"), AppManifest::Flags::Hidden);
CHECK_EQ(parseAppFlagsString("HideStatusBar,Hidden"), AppManifest::Flags::HideStatusBar | AppManifest::Flags::Hidden);
CHECK_EQ(parseAppFlagsString(" hidestatusbar , hidden "), AppManifest::Flags::HideStatusBar | AppManifest::Flags::Hidden);
CHECK_EQ(parseAppFlagsString("HideStatusBar, Hidden"), AppManifest::Flags::HideStatusBar | AppManifest::Flags::Hidden);
}
TEST_CASE("parseManifest() should parse V1 flags for fullscreen") {
TestFile file("test-manifest-v1-flags.properties");
file.writeData(
"[manifest]\n"
"version=0.1\n"
"[target]\n"
"sdk=0.0.0\n"
"platforms=esp32\n"
"[app]\n"
"id=one.tactility.sdktest\n"
"versionName=0.1.0\n"
"versionCode=1\n"
"name=SDK Test\n"
"flags=HideStatusBar\n"
);
AppManifest manifest;
CHECK_EQ(parseManifest(file.getPath(), manifest), true);
CHECK_EQ(manifest.appFlags & AppManifest::Flags::HideStatusBar, AppManifest::Flags::HideStatusBar);
}
TEST_CASE("parseManifest() should parse V2 flags for fullscreen") {
TestFile file("test-manifest-v2-flags.properties");
file.writeData(
"manifest.version=0.1\n"
"target.sdk=0.0.0\n"
"target.platforms=esp32\n"
"app.id=one.tactility.sdktest\n"
"app.version.name=0.1.0\n"
"app.version.code=1\n"
"app.name=SDK Test\n"
"app.flags=HideStatusBar\n"
);
AppManifest manifest;
CHECK_EQ(parseManifest(file.getPath(), manifest), true);
CHECK_EQ(manifest.appFlags & AppManifest::Flags::HideStatusBar, AppManifest::Flags::HideStatusBar);
}
TEST_CASE("parseManifest() should parse combined flags") {
TestFile file("test-manifest-v2-flags2.properties");
file.writeData(
"manifest.version=0.1\n"
"target.sdk=0.0.0\n"
"target.platforms=esp32\n"
"app.id=one.tactility.sdktest\n"
"app.version.name=0.1.0\n"
"app.version.code=1\n"
"app.name=SDK Test\n"
"app.flags=HideStatusBar,Hidden\n"
);
AppManifest manifest;
CHECK_EQ(parseManifest(file.getPath(), manifest), true);
CHECK_EQ(manifest.appFlags, (AppManifest::Flags::HideStatusBar | AppManifest::Flags::Hidden));
}