From 26f9b6b20c0949c79ec696d47183480f3d70387d Mon Sep 17 00:00:00 2001 From: Justin Oros Date: Thu, 9 Apr 2026 17:20:48 -0700 Subject: [PATCH] feat(lockscreen): add customizable lockscreen shown on wake/power-on --- plugins/locksreen/LockscreenActivity.cpp | 193 +++++++ plugins/locksreen/LockscreenActivity.h | 65 +++ plugins/locksreen/LockscreenPlugin.cpp | 40 ++ plugins/locksreen/LockscreenPlugin.h | 19 + plugins/locksreen/LockscreenSettingsPage.cpp | 22 + plugins/locksreen/LockscreenSettingsPage.h | 11 + plugins/locksreen/patch.py | 571 +++++++++++++++++++ 7 files changed, 921 insertions(+) create mode 100644 plugins/locksreen/LockscreenActivity.cpp create mode 100644 plugins/locksreen/LockscreenActivity.h create mode 100644 plugins/locksreen/LockscreenPlugin.cpp create mode 100644 plugins/locksreen/LockscreenPlugin.h create mode 100644 plugins/locksreen/LockscreenSettingsPage.cpp create mode 100644 plugins/locksreen/LockscreenSettingsPage.h create mode 100644 plugins/locksreen/patch.py diff --git a/plugins/locksreen/LockscreenActivity.cpp b/plugins/locksreen/LockscreenActivity.cpp new file mode 100644 index 0000000..4e99c22 --- /dev/null +++ b/plugins/locksreen/LockscreenActivity.cpp @@ -0,0 +1,193 @@ +#include "LockscreenActivity.h" + +#include +#include + +#include "CrossPointSettings.h" +#include "MappedInputManager.h" +#include "activities/settings/LockscreenPlugin.h" +#include "components/UITheme.h" +#include "fontIds.h" + +static constexpr int SCREEN_W = 480; +static constexpr int KEY_W = 130; +static constexpr int KEY_H = 80; +static constexpr int KEY_PAD = 8; +static constexpr int GRID_W = LockscreenActivity::COLS * (KEY_W + KEY_PAD) - KEY_PAD; +static constexpr int GRID_X = (SCREEN_W - GRID_W) / 2; + +void LockscreenActivity::onEnter() { + Activity::onEnter(); + success_ = false; + done_ = false; + renderPending_ = true; + pinLen_ = 0; + attempts_ = 0; + cursorRow_ = 0; + cursorCol_ = 0; + memset(pin_, 0, sizeof(pin_)); + enterTime_ = millis(); + requestUpdate(); +} + +void LockscreenActivity::onExit() { + Activity::onExit(); +} + +void LockscreenActivity::goToSleep() { + done_ = true; + success_ = false; +} + +void LockscreenActivity::pressCurrentKey() { + const char* key = KEYS[cursorRow_][cursorCol_]; + + if (strcmp(key, "<") == 0) { + if (pinLen_ > 0) { + pinLen_--; + pin_[pinLen_] = '\0'; + } + renderPending_ = true; + requestUpdate(); + return; + } + + if (strcmp(key, "OK") == 0) { + if (pinLen_ < PIN_LEN) return; + + if (purpose_ == Purpose::CREATE) { + LockscreenPlugin::hashPin(pin_, SETTINGS.lockscreenPinHash); + SETTINGS.saveToFile(); + success_ = true; + done_ = true; + finish(); + } else { + if (LockscreenPlugin::checkPin(pin_, SETTINGS.lockscreenPinHash)) { + success_ = true; + done_ = true; + } else { + attempts_++; + pinLen_ = 0; + memset(pin_, 0, sizeof(pin_)); + renderPending_ = true; + requestUpdate(); + if (attempts_ >= MAX_ATTEMPTS) { + goToSleep(); + } + } + } + return; + } + + if (pinLen_ < PIN_LEN) { + pin_[pinLen_++] = key[0]; + pin_[pinLen_] = '\0'; + if (pinLen_ == PIN_LEN) { + cursorRow_ = 3; + cursorCol_ = 2; + } + renderPending_ = true; + requestUpdate(); + } +} + +void LockscreenActivity::renderPinDisplay(int startY, bool masked) const { + constexpr int BOX_W = 50; + constexpr int BOX_H = 60; + constexpr int BOX_PAD = 12; + constexpr int TOTAL_W = 4 * BOX_W + 3 * BOX_PAD; + int x = (SCREEN_W - TOTAL_W) / 2; + + for (int i = 0; i < PIN_LEN; i++) { + int bx = x + i * (BOX_W + BOX_PAD); + renderer.drawRect(bx, startY, BOX_W, BOX_H); + if (i < pinLen_) { + char display[2] = {masked ? '*' : pin_[i], '\0'}; + renderer.drawText(UI_12_FONT_ID, bx + BOX_W / 2 - 8, startY + BOX_H / 2 - 10, display); + } + } +} + +void LockscreenActivity::renderKeypad(int startY) const { + for (int r = 0; r < ROWS; r++) { + for (int c = 0; c < COLS; c++) { + int kx = GRID_X + c * (KEY_W + KEY_PAD); + int ky = startY + r * (KEY_H + KEY_PAD); + bool isCursor = (r == cursorRow_ && c == cursorCol_); + if (isCursor) { + renderer.fillRect(kx, ky, KEY_W, KEY_H); + renderer.drawText(UI_12_FONT_ID, kx + KEY_W / 2 - 8, ky + KEY_H / 2 - 10, + KEYS[r][c], false, EpdFontFamily::BOLD); + } else { + renderer.drawRect(kx, ky, KEY_W, KEY_H); + renderer.drawText(UI_12_FONT_ID, kx + KEY_W / 2 - 8, ky + KEY_H / 2 - 10, + KEYS[r][c], true, EpdFontFamily::REGULAR); + } + } + } +} + +void LockscreenActivity::render(RenderLock&&) { + const auto& metrics = UITheme::getInstance().getMetrics(); + const auto pageH = renderer.getScreenHeight(); + const bool isCreate = (purpose_ == Purpose::CREATE); + const bool masked = !isCreate; + + renderer.clearScreen(); + + const char* title = isCreate ? "Set PIN" : "Unlock"; + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, SCREEN_W, metrics.headerHeight}, title); + + int y = metrics.topPadding + metrics.headerHeight + 80; + + renderPinDisplay(y, masked); + y += 90; + renderKeypad(y); + + if (isCreate) { + renderer.drawCenteredText(UI_10_FONT_ID, pageH - 40, "Navigate: Directional buttons | Select: Power"); + } else if (attempts_ > 0) { + char msg[40]; + snprintf(msg, sizeof(msg), "Incorrect PIN. %d attempt(s) remaining.", MAX_ATTEMPTS - attempts_); + renderer.drawCenteredText(UI_10_FONT_ID, pageH - 40, msg); + } + + renderer.displayBuffer(); +} + +void LockscreenActivity::loop() { + if (purpose_ == Purpose::UNLOCK) { + if (millis() - enterTime_ > TIMEOUT_MS) { + goToSleep(); + return; + } + } + + if (mappedInput.wasPressed(MappedInputManager::Button::Up)) { + cursorRow_ = (cursorRow_ - 1 + ROWS) % ROWS; + renderPending_ = true; + requestUpdate(); + } else if (mappedInput.wasPressed(MappedInputManager::Button::Down)) { + cursorRow_ = (cursorRow_ + 1) % ROWS; + renderPending_ = true; + requestUpdate(); + } else if (mappedInput.wasPressed(MappedInputManager::Button::Left)) { + cursorCol_ = (cursorCol_ - 1 + COLS) % COLS; + renderPending_ = true; + requestUpdate(); + } else if (mappedInput.wasPressed(MappedInputManager::Button::Right)) { + cursorCol_ = (cursorCol_ + 1) % COLS; + renderPending_ = true; + requestUpdate(); + } else if (mappedInput.wasPressed(MappedInputManager::Button::Power) || + mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { + pressCurrentKey(); + } else if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { + if (pinLen_ > 0) { + pinLen_--; + pin_[pinLen_] = '\0'; + renderPending_ = true; + requestUpdate(); + } + } +} diff --git a/plugins/locksreen/LockscreenActivity.h b/plugins/locksreen/LockscreenActivity.h new file mode 100644 index 0000000..811568a --- /dev/null +++ b/plugins/locksreen/LockscreenActivity.h @@ -0,0 +1,65 @@ +#pragma once + +#include "activities/settings/LockscreenPlugin.h" +#include "activities/Activity.h" + +class LockscreenActivity final : public Activity { + public: + enum class Purpose { UNLOCK, CREATE }; + + static constexpr int COLS = 3; + static constexpr int ROWS = 4; + + explicit LockscreenActivity(GfxRenderer& renderer, + MappedInputManager& mappedInput, + Purpose purpose) + : Activity("Lockscreen", renderer, mappedInput), purpose_(purpose) {} + + void onEnter() override; + void onExit() override; + void loop() override; + bool skipLoopDelay() override { return true; } + void render(RenderLock&&) override; + + bool wasSuccessful() const { return success_; } + bool isDone() const { return done_; } + bool needsRender() const { return renderPending_; } + + void renderDirect() { + renderPending_ = false; + RenderLock rl(*this); + render(std::move(rl)); + } + + private: + static constexpr int PIN_LEN = 4; + static constexpr unsigned long TIMEOUT_MS = 120000; + + static constexpr const char* KEYS[ROWS][COLS] = { + {"1", "2", "3"}, + {"4", "5", "6"}, + {"7", "8", "9"}, + {"<", "0", "OK"}, + }; + + Purpose purpose_; + bool success_ = false; + bool done_ = false; + bool renderPending_ = false; + + int cursorRow_ = 0; + int cursorCol_ = 0; + + char pin_[PIN_LEN + 1] = {}; + int pinLen_ = 0; + + int attempts_ = 0; + static constexpr int MAX_ATTEMPTS = 3; + + unsigned long enterTime_ = 0; + + void pressCurrentKey(); + void renderPinDisplay(int startY, bool masked) const; + void renderKeypad(int startY) const; + void goToSleep(); +}; diff --git a/plugins/locksreen/LockscreenPlugin.cpp b/plugins/locksreen/LockscreenPlugin.cpp new file mode 100644 index 0000000..059835e --- /dev/null +++ b/plugins/locksreen/LockscreenPlugin.cpp @@ -0,0 +1,40 @@ +#include "LockscreenPlugin.h" + +namespace LockscreenPlugin { + +const char* modeName(LockscreenMode mode) { + switch (mode) { + case LockscreenMode::ENABLED: return "Enabled"; + default: return "Disabled"; + } +} + +bool shouldLock(LockscreenMode mode) { + return mode == LockscreenMode::ENABLED; +} + +bool shouldLockOnWake(LockscreenMode mode) { + return shouldLock(mode); +} + +bool shouldLockOnPower(LockscreenMode mode) { + return shouldLock(mode); +} + +void hashPin(const char* pin, char outHash[9]) { + uint32_t hash = 5381; + for (const char* p = pin; *p; p++) { + hash = ((hash << 5) + hash) ^ static_cast(*p); + } + hash ^= 0xA3F1C2B4u; + snprintf(outHash, 9, "%08X", hash); +} + +bool checkPin(const char* pin, const char* storedHash) { + if (!pin || !storedHash || storedHash[0] == '\0') return false; + char computed[9]; + hashPin(pin, computed); + return strncmp(computed, storedHash, 9) == 0; +} + +} diff --git a/plugins/locksreen/LockscreenPlugin.h b/plugins/locksreen/LockscreenPlugin.h new file mode 100644 index 0000000..b267335 --- /dev/null +++ b/plugins/locksreen/LockscreenPlugin.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +enum class LockscreenMode : uint8_t { + MODE_OFF = 0, + ENABLED = 1, +}; + +namespace LockscreenPlugin { + +const char* modeName(LockscreenMode mode); +bool shouldLock(LockscreenMode mode); +bool shouldLockOnWake(LockscreenMode mode); +bool shouldLockOnPower(LockscreenMode mode); +void hashPin(const char* pin, char outHash[9]); +bool checkPin(const char* pin, const char* storedHash); + +} diff --git a/plugins/locksreen/LockscreenSettingsPage.cpp b/plugins/locksreen/LockscreenSettingsPage.cpp new file mode 100644 index 0000000..baa702a --- /dev/null +++ b/plugins/locksreen/LockscreenSettingsPage.cpp @@ -0,0 +1,22 @@ +#include "LockscreenSettingsPage.h" +#include "fontIds.h" + +void LockscreenSettingsActivity::render(GfxRenderer& renderer, LockscreenMode currentMode) { + const int LABEL_X = 10; + const int VALUE_X = 160; + const int ROW_Y = 60; + renderer.drawText(UI_12_FONT_ID, LABEL_X, ROW_Y, "Lockscreen"); + renderer.drawText(UI_12_FONT_ID, VALUE_X, ROW_Y, LockscreenPlugin::modeName(currentMode)); +} + +LockscreenMode LockscreenSettingsActivity::handleNext(LockscreenMode currentMode) { + uint8_t v = static_cast(currentMode); + if (v < 1) v++; + return static_cast(v); +} + +LockscreenMode LockscreenSettingsActivity::handlePrev(LockscreenMode currentMode) { + uint8_t v = static_cast(currentMode); + if (v > 0) v--; + return static_cast(v); +} diff --git a/plugins/locksreen/LockscreenSettingsPage.h b/plugins/locksreen/LockscreenSettingsPage.h new file mode 100644 index 0000000..dcb3380 --- /dev/null +++ b/plugins/locksreen/LockscreenSettingsPage.h @@ -0,0 +1,11 @@ +#pragma once + +#include +#include "LockscreenPlugin.h" + +class LockscreenSettingsActivity { +public: + static void render(GfxRenderer& renderer, LockscreenMode currentMode); + static LockscreenMode handleNext(LockscreenMode currentMode); + static LockscreenMode handlePrev(LockscreenMode currentMode); +}; diff --git a/plugins/locksreen/patch.py b/plugins/locksreen/patch.py new file mode 100644 index 0000000..75cf93a --- /dev/null +++ b/plugins/locksreen/patch.py @@ -0,0 +1,571 @@ +#!/usr/bin/env python3 + +import os +import sys +import glob +import shutil + + +def read_file(path): + with open(path, "r", encoding="utf-8") as f: + return f.read() + +def write_file(path, content): + with open(path, "w", encoding="utf-8") as f: + f.write(content) + +def find_first(filename, repo_dir): + results = glob.glob(os.path.join(repo_dir, "**", filename), recursive=True) + if not results: + sys.exit(f"ERROR: Could not locate {filename} in {repo_dir}") + return results[0] + + +def copy_plugin_sources(plugin_dir, repo_dir): + settings_dest = os.path.join(repo_dir, "src", "activities", "settings") + activities_dest = os.path.join(repo_dir, "src", "activities") + os.makedirs(settings_dest, exist_ok=True) + + settings_files = { + "LockscreenSettingsPage.h", "LockscreenSettingsPage.cpp", + "LockscreenPlugin.h", "LockscreenPlugin.cpp", + } + + for fname in os.listdir(plugin_dir): + if not fname.endswith((".h", ".cpp", ".hpp")): + continue + src = os.path.join(plugin_dir, fname) + dst = os.path.join(settings_dest if fname in settings_files else activities_dest, fname) + shutil.copy2(src, dst) + print(f" ✓ {fname}") + + +def patch_cross_point_settings_h(repo_dir): + path = find_first("CrossPointSettings.h", repo_dir) + content = read_file(path) + + if "lockscreenMode" in content: + print(" CrossPointSettings.h already patched, skipping.") + return + + content = content.replace( + '#pragma once\n#include ', + '#pragma once\n#include \n#include "activities/settings/LockscreenPlugin.h"' + ) + content = content.replace( + ' uint8_t imageRendering = IMAGES_DISPLAY;', + ' uint8_t imageRendering = IMAGES_DISPLAY;\n' + ' uint8_t lockscreenMode = 0;\n' + ' char lockscreenPinHash[9] = {};' + ) + + write_file(path, content) + print(" CrossPointSettings.h patched.") + + +def patch_settings_list_h(repo_dir): + path = find_first("SettingsList.h", repo_dir) + content = read_file(path) + + if "lockscreenMode" in content and "lockscreenPinHash" in content: + if '{StrId::STR_NONE_OPT, StrId::STR_NONE_OPT,\n StrId::STR_NONE_OPT, StrId::STR_NONE_OPT}' in content: + content = content.replace( + ' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT,\n' + ' StrId::STR_NONE_OPT, StrId::STR_NONE_OPT},\n', + ' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT},\n' + ) + write_file(path, content) + print(" SettingsList.h patched (reduced lockscreenMode to 2 options).") + else: + print(" SettingsList.h already patched, skipping.") + return + + if "lockscreenMode" in content and "lockscreenPinHash" not in content: + content = content.replace( + ' "lockscreenMode", StrId::STR_NONE_OPT),\n', + ' "lockscreenMode", StrId::STR_NONE_OPT),\n' + ' SettingInfo::String(StrId::STR_NONE_OPT, SETTINGS.lockscreenPinHash,\n' + ' sizeof(SETTINGS.lockscreenPinHash),\n' + ' "lockscreenPinHash", StrId::STR_NONE_OPT),\n' + ) + write_file(path, content) + print(" SettingsList.h patched (added lockscreenPinHash).") + return + + entry = ( + '\n' + ' SettingInfo::Enum(StrId::STR_NONE_OPT, &CrossPointSettings::lockscreenMode,\n' + ' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT},\n' + ' "lockscreenMode", StrId::STR_NONE_OPT),\n' + ' SettingInfo::String(StrId::STR_NONE_OPT, SETTINGS.lockscreenPinHash,\n' + ' sizeof(SETTINGS.lockscreenPinHash),\n' + ' "lockscreenPinHash", StrId::STR_NONE_OPT),\n' + ) + + content = content.replace( + ' };\n return list;\n}\n', + entry + ' };\n return list;\n}\n' + ) + + write_file(path, content) + print(" SettingsList.h patched.") + + +def patch_settings_h(repo_dir): + path = find_first("SettingsActivity.h", repo_dir) + content = read_file(path) + + already_has_plugins_tab = "categoryCount = 5" in content + already_has_plugins_vec = "pluginsSettings" in content + + if already_has_plugins_tab and already_has_plugins_vec: + print(" SettingsActivity.h already patched, skipping.") + return + + if not already_has_plugins_tab: + content = content.replace( + 'static constexpr int categoryCount = 4;', + 'static constexpr int categoryCount = 5;' + ) + + if not already_has_plugins_vec: + content = content.replace( + ' std::vector systemSettings;', + ' std::vector systemSettings;\n std::vector pluginsSettings;' + ) + + write_file(path, content) + print(" SettingsActivity.h patched.") + + +def patch_settings_cpp(repo_dir): + path = find_first("SettingsActivity.cpp", repo_dir) + content = read_file(path) + + if '"LockscreenPlugin.h"' in content and "lockscreenPinHash" in content and '"lockscreenMode") return "Lockscreen"' in content: + print(" SettingsActivity.cpp already patched, skipping.") + return + + changed = False + + if '"LockscreenPlugin.h"' not in content: + content = content.replace( + '#include "SettingsList.h"', + '#include "SettingsList.h"\n' + '#include "activities/settings/LockscreenPlugin.h"\n' + '#include "activities/LockscreenActivity.h"' + ) + changed = True + + if 'STR_NONE_OPT};' not in content: + content = content.replace( + 'StrId::STR_CAT_CONTROLS, StrId::STR_CAT_SYSTEM};', + 'StrId::STR_CAT_CONTROLS, StrId::STR_CAT_SYSTEM,\n' + ' StrId::STR_NONE_OPT};' + ) + changed = True + + if 'pluginsSettings.clear()' not in content: + content = content.replace( + ' systemSettings.clear();\n', + ' systemSettings.clear();\n' + ' pluginsSettings.clear();\n' + ' for (const auto& setting : getSettingsList()) {\n' + ' if (setting.key && std::string(setting.key) == "lockscreenMode") {\n' + ' pluginsSettings.push_back(setting);\n' + ' }\n' + ' }\n' + ) + changed = True + elif 'pluginsSettings.push_back' not in content: + content = content.replace( + ' pluginsSettings.clear();\n', + ' pluginsSettings.clear();\n' + ' for (const auto& setting : getSettingsList()) {\n' + ' if (setting.key && std::string(setting.key) == "lockscreenMode") {\n' + ' pluginsSettings.push_back(setting);\n' + ' }\n' + ' }\n' + ) + changed = True + elif '"lockscreenMode") {\n pluginsSettings.push_back' not in content: + content = content.replace( + ' for (const auto& setting : getSettingsList()) {\n' + ' if (setting.key && std::string(setting.key) == "lockscreenMode") {\n' + ' pluginsSettings.push_back(setting);\n' + ' }\n' + ' }\n' + '\n' + ' for (const auto& setting : getSettingsList()) {', + ' for (const auto& setting : getSettingsList()) {\n' + ' if (setting.key && std::string(setting.key) == "lockscreenMode") {\n' + ' pluginsSettings.push_back(setting);\n' + ' }\n' + ' }\n' + '\n' + ' for (const auto& setting : getSettingsList()) {' + ) + # If pluginsSettings.push_back exists from another plugin but not lockscreen, + # append after the existing population loop + if '"lockscreenMode") {\n pluginsSettings.push_back' not in content: + import re + content = re.sub( + r'( pluginsSettings\.clear\(\);(\n for.*?push_back.*?\n \}\n)*)', + lambda m: m.group(0) + + ' for (const auto& setting : getSettingsList()) {\n' + ' if (setting.key && std::string(setting.key) == "lockscreenMode") {\n' + ' pluginsSettings.push_back(setting);\n' + ' }\n' + ' }\n', + content, + count=1, + flags=re.DOTALL + ) + changed = True + + if 'case 4:' not in content: + content = content.replace( + ' case 3:\n' + ' currentSettings = &systemSettings;\n' + ' break;\n', + ' case 3:\n' + ' currentSettings = &systemSettings;\n' + ' break;\n' + ' case 4:\n' + ' currentSettings = &pluginsSettings;\n' + ' break;\n' + ) + changed = True + + if '"Plugins"' not in content: + content = content.replace( + ' tabs.push_back({I18N.get(categoryNames[i]), selectedCategoryIndex == i});\n' + ' }', + ' const char* tabLabel = (i == 4) ? "Plugins" : I18N.get(categoryNames[i]);\n' + ' tabs.push_back({tabLabel, selectedCategoryIndex == i});\n' + ' }' + ) + changed = True + + if 'nextCatIdx' not in content: + content = content.replace( + ' const auto confirmLabel = (selectedSettingIndex == 0)\n' + ' ? I18N.get(categoryNames[(selectedCategoryIndex + 1) % categoryCount])\n' + ' : tr(STR_TOGGLE);', + ' const int nextCatIdx = (selectedCategoryIndex + 1) % categoryCount;\n' + ' const char* nextCatLabel = (nextCatIdx == 4) ? "Plugins" : I18N.get(categoryNames[nextCatIdx]);\n' + ' const auto confirmLabel = (selectedSettingIndex == 0) ? nextCatLabel : tr(STR_TOGGLE);' + ) + changed = True + + if 'selectedCategoryIndex == 4' not in content: + content = content.replace( + ' [&settings](int index) { return std::string(I18N.get(settings[index].nameId)); },', + ' [&settings, this](int index) -> std::string {\n' + ' if (selectedCategoryIndex == 4) {\n' + ' const auto& s = settings[index];\n' + ' if (s.key && std::string(s.key) == "lockscreenMode") return "Lockscreen";\n' + ' }\n' + ' return std::string(I18N.get(settings[index].nameId));\n' + ' },' + ) + changed = True + elif '"lockscreenMode") return "Lockscreen"' not in content: + content = content.replace( + ' if (selectedCategoryIndex == 4) {\n' + ' const auto& s = settings[index];\n', + ' if (selectedCategoryIndex == 4) {\n' + ' const auto& s = settings[index];\n' + ' if (s.key && std::string(s.key) == "lockscreenMode") return "Lockscreen";\n' + ) + changed = True + + if 'LockscreenPlugin::modeName' not in content: + content = content.replace( + ' } else {\n' + ' valueText = I18N.get(setting.enumValues[value]);\n' + ' }', + ' } else if (setting.key && std::string(setting.key) == "lockscreenMode") {\n' + ' valueText = LockscreenPlugin::modeName(static_cast(value));\n' + ' } else {\n' + ' valueText = I18N.get(setting.enumValues[value]);\n' + ' }' + ) + changed = True + + if 'lockscreenPinHash' not in content: + content = content.replace( + ' SETTINGS.saveToFile();\n' + '}\n' + '\n' + 'void SettingsActivity::render', + ' if (setting.key && std::string(setting.key) == "lockscreenMode") {\n' + ' if (SETTINGS.lockscreenMode == 0) {\n' + ' memset(SETTINGS.lockscreenPinHash, 0, sizeof(SETTINGS.lockscreenPinHash));\n' + " } else if (SETTINGS.lockscreenPinHash[0] == '\\0') {\n" + ' startActivityForResult(\n' + ' std::make_unique(renderer, mappedInput, LockscreenActivity::Purpose::CREATE),\n' + ' [](const ActivityResult&) { SETTINGS.saveToFile(); });\n' + ' return;\n' + ' }\n' + ' }\n' + '\n' + ' SETTINGS.saveToFile();\n' + '}\n' + '\n' + 'void SettingsActivity::render' + ) + changed = True + + if changed: + write_file(path, content) + print(" SettingsActivity.cpp patched.") + else: + print(" SettingsActivity.cpp already patched, skipping.") + +def patch_web_server(repo_dir): + path = find_first("CrossPointWebServer.cpp", repo_dir) + content = read_file(path) + + if 'lockscreenMode") continue' in content: + print(" CrossPointWebServer.cpp already patched, skipping.") + return + + target = ( + ' if (s.key && strcmp(s.key, "lockscreenMode") == 0) {\n' + ' doc["name"] = "Lockscreen";\n' + ' doc["category"] = "Plugins";\n' + ' }\n' + ) + + if target in content: + content = content.replace( + target, + ' if (s.key && (strcmp(s.key, "lockscreenMode") == 0 || strcmp(s.key, "lockscreenPinHash") == 0)) continue;\n' + ) + write_file(path, content) + print(" CrossPointWebServer.cpp patched (lockscreen hidden from WUI).") + return + + name_override = ( + '\n' + ' if (s.key && (strcmp(s.key, "lockscreenMode") == 0 || strcmp(s.key, "lockscreenPinHash") == 0)) continue;\n' + ) + + content = content.replace( + ' doc["category"] = I18N.get(s.category);\n' + '\n' + ' switch (s.type) {', + ' doc["category"] = I18N.get(s.category);\n' + + name_override + + '\n' + ' switch (s.type) {' + ) + + write_file(path, content) + print(" CrossPointWebServer.cpp patched (lockscreen hidden from WUI).") + + +def patch_main_cpp(repo_dir): + path = find_first("main.cpp", repo_dir) + content = read_file(path) + + if "LockscreenPlugin" in content: + if "shouldLockOnWake" in content or "shouldLockOnPower" in content or "wasInReader" in content: + content = content.replace( + ' const bool wasInReader = APP_STATE.lastSleepFromReader;\n' + ' const bool shouldLock =\n' + ' hasPinSet &&\n' + ' ((lsMode == LockscreenMode::ENABLED) ||\n' + ' (lsMode == LockscreenMode::ON_POWER && !wasInReader) ||\n' + ' (lsMode == LockscreenMode::ON_WAKE && wasInReader));\n', + ' const bool shouldLock = hasPinSet && LockscreenPlugin::shouldLock(lsMode);\n' + ) + write_file(path, content) + print(" main.cpp patched (updated shouldLock call).") + else: + print(" main.cpp already patched, skipping.") + return + + content = content.replace( + '#include "CrossPointSettings.h"', + '#include "CrossPointSettings.h"\n' + '#include "activities/settings/LockscreenPlugin.h"\n' + '#include "activities/LockscreenActivity.h"' + ) + + lockscreen_block = ( + '\n' + ' {\n' + ' const auto lsMode = static_cast(SETTINGS.lockscreenMode);\n' + ' const bool hasPinSet = SETTINGS.lockscreenPinHash[0] != \'\\0\';\n' + ' const bool shouldLock = hasPinSet && LockscreenPlugin::shouldLock(lsMode);\n' + '\n' + ' if (shouldLock) {\n' + ' LockscreenActivity lockAct(renderer, mappedInputManager, LockscreenActivity::Purpose::UNLOCK);\n' + ' lockAct.onEnter();\n' + ' lockAct.renderDirect();\n' + ' while (!lockAct.isDone()) {\n' + ' gpio.update();\n' + ' lockAct.loop();\n' + ' if (lockAct.needsRender()) {\n' + ' lockAct.renderDirect();\n' + ' }\n' + ' delay(10);\n' + ' }\n' + ' lockAct.onExit();\n' + ' if (!lockAct.wasSuccessful()) {\n' + ' enterDeepSleep();\n' + ' return;\n' + ' }\n' + ' }\n' + ' }\n' + ) + + content = content.replace( + ' RECENT_BOOKS.loadFromFile();\n' + '\n' + ' // Boot to home screen if no book is open', + ' RECENT_BOOKS.loadFromFile();\n' + + lockscreen_block + + '\n' + ' // Boot to home screen if no book is open' + ) + + write_file(path, content) + print(" main.cpp patched.") + + +def patch_settings_html(repo_dir): + path = find_first("SettingsPage.html", repo_dir) + content = read_file(path) + + if "lockscreenPin" in content: + print(" SettingsPage.html already patched, skipping.") + return + + lines = [ + "", + " ", + "", + ] + + js = "\n".join(lines) + content = content.replace("", js + "") + write_file(path, content) + print(" SettingsPage.html patched.") + + +def patch(repo_dir: str): + plugin_dir = os.path.dirname(os.path.abspath(__file__)) + + print(" Copying plugin sources...") + copy_plugin_sources(plugin_dir, repo_dir) + + print(" Patching CrossPointSettings.h...") + patch_cross_point_settings_h(repo_dir) + + print(" Patching SettingsList.h...") + patch_settings_list_h(repo_dir) + + print(" Patching SettingsActivity.h...") + patch_settings_h(repo_dir) + + print(" Patching SettingsActivity.cpp...") + patch_settings_cpp(repo_dir) + + print(" Patching CrossPointWebServer.cpp...") + patch_web_server(repo_dir) + + print(" Patching main.cpp...") + patch_main_cpp(repo_dir) + + print(" Patching SettingsPage.html (WUI PIN entry)...") + patch_settings_html(repo_dir) + + +if __name__ == "__main__": + if len(sys.argv) < 2: + sys.exit("Usage: python patch.py ") + patch(sys.argv[1])