From 0e052e6d7ad5627318922fc98bb657870505990c Mon Sep 17 00:00:00 2001 From: dijunkun Date: Tue, 27 Oct 2026 03:46:19 +0800 Subject: [PATCH] [fix] hide privacy controls on locked desktops across clients --- .../cn/crossdesk/mobile/NativeSession.java | 4 ++++ .../crossdesk/mobile/RemotePrivacyScreen.java | 9 ++++++- .../cn/crossdesk/mobile/SessionControls.java | 12 ++++++---- .../src/gui/application/gui_application.cpp | 6 +++-- apps/desktop/src/gui/runtime/remote_session.h | 1 + apps/desktop/src/gui/ui/stream_window.slint | 22 +++++++++-------- .../common/gui/service_status_runtime.cpp | 7 ++++++ .../src/main/ets/mobile/MobileController.ets | 10 ++++++-- .../src/main/ets/mobile/MobileRemoteView.ets | 6 ++++- .../main/ets/mobile/RemotePrivacyScreen.ets | 8 ++++++- .../Bridge/CrossDeskRTCBridge.h | 3 ++- .../Bridge/CrossDeskRTCBridge.mm | 5 ++-- .../CrossDeskMobile/RemotePrivacyScreen.swift | 8 ++++++- .../CrossDeskMobile/RemoteSessionModel.swift | 4 +++- .../CrossDeskMobile/RemoteSessionView.swift | 24 ++++++++++--------- 15 files changed, 92 insertions(+), 37 deletions(-) diff --git a/apps/android/app/src/main/java/cn/crossdesk/mobile/NativeSession.java b/apps/android/app/src/main/java/cn/crossdesk/mobile/NativeSession.java index 7354bac7..317d5e2a 100644 --- a/apps/android/app/src/main/java/cn/crossdesk/mobile/NativeSession.java +++ b/apps/android/app/src/main/java/cn/crossdesk/mobile/NativeSession.java @@ -201,6 +201,10 @@ final class NativeSession implements AutoCloseable { if(event.optInt("type",-1)==10){ privacyScreen.receive(event,android.os.SystemClock.elapsedRealtime()); listener.privacyChanged(); + } else if(event.optInt("type",-1)==5){ + JSONObject service=event.optJSONObject("service_status"); + privacyScreen.setInteractiveStage(service==null?"":service.optString("interactive_stage","")); + listener.privacyChanged(); } listener.data(event); }); diff --git a/apps/android/app/src/main/java/cn/crossdesk/mobile/RemotePrivacyScreen.java b/apps/android/app/src/main/java/cn/crossdesk/mobile/RemotePrivacyScreen.java index a0110818..f1710cd5 100644 --- a/apps/android/app/src/main/java/cn/crossdesk/mobile/RemotePrivacyScreen.java +++ b/apps/android/app/src/main/java/cn/crossdesk/mobile/RemotePrivacyScreen.java @@ -4,11 +4,17 @@ import org.json.JSONObject; /** Host-confirmed privacy state; all access belongs to the main thread. */ final class RemotePrivacyScreen { + boolean desktopAvailable = true; boolean received, supported, inputBlockSupported, overlay, inputBlocked, remotePaused, pending; int state; long revision, requestRevision, requestId, receivedAt, requestedAt; String reason = "", failure = ""; + void setInteractiveStage(String stage) { + desktopAvailable = !"lock-screen".equals(stage) && + !"credential-ui".equals(stage) && !"secure-desktop".equals(stage); + } + void receive(JSONObject message, long now) { try { if (message.getInt("type") != 10) return; @@ -37,6 +43,7 @@ final class RemotePrivacyScreen { return "隐私屏"; } String feedback(long now) { + if (!desktopAvailable) return ""; if (failure.isEmpty() && (pending || fresh(now) && (state == 2 || state == 4))) return ""; if (failure.isEmpty() && !pending && fresh(now) && supported && (enabled(now) || state == 1 && !active())) return ""; @@ -44,7 +51,7 @@ final class RemotePrivacyScreen { return status(now) + (detail.isEmpty() ? "" : ":" + detail); } boolean canToggle(long now) { - return !pending && fresh(now) && state != 4 && (active() || supported && state == 1); + return desktopAvailable && !pending && fresh(now) && state != 4 && (active() || supported && state == 1); } long begin(long now) { tick(now); diff --git a/apps/android/app/src/main/java/cn/crossdesk/mobile/SessionControls.java b/apps/android/app/src/main/java/cn/crossdesk/mobile/SessionControls.java index b74ff0af..7dde0c16 100644 --- a/apps/android/app/src/main/java/cn/crossdesk/mobile/SessionControls.java +++ b/apps/android/app/src/main/java/cn/crossdesk/mobile/SessionControls.java @@ -130,10 +130,11 @@ final class SessionControls extends FrameLayout { String[] icons={"keyboard","mouse","mouse","display","sliders","audio","folder","chart","lock","clipboard","lock"}; Runnable[] clicks={()->{closeMenu();toggleKeyboard();},this::toggleMouse,actions::mouse,actions::display,()->openMenu("video"),actions::mute,actions::sendFile,()->openMenu("network"),actions::secureAttention,actions::clipboard,actions::togglePrivacy}; menuColumns=controlColumns(); - for(int row=0;row<(labels.length+menuColumns-1)/menuColumns;row++){LinearLayout line=ui.row();for(int col=0;col=labels.length){View spacer=new View(getContext());LinearLayout.LayoutParams p=new LinearLayout.LayoutParams(0,ui.dp(52),1);if(col>0)p.leftMargin=ui.dp(7);line.addView(spacer,p);continue;}LinearLayout control=ui.column();control.setGravity(Gravity.CENTER);control.setBackground(ui.background(0xFFF0F0F2,10));control.addView(ui.icon(icons[index],INK),ui.size(19,19));ui.gap(control,4);TextView caption=ui.text(labels[index],10,false);caption.setSingleLine();caption.setGravity(Gravity.CENTER);control.addView(caption);control.setContentDescription(labels[index]);control.setClickable(true);control.setFocusable(true);control.setOnClickListener(v->clicks[index].run()); + int controlCount=privacy.desktopAvailable?labels.length:labels.length-1; + for(int row=0;row<(controlCount+menuColumns-1)/menuColumns;row++){LinearLayout line=ui.row();for(int col=0;col=controlCount){View spacer=new View(getContext());LinearLayout.LayoutParams p=new LinearLayout.LayoutParams(0,ui.dp(52),1);if(col>0)p.leftMargin=ui.dp(7);line.addView(spacer,p);continue;}LinearLayout control=ui.column();control.setGravity(Gravity.CENTER);control.setBackground(ui.background(0xFFF0F0F2,10));control.addView(ui.icon(icons[index],INK),ui.size(19,19));ui.gap(control,4);TextView caption=ui.text(labels[index],10,false);caption.setSingleLine();caption.setGravity(Gravity.CENTER);control.addView(caption);control.setContentDescription(labels[index]);control.setClickable(true);control.setFocusable(true);control.setOnClickListener(v->clicks[index].run()); if(index==10){privacyButton=control;privacyCaption=caption;} LinearLayout.LayoutParams p=new LinearLayout.LayoutParams(0,ui.dp(52),1);if(col>0)p.leftMargin=ui.dp(7);line.addView(control,p); - }body.addView(line);if((row+1)*menuColumnsprivacy_status_received_, props->privacy_command_pending_, now - props->privacy_status_tick_, now - props->privacy_command_tick_); - if (!controls.can_toggle) return; + if (!props->privacy_desktop_available_ || !controls.can_toggle) return; action.pc.flag = controls.active ? PrivacyCommandFlag::disable : PrivacyCommandFlag::enable; action.pc.block_local_input = true; @@ -3831,8 +3831,10 @@ void GuiApplication::SyncStreamWindow() { (*ui_->stream)->set_privacy_active(controls.active); (*ui_->stream)->set_privacy_off(connected && controls.off); (*ui_->stream)->set_privacy_unsupported(controls.unsupported); + (*ui_->stream)->set_privacy_visible(props->privacy_desktop_available_); (*ui_->stream)->set_privacy_can_toggle( - props->control_mouse_ && connected && controls.can_toggle); + props->privacy_desktop_available_ && props->control_mouse_ && + connected && controls.can_toggle); const int language = localization_language_index_; std::string text = !connected || !props->privacy_status_received_ ? localization::privacy_screen[language] diff --git a/apps/desktop/src/gui/runtime/remote_session.h b/apps/desktop/src/gui/runtime/remote_session.h index 8668cd40..c9fad7da 100644 --- a/apps/desktop/src/gui/runtime/remote_session.h +++ b/apps/desktop/src/gui/runtime/remote_session.h @@ -193,6 +193,7 @@ struct RemoteSession { std::mutex privacy_status_mutex_; PrivacyStatus privacy_status_{}; bool privacy_status_received_ = false; + bool privacy_desktop_available_ = true; bool privacy_command_pending_ = false; uint64_t privacy_command_tick_ = 0; uint32_t privacy_request_revision_ = 0; diff --git a/apps/desktop/src/gui/ui/stream_window.slint b/apps/desktop/src/gui/ui/stream_window.slint index 70c6193a..66504cba 100644 --- a/apps/desktop/src/gui/ui/stream_window.slint +++ b/apps/desktop/src/gui/ui/stream_window.slint @@ -300,6 +300,7 @@ export component StreamWindow inherits Window { in property <[string]> displays; in-out property selected-display: 0; in property mouse-control-enabled: true; + in property privacy-visible: true; in property privacy-can-toggle: false; in property privacy-active: false; in property privacy-off: false; @@ -363,6 +364,7 @@ export component StreamWindow inherits Window { private property control-visual-expanded: false; private property control-toolbar-height: 38px; private property control-button-step: 32px; + private property control-privacy-width: root.privacy-visible ? root.control-button-step : 0px; private property control-disconnect-gap: 8px; private property control-buttons-x: root.control-docked-left ? 43px : 7px; private property control-panel-gap: 8px; @@ -517,7 +519,7 @@ export component StreamWindow inherits Window { if display-button.has-hover { return display-button.tooltip-info; } if shortcut-button.has-hover { return shortcut-button.tooltip-info; } if mouse-button.has-hover { return mouse-button.tooltip-info; } - if privacy-button.has-hover { return privacy-button.tooltip-info; } + if root.privacy-visible && privacy-button.has-hover { return privacy-button.tooltip-info; } if video-button.has-hover { return video-button.tooltip-info; } if audio-button.has-hover { return audio-button.tooltip-info; } if file-button.has-hover { return file-button.tooltip-info; } @@ -1083,7 +1085,7 @@ export component StreamWindow inherits Window { control := Rectangle { x: root.control-dragging ? root.control-drag-x : root.control-docked-left ? 0px : parent.width - self.width; y: min(max(0px, root.control-pos-y), parent.height - self.height); - width: root.control-visual-expanded ? min(80px + 9 * root.control-button-step + root.control-disconnect-gap, parent.width) : root.control-toolbar-height; + width: root.control-visual-expanded ? min(80px + 8 * root.control-button-step + root.control-privacy-width + root.control-disconnect-gap, parent.width) : root.control-toolbar-height; height: root.control-toolbar-height; z: 10; @@ -1157,7 +1159,7 @@ export component StreamWindow inherits Window { reveal-progress: root.control-content-progress; visible: root.control-content-visible; action-enabled: root.control-expanded; - x: root.control-buttons-x + 4 * root.control-button-step; + x: root.control-buttons-x + 3 * root.control-button-step + root.control-privacy-width; y: (parent.height - self.height) / 2; icon: FontAwesomeIcons.volume-high; slashed: !root.audio-enabled; tooltip: root.audio-enabled ? StreamStrings.mute : StreamStrings.audio; @@ -1167,7 +1169,7 @@ export component StreamWindow inherits Window { reveal-progress: root.control-content-progress; visible: root.control-content-visible; action-enabled: root.control-expanded; - x: root.control-buttons-x + 5 * root.control-button-step; + x: root.control-buttons-x + 4 * root.control-button-step + root.control-privacy-width; y: (parent.height - self.height) / 2; icon: FontAwesomeIcons.folder-open; tooltip: StreamStrings.select-file; clicked => { root.close-control-menus(); root.select-file(); } @@ -1176,7 +1178,7 @@ export component StreamWindow inherits Window { reveal-progress: root.control-content-progress; visible: root.control-content-visible; action-enabled: root.control-expanded; - x: root.control-buttons-x + 6 * root.control-button-step; + x: root.control-buttons-x + 5 * root.control-button-step + root.control-privacy-width; y: (parent.height - self.height) / 2; icon: "\u{f1de}"; selected: root.video-menu-open; tooltip: StreamStrings.video-settings; @@ -1191,7 +1193,7 @@ export component StreamWindow inherits Window { reveal-progress: root.control-content-progress; visible: root.control-content-visible; action-enabled: root.control-expanded; - x: root.control-buttons-x + 7 * root.control-button-step; + x: root.control-buttons-x + 6 * root.control-button-step + root.control-privacy-width; y: (parent.height - self.height) / 2; icon: FontAwesomeIcons.signal; selected: root.stats-visible; tooltip: root.stats-visible ? StreamStrings.hide-stats : StreamStrings.show-stats; @@ -1201,7 +1203,7 @@ export component StreamWindow inherits Window { reveal-progress: root.control-content-progress; visible: root.control-content-visible; action-enabled: root.control-expanded; - x: root.control-buttons-x + 8 * root.control-button-step; + x: root.control-buttons-x + 7 * root.control-button-step + root.control-privacy-width; y: (parent.height - self.height) / 2; icon: root.fullscreen-enabled ? FontAwesomeIcons.compress : FontAwesomeIcons.expand; tooltip: root.fullscreen-enabled ? StreamStrings.exit-fullscreen : StreamStrings.fullscreen; @@ -1217,7 +1219,7 @@ export component StreamWindow inherits Window { reveal-progress: root.control-content-progress; visible: root.control-content-visible; action-enabled: root.control-expanded; - x: root.control-buttons-x + 9 * root.control-button-step + root.control-disconnect-gap; + x: root.control-buttons-x + 8 * root.control-button-step + root.control-privacy-width + root.control-disconnect-gap; y: (parent.height - self.height) / 2; icon: FontAwesomeIcons.xmark; tooltip: StreamStrings.disconnect; danger: true; @@ -1252,13 +1254,13 @@ export component StreamWindow inherits Window { privacy-button := ControlBarButton { reveal-progress: root.control-content-progress; - visible: root.control-content-visible; + visible: root.control-content-visible && root.privacy-visible; x: root.control-buttons-x + 3 * root.control-button-step; y: (parent.height - self.height) / 2; icon: FontAwesomeIcons.user-secret; slashed: root.privacy-off && !root.privacy-unsupported; muted: root.privacy-unsupported; - action-enabled: root.control-expanded && root.privacy-can-toggle; + action-enabled: root.control-expanded && root.privacy-visible && root.privacy-can-toggle; tooltip: root.privacy-status-text + (root.privacy-can-toggle ? "\n" + (root.privacy-active ? StreamStrings.privacy-disable : StreamStrings.privacy-enable) : ""); diff --git a/apps/desktop/src/platform/common/gui/service_status_runtime.cpp b/apps/desktop/src/platform/common/gui/service_status_runtime.cpp index 8253241e..abc45517 100644 --- a/apps/desktop/src/platform/common/gui/service_status_runtime.cpp +++ b/apps/desktop/src/platform/common/gui/service_status_runtime.cpp @@ -7,6 +7,7 @@ void GuiRuntime::ResetRemoteServiceStatus(RemoteSession& props) { std::lock_guard lock(props.privacy_status_mutex_); props.privacy_status_ = {}; props.privacy_status_received_ = false; + props.privacy_desktop_available_ = true; props.privacy_command_pending_ = false; props.privacy_status_tick_ = 0; } @@ -17,6 +18,12 @@ void GuiRuntime::ResetRemoteServiceStatus(RemoteSession& props) { void GuiRuntime::ApplyRemoteServiceStatus(RemoteSession& props, const ServiceStatus& status) { + { + std::lock_guard lock(props.privacy_status_mutex_); + const std::string stage = status.interactive_stage; + props.privacy_desktop_available_ = stage != "lock-screen" && + stage != "credential-ui" && stage != "secure-desktop"; + } props.remote_service_status_received_ = true; props.remote_service_available_ = status.available; props.remote_interactive_stage_ = status.interactive_stage; diff --git a/apps/harmony/entry/src/main/ets/mobile/MobileController.ets b/apps/harmony/entry/src/main/ets/mobile/MobileController.ets index 15602863..bb2e576f 100644 --- a/apps/harmony/entry/src/main/ets/mobile/MobileController.ets +++ b/apps/harmony/entry/src/main/ets/mobile/MobileController.ets @@ -25,7 +25,7 @@ interface NativeEvent { export interface RemoteDisplay { name: string; } interface HostInfo { host_name: string; platform: string; app_version: string; supports_video_settings: boolean; displays: RemoteDisplay[]; } interface SettingAck { quality: number; frame_rate: number; preference: number; request_id: number; accepted: boolean; } -interface ServiceStatus { available: boolean; } +interface ServiceStatus { available: boolean; interactive_stage?: string; } interface ControlData { type: number; host_info?: HostInfo; video_settings?: SettingAck; service_status?: ServiceStatus; privacy_status?: PrivacyStatus; } interface PresenceRequest { type: string; devices: string[]; subscribe: boolean; } interface SignalType { type: string; } @@ -80,6 +80,7 @@ export class MobileState { privacyEnabled: boolean = false; privacyActive: boolean = false; privacyCanToggle: boolean = false; + privacyVisible: boolean = true; networkTime: number = 0; unread: number = 0; announcements: AnnouncementItem[] = []; @@ -361,6 +362,7 @@ export class MobileController { const now = this.now(); this.privacyScreen.tick(now); this.state.privacyStatus = this.privacyScreen.status(now); this.state.privacyFeedback = this.privacyScreen.feedback(now); this.state.privacyButtonTitle = this.privacyScreen.buttonTitle(now); + this.state.privacyVisible = this.privacyScreen.desktopAvailable; this.state.privacyEnabled = this.privacyScreen.enabled(now); this.state.privacyActive = this.privacyScreen.active(); this.state.privacyCanToggle = this.privacyScreen.canToggle(now); } @@ -463,7 +465,11 @@ export class MobileController { } private data(message: ControlData): void { if (message.type === 10 && message.privacy_status) { this.privacyScreen.receive(message.privacy_status, this.now()); this.syncPrivacy(); return; } - if (message.type === 5) { this.serviceAvailable = message.service_status?.available === true; return; } + if (message.type === 5) { + this.serviceAvailable = message.service_status?.available === true; + this.privacyScreen.setInteractiveStage(message.service_status?.interactive_stage ?? ''); + this.syncPrivacy(); return; + } if (message.type === 12 && message.video_settings) { const v = message.video_settings; this.video.receive({ quality: v.quality, frameRate: v.frame_rate, preference: v.preference }, v.request_id, v.accepted === true); diff --git a/apps/harmony/entry/src/main/ets/mobile/MobileRemoteView.ets b/apps/harmony/entry/src/main/ets/mobile/MobileRemoteView.ets index 72aef149..f007c6df 100644 --- a/apps/harmony/entry/src/main/ets/mobile/MobileRemoteView.ets +++ b/apps/harmony/entry/src/main/ets/mobile/MobileRemoteView.ets @@ -135,7 +135,11 @@ export struct MobileRemoteView { if (index === 9) { this.state.modal = 'clipboard'; this.menu = ''; } if (index === 10) this.controller?.togglePrivacy(); } - private labels(): string[] { return ['键盘', this.state.relative ? '相对鼠标' : '绝对鼠标', this.mouseVisible ? '收起鼠标' : '虚拟鼠标', '显示器', '画面设置', this.state.muted ? '静音' : '声音', '发送文件', '网络状态', 'Ctrl+Alt+Del', '剪贴板', this.state.privacyButtonTitle]; } + private labels(): string[] { + const labels: string[] = ['键盘', this.state.relative ? '相对鼠标' : '绝对鼠标', this.mouseVisible ? '收起鼠标' : '虚拟鼠标', '显示器', '画面设置', this.state.muted ? '静音' : '声音', '发送文件', '网络状态', 'Ctrl+Alt+Del', '剪贴板']; + if (this.state.privacyVisible) labels.push(this.state.privacyButtonTitle); + return labels; + } private icons(): string[] { return ['keyboard', 'mouse', 'mouse', 'display', 'sliders', this.state.muted ? 'muted' : 'audio', 'folder', 'chart', 'lock', 'clipboard', 'lock']; } private columns(): number { return this.viewHeight < 400 && this.viewWidth >= 400 ? 4 : 3; } private menuWidth(): number { return Math.max(1, Math.min(this.viewWidth - 16, this.columns() === 4 ? 380 : Math.max(280, Math.min(340, this.viewWidth - 160)))); } diff --git a/apps/harmony/entry/src/main/ets/mobile/RemotePrivacyScreen.ets b/apps/harmony/entry/src/main/ets/mobile/RemotePrivacyScreen.ets index b292ef57..41d095a2 100644 --- a/apps/harmony/entry/src/main/ets/mobile/RemotePrivacyScreen.ets +++ b/apps/harmony/entry/src/main/ets/mobile/RemotePrivacyScreen.ets @@ -5,6 +5,7 @@ export interface PrivacyStatus { } export class RemotePrivacyScreen { + desktopAvailable: boolean = true; snapshot?: PrivacyStatus; pending: boolean = false; requestId: number = 0; @@ -13,6 +14,10 @@ export class RemotePrivacyScreen { private requestedAt: number = 0; private failure: string = ''; + setInteractiveStage(stage: string): void { + this.desktopAvailable = stage !== 'lock-screen' && stage !== 'credential-ui' && stage !== 'secure-desktop'; + } + receive(s: PrivacyStatus, now: number): void { if (!Number.isInteger(s.state) || s.state < 0 || s.state > 5 || !Number.isInteger(s.revision) || s.revision < 0 || s.revision > 0xffffffff || @@ -40,6 +45,7 @@ export class RemotePrivacyScreen { return '隐私屏'; } feedback(now: number): string { + if (!this.desktopAvailable) return ''; const s = this.snapshot; if (!this.failure && (this.pending || this.fresh(now) && (s?.state === 2 || s?.state === 4))) return ''; if (!this.failure && !this.pending && this.fresh(now) && s?.supported && @@ -49,7 +55,7 @@ export class RemotePrivacyScreen { } canToggle(now: number): boolean { const s = this.snapshot; - return s !== undefined && !this.pending && this.fresh(now) && s.state !== 4 && (this.active() || s.supported && s.state === 1); + return this.desktopAvailable && s !== undefined && !this.pending && this.fresh(now) && s.state !== 4 && (this.active() || s.supported && s.state === 1); } begin(now: number): number { this.tick(now); diff --git a/apps/ios/CrossDeskMobile/Bridge/CrossDeskRTCBridge.h b/apps/ios/CrossDeskMobile/Bridge/CrossDeskRTCBridge.h index c883865b..b9020186 100644 --- a/apps/ios/CrossDeskMobile/Bridge/CrossDeskRTCBridge.h +++ b/apps/ios/CrossDeskMobile/Bridge/CrossDeskRTCBridge.h @@ -120,7 +120,8 @@ typedef struct { - (void)rtcBridge:(CrossDeskRTCBridge *)bridge didReceiveClipboardText:(NSString *)text; - (void)rtcBridge:(CrossDeskRTCBridge *)bridge - didReceiveServiceAvailable:(BOOL)available; + didReceiveServiceAvailable:(BOOL)available + interactiveStage:(NSString *)stage; - (void)rtcBridge:(CrossDeskRTCBridge *)bridge didSendSecureAttention:(BOOL)success; - (void)rtcBridge:(CrossDeskRTCBridge *)bridge diff --git a/apps/ios/CrossDeskMobile/Bridge/CrossDeskRTCBridge.mm b/apps/ios/CrossDeskMobile/Bridge/CrossDeskRTCBridge.mm index d8edfe48..b2c9f254 100644 --- a/apps/ios/CrossDeskMobile/Bridge/CrossDeskRTCBridge.mm +++ b/apps/ios/CrossDeskMobile/Bridge/CrossDeskRTCBridge.mm @@ -1736,11 +1736,12 @@ Params MakeParams(const RTCState &state, const std::string &user_id, } if (action.type == ControlType::service_status) { const BOOL available = action.ss.available; + NSString *stage = [NSString stringWithUTF8String:action.ss.interactive_stage]; DispatchMain(^{ if (![self isControllerGenerationActive:generation]) return; id delegate = self.delegate; - if ([delegate respondsToSelector:@selector(rtcBridge:didReceiveServiceAvailable:)]) { - [delegate rtcBridge:self didReceiveServiceAvailable:available]; + if ([delegate respondsToSelector:@selector(rtcBridge:didReceiveServiceAvailable:interactiveStage:)]) { + [delegate rtcBridge:self didReceiveServiceAvailable:available interactiveStage:stage ?: @""]; } }); return; diff --git a/apps/ios/CrossDeskMobile/RemotePrivacyScreen.swift b/apps/ios/CrossDeskMobile/RemotePrivacyScreen.swift index d6f1b8bf..f1fe4393 100644 --- a/apps/ios/CrossDeskMobile/RemotePrivacyScreen.swift +++ b/apps/ios/CrossDeskMobile/RemotePrivacyScreen.swift @@ -12,6 +12,7 @@ struct RemotePrivacyStatus: Decodable { } struct RemotePrivacyScreen { + private(set) var desktopAvailable = true private(set) var snapshot: RemotePrivacyStatus? private(set) var pending = false private(set) var requestID: UInt32 = 0 @@ -20,6 +21,10 @@ struct RemotePrivacyScreen { private var requestedAt: TimeInterval = 0 private var failure = "" + mutating func setInteractiveStage(_ stage: String) { + desktopAvailable = !["lock-screen", "credential-ui", "secure-desktop"].contains(stage) + } + var active: Bool { guard let s = snapshot else { return false } return s.overlayActive || s.remotePaused || [2, 3, 4].contains(s.state) @@ -37,6 +42,7 @@ struct RemotePrivacyScreen { return "隐私屏" } func feedback(at now: TimeInterval) -> String { + if !desktopAvailable { return "" } if failure.isEmpty && (pending || fresh(at: now) && [2, 4].contains(snapshot?.state ?? -1)) { return "" } @@ -47,7 +53,7 @@ struct RemotePrivacyScreen { } func canToggle(at now: TimeInterval) -> Bool { guard let s = snapshot else { return false } - return !pending && fresh(at: now) && s.state != 4 && (active || s.supported && s.state == 1) + return desktopAvailable && !pending && fresh(at: now) && s.state != 4 && (active || s.supported && s.state == 1) } mutating func receive(_ data: Data, at now: TimeInterval) { struct Envelope: Decodable { let type: Int; let privacyStatus: RemotePrivacyStatus } diff --git a/apps/ios/CrossDeskMobile/RemoteSessionModel.swift b/apps/ios/CrossDeskMobile/RemoteSessionModel.swift index ce8eb8cf..c99aff6c 100644 --- a/apps/ios/CrossDeskMobile/RemoteSessionModel.swift +++ b/apps/ios/CrossDeskMobile/RemoteSessionModel.swift @@ -1439,9 +1439,11 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe clipboardStatus = "已接收远端剪贴板" } - func rtcBridge(_ bridge: CrossDeskRTCBridge, didReceiveServiceAvailable available: Bool) { + func rtcBridge(_ bridge: CrossDeskRTCBridge, didReceiveServiceAvailable available: Bool, + interactiveStage stage: String) { guard hasNetworkConsent, isConnecting || isConnected else { return } remoteServiceAvailable = available + privacyScreen.setInteractiveStage(stage) } func rtcBridge(_ bridge: CrossDeskRTCBridge, didSendSecureAttention success: Bool) { diff --git a/apps/ios/CrossDeskMobile/RemoteSessionView.swift b/apps/ios/CrossDeskMobile/RemoteSessionView.swift index 8a83d9d9..063b6f18 100644 --- a/apps/ios/CrossDeskMobile/RemoteSessionView.swift +++ b/apps/ios/CrossDeskMobile/RemoteSessionView.swift @@ -1250,17 +1250,19 @@ private struct FloatingSessionMenu: View { action: session.sendSecureAttention) FloatingControlButton(title: "剪贴板", symbol: "doc.on.clipboard", action: session.sendClipboard) - FloatingControlButton( - title: session.privacyScreen.buttonTitle(at: session.privacyClock), - symbol: "eye.slash", - selected: session.privacyScreen.enabled(at: session.privacyClock), - action: session.togglePrivacyScreen - ) - .disabled(!session.isConnected || !session.privacyScreen.canToggle(at: session.privacyClock)) - .opacity(session.isConnected && session.privacyScreen.canToggle(at: session.privacyClock) ? 1 : 0.45) - .accessibilityLabel("隐私屏") - .accessibilityValue(session.privacyScreen.status(at: session.privacyClock)) - .accessibilityHint(session.privacyScreen.active ? "关闭隐私屏" : "开启隐私屏") + if session.privacyScreen.desktopAvailable { + FloatingControlButton( + title: session.privacyScreen.buttonTitle(at: session.privacyClock), + symbol: "eye.slash", + selected: session.privacyScreen.enabled(at: session.privacyClock), + action: session.togglePrivacyScreen + ) + .disabled(!session.isConnected || !session.privacyScreen.canToggle(at: session.privacyClock)) + .opacity(session.isConnected && session.privacyScreen.canToggle(at: session.privacyClock) ? 1 : 0.45) + .accessibilityLabel("隐私屏") + .accessibilityValue(session.privacyScreen.status(at: session.privacyClock)) + .accessibilityHint(session.privacyScreen.active ? "关闭隐私屏" : "开启隐私屏") + } } let privacyFeedback = session.privacyScreen.feedback(at: session.privacyClock)