mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-02 20:12:44 +08:00
[feat] add privacy unlock controls and remote relocking
This commit is contained in:
@@ -63,10 +63,12 @@ class LinuxPrivacyBackend final : public PrivacyBackend {
|
||||
: "Checking Linux privacy support"};
|
||||
}
|
||||
|
||||
bool Enable(bool block_input, const PrivacyScreenText&,
|
||||
bool Enable(bool block_input, const PrivacyScreenText& text,
|
||||
std::string& error) override {
|
||||
failure_.clear();
|
||||
emergency_ = false;
|
||||
UpdateText(text);
|
||||
if (!failure_.empty()) { error = failure_; return false; }
|
||||
if (!Send(block_input ? LinuxPrivacyCommand::enable_blocking
|
||||
: LinuxPrivacyCommand::enable)) {
|
||||
error = failure_;
|
||||
@@ -80,6 +82,11 @@ class LinuxPrivacyBackend final : public PrivacyBackend {
|
||||
return true;
|
||||
}
|
||||
|
||||
void UpdateText(const PrivacyScreenText& text) override {
|
||||
if (socket_ >= 0 && !Send(text.allow_local_unlock ? LinuxPrivacyCommand::allow_local_unlock
|
||||
: LinuxPrivacyCommand::forbid_local_unlock))
|
||||
failure_ = "Cannot update the local privacy unlock setting";
|
||||
}
|
||||
void Recover() override {
|
||||
ReadReplies();
|
||||
if (recovered_) {
|
||||
|
||||
@@ -129,6 +129,8 @@ class NativePrivacy {
|
||||
"blocking is unavailable");
|
||||
}
|
||||
|
||||
void SetLocalUnlockAllowed(bool allowed) { allow_local_unlock_ = allowed; }
|
||||
|
||||
void Enable(bool block_input) {
|
||||
++reply_.generation;
|
||||
reply_.failure[0] = '\0';
|
||||
@@ -570,7 +572,7 @@ class NativePrivacy {
|
||||
shift |= symbol == XK_Shift_L || symbol == XK_Shift_R;
|
||||
}
|
||||
}
|
||||
if (down && ctrl && alt && shift &&
|
||||
if (allow_local_unlock_ && down && ctrl && alt && shift &&
|
||||
XkbKeycodeToKeysym(display_, keycode, 0, 0) == XK_F12) {
|
||||
reply_.emergency = true;
|
||||
Recover();
|
||||
@@ -624,6 +626,7 @@ class NativePrivacy {
|
||||
std::vector<Window> hidden_roots_;
|
||||
std::map<int, std::array<bool, 256>> keys_;
|
||||
LinuxPrivacyReply reply_;
|
||||
bool allow_local_unlock_ = true;
|
||||
Clock::time_point checked_{};
|
||||
};
|
||||
} // namespace
|
||||
@@ -676,6 +679,12 @@ int RunLinuxPrivacyGuard() {
|
||||
case LinuxPrivacyCommand::recover:
|
||||
privacy.Recover();
|
||||
break;
|
||||
case LinuxPrivacyCommand::allow_local_unlock:
|
||||
privacy.SetLocalUnlockAllowed(true);
|
||||
break;
|
||||
case LinuxPrivacyCommand::forbid_local_unlock:
|
||||
privacy.SetLocalUnlockAllowed(false);
|
||||
break;
|
||||
case LinuxPrivacyCommand::heartbeat:
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -22,6 +22,8 @@ enum class LinuxPrivacyCommand : uint8_t {
|
||||
enable_blocking,
|
||||
recover,
|
||||
heartbeat,
|
||||
allow_local_unlock,
|
||||
forbid_local_unlock,
|
||||
};
|
||||
|
||||
struct LinuxPrivacyReply {
|
||||
|
||||
@@ -200,6 +200,7 @@ class MacPrivacyBackend final : public PrivacyBackend {
|
||||
const auto caps = Query();
|
||||
if (!caps.overlay) { error = caps.reason; return false; }
|
||||
block_input_ = block_input;
|
||||
allow_local_unlock_ = text.allow_local_unlock;
|
||||
emergency_ = false;
|
||||
tap_failed_ = false;
|
||||
CGEventMask mask = CGEventMaskBit(kCGEventKeyDown) | CGEventMaskBit(kCGEventKeyUp) |
|
||||
@@ -239,7 +240,8 @@ class MacPrivacyBackend final : public PrivacyBackend {
|
||||
m.frame = screen.frame;
|
||||
NSView* content = CreateMacPrivacyCoverView(
|
||||
NSMakeRect(0, 0, m.frame.size.width, m.frame.size.height),
|
||||
[NSString stringWithUTF8String:text.unlock_hint.c_str()]);
|
||||
[NSString stringWithUTF8String:(text.allow_local_unlock ? text.unlock_hint : text.locked_hint).c_str()],
|
||||
text.allow_local_unlock);
|
||||
if (!content) {
|
||||
state->failure = "Could not load the CrossDesk privacy screen logo";
|
||||
break;
|
||||
@@ -260,6 +262,17 @@ class MacPrivacyBackend final : public PrivacyBackend {
|
||||
});
|
||||
return true;
|
||||
}
|
||||
void UpdateText(const PrivacyScreenText& text) override {
|
||||
allow_local_unlock_ = text.allow_local_unlock;
|
||||
auto state = state_;
|
||||
OnMain([state, text] {
|
||||
std::lock_guard lock(state->mutex);
|
||||
for (const auto& monitor : state->monitors)
|
||||
UpdateMacPrivacyCoverView(monitor.cover.contentView,
|
||||
[NSString stringWithUTF8String:(text.allow_local_unlock ? text.unlock_hint : text.locked_hint).c_str()],
|
||||
text.allow_local_unlock);
|
||||
});
|
||||
}
|
||||
void Recover() override {
|
||||
if (tap_) { CGEventTapEnable(tap_, false); CFMachPortInvalidate(tap_); }
|
||||
if (tap_source_) {
|
||||
@@ -359,7 +372,7 @@ class MacPrivacyBackend final : public PrivacyBackend {
|
||||
CGEventGetIntegerValueField(event, kCGEventSourceUnixProcessID) == getpid()) return event;
|
||||
const auto flags = CGEventGetFlags(event);
|
||||
const auto chord = kCGEventFlagMaskControl | kCGEventFlagMaskAlternate | kCGEventFlagMaskShift;
|
||||
if (type == kCGEventKeyDown && (flags & chord) == chord &&
|
||||
if (self->allow_local_unlock_ && type == kCGEventKeyDown && (flags & chord) == chord &&
|
||||
CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode) == 53) {
|
||||
self->emergency_ = true;
|
||||
return nullptr;
|
||||
@@ -372,6 +385,7 @@ class MacPrivacyBackend final : public PrivacyBackend {
|
||||
CFMachPortRef tap_ = nullptr;
|
||||
CFRunLoopSourceRef tap_source_ = nullptr;
|
||||
bool block_input_ = false;
|
||||
bool allow_local_unlock_ = true;
|
||||
bool emergency_ = false;
|
||||
bool tap_failed_ = false;
|
||||
};
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
|
||||
namespace crossdesk {
|
||||
// Returns nil if the CrossDesk icon resource cannot be loaded.
|
||||
NSView* CreateMacPrivacyCoverView(NSRect frame, NSString* unlock_hint);
|
||||
NSView* CreateMacPrivacyCoverView(NSRect frame, NSString* hint, bool allow_local_unlock = true);
|
||||
void UpdateMacPrivacyCoverView(NSView* view, NSString* hint, bool allow_local_unlock);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -52,6 +52,7 @@ CGFloat TextHeight(NSAttributedString* text, CGFloat width) {
|
||||
@interface CrossDeskPrivacyCoverView : NSView
|
||||
@property(nonatomic, strong) NSImage* logo;
|
||||
@property(nonatomic, copy) NSString* unlockHint;
|
||||
@property(nonatomic) BOOL allowLocalUnlock;
|
||||
@end
|
||||
|
||||
@implementation CrossDeskPrivacyCoverView
|
||||
@@ -77,7 +78,7 @@ CGFloat TextHeight(NSAttributedString* text, CGFloat width) {
|
||||
const CGFloat logo_size = std::ceil(brand.size.width);
|
||||
const CGFloat brand_height = TextHeight(brand, logo_size);
|
||||
const CGFloat hint_height = TextHeight(hint, text_width);
|
||||
const CGFloat shortcut_height = TextHeight(shortcut, text_width);
|
||||
const CGFloat shortcut_height = self.allowLocalUnlock ? TextHeight(shortcut, text_width) : 0;
|
||||
const CGFloat bottom = height - margin;
|
||||
const CGFloat logo_top = bottom - brand_height - 6 * scale - logo_size;
|
||||
[self.logo drawInRect:NSMakeRect(margin, logo_top, logo_size, logo_size)
|
||||
@@ -86,19 +87,26 @@ CGFloat TextHeight(NSAttributedString* text, CGFloat width) {
|
||||
[brand drawWithRect:NSMakeRect(margin, bottom - brand_height, logo_size, brand_height)
|
||||
options:kTextOptions];
|
||||
const CGFloat text_left = margin + logo_size + 24 * scale;
|
||||
[hint drawWithRect:NSMakeRect(text_left, bottom - shortcut_height - 12 * scale - hint_height,
|
||||
[hint drawWithRect:NSMakeRect(text_left, bottom - shortcut_height - (self.allowLocalUnlock ? 12 * scale : 0) - hint_height,
|
||||
text_width, hint_height) options:kTextOptions];
|
||||
[shortcut drawWithRect:NSMakeRect(text_left, bottom - shortcut_height,
|
||||
if (self.allowLocalUnlock) [shortcut drawWithRect:NSMakeRect(text_left, bottom - shortcut_height,
|
||||
text_width, shortcut_height) options:kTextOptions];
|
||||
}
|
||||
@end
|
||||
|
||||
namespace crossdesk {
|
||||
NSView* CreateMacPrivacyCoverView(NSRect frame, NSString* unlock_hint) {
|
||||
void UpdateMacPrivacyCoverView(NSView* view, NSString* hint, bool allow_local_unlock) {
|
||||
auto* cover = static_cast<CrossDeskPrivacyCoverView*>(view);
|
||||
cover.unlockHint = hint;
|
||||
cover.allowLocalUnlock = allow_local_unlock;
|
||||
cover.needsDisplay = YES;
|
||||
}
|
||||
NSView* CreateMacPrivacyCoverView(NSRect frame, NSString* unlock_hint, bool allow_local_unlock) {
|
||||
NSImage* logo = LoadPrivacyLogo();
|
||||
if (!logo) return nil;
|
||||
auto* view = [[CrossDeskPrivacyCoverView alloc] initWithFrame:frame];
|
||||
view.logo = logo;
|
||||
view.allowLocalUnlock = allow_local_unlock;
|
||||
view.unlockHint = unlock_hint ?: @"To turn off the privacy screen, press";
|
||||
view.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
||||
return view;
|
||||
|
||||
@@ -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]{};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user