[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
@@ -195,6 +195,7 @@ typedef struct {
- (void)sendFileAtURL:(NSURL *)fileURL;
- (void)sendSecureAttentionSequence;
- (void)sendPrivacyCommand:(BOOL)enable
lock:(BOOL)lock
blockLocalInput:(BOOL)blockLocalInput
requestID:(uint32_t)requestID;
@@ -1044,6 +1044,7 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
}
- (void)sendPrivacyCommand:(BOOL)enable
lock:(BOOL)lock
blockLocalInput:(BOOL)blockLocalInput
requestID:(uint32_t)requestID {
const uint64_t generation = _activeControllerGeneration.load();
@@ -1051,7 +1052,8 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
if (![self isControllerGenerationActive:generation]) return;
RemoteAction action{};
action.type = ControlType::privacy_command;
action.pc = {enable ? crossdesk::PrivacyCommandFlag::enable
action.pc = {lock ? crossdesk::PrivacyCommandFlag::lock
: enable ? crossdesk::PrivacyCommandFlag::enable
: crossdesk::PrivacyCommandFlag::disable,
static_cast<bool>(blockLocalInput)};
const auto message = action.to_json();
@@ -9,11 +9,13 @@ struct RemotePrivacyStatus: Decodable {
let remotePaused: Bool
let revision: UInt32
let reason: String
let localUnlockRevision: UInt32?
}
struct RemotePrivacyScreen {
private(set) var desktopAvailable = true
private(set) var snapshot: RemotePrivacyStatus?
var unlockNotice = false
private(set) var pending = false
private(set) var requestID: UInt32 = 0
private var requestRevision: UInt32 = 0
@@ -64,14 +66,22 @@ struct RemotePrivacyScreen {
!s.reason.contains("\0"),
s.state != 3 || (s.supported && s.overlayActive && !s.remotePaused) else { return }
if let previous = snapshot, Int32(bitPattern: s.revision &- previous.revision) < 0 { return }
if let previous = snapshot,
Int32(bitPattern: (s.localUnlockRevision ?? 0) &- (previous.localUnlockRevision ?? 0)) > 0 {
unlockNotice = true
}
if s.state == 3 { unlockNotice = false }
snapshot = s; receivedAt = now; failure = ""
if pending && Int32(bitPattern: s.revision &- requestRevision) > 0 && ![2, 4].contains(s.state) {
pending = false
}
}
mutating func begin(at now: TimeInterval) -> UInt32? {
func canLock(at now: TimeInterval) -> Bool {
desktopAvailable && !pending && fresh(at: now) && snapshot?.supported == true
}
mutating func begin(at now: TimeInterval, lock: Bool = false) -> UInt32? {
tick(at: now)
guard canToggle(at: now), let s = snapshot else { return nil }
guard (lock ? canLock(at: now) : canToggle(at: now)), let s = snapshot else { return nil }
pending = true; requestedAt = now; requestRevision = s.revision; failure = ""
requestID &+= 1
return requestID
@@ -1106,12 +1106,14 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
privacyTimer = timer
}
func togglePrivacyScreen() {
func lockPrivacyScreen() { sendPrivacyScreen(lock: true) }
func togglePrivacyScreen() { sendPrivacyScreen(lock: false) }
private func sendPrivacyScreen(lock: Bool) {
guard hasNetworkConsent, isConnected else { return }
privacyClock = ProcessInfo.processInfo.systemUptime
let enable = !privacyScreen.active
guard let request = privacyScreen.begin(at: privacyClock) else { return }
bridge.sendPrivacyCommand(enable, blockLocalInput: true, requestID: request)
guard let request = privacyScreen.begin(at: privacyClock, lock: lock) else { return }
bridge.sendPrivacyCommand(enable, lock: lock, blockLocalInput: true, requestID: request)
}
func rtcBridge(_ bridge: CrossDeskRTCBridge, didReceivePrivacyStatus data: Data) {
@@ -181,19 +181,28 @@ struct RemoteSessionView: View {
floatingStatusControls
.zIndex(30)
if session.isConnected && session.privacyScreen.unlockNotice {
privacyUnlockOverlay.zIndex(60)
}
if disconnectConfirmationVisible {
disconnectConfirmationOverlay
.zIndex(50)
}
}
.alert("升级提示", isPresented: Binding(
get: { session.remoteUpdateMessage != nil },
set: { if !$0 { session.dismissRemoteUpdate() } }
get: { !session.privacyScreen.unlockNotice && session.remoteUpdateMessage != nil },
set: { if !$0 && !session.privacyScreen.unlockNotice { session.dismissRemoteUpdate() } }
)) {
Button("知道了", role: .cancel) { session.dismissRemoteUpdate() }
} message: {
Text(session.remoteUpdateMessage ?? "")
}
.onChange(of: session.privacyScreen.unlockNotice) { visible in
if visible {
mouseInput.releaseAll()
keyboardInputVisible = false
}
}
.onAppear {
mouseInput.send = { x, y, flag, wheel in
cursorPosition = CGPoint(x: CGFloat(x), y: CGFloat(y))
@@ -268,6 +277,35 @@ struct RemoteSessionView: View {
}
}
private var privacyUnlockOverlay: some View {
ZStack {
Color.black.opacity(0.28).ignoresSafeArea().contentShape(Rectangle()).onTapGesture { }
VStack(spacing: 0) {
VStack(spacing: 12) {
Text("隐私屏已被解锁").font(.headline)
Text("被控端已通过组合键解除隐私屏。点击“锁定”将重新开启隐私屏,并禁止本机使用组合键解锁。")
.font(.subheadline).foregroundStyle(.secondary)
let feedback = session.privacyScreen.feedback(at: session.privacyClock)
if !feedback.isEmpty { Text(feedback).font(.caption).foregroundStyle(.secondary) }
}.padding(24)
Divider()
HStack(spacing: 0) {
Button("确认") { session.privacyScreen.unlockNotice = false }
.frame(maxWidth: .infinity, minHeight: 48)
Divider().frame(height: 48)
Button("锁定") { session.lockPrivacyScreen() }
.fontWeight(.semibold).frame(maxWidth: .infinity, minHeight: 48)
.disabled(!session.privacyScreen.canLock(at: session.privacyClock))
}
}
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16))
.clipShape(RoundedRectangle(cornerRadius: 16))
.shadow(color: .black.opacity(0.2), radius: 18, y: 7)
.frame(maxWidth: 380).padding(20)
}
.accessibilityAddTraits(.isModal)
}
private var disconnectConfirmationOverlay: some View {
GeometryReader { proxy in
ZStack {