[feat] add privacy unlock controls and remote relocking

This commit is contained in:
dijunkun
2026-10-27 10:22:25 +08:00
parent d1e4a31efb
commit ea3a2e1faa
41 changed files with 470 additions and 72 deletions
@@ -190,6 +190,10 @@ class Windows {
return TRUE;
}
if (message == WM_CLOSE) return 0;
if (message == kPrivacyTextChanged) {
InvalidateRect(hwnd, nullptr, TRUE);
return 0;
}
if ((message == WM_DISPLAYCHANGE || message == WM_DPICHANGED) && self) {
self->changed = true;
InvalidateRect(hwnd, nullptr, TRUE);
@@ -200,8 +204,12 @@ class Windows {
HDC dc = BeginPaint(hwnd, &paint);
RECT rect{};
GetClientRect(hwnd, &rect);
const bool allow_unlock = self &&
InterlockedCompareExchange(&self->shared_->allow_local_unlock, 0, 0);
if (self && !DrawPrivacyCover(dc, rect, PrivacyWindowDpi(hwnd), module,
self->shared_->unlock_hint)) {
allow_unlock ? self->shared_->unlock_hint
: self->shared_->locked_hint,
allow_unlock)) {
self->paint_failed_ = true;
self->Fail(L"Paint privacy logo and unlock instructions");
}
@@ -37,7 +37,7 @@ bool Select(HDC dc, HGDIOBJ object) {
} // namespace
bool DrawPrivacyCover(HDC dc, const RECT& rect, UINT dpi, HINSTANCE module,
const wchar_t* unlock_hint) {
const wchar_t* unlock_hint, bool allow_local_unlock) {
if (!FillRect(dc, &rect, static_cast<HBRUSH>(GetStockObject(BLACK_BRUSH))))
return false;
const int width = rect.right - rect.left;
@@ -68,6 +68,7 @@ bool DrawPrivacyCover(HDC dc, const RECT& rect, UINT dpi, HINSTANCE module,
}};
constexpr UINT flags = DT_LEFT | DT_WORDBREAK | DT_NOPREFIX;
for (size_t i = 0; i < lines.size(); ++i) {
if (i == 1 && !allow_local_unlock) continue;
auto& line = lines[i];
resources.fonts[i] =
CreateFontW(-px(line.size), 0, 0, 0, line.weight, FALSE, FALSE, FALSE,
@@ -81,7 +82,7 @@ bool DrawPrivacyCover(HDC dc, const RECT& rect, UINT dpi, HINSTANCE module,
}
const int content_height =
static_cast<int>(lines[0].bounds.bottom + lines[1].bounds.bottom) +
px(12);
(allow_local_unlock ? px(12) : 0);
// Match the icon width to the name; each side keeps its own height.
const int brand_gap = px(6);
const int brand_width = static_cast<int>(lines[kBrandLine].bounds.right);
@@ -110,6 +111,7 @@ bool DrawPrivacyCover(HDC dc, const RECT& rect, UINT dpi, HINSTANCE module,
DT_CENTER | DT_SINGLELINE | DT_NOPREFIX))
return false;
for (size_t i = 0; i < kInstructionLines; ++i) {
if (i == 1 && !allow_local_unlock) continue;
const auto& line = lines[i];
if (!Select(dc, resources.fonts[i])) return false;
RECT bounds{left, top, left + text_width, top + line.bounds.bottom};
@@ -13,7 +13,7 @@ namespace crossdesk {
// Paints the local, opaque cover. The caller owns the HWND and its DPI context.
bool DrawPrivacyCover(HDC dc, const RECT& rect, UINT dpi, HINSTANCE module,
const wchar_t* unlock_hint);
const wchar_t* unlock_hint, bool allow_local_unlock = true);
} // namespace crossdesk
@@ -120,6 +120,7 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
}
if (!Initialize(error)) return false;
emergency_ = false;
allow_local_unlock_ = text.allow_local_unlock;
health_error_.clear();
if (!RegisterHotKey(sink_, kHotkeyId,
MOD_CONTROL | MOD_ALT | MOD_SHIFT | MOD_NOREPEAT,
@@ -149,6 +150,10 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
return true;
}
void UpdateText(const PrivacyScreenText& text) override {
allow_local_unlock_ = text.allow_local_unlock;
band_guard_.SetLocalUnlockAllowed(text.allow_local_unlock);
}
void Recover() override {
for (auto& hook : window_events_) {
if (UnhookWinEvent(hook))
@@ -475,7 +480,8 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
auto self = reinterpret_cast<WindowsPrivacyBackend*>(
GetWindowLongPtrW(hwnd, GWLP_USERDATA));
if (msg == WM_HOTKEY || msg == kEmergency) {
if (self && hwnd == self->sink_) self->emergency_ = true;
if (self && hwnd == self->sink_ && self->allow_local_unlock_)
self->emergency_ = true;
return 0;
}
if (msg == WM_MOUSEACTIVATE) return MA_NOACTIVATE;
@@ -522,6 +528,7 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
std::unique_ptr<ScopedWindowsPhysicalCoordinates> dpi_scope_;
bool registered_ = false;
bool hotkey_ = false;
bool allow_local_unlock_ = true;
bool emergency_ = false;
bool layout_changed_ = false;
bool topology_pending_ = false;
@@ -153,6 +153,11 @@ bool PrivacyBandGuard::Start(const PrivacyScreenText& text,
text.unlock_hint.c_str(), -1, shared_->unlock_hint,
static_cast<int>(std::size(shared_->unlock_hint))))
return Failed("Encode privacy screen text", error);
if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
text.locked_hint.c_str(), -1, shared_->locked_hint,
static_cast<int>(std::size(shared_->locked_hint))))
return Failed("Encode locked privacy screen text", error);
shared_->allow_local_unlock = text.allow_local_unlock;
shared_->parent_pid = GetCurrentProcessId();
HANDLE handles[] = {parent.value, stop_, ready_, mapping_};
std::wstring bootstrap;
@@ -299,6 +304,16 @@ bool PrivacyBandGuard::Windows(std::vector<PrivacyBandWindow>& windows,
return retry();
}
void PrivacyBandGuard::SetLocalUnlockAllowed(bool allowed) {
if (!shared_ || InterlockedExchange(&shared_->allow_local_unlock, allowed) == allowed) return;
std::vector<PrivacyBandWindow> windows;
std::string error;
if (Windows(windows, error))
for (const auto& window : windows)
PostMessageW(reinterpret_cast<HWND>(static_cast<uintptr_t>(window.hwnd)),
kPrivacyTextChanged, 0, 0);
}
bool PrivacyBandGuard::Stop(std::string& error) {
bool ok = true;
if (process_) {
@@ -27,6 +27,7 @@ class PrivacyBandGuard {
~PrivacyBandGuard();
static bool Available(std::string& error);
bool Start(const PrivacyScreenText& text, std::string& error);
void SetLocalUnlockAllowed(bool allowed);
bool Stop(std::string& error);
bool Healthy(std::string& error);
bool Windows(std::vector<PrivacyBandWindow>& windows, std::string& error);
@@ -14,7 +14,8 @@
namespace crossdesk {
constexpr uint32_t kPrivacyBandMagic = 0x43534231;
constexpr uint32_t kPrivacyBandVersion = 3;
constexpr uint32_t kPrivacyBandVersion = 4;
constexpr UINT kPrivacyTextChanged = WM_APP + 72;
constexpr DWORD kPrivacyWindowBand = 18; // ZBID_ABOVELOCK_UX, current desktop.
constexpr size_t kPrivacyMaxMonitors = 64;
constexpr wchar_t kPrivacyBandBootstrap[] = L"CROSSDESK_PRIVACY_BAND_BOOTSTRAP";
@@ -40,6 +41,8 @@ struct PrivacyBandShared {
alignas(8) volatile LONG64 heartbeat = 0;
wchar_t operation[128]{};
wchar_t unlock_hint[128]{};
wchar_t locked_hint[128]{};
volatile LONG allow_local_unlock = 1;
uint32_t count = 0;
PrivacyBandWindow windows[kPrivacyMaxMonitors]{};
};