mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-28 00:33:06 +08:00
[feat] add privacy unlock controls and remote relocking
This commit is contained in:
@@ -79,6 +79,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
|||||||
private AlertDialog connectionDialog;
|
private AlertDialog connectionDialog;
|
||||||
private String pendingConnectionMessage="";
|
private String pendingConnectionMessage="";
|
||||||
private AlertDialog remoteUpdateDialog;
|
private AlertDialog remoteUpdateDialog;
|
||||||
|
private AlertDialog privacyUnlockDialog;
|
||||||
private RemoteVersionCheck remoteVersionCheck;
|
private RemoteVersionCheck remoteVersionCheck;
|
||||||
private String host, connectedRemote="", hostPlatform="", remoteClipboard="", pendingPassword="", page="home";
|
private String host, connectedRemote="", hostPlatform="", remoteClipboard="", pendingPassword="", page="home";
|
||||||
private int port,selectedDisplay;
|
private int port,selectedDisplay;
|
||||||
@@ -350,6 +351,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
|||||||
}
|
}
|
||||||
private void confirmDisconnect(){disconnectDialog=new AlertDialog.Builder(this).setTitle("断开远程连接?").setMessage("断开后将返回首页").setNegativeButton("取消",null).setPositiveButton("断开连接",(d,w)->end("")).show();}
|
private void confirmDisconnect(){disconnectDialog=new AlertDialog.Builder(this).setTitle("断开远程连接?").setMessage("断开后将返回首页").setNegativeButton("取消",null).setPositiveButton("断开连接",(d,w)->end("")).show();}
|
||||||
private void end(String reason){
|
private void end(String reason){
|
||||||
|
dismissPrivacyUnlockDialog();
|
||||||
remoteOrientation.end();
|
remoteOrientation.end();
|
||||||
if(backgroundSession!=null){BackgroundSession retained=backgroundSession;backgroundSession=null;retained.detach(this);retained.finish(reason);}
|
if(backgroundSession!=null){BackgroundSession retained=backgroundSession;backgroundSession=null;retained.detach(this);retained.finish(reason);}
|
||||||
boolean closeWindow=isInPictureInPictureMode();
|
boolean closeWindow=isInPictureInPictureMode();
|
||||||
@@ -658,11 +660,42 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
|||||||
if(foreground&&preferences.getBoolean("networkConsent",false))remoteVersionCheck.hostInfo(info);
|
if(foreground&&preferences.getBoolean("networkConsent",false))remoteVersionCheck.hostInfo(info);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@Override public void privacyChanged(){if(controls!=null&&session!=null)controls.privacyScreen(session.privacyScreen);}
|
private void dismissPrivacyUnlockDialog() {
|
||||||
|
if (privacyUnlockDialog != null) { privacyUnlockDialog.dismiss(); privacyUnlockDialog = null; }
|
||||||
|
}
|
||||||
|
@Override public void privacyChanged() {
|
||||||
|
if (controls != null && session != null) controls.privacyScreen(session.privacyScreen);
|
||||||
|
if (!ready || session == null || !session.privacyScreen.unlockNotice) {
|
||||||
|
dismissPrivacyUnlockDialog(); return;
|
||||||
|
}
|
||||||
|
if (!foreground || pictureInPictureUi || isInPictureInPictureMode() || isFinishing()) return;
|
||||||
|
if (privacyUnlockDialog == null) {
|
||||||
|
releaseKeys();
|
||||||
|
if (controls != null) { controls.closeKeyboard(); controls.cancelMouseInput(); }
|
||||||
|
if (video != null) video.releaseMouse();
|
||||||
|
privacyUnlockDialog = new AlertDialog.Builder(this).setTitle("隐私屏已被解锁")
|
||||||
|
.setMessage("被控端已通过组合键解除隐私屏。点击“锁定”将重新开启隐私屏,并禁止本机使用组合键解锁。")
|
||||||
|
.setNegativeButton("确认", (dialog, which) -> {
|
||||||
|
if (session != null) session.privacyScreen.unlockNotice = false;
|
||||||
|
dismissPrivacyUnlockDialog();
|
||||||
|
}).setPositiveButton("锁定", null).create();
|
||||||
|
privacyUnlockDialog.setCancelable(false);
|
||||||
|
privacyUnlockDialog.show();
|
||||||
|
privacyUnlockDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(view -> {
|
||||||
|
if (session != null) session.lockPrivacy();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
String feedback = session.privacyScreen.feedback(android.os.SystemClock.elapsedRealtime());
|
||||||
|
privacyUnlockDialog.setMessage("被控端已通过组合键解除隐私屏。点击“锁定”将重新开启隐私屏,并禁止本机使用组合键解锁。"
|
||||||
|
+ (feedback.isEmpty() ? "" : "\n\n" + feedback));
|
||||||
|
privacyUnlockDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(
|
||||||
|
session.privacyScreen.canLock(android.os.SystemClock.elapsedRealtime()));
|
||||||
|
}
|
||||||
@Override public void videoSize(int width,int height){videoWidth=width;videoHeight=height;if(video!=null)video.videoSize(width,height);if(controls!=null)controls.videoSize(width,height);pictureInPicture.videoSize(width,height);updatePictureInPicture();}
|
@Override public void videoSize(int width,int height){videoWidth=width;videoHeight=height;if(video!=null)video.videoSize(width,height);if(controls!=null)controls.videoSize(width,height);pictureInPicture.videoSize(width,height);updatePictureInPicture();}
|
||||||
@Override public void clipboard(String value){remoteClipboard=value;}
|
@Override public void clipboard(String value){remoteClipboard=value;}
|
||||||
@Override public void statistics(RemoteNetworkStatistics.Snapshot value){if(controls!=null)controls.statistics(value);if(value.fps>0)capturePreviewIfNeeded();}
|
@Override public void statistics(RemoteNetworkStatistics.Snapshot value){if(controls!=null)controls.statistics(value);if(value.fps>0)capturePreviewIfNeeded();}
|
||||||
@Override public boolean dispatchKeyEvent(KeyEvent event){
|
@Override public boolean dispatchKeyEvent(KeyEvent event){
|
||||||
|
if (privacyUnlockDialog != null) return super.dispatchKeyEvent(event);
|
||||||
int code=KeyMap.windowsCode(event.getKeyCode());View focus=getCurrentFocus();
|
int code=KeyMap.windowsCode(event.getKeyCode());View focus=getCurrentFocus();
|
||||||
boolean remoteEditor=focus instanceof RemoteKeyboardInput;
|
boolean remoteEditor=focus instanceof RemoteKeyboardInput;
|
||||||
boolean desktopKey=event.isCtrlPressed()||event.isAltPressed()||event.isMetaPressed()||pressed.contains(code)
|
boolean desktopKey=event.isCtrlPressed()||event.isAltPressed()||event.isMetaPressed()||pressed.contains(code)
|
||||||
@@ -790,7 +823,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
|||||||
// onStart marks a new visit; focus/resume callbacks must not reopen a refused notice.
|
// onStart marks a new visit; focus/resume callbacks must not reopen a refused notice.
|
||||||
@Override protected void onPause(){releaseKeys();if(controls!=null)controls.cancelMouseInput();if(video!=null)video.releaseMouse();super.onPause();}
|
@Override protected void onPause(){releaseKeys();if(controls!=null)controls.cancelMouseInput();if(video!=null)video.releaseMouse();super.onPause();}
|
||||||
@Override protected void onStart(){super.onStart();foreground=true;restoreBackgroundSession();appUpdates.setEnabled(preferences.getBoolean("networkConsent",false));if(!preferences.getBoolean("networkConsent",false))consent(this::renderSignaling);ensureSignaling();showConnectionMessage();}
|
@Override protected void onStart(){super.onStart();foreground=true;restoreBackgroundSession();appUpdates.setEnabled(preferences.getBoolean("networkConsent",false));if(!preferences.getBoolean("networkConsent",false))consent(this::renderSignaling);ensureSignaling();showConnectionMessage();}
|
||||||
@Override protected void onStop(){if(controls!=null)controls.closeKeyboard();foreground=false;appUpdates.setEnabled(false);if(privacyDialog!=null)privacyDialog.dismiss();pageHost.finish();
|
@Override protected void onStop(){dismissPrivacyUnlockDialog();if(controls!=null)controls.closeKeyboard();foreground=false;appUpdates.setEnabled(false);if(privacyDialog!=null)privacyDialog.dismiss();pageHost.finish();
|
||||||
if(connectionDialog!=null)connectionDialog.dismiss();
|
if(connectionDialog!=null)connectionDialog.dismiss();
|
||||||
// Closing native PiP is an explicit end, not a request for invisible
|
// Closing native PiP is an explicit end, not a request for invisible
|
||||||
// fallback. An unavailable/failed PiP can hand its live session off.
|
// fallback. An unavailable/failed PiP can hand its live session off.
|
||||||
@@ -801,7 +834,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
|||||||
resetRemoteVersionCheck();if(disconnectDialog!=null)disconnectDialog.dismiss();
|
resetRemoteVersionCheck();if(disconnectDialog!=null)disconnectDialog.dismiss();
|
||||||
}else if(documentSession==null||documentSession!=session||isFinishing()){if(session!=null)end(isFinishing()?"":"远程窗口已关闭,或后台保活未能启动,会话已结束");stopSignaling();}
|
}else if(documentSession==null||documentSession!=session||isFinishing()){if(session!=null)end(isFinishing()?"":"远程窗口已关闭,或后台保活未能启动,会话已结束");stopSignaling();}
|
||||||
super.onStop();}
|
super.onStop();}
|
||||||
@Override protected void onDestroy(){mainHandler.removeCallbacks(documentTimeout);documentSession=null;documentToSave=null;appUpdates.setEnabled(false);pageHost.finish();resetRemoteVersionCheck();cancelVideoSettingsTimeout();previews.removeListener(previewChanged);if(privacyDialog!=null)privacyDialog.dismiss();if(previewDialog!=null)previewDialog.dismiss();announcementInbox.close();if(disconnectDialog!=null)disconnectDialog.dismiss();if(passwordDialog!=null)passwordDialog.dismiss();if(progressDialog!=null)progressDialog.dismiss();
|
@Override protected void onDestroy(){dismissPrivacyUnlockDialog();mainHandler.removeCallbacks(documentTimeout);documentSession=null;documentToSave=null;appUpdates.setEnabled(false);pageHost.finish();resetRemoteVersionCheck();cancelVideoSettingsTimeout();previews.removeListener(previewChanged);if(privacyDialog!=null)privacyDialog.dismiss();if(previewDialog!=null)previewDialog.dismiss();announcementInbox.close();if(disconnectDialog!=null)disconnectDialog.dismiss();if(passwordDialog!=null)passwordDialog.dismiss();if(progressDialog!=null)progressDialog.dismiss();
|
||||||
if(connectionDialog!=null)connectionDialog.dismiss();
|
if(connectionDialog!=null)connectionDialog.dismiss();
|
||||||
boolean retained=!isFinishing()&&backgroundSession!=null&&!backgroundSession.ended&&RemoteSessionService.current()==backgroundSession;
|
boolean retained=!isFinishing()&&backgroundSession!=null&&!backgroundSession.ended&&RemoteSessionService.current()==backgroundSession;
|
||||||
if(backgroundSession!=null)backgroundSession.detach(this);
|
if(backgroundSession!=null)backgroundSession.detach(this);
|
||||||
|
|||||||
@@ -284,13 +284,15 @@ final class NativeSession implements AutoCloseable {
|
|||||||
if (type == 2 && player != null) player.setEnabled(value != 0);
|
if (type == 2 && player != null) player.setEnabled(value != 0);
|
||||||
execute(() -> { if (connected){nControl(handle, type, value);if(type==4)networkStatistics.resetVideo();} });
|
execute(() -> { if (connected){nControl(handle, type, value);if(type==4)networkStatistics.resetVideo();} });
|
||||||
}
|
}
|
||||||
void togglePrivacy() {
|
void lockPrivacy() { sendPrivacy(true); }
|
||||||
|
void togglePrivacy() { sendPrivacy(false); }
|
||||||
|
private void sendPrivacy(boolean lock) {
|
||||||
if(closed||!connected)return;
|
if(closed||!connected)return;
|
||||||
boolean enable=!privacyScreen.active();
|
boolean enable=!privacyScreen.active();
|
||||||
long request=privacyScreen.begin(android.os.SystemClock.elapsedRealtime());
|
long request=privacyScreen.begin(android.os.SystemClock.elapsedRealtime(),lock);
|
||||||
if(request<0)return;
|
if(request<0)return;
|
||||||
listener.privacyChanged();
|
listener.privacyChanged();
|
||||||
execute(()->{if(connected&&!nPrivacy(handle,enable?1:2,true))ui(()->{
|
execute(()->{if(connected&&!nPrivacy(handle,lock?3:enable?1:2,true))ui(()->{
|
||||||
privacyScreen.failed(request);listener.privacyChanged();
|
privacyScreen.failed(request);listener.privacyChanged();
|
||||||
});});
|
});});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import org.json.JSONObject;
|
|||||||
final class RemotePrivacyScreen {
|
final class RemotePrivacyScreen {
|
||||||
boolean desktopAvailable = true;
|
boolean desktopAvailable = true;
|
||||||
boolean received, supported, inputBlockSupported, overlay, inputBlocked, remotePaused, pending;
|
boolean received, supported, inputBlockSupported, overlay, inputBlocked, remotePaused, pending;
|
||||||
|
boolean unlockNotice;
|
||||||
|
long localUnlockRevision;
|
||||||
int state;
|
int state;
|
||||||
long revision, requestRevision, requestId, receivedAt, requestedAt;
|
long revision, requestRevision, requestId, receivedAt, requestedAt;
|
||||||
String reason = "", failure = "";
|
String reason = "", failure = "";
|
||||||
@@ -28,6 +30,11 @@ final class RemotePrivacyScreen {
|
|||||||
if (next == 3 && (!support || !cover || paused)) return;
|
if (next == 3 && (!support || !cover || paused)) return;
|
||||||
boolean blockSupport = value.getBoolean("input_block_supported"), blocked = value.getBoolean("input_blocked");
|
boolean blockSupport = value.getBoolean("input_block_supported"), blocked = value.getBoolean("input_blocked");
|
||||||
String detail = value.getString("reason");
|
String detail = value.getString("reason");
|
||||||
|
long unlock = value.has("local_unlock_revision") ? value.getLong("local_unlock_revision") : 0;
|
||||||
|
if (unlock < 0 || unlock > 0xffffffffL) return;
|
||||||
|
if (received && (int)(unlock - localUnlockRevision) > 0) unlockNotice = true;
|
||||||
|
if (next == 3) unlockNotice = false;
|
||||||
|
localUnlockRevision = unlock;
|
||||||
state = next; revision = rev; supported = support; overlay = cover; remotePaused = paused;
|
state = next; revision = rev; supported = support; overlay = cover; remotePaused = paused;
|
||||||
inputBlockSupported = blockSupport; inputBlocked = blocked; reason = detail;
|
inputBlockSupported = blockSupport; inputBlocked = blocked; reason = detail;
|
||||||
received = true; receivedAt = now; failure = "";
|
received = true; receivedAt = now; failure = "";
|
||||||
@@ -53,9 +60,13 @@ final class RemotePrivacyScreen {
|
|||||||
boolean canToggle(long now) {
|
boolean canToggle(long now) {
|
||||||
return desktopAvailable && !pending && fresh(now) && state != 4 && (active() || supported && state == 1);
|
return desktopAvailable && !pending && fresh(now) && state != 4 && (active() || supported && state == 1);
|
||||||
}
|
}
|
||||||
long begin(long now) {
|
boolean canLock(long now) {
|
||||||
|
return desktopAvailable && !pending && fresh(now) && supported;
|
||||||
|
}
|
||||||
|
long begin(long now) { return begin(now, false); }
|
||||||
|
long begin(long now, boolean lock) {
|
||||||
tick(now);
|
tick(now);
|
||||||
if (!canToggle(now)) return -1;
|
if (!(lock ? canLock(now) : canToggle(now))) return -1;
|
||||||
pending = true; requestedAt = now; requestRevision = revision; failure = "";
|
pending = true; requestedAt = now; requestRevision = revision; failure = "";
|
||||||
return ++requestId;
|
return ++requestId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,6 +137,8 @@ int ConfigCenter::Load() {
|
|||||||
enable_daemon_ = ini_->GetBoolValue(section_, "enable_daemon", enable_daemon_);
|
enable_daemon_ = ini_->GetBoolValue(section_, "enable_daemon", enable_daemon_);
|
||||||
enable_privacy_screen_.store(
|
enable_privacy_screen_.store(
|
||||||
ini_->GetBoolValue(section_, "enable_privacy_screen", true));
|
ini_->GetBoolValue(section_, "enable_privacy_screen", true));
|
||||||
|
privacy_local_unlock_allowed_.store(
|
||||||
|
ini_->GetBoolValue(section_, "privacy_local_unlock_allowed", true));
|
||||||
lock_on_disconnect_.store(
|
lock_on_disconnect_.store(
|
||||||
ini_->GetBoolValue(section_, "lock_on_disconnect", true));
|
ini_->GetBoolValue(section_, "lock_on_disconnect", true));
|
||||||
save_remote_previews_.store(
|
save_remote_previews_.store(
|
||||||
@@ -234,6 +236,8 @@ int ConfigCenter::Save() {
|
|||||||
ini_->SetBoolValue(section_, "enable_daemon", enable_daemon_);
|
ini_->SetBoolValue(section_, "enable_daemon", enable_daemon_);
|
||||||
ini_->SetBoolValue(section_, "enable_privacy_screen",
|
ini_->SetBoolValue(section_, "enable_privacy_screen",
|
||||||
enable_privacy_screen_.load());
|
enable_privacy_screen_.load());
|
||||||
|
ini_->SetBoolValue(section_, "privacy_local_unlock_allowed",
|
||||||
|
privacy_local_unlock_allowed_.load());
|
||||||
ini_->SetBoolValue(section_, "lock_on_disconnect", lock_on_disconnect_.load());
|
ini_->SetBoolValue(section_, "lock_on_disconnect", lock_on_disconnect_.load());
|
||||||
ini_->SetBoolValue(section_, "save_remote_previews",
|
ini_->SetBoolValue(section_, "save_remote_previews",
|
||||||
save_remote_previews_.load());
|
save_remote_previews_.load());
|
||||||
@@ -409,6 +413,18 @@ int ConfigCenter::SetPrivacyScreen(bool enable_privacy_screen) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int ConfigCenter::SetPrivacyLocalUnlock(bool allowed) {
|
||||||
|
if (privacy_local_unlock_allowed_.load() == allowed) return 0;
|
||||||
|
if (StoreValues({{"privacy_local_unlock_allowed", allowed ? "true" : "false"}}) != 0)
|
||||||
|
return -1;
|
||||||
|
privacy_local_unlock_allowed_.store(allowed);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ConfigCenter::IsPrivacyLocalUnlockAllowed() const {
|
||||||
|
return privacy_local_unlock_allowed_.load();
|
||||||
|
}
|
||||||
|
|
||||||
int ConfigCenter::SetLockOnDisconnect(bool enabled) {
|
int ConfigCenter::SetLockOnDisconnect(bool enabled) {
|
||||||
if (StoreValues({{"lock_on_disconnect", enabled ? "true" : "false"}}) != 0)
|
if (StoreValues({{"lock_on_disconnect", enabled ? "true" : "false"}}) != 0)
|
||||||
return -1;
|
return -1;
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ class ConfigCenter {
|
|||||||
int SetAutostart(bool enable_autostart);
|
int SetAutostart(bool enable_autostart);
|
||||||
int SetDaemon(bool enable_daemon);
|
int SetDaemon(bool enable_daemon);
|
||||||
int SetPrivacyScreen(bool enable_privacy_screen);
|
int SetPrivacyScreen(bool enable_privacy_screen);
|
||||||
|
int SetPrivacyLocalUnlock(bool allowed);
|
||||||
int SetLockOnDisconnect(bool enabled);
|
int SetLockOnDisconnect(bool enabled);
|
||||||
int SetSaveRemotePreviews(bool enabled);
|
int SetSaveRemotePreviews(bool enabled);
|
||||||
int SetPortableServicePromptSuppressed(bool suppressed);
|
int SetPortableServicePromptSuppressed(bool suppressed);
|
||||||
@@ -73,6 +74,7 @@ class ConfigCenter {
|
|||||||
bool IsEnableAutostart() const;
|
bool IsEnableAutostart() const;
|
||||||
bool IsEnableDaemon() const;
|
bool IsEnableDaemon() const;
|
||||||
bool IsEnablePrivacyScreen() const;
|
bool IsEnablePrivacyScreen() const;
|
||||||
|
bool IsPrivacyLocalUnlockAllowed() const;
|
||||||
bool IsLockOnDisconnect() const;
|
bool IsLockOnDisconnect() const;
|
||||||
bool IsSaveRemotePreviews() const;
|
bool IsSaveRemotePreviews() const;
|
||||||
bool IsPortableServicePromptSuppressed() const;
|
bool IsPortableServicePromptSuppressed() const;
|
||||||
@@ -105,6 +107,7 @@ class ConfigCenter {
|
|||||||
bool enable_autostart_ = false;
|
bool enable_autostart_ = false;
|
||||||
bool enable_daemon_ = false;
|
bool enable_daemon_ = false;
|
||||||
std::atomic<bool> enable_privacy_screen_{true};
|
std::atomic<bool> enable_privacy_screen_{true};
|
||||||
|
std::atomic<bool> privacy_local_unlock_allowed_{true};
|
||||||
std::atomic<bool> lock_on_disconnect_{true};
|
std::atomic<bool> lock_on_disconnect_{true};
|
||||||
std::atomic<bool> save_remote_previews_{true};
|
std::atomic<bool> save_remote_previews_{true};
|
||||||
bool portable_service_prompt_suppressed_ = false;
|
bool portable_service_prompt_suppressed_ = false;
|
||||||
|
|||||||
@@ -1504,6 +1504,7 @@ void GuiApplication::ResetSettingsUi(bool gallery_window) {
|
|||||||
main->set_save_remote_previews(config_center_->IsSaveRemotePreviews());
|
main->set_save_remote_previews(config_center_->IsSaveRemotePreviews());
|
||||||
main->set_privacy_on_connect_enabled(
|
main->set_privacy_on_connect_enabled(
|
||||||
config_center_->IsEnablePrivacyScreen());
|
config_center_->IsEnablePrivacyScreen());
|
||||||
|
main->set_privacy_local_unlock_allowed(config_center_->IsPrivacyLocalUnlockAllowed());
|
||||||
main->set_lock_on_disconnect_enabled(config_center_->IsLockOnDisconnect());
|
main->set_lock_on_disconnect_enabled(config_center_->IsLockOnDisconnect());
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
main->set_capture_method_visible(true);
|
main->set_capture_method_visible(true);
|
||||||
@@ -2155,9 +2156,9 @@ void GuiApplication::BindStreamCallbacks() {
|
|||||||
SendReliableDataFrame(props->peer_, message.c_str(), message.size(),
|
SendReliableDataFrame(props->peer_, message.c_str(), message.size(),
|
||||||
props->control_data_label_.c_str());
|
props->control_data_label_.c_str());
|
||||||
});
|
});
|
||||||
stream->on_toggle_privacy([this] {
|
const auto send_privacy = [this](bool relock) {
|
||||||
auto props = SelectedSession();
|
auto props = SelectedSession();
|
||||||
if (!props || !props->peer_ || !props->control_mouse_ ||
|
if (!props || !props->peer_ || (!relock && !props->control_mouse_) ||
|
||||||
props->connection_status_.load() != ConnectionStatus::Connected) return;
|
props->connection_status_.load() != ConnectionStatus::Connected) return;
|
||||||
RemoteAction action{};
|
RemoteAction action{};
|
||||||
action.type = ControlType::privacy_command;
|
action.type = ControlType::privacy_command;
|
||||||
@@ -2168,9 +2169,12 @@ void GuiApplication::BindStreamCallbacks() {
|
|||||||
const auto controls = GetPrivacyControlState(
|
const auto controls = GetPrivacyControlState(
|
||||||
status, props->privacy_status_received_, props->privacy_command_pending_,
|
status, props->privacy_status_received_, props->privacy_command_pending_,
|
||||||
now - props->privacy_status_tick_, now - props->privacy_command_tick_);
|
now - props->privacy_status_tick_, now - props->privacy_command_tick_);
|
||||||
if (!props->privacy_desktop_available_ || !controls.can_toggle) return;
|
const bool can_lock = props->privacy_status_received_ && status.supported &&
|
||||||
action.pc.flag = controls.active ? PrivacyCommandFlag::disable
|
now - props->privacy_status_tick_ <= 5000 &&
|
||||||
: PrivacyCommandFlag::enable;
|
(!props->privacy_command_pending_ || now - props->privacy_command_tick_ >= 15000);
|
||||||
|
if (!props->privacy_desktop_available_ || !(relock ? can_lock : controls.can_toggle)) return;
|
||||||
|
action.pc.flag = relock ? PrivacyCommandFlag::lock
|
||||||
|
: controls.active ? PrivacyCommandFlag::disable : PrivacyCommandFlag::enable;
|
||||||
action.pc.block_local_input = true;
|
action.pc.block_local_input = true;
|
||||||
props->privacy_command_pending_ = true;
|
props->privacy_command_pending_ = true;
|
||||||
props->privacy_command_tick_ = now;
|
props->privacy_command_tick_ = now;
|
||||||
@@ -2181,9 +2185,17 @@ void GuiApplication::BindStreamCallbacks() {
|
|||||||
props->control_data_label_.c_str()) != 0) {
|
props->control_data_label_.c_str()) != 0) {
|
||||||
std::lock_guard lock(props->privacy_status_mutex_);
|
std::lock_guard lock(props->privacy_status_mutex_);
|
||||||
props->privacy_command_pending_ = false;
|
props->privacy_command_pending_ = false;
|
||||||
props->privacy_status_received_ = false;
|
if (!relock) props->privacy_status_received_ = false;
|
||||||
LOG_WARN("Privacy command send failed, remote_id={}", props->remote_id_);
|
LOG_WARN("Privacy command send failed, remote_id={}", props->remote_id_);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
stream->on_toggle_privacy([send_privacy] { send_privacy(false); });
|
||||||
|
stream->on_lock_privacy([send_privacy] { send_privacy(true); });
|
||||||
|
stream->on_dismiss_privacy_unlock([this] {
|
||||||
|
if (auto props = SelectedSession()) {
|
||||||
|
std::lock_guard lock(props->privacy_status_mutex_);
|
||||||
|
props->privacy_unlock_notice_ = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
stream->on_toggle_mouse_control([this] {
|
stream->on_toggle_mouse_control([this] {
|
||||||
auto props = SelectedSession();
|
auto props = SelectedSession();
|
||||||
@@ -2590,6 +2602,11 @@ void GuiApplication::Tick() {
|
|||||||
HandlePendingPresenceProbe();
|
HandlePendingPresenceProbe();
|
||||||
HandlePresenceProbeTimeout();
|
HandlePresenceProbeTimeout();
|
||||||
HandlePrivacy();
|
HandlePrivacy();
|
||||||
|
if (privacy_settings_changed_) {
|
||||||
|
for (const auto& window : {ui_->main, ui_->wall})
|
||||||
|
window->set_privacy_local_unlock_allowed(config_center_->IsPrivacyLocalUnlockAllowed());
|
||||||
|
privacy_settings_changed_ = false;
|
||||||
|
}
|
||||||
HandleScreenLock();
|
HandleScreenLock();
|
||||||
HandleWindowsServiceIntegration();
|
HandleWindowsServiceIntegration();
|
||||||
#if (defined(__linux__) && !defined(__OHOS__)) && !defined(__APPLE__)
|
#if (defined(__linux__) && !defined(__OHOS__)) && !defined(__APPLE__)
|
||||||
@@ -3569,9 +3586,9 @@ void GuiApplication::SyncXWaylandWindowActivation() {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
void GuiApplication::SetStreamKeyboardFocus(bool focused) {
|
void GuiApplication::SetStreamKeyboardFocus(bool focused) {
|
||||||
// The local upgrade dialog owns input even while the native window is active.
|
// Local dialogs own input even while the native window is active.
|
||||||
if (focused && ui_ && ui_->stream &&
|
if (focused && ui_ && ui_->stream &&
|
||||||
(*ui_->stream)->get_remote_update_visible()) {
|
(*ui_->stream)->get_modal_visible()) {
|
||||||
focused = false;
|
focused = false;
|
||||||
}
|
}
|
||||||
if (focus_on_stream_window_ == focused) {
|
if (focus_on_stream_window_ == focused) {
|
||||||
@@ -3826,6 +3843,11 @@ void GuiApplication::SyncStreamWindow() {
|
|||||||
now - props->privacy_status_tick_, now - props->privacy_command_tick_);
|
now - props->privacy_status_tick_, now - props->privacy_command_tick_);
|
||||||
const bool connected =
|
const bool connected =
|
||||||
props->connection_status_.load() == ConnectionStatus::Connected;
|
props->connection_status_.load() == ConnectionStatus::Connected;
|
||||||
|
(*ui_->stream)->set_privacy_unlock_visible(connected && props->privacy_unlock_notice_);
|
||||||
|
(*ui_->stream)->set_privacy_can_lock(connected && props->privacy_desktop_available_ &&
|
||||||
|
props->privacy_status_received_ && status.supported &&
|
||||||
|
now - props->privacy_status_tick_ <= 5000 &&
|
||||||
|
(!props->privacy_command_pending_ || now - props->privacy_command_tick_ >= 15000));
|
||||||
(*ui_->stream)->set_privacy_active(controls.active);
|
(*ui_->stream)->set_privacy_active(controls.active);
|
||||||
(*ui_->stream)->set_privacy_off(connected && controls.off);
|
(*ui_->stream)->set_privacy_off(connected && controls.off);
|
||||||
(*ui_->stream)->set_privacy_unsupported(controls.unsupported);
|
(*ui_->stream)->set_privacy_unsupported(controls.unsupported);
|
||||||
@@ -4580,6 +4602,14 @@ bool GuiApplication::SaveSettingsFromUi(bool gallery_window) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
if (config_center_->SetPrivacyLocalUnlock(main->get_privacy_local_unlock_allowed()) != 0) {
|
||||||
|
if (wall_changed) ui_->gallery.Save(ui_->gallery_path);
|
||||||
|
main->set_settings_save_error(UiText(
|
||||||
|
localization_language_index_ == 0 ? "无法保存隐私屏解锁设置,请重试。"
|
||||||
|
: localization_language_index_ == 2 ? "Не удалось сохранить настройку разблокировки. Повторите попытку."
|
||||||
|
: "Could not save privacy unlock settings. Try again."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
language_button_value_ =
|
language_button_value_ =
|
||||||
localization::detail::ClampLanguageIndex(main->get_language_index());
|
localization::detail::ClampLanguageIndex(main->get_language_index());
|
||||||
video_encode_format_button_value_ = std::clamp(main->get_codec_index(), 0, 1);
|
video_encode_format_button_value_ = std::clamp(main->get_codec_index(), 0, 1);
|
||||||
@@ -4594,7 +4624,9 @@ bool GuiApplication::SaveSettingsFromUi(bool gallery_window) {
|
|||||||
localization_language_index_ = language_button_value_;
|
localization_language_index_ = language_button_value_;
|
||||||
config_center_->SetLanguage(localization_language_);
|
config_center_->SetLanguage(localization_language_);
|
||||||
privacy_.SetText(
|
privacy_.SetText(
|
||||||
{localization::privacy_screen_unlock_hint[localization_language_index_]});
|
{localization::privacy_screen_unlock_hint[localization_language_index_],
|
||||||
|
localization::privacy_screen_locked_hint[localization_language_index_],
|
||||||
|
config_center_->IsPrivacyLocalUnlockAllowed()});
|
||||||
config_center_->SetVideoEncodeFormat(
|
config_center_->SetVideoEncodeFormat(
|
||||||
static_cast<ConfigCenter::VIDEO_ENCODE_FORMAT>(
|
static_cast<ConfigCenter::VIDEO_ENCODE_FORMAT>(
|
||||||
video_encode_format_button_value_));
|
video_encode_format_button_value_));
|
||||||
@@ -4667,7 +4699,9 @@ void GuiApplication::ApplySettingsFromConfig(bool reconnect) {
|
|||||||
enable_self_hosted_ = config_center_->IsSelfHosted();
|
enable_self_hosted_ = config_center_->IsSelfHosted();
|
||||||
enable_autostart_ = config_center_->IsEnableAutostart();
|
enable_autostart_ = config_center_->IsEnableAutostart();
|
||||||
enable_daemon_ = config_center_->IsEnableDaemon();
|
enable_daemon_ = config_center_->IsEnableDaemon();
|
||||||
privacy_.SetText({localization::privacy_screen_unlock_hint[localization_language_index_]});
|
privacy_.SetText({localization::privacy_screen_unlock_hint[localization_language_index_],
|
||||||
|
localization::privacy_screen_locked_hint[localization_language_index_],
|
||||||
|
config_center_->IsPrivacyLocalUnlockAllowed()});
|
||||||
const auto path = config_center_->GetFileTransferSavePath();
|
const auto path = config_center_->GetFileTransferSavePath();
|
||||||
std::snprintf(file_transfer_save_path_buf_, sizeof(file_transfer_save_path_buf_), "%s", path.c_str());
|
std::snprintf(file_transfer_save_path_buf_, sizeof(file_transfer_save_path_buf_), "%s", path.c_str());
|
||||||
const auto host = config_center_->GetSignalServerHost();
|
const auto host = config_center_->GetSignalServerHost();
|
||||||
|
|||||||
@@ -404,6 +404,12 @@ struct TranslationRow {
|
|||||||
X(wayland_notice_acknowledge, u8"我知道了", "Got it", u8"Понятно") \
|
X(wayland_notice_acknowledge, u8"我知道了", "Got it", u8"Понятно") \
|
||||||
X(show_main_window, u8"显示主界面", "Show Main Window", \
|
X(show_main_window, u8"显示主界面", "Show Main Window", \
|
||||||
u8"Показать главное окно") \
|
u8"Показать главное окно") \
|
||||||
|
X(privacy_local_unlock, u8"允许被控时解锁隐私屏:", "Allow privacy screen unlock while controlled:", u8"Разрешить разблокировку приватного экрана при удалённом управлении:") \
|
||||||
|
X(privacy_screen_locked_hint, u8"隐私屏已开启,本机解锁已禁用", "Privacy screen is on. Local unlocking is disabled.", u8"Приватный экран включён. Локальная разблокировка отключена.") \
|
||||||
|
X(privacy_unlocked_title, u8"隐私屏已被解锁", "Privacy screen unlocked", u8"Приватный экран разблокирован") \
|
||||||
|
X(privacy_unlocked_message, u8"被控端已通过组合键解除隐私屏。点击“锁定”将重新开启隐私屏,并禁止本机使用组合键解锁。", "The remote privacy screen was unlocked using the local shortcut. Lock it again to restore privacy and disable local shortcut unlocking.", u8"Приватный экран отключён локальным сочетанием клавиш. Заблокируйте его снова, чтобы восстановить защиту и запретить локальную разблокировку.") \
|
||||||
|
X(privacy_unlock_confirm, u8"确认", "Confirm", u8"Подтвердить") \
|
||||||
|
X(privacy_unlock_lock, u8"锁定", "Lock", u8"Заблокировать") \
|
||||||
X(privacy_screen_unlock_hint, u8"按下快捷键,解除隐私屏", \
|
X(privacy_screen_unlock_hint, u8"按下快捷键,解除隐私屏", \
|
||||||
"To turn off the privacy screen, press", \
|
"To turn off the privacy screen, press", \
|
||||||
u8"Чтобы отключить приватный экран, нажмите") \
|
u8"Чтобы отключить приватный экран, нажмите") \
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ class GuiRuntime : protected gui_detail::GuiState {
|
|||||||
DisconnectLockPolicy disconnect_lock_;
|
DisconnectLockPolicy disconnect_lock_;
|
||||||
std::mutex privacy_commands_mutex_;
|
std::mutex privacy_commands_mutex_;
|
||||||
std::deque<std::pair<std::string, PrivacyCommand>> privacy_commands_;
|
std::deque<std::pair<std::string, PrivacyCommand>> privacy_commands_;
|
||||||
|
bool privacy_settings_changed_ = false;
|
||||||
uint32_t last_privacy_revision_ = 0;
|
uint32_t last_privacy_revision_ = 0;
|
||||||
uint64_t last_privacy_status_tick_ = 0;
|
uint64_t last_privacy_status_tick_ = 0;
|
||||||
ClipboardController clipboard_;
|
ClipboardController clipboard_;
|
||||||
|
|||||||
@@ -316,6 +316,12 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
|||||||
std::lock_guard lock(props->privacy_status_mutex_);
|
std::lock_guard lock(props->privacy_status_mutex_);
|
||||||
if (!props->privacy_status_received_ ||
|
if (!props->privacy_status_received_ ||
|
||||||
static_cast<int32_t>(remote_action.ps.revision - props->privacy_status_.revision) >= 0) {
|
static_cast<int32_t>(remote_action.ps.revision - props->privacy_status_.revision) >= 0) {
|
||||||
|
if (props->privacy_status_received_ &&
|
||||||
|
static_cast<int32_t>(remote_action.ps.local_unlock_revision -
|
||||||
|
props->privacy_status_.local_unlock_revision) > 0)
|
||||||
|
props->privacy_unlock_notice_ = true;
|
||||||
|
if (remote_action.ps.state == PrivacyState::on)
|
||||||
|
props->privacy_unlock_notice_ = false;
|
||||||
props->privacy_status_ = remote_action.ps;
|
props->privacy_status_ = remote_action.ps;
|
||||||
props->privacy_status_received_ = true;
|
props->privacy_status_received_ = true;
|
||||||
props->privacy_status_tick_ = SDL_GetTicks();
|
props->privacy_status_tick_ = SDL_GetTicks();
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include <shared_mutex>
|
#include <shared_mutex>
|
||||||
|
|
||||||
#include "rd_log.h"
|
#include "rd_log.h"
|
||||||
|
#include "localization.h"
|
||||||
#include "runtime/gui_runtime.h"
|
#include "runtime/gui_runtime.h"
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
@@ -71,6 +72,14 @@ void GuiRuntime::QueuePrivacyCommand(const std::string& remote_id,
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GuiRuntime::HandlePrivacy() {
|
void GuiRuntime::HandlePrivacy() {
|
||||||
|
const auto sync_text = [this] {
|
||||||
|
const int language = localization::detail::ClampLanguageIndex(
|
||||||
|
static_cast<int>(config_center_->GetLanguage()));
|
||||||
|
privacy_.SetText({localization::privacy_screen_unlock_hint[language],
|
||||||
|
localization::privacy_screen_locked_hint[language],
|
||||||
|
config_center_->IsPrivacyLocalUnlockAllowed()});
|
||||||
|
};
|
||||||
|
sync_text();
|
||||||
std::deque<std::pair<std::string, PrivacyCommand>> commands;
|
std::deque<std::pair<std::string, PrivacyCommand>> commands;
|
||||||
{
|
{
|
||||||
std::lock_guard lock(privacy_commands_mutex_);
|
std::lock_guard lock(privacy_commands_mutex_);
|
||||||
@@ -87,6 +96,16 @@ void GuiRuntime::HandlePrivacy() {
|
|||||||
if (command.flag == PrivacyCommandFlag::enable) {
|
if (command.flag == PrivacyCommandFlag::enable) {
|
||||||
if (privacy_sessions_.CanEnable())
|
if (privacy_sessions_.CanEnable())
|
||||||
privacy_.Enable(command.block_local_input);
|
privacy_.Enable(command.block_local_input);
|
||||||
|
} else if (command.flag == PrivacyCommandFlag::lock) {
|
||||||
|
if (privacy_sessions_.CanEnable()) {
|
||||||
|
if (config_center_->SetPrivacyLocalUnlock(false) == 0) {
|
||||||
|
privacy_settings_changed_ = true;
|
||||||
|
sync_text();
|
||||||
|
privacy_.Enable(true, true);
|
||||||
|
} else {
|
||||||
|
privacy_.RejectCommand("Could not save the local privacy unlock setting; try again");
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (command.flag == PrivacyCommandFlag::disable) {
|
} else if (command.flag == PrivacyCommandFlag::disable) {
|
||||||
privacy_.Disable();
|
privacy_.Disable();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,6 +193,7 @@ struct RemoteSession {
|
|||||||
std::mutex privacy_status_mutex_;
|
std::mutex privacy_status_mutex_;
|
||||||
PrivacyStatus privacy_status_{};
|
PrivacyStatus privacy_status_{};
|
||||||
bool privacy_status_received_ = false;
|
bool privacy_status_received_ = false;
|
||||||
|
bool privacy_unlock_notice_ = false;
|
||||||
bool privacy_desktop_available_ = true;
|
bool privacy_desktop_available_ = true;
|
||||||
bool privacy_command_pending_ = false;
|
bool privacy_command_pending_ = false;
|
||||||
uint64_t privacy_command_tick_ = 0;
|
uint64_t privacy_command_tick_ = 0;
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ export global UiStrings {
|
|||||||
in-out property <string> daemon: "Run in background";
|
in-out property <string> daemon: "Run in background";
|
||||||
in-out property <string> save-remote-previews: "Save remote screen previews:";
|
in-out property <string> save-remote-previews: "Save remote screen previews:";
|
||||||
in-out property <string> unknown-platform: "Unknown platform";
|
in-out property <string> unknown-platform: "Unknown platform";
|
||||||
|
in-out property <string> privacy-local-unlock: "Allow privacy screen unlock while controlled:";
|
||||||
in-out property <string> privacy-on-connect: "Privacy screen on connect";
|
in-out property <string> privacy-on-connect: "Privacy screen on connect";
|
||||||
in-out property <string> lock-on-disconnect: "Lock screen on disconnect";
|
in-out property <string> lock-on-disconnect: "Lock screen on disconnect";
|
||||||
in-out property <string> file-save-path: "File save path";
|
in-out property <string> file-save-path: "File save path";
|
||||||
@@ -651,6 +652,7 @@ export component MainWindow inherits Window {
|
|||||||
// Show the preference on every platform; only adapted hosts can change it.
|
// Show the preference on every platform; only adapted hosts can change it.
|
||||||
in property <bool> privacy-setting-available: false;
|
in property <bool> privacy-setting-available: false;
|
||||||
in-out property <bool> privacy-on-connect-enabled: true;
|
in-out property <bool> privacy-on-connect-enabled: true;
|
||||||
|
in-out property <bool> privacy-local-unlock-allowed: true;
|
||||||
in property <bool> lock-on-disconnect-available: false;
|
in property <bool> lock-on-disconnect-available: false;
|
||||||
in-out property <bool> lock-on-disconnect-enabled: true;
|
in-out property <bool> lock-on-disconnect-enabled: true;
|
||||||
in-out property <string> file-save-path: "";
|
in-out property <string> file-save-path: "";
|
||||||
@@ -2728,8 +2730,9 @@ export component MainWindow inherits Window {
|
|||||||
font-size: 11px; color: WorkbenchStyle.muted; wrap: word-wrap;
|
font-size: 11px; color: WorkbenchStyle.muted; wrap: word-wrap;
|
||||||
}
|
}
|
||||||
SettingsGroup {
|
SettingsGroup {
|
||||||
title: GalleryStrings.tr("隐私保护", "Privacy", "Приватность"); rows: 2;
|
title: GalleryStrings.tr("隐私保护", "Privacy", "Приватность"); rows: 3;
|
||||||
SettingsToggleRow { label: UiStrings.privacy-on-connect; checked <=> root.privacy-on-connect-enabled; enabled: root.privacy-setting-available; }
|
SettingsToggleRow { label: UiStrings.privacy-on-connect; checked <=> root.privacy-on-connect-enabled; enabled: root.privacy-setting-available; }
|
||||||
|
SettingsToggleRow { label: UiStrings.privacy-local-unlock; checked <=> root.privacy-local-unlock-allowed; enabled: root.privacy-setting-available; }
|
||||||
SettingsToggleRow { label: UiStrings.lock-on-disconnect; checked <=> root.lock-on-disconnect-enabled; enabled: root.lock-on-disconnect-available; divider: false; }
|
SettingsToggleRow { label: UiStrings.lock-on-disconnect; checked <=> root.lock-on-disconnect-enabled; enabled: root.lock-on-disconnect-available; divider: false; }
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ export global StreamStrings {
|
|||||||
|
|
||||||
in-out property <string> select-display: "Select Display";
|
in-out property <string> select-display: "Select Display";
|
||||||
in-out property <string> send-shortcut: "Send Shortcut";
|
in-out property <string> send-shortcut: "Send Shortcut";
|
||||||
|
in-out property <string> privacy-unlocked-title: "Privacy screen unlocked";
|
||||||
|
in-out property <string> privacy-unlocked-message: "The remote privacy screen was unlocked using the local shortcut. Lock it again to restore privacy and disable local shortcut unlocking.";
|
||||||
|
in-out property <string> privacy-unlock-confirm: "Confirm";
|
||||||
|
in-out property <string> privacy-unlock-lock: "Lock";
|
||||||
in-out property <string> privacy-enable: "Enable privacy screen";
|
in-out property <string> privacy-enable: "Enable privacy screen";
|
||||||
in-out property <string> privacy-disable: "Turn privacy off / recover";
|
in-out property <string> privacy-disable: "Turn privacy off / recover";
|
||||||
in-out property <string> control-mouse: "Control Mouse";
|
in-out property <string> control-mouse: "Control Mouse";
|
||||||
@@ -296,6 +300,11 @@ export component StreamWindow inherits Window {
|
|||||||
in property <string> status-text: "";
|
in property <string> status-text: "";
|
||||||
in property <string> receiving-text: "";
|
in property <string> receiving-text: "";
|
||||||
in property <bool> remote-update-visible: false;
|
in property <bool> remote-update-visible: false;
|
||||||
|
in property <bool> privacy-unlock-visible: false;
|
||||||
|
in property <bool> privacy-can-lock: false;
|
||||||
|
out property <bool> modal-visible: root.remote-update-visible || root.privacy-unlock-visible;
|
||||||
|
callback dismiss-privacy-unlock;
|
||||||
|
callback lock-privacy;
|
||||||
callback dismiss-remote-update;
|
callback dismiss-remote-update;
|
||||||
in property <[string]> displays;
|
in property <[string]> displays;
|
||||||
in-out property <int> selected-display: 0;
|
in-out property <int> selected-display: 0;
|
||||||
@@ -491,8 +500,8 @@ export component StreamWindow inherits Window {
|
|||||||
changed mouse-control-enabled => {
|
changed mouse-control-enabled => {
|
||||||
if !root.mouse-control-enabled { root.cancel-remote-pointer(); }
|
if !root.mouse-control-enabled { root.cancel-remote-pointer(); }
|
||||||
}
|
}
|
||||||
changed remote-update-visible => {
|
changed modal-visible => {
|
||||||
if root.remote-update-visible {
|
if root.modal-visible {
|
||||||
root.close-control-menus();
|
root.close-control-menus();
|
||||||
root.control-drag-pending = false;
|
root.control-drag-pending = false;
|
||||||
root.end-control-drag();
|
root.end-control-drag();
|
||||||
@@ -502,7 +511,7 @@ export component StreamWindow inherits Window {
|
|||||||
// adapter and aborts the process; defer focus to the event loop.
|
// adapter and aborts the process; defer focus to the event loop.
|
||||||
input-focus-timer.running = true;
|
input-focus-timer.running = true;
|
||||||
input-focus-timer.restart();
|
input-focus-timer.restart();
|
||||||
root.keyboard-focus-changed(!root.remote-update-visible);
|
root.keyboard-focus-changed(!root.modal-visible);
|
||||||
}
|
}
|
||||||
input-focus-timer := Timer {
|
input-focus-timer := Timer {
|
||||||
interval: 1ms;
|
interval: 1ms;
|
||||||
@@ -617,7 +626,7 @@ export component StreamWindow inherits Window {
|
|||||||
root.set-remote-button(button, false);
|
root.set-remote-button(button, false);
|
||||||
root.pointer-input(button, kind, x, y);
|
root.pointer-input(button, kind, x, y);
|
||||||
}
|
}
|
||||||
} else if root.window-active && root.mouse-control-enabled && !root.remote-update-visible {
|
} else if root.window-active && root.mouse-control-enabled && !root.modal-visible {
|
||||||
let remote-target = root.remote-pointer-active
|
let remote-target = root.remote-pointer-active
|
||||||
|| (!root.control-menu-open && !root.is-local-control-area(x, y));
|
|| (!root.control-menu-open && !root.is-local-control-area(x, y));
|
||||||
if kind == PointerEventKind.down && remote-target
|
if kind == PointerEventKind.down && remote-target
|
||||||
@@ -653,7 +662,7 @@ export component StreamWindow inherits Window {
|
|||||||
// so a captured drag can never also toggle the toolbar.
|
// so a captured drag can never also toggle the toolbar.
|
||||||
let toggle-expanded = kind == PointerEventKind.up && root.control-drag-pending
|
let toggle-expanded = kind == PointerEventKind.up && root.control-drag-pending
|
||||||
&& !root.control-dragging
|
&& !root.control-dragging
|
||||||
&& root.window-active && !root.remote-update-visible
|
&& root.window-active && !root.modal-visible
|
||||||
&& x >= control.x + logo-button.x && x < control.x + logo-button.x + logo-button.width
|
&& x >= control.x + logo-button.x && x < control.x + logo-button.x + logo-button.width
|
||||||
&& y >= control.y + logo-button.y && y < control.y + logo-button.y + logo-button.height;
|
&& y >= control.y + logo-button.y && y < control.y + logo-button.y + logo-button.height;
|
||||||
root.control-drag-pending = false;
|
root.control-drag-pending = false;
|
||||||
@@ -702,7 +711,7 @@ export component StreamWindow inherits Window {
|
|||||||
}
|
}
|
||||||
|
|
||||||
is-local-control-area(pointer-x, pointer-y) => {
|
is-local-control-area(pointer-x, pointer-y) => {
|
||||||
if root.remote-update-visible { return true; }
|
if root.modal-visible { return true; }
|
||||||
let over-control = pointer-x >= root.control-surface-x
|
let over-control = pointer-x >= root.control-surface-x
|
||||||
&& pointer-x <= root.control-surface-x + root.control-surface-width
|
&& pointer-x <= root.control-surface-x + root.control-surface-width
|
||||||
&& pointer-y >= control.y
|
&& pointer-y >= control.y
|
||||||
@@ -1015,11 +1024,11 @@ export component StreamWindow inherits Window {
|
|||||||
|
|
||||||
drop-zone := DropArea {
|
drop-zone := DropArea {
|
||||||
can-drop(event) => {
|
can-drop(event) => {
|
||||||
return !root.remote-update-visible && root.can-drop-file(event.data)
|
return !root.modal-visible && root.can-drop-file(event.data)
|
||||||
? DragAction.copy : DragAction.none;
|
? DragAction.copy : DragAction.none;
|
||||||
}
|
}
|
||||||
dropped(event) => {
|
dropped(event) => {
|
||||||
if root.remote-update-visible { return DragAction.none; }
|
if root.modal-visible { return DragAction.none; }
|
||||||
root.file-dropped(event.data);
|
root.file-dropped(event.data);
|
||||||
return DragAction.copy;
|
return DragAction.copy;
|
||||||
}
|
}
|
||||||
@@ -1030,16 +1039,16 @@ export component StreamWindow inherits Window {
|
|||||||
focus-gained => { root.keyboard-focus-changed(true); }
|
focus-gained => { root.keyboard-focus-changed(true); }
|
||||||
focus-lost => { root.keyboard-focus-changed(false); }
|
focus-lost => { root.keyboard-focus-changed(false); }
|
||||||
key-pressed(event) => {
|
key-pressed(event) => {
|
||||||
if !root.remote-update-visible {
|
if !root.modal-visible {
|
||||||
root.key-input(event.text, true, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta);
|
root.key-input(event.text, true, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta);
|
||||||
}
|
}
|
||||||
accept
|
accept
|
||||||
}
|
}
|
||||||
key-released(event) => {
|
key-released(event) => {
|
||||||
if root.remote-update-visible {
|
if root.modal-visible {
|
||||||
// Dismiss on release so the same key cannot reach the peer.
|
// Dismiss on release so the same key cannot reach the peer.
|
||||||
if event.text == Key.Escape || event.text == Key.Return {
|
if event.text == Key.Escape || event.text == Key.Return {
|
||||||
root.dismiss-remote-update();
|
if root.privacy-unlock-visible { root.dismiss-privacy-unlock(); } else { root.dismiss-remote-update(); }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
root.key-input(event.text, false, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta);
|
root.key-input(event.text, false, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta);
|
||||||
@@ -1743,7 +1752,7 @@ export component StreamWindow inherits Window {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if root.remote-update-visible: Rectangle {
|
if root.modal-visible: Rectangle {
|
||||||
width: parent.width; height: parent.height;
|
width: parent.width; height: parent.height;
|
||||||
background: #0c162c52;
|
background: #0c162c52;
|
||||||
z: 13;
|
z: 13;
|
||||||
@@ -1752,8 +1761,43 @@ export component StreamWindow inherits Window {
|
|||||||
scroll-event(event) => { accept }
|
scroll-event(event) => { accept }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if root.privacy-unlock-visible: DialogSurface {
|
||||||
|
x: (parent.width - self.width) / 2;
|
||||||
|
y: max(16px, (parent.height - self.height) / 2);
|
||||||
|
width: min(460px, parent.width - 32px);
|
||||||
|
height: unlock-message.y + unlock-message.height + 84px;
|
||||||
|
outline-color: UiSurface.edge;
|
||||||
|
border-radius: UiRadius.surface;
|
||||||
|
z: 14;
|
||||||
|
TouchArea { scroll-event(event) => { accept } }
|
||||||
|
unlock-title := Text {
|
||||||
|
x: 24px; y: 24px; width: parent.width - 48px;
|
||||||
|
height: max(28px, self.preferred-height);
|
||||||
|
text: StreamStrings.privacy-unlocked-title;
|
||||||
|
color: #1b2433; font-size: 20px; font-weight: 600; wrap: word-wrap;
|
||||||
|
}
|
||||||
|
unlock-message := Text {
|
||||||
|
x: 24px; y: unlock-title.y + unlock-title.height + 8px;
|
||||||
|
width: parent.width - 48px; height: max(22px, self.preferred-height);
|
||||||
|
text: StreamStrings.privacy-unlocked-message;
|
||||||
|
color: #667085; font-size: 15px; wrap: word-wrap;
|
||||||
|
}
|
||||||
|
SecondaryButton {
|
||||||
|
x: parent.width - 244px; y: parent.height - 60px;
|
||||||
|
width: 104px; height: 36px;
|
||||||
|
text: StreamStrings.privacy-unlock-confirm;
|
||||||
|
clicked => { root.dismiss-privacy-unlock(); }
|
||||||
|
}
|
||||||
|
PrimaryButton {
|
||||||
|
x: parent.width - 128px; y: parent.height - 60px;
|
||||||
|
width: 104px; height: 36px;
|
||||||
|
text: StreamStrings.privacy-unlock-lock;
|
||||||
|
enabled: root.privacy-can-lock;
|
||||||
|
clicked => { root.lock-privacy(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
remote-update-banner := DialogSurface {
|
remote-update-banner := DialogSurface {
|
||||||
visible: root.remote-update-visible;
|
visible: root.remote-update-visible && !root.privacy-unlock-visible;
|
||||||
x: (parent.width - self.width) / 2;
|
x: (parent.width - self.width) / 2;
|
||||||
y: min(max(64px, parent.height * 0.18), max(16px, parent.height - self.height - 16px));
|
y: min(max(64px, parent.height * 0.18), max(16px, parent.height - self.height - 16px));
|
||||||
width: min(400px, parent.width - 48px);
|
width: min(400px, parent.width - 48px);
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ inline int ApplyMainWindowStrings(
|
|||||||
strings.set_daemon(Text(localization::enable_daemon[language]));
|
strings.set_daemon(Text(localization::enable_daemon[language]));
|
||||||
strings.set_save_remote_previews(Text(localization::save_remote_previews[language]));
|
strings.set_save_remote_previews(Text(localization::save_remote_previews[language]));
|
||||||
strings.set_unknown_platform(Text(localization::unknown_platform[language]));
|
strings.set_unknown_platform(Text(localization::unknown_platform[language]));
|
||||||
|
strings.set_privacy_local_unlock(Text(localization::privacy_local_unlock[language]));
|
||||||
strings.set_privacy_on_connect(Text(localization::privacy_on_connect[language]));
|
strings.set_privacy_on_connect(Text(localization::privacy_on_connect[language]));
|
||||||
strings.set_lock_on_disconnect(Text(localization::lock_on_disconnect[language]));
|
strings.set_lock_on_disconnect(Text(localization::lock_on_disconnect[language]));
|
||||||
strings.set_file_save_path(
|
strings.set_file_save_path(
|
||||||
@@ -206,6 +207,10 @@ inline int ApplyStreamWindowStrings(
|
|||||||
strings.set_remote_update_dismiss(Text(localization::remote_update_dismiss[language]));
|
strings.set_remote_update_dismiss(Text(localization::remote_update_dismiss[language]));
|
||||||
strings.set_select_display(Text(localization::select_display[language]));
|
strings.set_select_display(Text(localization::select_display[language]));
|
||||||
strings.set_send_shortcut(Text(localization::send_shortcut[language]));
|
strings.set_send_shortcut(Text(localization::send_shortcut[language]));
|
||||||
|
strings.set_privacy_unlocked_title(Text(localization::privacy_unlocked_title[language]));
|
||||||
|
strings.set_privacy_unlocked_message(Text(localization::privacy_unlocked_message[language]));
|
||||||
|
strings.set_privacy_unlock_confirm(Text(localization::privacy_unlock_confirm[language]));
|
||||||
|
strings.set_privacy_unlock_lock(Text(localization::privacy_unlock_lock[language]));
|
||||||
strings.set_privacy_enable(Text(localization::privacy_enable[language]));
|
strings.set_privacy_enable(Text(localization::privacy_enable[language]));
|
||||||
strings.set_privacy_disable(Text(localization::privacy_disable[language]));
|
strings.set_privacy_disable(Text(localization::privacy_disable[language]));
|
||||||
strings.set_control_mouse(Text(localization::control_mouse[language]));
|
strings.set_control_mouse(Text(localization::control_mouse[language]));
|
||||||
|
|||||||
@@ -63,10 +63,12 @@ class LinuxPrivacyBackend final : public PrivacyBackend {
|
|||||||
: "Checking Linux privacy support"};
|
: "Checking Linux privacy support"};
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Enable(bool block_input, const PrivacyScreenText&,
|
bool Enable(bool block_input, const PrivacyScreenText& text,
|
||||||
std::string& error) override {
|
std::string& error) override {
|
||||||
failure_.clear();
|
failure_.clear();
|
||||||
emergency_ = false;
|
emergency_ = false;
|
||||||
|
UpdateText(text);
|
||||||
|
if (!failure_.empty()) { error = failure_; return false; }
|
||||||
if (!Send(block_input ? LinuxPrivacyCommand::enable_blocking
|
if (!Send(block_input ? LinuxPrivacyCommand::enable_blocking
|
||||||
: LinuxPrivacyCommand::enable)) {
|
: LinuxPrivacyCommand::enable)) {
|
||||||
error = failure_;
|
error = failure_;
|
||||||
@@ -80,6 +82,11 @@ class LinuxPrivacyBackend final : public PrivacyBackend {
|
|||||||
return true;
|
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 {
|
void Recover() override {
|
||||||
ReadReplies();
|
ReadReplies();
|
||||||
if (recovered_) {
|
if (recovered_) {
|
||||||
|
|||||||
@@ -129,6 +129,8 @@ class NativePrivacy {
|
|||||||
"blocking is unavailable");
|
"blocking is unavailable");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SetLocalUnlockAllowed(bool allowed) { allow_local_unlock_ = allowed; }
|
||||||
|
|
||||||
void Enable(bool block_input) {
|
void Enable(bool block_input) {
|
||||||
++reply_.generation;
|
++reply_.generation;
|
||||||
reply_.failure[0] = '\0';
|
reply_.failure[0] = '\0';
|
||||||
@@ -570,7 +572,7 @@ class NativePrivacy {
|
|||||||
shift |= symbol == XK_Shift_L || symbol == XK_Shift_R;
|
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) {
|
XkbKeycodeToKeysym(display_, keycode, 0, 0) == XK_F12) {
|
||||||
reply_.emergency = true;
|
reply_.emergency = true;
|
||||||
Recover();
|
Recover();
|
||||||
@@ -624,6 +626,7 @@ class NativePrivacy {
|
|||||||
std::vector<Window> hidden_roots_;
|
std::vector<Window> hidden_roots_;
|
||||||
std::map<int, std::array<bool, 256>> keys_;
|
std::map<int, std::array<bool, 256>> keys_;
|
||||||
LinuxPrivacyReply reply_;
|
LinuxPrivacyReply reply_;
|
||||||
|
bool allow_local_unlock_ = true;
|
||||||
Clock::time_point checked_{};
|
Clock::time_point checked_{};
|
||||||
};
|
};
|
||||||
} // namespace
|
} // namespace
|
||||||
@@ -676,6 +679,12 @@ int RunLinuxPrivacyGuard() {
|
|||||||
case LinuxPrivacyCommand::recover:
|
case LinuxPrivacyCommand::recover:
|
||||||
privacy.Recover();
|
privacy.Recover();
|
||||||
break;
|
break;
|
||||||
|
case LinuxPrivacyCommand::allow_local_unlock:
|
||||||
|
privacy.SetLocalUnlockAllowed(true);
|
||||||
|
break;
|
||||||
|
case LinuxPrivacyCommand::forbid_local_unlock:
|
||||||
|
privacy.SetLocalUnlockAllowed(false);
|
||||||
|
break;
|
||||||
case LinuxPrivacyCommand::heartbeat:
|
case LinuxPrivacyCommand::heartbeat:
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ enum class LinuxPrivacyCommand : uint8_t {
|
|||||||
enable_blocking,
|
enable_blocking,
|
||||||
recover,
|
recover,
|
||||||
heartbeat,
|
heartbeat,
|
||||||
|
allow_local_unlock,
|
||||||
|
forbid_local_unlock,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct LinuxPrivacyReply {
|
struct LinuxPrivacyReply {
|
||||||
|
|||||||
@@ -200,6 +200,7 @@ class MacPrivacyBackend final : public PrivacyBackend {
|
|||||||
const auto caps = Query();
|
const auto caps = Query();
|
||||||
if (!caps.overlay) { error = caps.reason; return false; }
|
if (!caps.overlay) { error = caps.reason; return false; }
|
||||||
block_input_ = block_input;
|
block_input_ = block_input;
|
||||||
|
allow_local_unlock_ = text.allow_local_unlock;
|
||||||
emergency_ = false;
|
emergency_ = false;
|
||||||
tap_failed_ = false;
|
tap_failed_ = false;
|
||||||
CGEventMask mask = CGEventMaskBit(kCGEventKeyDown) | CGEventMaskBit(kCGEventKeyUp) |
|
CGEventMask mask = CGEventMaskBit(kCGEventKeyDown) | CGEventMaskBit(kCGEventKeyUp) |
|
||||||
@@ -239,7 +240,8 @@ class MacPrivacyBackend final : public PrivacyBackend {
|
|||||||
m.frame = screen.frame;
|
m.frame = screen.frame;
|
||||||
NSView* content = CreateMacPrivacyCoverView(
|
NSView* content = CreateMacPrivacyCoverView(
|
||||||
NSMakeRect(0, 0, m.frame.size.width, m.frame.size.height),
|
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) {
|
if (!content) {
|
||||||
state->failure = "Could not load the CrossDesk privacy screen logo";
|
state->failure = "Could not load the CrossDesk privacy screen logo";
|
||||||
break;
|
break;
|
||||||
@@ -260,6 +262,17 @@ class MacPrivacyBackend final : public PrivacyBackend {
|
|||||||
});
|
});
|
||||||
return true;
|
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 {
|
void Recover() override {
|
||||||
if (tap_) { CGEventTapEnable(tap_, false); CFMachPortInvalidate(tap_); }
|
if (tap_) { CGEventTapEnable(tap_, false); CFMachPortInvalidate(tap_); }
|
||||||
if (tap_source_) {
|
if (tap_source_) {
|
||||||
@@ -359,7 +372,7 @@ class MacPrivacyBackend final : public PrivacyBackend {
|
|||||||
CGEventGetIntegerValueField(event, kCGEventSourceUnixProcessID) == getpid()) return event;
|
CGEventGetIntegerValueField(event, kCGEventSourceUnixProcessID) == getpid()) return event;
|
||||||
const auto flags = CGEventGetFlags(event);
|
const auto flags = CGEventGetFlags(event);
|
||||||
const auto chord = kCGEventFlagMaskControl | kCGEventFlagMaskAlternate | kCGEventFlagMaskShift;
|
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) {
|
CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode) == 53) {
|
||||||
self->emergency_ = true;
|
self->emergency_ = true;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -372,6 +385,7 @@ class MacPrivacyBackend final : public PrivacyBackend {
|
|||||||
CFMachPortRef tap_ = nullptr;
|
CFMachPortRef tap_ = nullptr;
|
||||||
CFRunLoopSourceRef tap_source_ = nullptr;
|
CFRunLoopSourceRef tap_source_ = nullptr;
|
||||||
bool block_input_ = false;
|
bool block_input_ = false;
|
||||||
|
bool allow_local_unlock_ = true;
|
||||||
bool emergency_ = false;
|
bool emergency_ = false;
|
||||||
bool tap_failed_ = false;
|
bool tap_failed_ = false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,7 +11,8 @@
|
|||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
// Returns nil if the CrossDesk icon resource cannot be loaded.
|
// 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
|
#endif
|
||||||
@@ -52,6 +52,7 @@ CGFloat TextHeight(NSAttributedString* text, CGFloat width) {
|
|||||||
@interface CrossDeskPrivacyCoverView : NSView
|
@interface CrossDeskPrivacyCoverView : NSView
|
||||||
@property(nonatomic, strong) NSImage* logo;
|
@property(nonatomic, strong) NSImage* logo;
|
||||||
@property(nonatomic, copy) NSString* unlockHint;
|
@property(nonatomic, copy) NSString* unlockHint;
|
||||||
|
@property(nonatomic) BOOL allowLocalUnlock;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation CrossDeskPrivacyCoverView
|
@implementation CrossDeskPrivacyCoverView
|
||||||
@@ -77,7 +78,7 @@ CGFloat TextHeight(NSAttributedString* text, CGFloat width) {
|
|||||||
const CGFloat logo_size = std::ceil(brand.size.width);
|
const CGFloat logo_size = std::ceil(brand.size.width);
|
||||||
const CGFloat brand_height = TextHeight(brand, logo_size);
|
const CGFloat brand_height = TextHeight(brand, logo_size);
|
||||||
const CGFloat hint_height = TextHeight(hint, text_width);
|
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 bottom = height - margin;
|
||||||
const CGFloat logo_top = bottom - brand_height - 6 * scale - logo_size;
|
const CGFloat logo_top = bottom - brand_height - 6 * scale - logo_size;
|
||||||
[self.logo drawInRect:NSMakeRect(margin, logo_top, logo_size, 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)
|
[brand drawWithRect:NSMakeRect(margin, bottom - brand_height, logo_size, brand_height)
|
||||||
options:kTextOptions];
|
options:kTextOptions];
|
||||||
const CGFloat text_left = margin + logo_size + 24 * scale;
|
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];
|
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];
|
text_width, shortcut_height) options:kTextOptions];
|
||||||
}
|
}
|
||||||
@end
|
@end
|
||||||
|
|
||||||
namespace crossdesk {
|
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();
|
NSImage* logo = LoadPrivacyLogo();
|
||||||
if (!logo) return nil;
|
if (!logo) return nil;
|
||||||
auto* view = [[CrossDeskPrivacyCoverView alloc] initWithFrame:frame];
|
auto* view = [[CrossDeskPrivacyCoverView alloc] initWithFrame:frame];
|
||||||
view.logo = logo;
|
view.logo = logo;
|
||||||
|
view.allowLocalUnlock = allow_local_unlock;
|
||||||
view.unlockHint = unlock_hint ?: @"To turn off the privacy screen, press";
|
view.unlockHint = unlock_hint ?: @"To turn off the privacy screen, press";
|
||||||
view.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
view.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
||||||
return view;
|
return view;
|
||||||
|
|||||||
@@ -190,6 +190,10 @@ class Windows {
|
|||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
if (message == WM_CLOSE) return 0;
|
if (message == WM_CLOSE) return 0;
|
||||||
|
if (message == kPrivacyTextChanged) {
|
||||||
|
InvalidateRect(hwnd, nullptr, TRUE);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
if ((message == WM_DISPLAYCHANGE || message == WM_DPICHANGED) && self) {
|
if ((message == WM_DISPLAYCHANGE || message == WM_DPICHANGED) && self) {
|
||||||
self->changed = true;
|
self->changed = true;
|
||||||
InvalidateRect(hwnd, nullptr, TRUE);
|
InvalidateRect(hwnd, nullptr, TRUE);
|
||||||
@@ -200,8 +204,12 @@ class Windows {
|
|||||||
HDC dc = BeginPaint(hwnd, &paint);
|
HDC dc = BeginPaint(hwnd, &paint);
|
||||||
RECT rect{};
|
RECT rect{};
|
||||||
GetClientRect(hwnd, &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,
|
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->paint_failed_ = true;
|
||||||
self->Fail(L"Paint privacy logo and unlock instructions");
|
self->Fail(L"Paint privacy logo and unlock instructions");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ bool Select(HDC dc, HGDIOBJ object) {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
bool DrawPrivacyCover(HDC dc, const RECT& rect, UINT dpi, HINSTANCE module,
|
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))))
|
if (!FillRect(dc, &rect, static_cast<HBRUSH>(GetStockObject(BLACK_BRUSH))))
|
||||||
return false;
|
return false;
|
||||||
const int width = rect.right - rect.left;
|
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;
|
constexpr UINT flags = DT_LEFT | DT_WORDBREAK | DT_NOPREFIX;
|
||||||
for (size_t i = 0; i < lines.size(); ++i) {
|
for (size_t i = 0; i < lines.size(); ++i) {
|
||||||
|
if (i == 1 && !allow_local_unlock) continue;
|
||||||
auto& line = lines[i];
|
auto& line = lines[i];
|
||||||
resources.fonts[i] =
|
resources.fonts[i] =
|
||||||
CreateFontW(-px(line.size), 0, 0, 0, line.weight, FALSE, FALSE, FALSE,
|
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 =
|
const int content_height =
|
||||||
static_cast<int>(lines[0].bounds.bottom + lines[1].bounds.bottom) +
|
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.
|
// Match the icon width to the name; each side keeps its own height.
|
||||||
const int brand_gap = px(6);
|
const int brand_gap = px(6);
|
||||||
const int brand_width = static_cast<int>(lines[kBrandLine].bounds.right);
|
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))
|
DT_CENTER | DT_SINGLELINE | DT_NOPREFIX))
|
||||||
return false;
|
return false;
|
||||||
for (size_t i = 0; i < kInstructionLines; ++i) {
|
for (size_t i = 0; i < kInstructionLines; ++i) {
|
||||||
|
if (i == 1 && !allow_local_unlock) continue;
|
||||||
const auto& line = lines[i];
|
const auto& line = lines[i];
|
||||||
if (!Select(dc, resources.fonts[i])) return false;
|
if (!Select(dc, resources.fonts[i])) return false;
|
||||||
RECT bounds{left, top, left + text_width, top + line.bounds.bottom};
|
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.
|
// 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,
|
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
|
} // namespace crossdesk
|
||||||
|
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
|
|||||||
}
|
}
|
||||||
if (!Initialize(error)) return false;
|
if (!Initialize(error)) return false;
|
||||||
emergency_ = false;
|
emergency_ = false;
|
||||||
|
allow_local_unlock_ = text.allow_local_unlock;
|
||||||
health_error_.clear();
|
health_error_.clear();
|
||||||
if (!RegisterHotKey(sink_, kHotkeyId,
|
if (!RegisterHotKey(sink_, kHotkeyId,
|
||||||
MOD_CONTROL | MOD_ALT | MOD_SHIFT | MOD_NOREPEAT,
|
MOD_CONTROL | MOD_ALT | MOD_SHIFT | MOD_NOREPEAT,
|
||||||
@@ -149,6 +150,10 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
|
|||||||
return true;
|
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 {
|
void Recover() override {
|
||||||
for (auto& hook : window_events_) {
|
for (auto& hook : window_events_) {
|
||||||
if (UnhookWinEvent(hook))
|
if (UnhookWinEvent(hook))
|
||||||
@@ -475,7 +480,8 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
|
|||||||
auto self = reinterpret_cast<WindowsPrivacyBackend*>(
|
auto self = reinterpret_cast<WindowsPrivacyBackend*>(
|
||||||
GetWindowLongPtrW(hwnd, GWLP_USERDATA));
|
GetWindowLongPtrW(hwnd, GWLP_USERDATA));
|
||||||
if (msg == WM_HOTKEY || msg == kEmergency) {
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
if (msg == WM_MOUSEACTIVATE) return MA_NOACTIVATE;
|
if (msg == WM_MOUSEACTIVATE) return MA_NOACTIVATE;
|
||||||
@@ -522,6 +528,7 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
|
|||||||
std::unique_ptr<ScopedWindowsPhysicalCoordinates> dpi_scope_;
|
std::unique_ptr<ScopedWindowsPhysicalCoordinates> dpi_scope_;
|
||||||
bool registered_ = false;
|
bool registered_ = false;
|
||||||
bool hotkey_ = false;
|
bool hotkey_ = false;
|
||||||
|
bool allow_local_unlock_ = true;
|
||||||
bool emergency_ = false;
|
bool emergency_ = false;
|
||||||
bool layout_changed_ = false;
|
bool layout_changed_ = false;
|
||||||
bool topology_pending_ = false;
|
bool topology_pending_ = false;
|
||||||
|
|||||||
@@ -153,6 +153,11 @@ bool PrivacyBandGuard::Start(const PrivacyScreenText& text,
|
|||||||
text.unlock_hint.c_str(), -1, shared_->unlock_hint,
|
text.unlock_hint.c_str(), -1, shared_->unlock_hint,
|
||||||
static_cast<int>(std::size(shared_->unlock_hint))))
|
static_cast<int>(std::size(shared_->unlock_hint))))
|
||||||
return Failed("Encode privacy screen text", error);
|
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();
|
shared_->parent_pid = GetCurrentProcessId();
|
||||||
HANDLE handles[] = {parent.value, stop_, ready_, mapping_};
|
HANDLE handles[] = {parent.value, stop_, ready_, mapping_};
|
||||||
std::wstring bootstrap;
|
std::wstring bootstrap;
|
||||||
@@ -299,6 +304,16 @@ bool PrivacyBandGuard::Windows(std::vector<PrivacyBandWindow>& windows,
|
|||||||
return retry();
|
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 PrivacyBandGuard::Stop(std::string& error) {
|
||||||
bool ok = true;
|
bool ok = true;
|
||||||
if (process_) {
|
if (process_) {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class PrivacyBandGuard {
|
|||||||
~PrivacyBandGuard();
|
~PrivacyBandGuard();
|
||||||
static bool Available(std::string& error);
|
static bool Available(std::string& error);
|
||||||
bool Start(const PrivacyScreenText& text, std::string& error);
|
bool Start(const PrivacyScreenText& text, std::string& error);
|
||||||
|
void SetLocalUnlockAllowed(bool allowed);
|
||||||
bool Stop(std::string& error);
|
bool Stop(std::string& error);
|
||||||
bool Healthy(std::string& error);
|
bool Healthy(std::string& error);
|
||||||
bool Windows(std::vector<PrivacyBandWindow>& windows, std::string& error);
|
bool Windows(std::vector<PrivacyBandWindow>& windows, std::string& error);
|
||||||
|
|||||||
@@ -14,7 +14,8 @@
|
|||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
constexpr uint32_t kPrivacyBandMagic = 0x43534231;
|
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 DWORD kPrivacyWindowBand = 18; // ZBID_ABOVELOCK_UX, current desktop.
|
||||||
constexpr size_t kPrivacyMaxMonitors = 64;
|
constexpr size_t kPrivacyMaxMonitors = 64;
|
||||||
constexpr wchar_t kPrivacyBandBootstrap[] = L"CROSSDESK_PRIVACY_BAND_BOOTSTRAP";
|
constexpr wchar_t kPrivacyBandBootstrap[] = L"CROSSDESK_PRIVACY_BAND_BOOTSTRAP";
|
||||||
@@ -40,6 +41,8 @@ struct PrivacyBandShared {
|
|||||||
alignas(8) volatile LONG64 heartbeat = 0;
|
alignas(8) volatile LONG64 heartbeat = 0;
|
||||||
wchar_t operation[128]{};
|
wchar_t operation[128]{};
|
||||||
wchar_t unlock_hint[128]{};
|
wchar_t unlock_hint[128]{};
|
||||||
|
wchar_t locked_hint[128]{};
|
||||||
|
volatile LONG allow_local_unlock = 1;
|
||||||
uint32_t count = 0;
|
uint32_t count = 0;
|
||||||
PrivacyBandWindow windows[kPrivacyMaxMonitors]{};
|
PrivacyBandWindow windows[kPrivacyMaxMonitors]{};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ namespace crossdesk {
|
|||||||
|
|
||||||
struct PrivacyScreenText {
|
struct PrivacyScreenText {
|
||||||
std::string unlock_hint = "To turn off the privacy screen, press";
|
std::string unlock_hint = "To turn off the privacy screen, press";
|
||||||
|
std::string locked_hint = "Privacy screen is on. Local unlocking is disabled.";
|
||||||
|
bool allow_local_unlock = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PrivacyCapabilities {
|
struct PrivacyCapabilities {
|
||||||
@@ -38,6 +40,8 @@ class PrivacyBackend {
|
|||||||
virtual PrivacyCapabilities Query() = 0;
|
virtual PrivacyCapabilities Query() = 0;
|
||||||
virtual bool Enable(bool block_input, const PrivacyScreenText& text,
|
virtual bool Enable(bool block_input, const PrivacyScreenText& text,
|
||||||
std::string& error) = 0;
|
std::string& error) = 0;
|
||||||
|
// Update a live cover and its local shortcut policy without exposing the desktop.
|
||||||
|
virtual void UpdateText(const PrivacyScreenText& text) {}
|
||||||
virtual void Recover() = 0;
|
virtual void Recover() = 0;
|
||||||
virtual bool IsRecovered() const = 0;
|
virtual bool IsRecovered() const = 0;
|
||||||
virtual bool RecoveryPending() const { return false; }
|
virtual bool RecoveryPending() const { return false; }
|
||||||
|
|||||||
@@ -43,7 +43,11 @@ PrivacyStatus PrivacyController::Snapshot() const {
|
|||||||
}
|
}
|
||||||
void PrivacyController::SetText(const PrivacyScreenText& text) {
|
void PrivacyController::SetText(const PrivacyScreenText& text) {
|
||||||
std::lock_guard lock(mutex_);
|
std::lock_guard lock(mutex_);
|
||||||
|
if (text_.unlock_hint == text.unlock_hint && text_.locked_hint == text.locked_hint &&
|
||||||
|
text_.allow_local_unlock == text.allow_local_unlock) return;
|
||||||
text_ = text;
|
text_ = text;
|
||||||
|
text_pending_ = true;
|
||||||
|
WakeLocked();
|
||||||
}
|
}
|
||||||
void PrivacyController::SetState(PrivacyState state, const std::string& reason) {
|
void PrivacyController::SetState(PrivacyState state, const std::string& reason) {
|
||||||
status_.state = state;
|
status_.state = state;
|
||||||
@@ -54,9 +58,28 @@ void PrivacyController::SetState(PrivacyState state, const std::string& reason)
|
|||||||
LOG_INFO("Privacy: state={}, overlay={}, input_blocked={}, reason={}",
|
LOG_INFO("Privacy: state={}, overlay={}, input_blocked={}, reason={}",
|
||||||
static_cast<int>(state), status_.overlay_active, status_.input_blocked, reason);
|
static_cast<int>(state), status_.overlay_active, status_.input_blocked, reason);
|
||||||
}
|
}
|
||||||
void PrivacyController::Enable(bool block_input) {
|
void PrivacyController::RejectCommand(const std::string& reason) {
|
||||||
std::lock_guard lock(mutex_);
|
std::lock_guard lock(mutex_);
|
||||||
if (quit_ || engaged_.load() || disable_pending_) return;
|
if (disable_pending_) {
|
||||||
|
failure_ = reason;
|
||||||
|
SetState(PrivacyState::failed, reason);
|
||||||
|
} else if (engaged_.load()) {
|
||||||
|
// A rejected command must not remove an already working cover.
|
||||||
|
SetState(status_.state, reason);
|
||||||
|
} else {
|
||||||
|
SetState(PrivacyState::failed, reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void PrivacyController::Enable(bool block_input, bool wait_for_recovery) {
|
||||||
|
std::lock_guard lock(mutex_);
|
||||||
|
if (quit_) return;
|
||||||
|
if (engaged_.load() && !disable_pending_) {
|
||||||
|
// Another controller may have restored privacy before this lock arrived.
|
||||||
|
// Acknowledge it so this controller does not wait for a nonexistent transition.
|
||||||
|
if (wait_for_recovery) SetState(status_.state, status_.reason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (disable_pending_ && !wait_for_recovery) return;
|
||||||
if (!status_.supported || (block_input && !status_.input_block_supported)) {
|
if (!status_.supported || (block_input && !status_.input_block_supported)) {
|
||||||
SetState(PrivacyState::unsupported, "Privacy screen is unavailable");
|
SetState(PrivacyState::unsupported, "Privacy screen is unavailable");
|
||||||
return;
|
return;
|
||||||
@@ -227,9 +250,17 @@ void PrivacyController::Run(Factory factory) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (text_pending_ && backend_) {
|
||||||
|
const auto text = text_;
|
||||||
|
text_pending_ = false;
|
||||||
|
lock.unlock();
|
||||||
|
backend_->UpdateText(text);
|
||||||
|
lock.lock();
|
||||||
|
}
|
||||||
const auto health = backend_ ? backend_->Poll() : PrivacyHealth{};
|
const auto health = backend_ ? backend_->Poll() : PrivacyHealth{};
|
||||||
if (engaged_.load() && !disable_pending_) {
|
if (engaged_.load() && !disable_pending_) {
|
||||||
if (health.emergency_exit) {
|
if (health.emergency_exit && text_.allow_local_unlock) {
|
||||||
|
++status_.local_unlock_revision;
|
||||||
automatic_enable_ = false;
|
automatic_enable_ = false;
|
||||||
resume_after_unlock_ = false;
|
resume_after_unlock_ = false;
|
||||||
failure_.clear();
|
failure_.clear();
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ class PrivacyController {
|
|||||||
PrivacyController& operator=(const PrivacyController&) = delete;
|
PrivacyController& operator=(const PrivacyController&) = delete;
|
||||||
PrivacyStatus Snapshot() const;
|
PrivacyStatus Snapshot() const;
|
||||||
void SetText(const PrivacyScreenText& text);
|
void SetText(const PrivacyScreenText& text);
|
||||||
void Enable(bool block_input);
|
void Enable(bool block_input, bool wait_for_recovery = false);
|
||||||
|
void RejectCommand(const std::string& reason);
|
||||||
void EnableOnConnection();
|
void EnableOnConnection();
|
||||||
// Preserve automatic session intent while Windows shows another desktop.
|
// Preserve automatic session intent while Windows shows another desktop.
|
||||||
void SuspendForDesktop();
|
void SuspendForDesktop();
|
||||||
@@ -50,6 +51,7 @@ class PrivacyController {
|
|||||||
std::unique_ptr<PrivacyBackend> backend_;
|
std::unique_ptr<PrivacyBackend> backend_;
|
||||||
PrivacyStatus status_{};
|
PrivacyStatus status_{};
|
||||||
PrivacyScreenText text_;
|
PrivacyScreenText text_;
|
||||||
|
bool text_pending_ = false;
|
||||||
std::atomic<bool> engaged_{false};
|
std::atomic<bool> engaged_{false};
|
||||||
bool quit_ = false;
|
bool quit_ = false;
|
||||||
bool enable_pending_ = false;
|
bool enable_pending_ = false;
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ export class MobileState {
|
|||||||
privacyEnabled: boolean = false;
|
privacyEnabled: boolean = false;
|
||||||
privacyActive: boolean = false;
|
privacyActive: boolean = false;
|
||||||
privacyCanToggle: boolean = false;
|
privacyCanToggle: boolean = false;
|
||||||
|
privacyCanLock: boolean = false;
|
||||||
privacyVisible: boolean = true;
|
privacyVisible: boolean = true;
|
||||||
networkTime: number = 0;
|
networkTime: number = 0;
|
||||||
unread: number = 0;
|
unread: number = 0;
|
||||||
@@ -266,9 +267,9 @@ export class MobileController {
|
|||||||
if (this.foreground && this.state.consent) this.checkLocal();
|
if (this.foreground && this.state.consent) this.checkLocal();
|
||||||
}
|
}
|
||||||
release(): void { this.releaseInput(); this.send({ op: 'release' }); }
|
release(): void { this.releaseInput(); this.send({ op: 'release' }); }
|
||||||
pointer(x: number, y: number, flag: number, scroll: number): void { this.send({ op: 'pointer', x: x, y: y, flag: flag, scroll: scroll }); }
|
pointer(x: number, y: number, flag: number, scroll: number): void { if (this.state.modal === 'privacyUnlock') return; this.send({ op: 'pointer', x: x, y: y, flag: flag, scroll: scroll }); }
|
||||||
physicalKey(code: number, down: boolean): void { this.send({ op: 'key', code: code, down: down }); }
|
physicalKey(code: number, down: boolean): void { if (this.state.modal === 'privacyUnlock') return; this.send({ op: 'key', code: code, down: down }); }
|
||||||
key(code: number, down: boolean): void { this.send({ op: 'virtualKey', code: code, down: down }); }
|
key(code: number, down: boolean): void { if (this.state.modal === 'privacyUnlock') return; this.send({ op: 'virtualKey', code: code, down: down }); }
|
||||||
chord(code: number, modifiers: number[] = []): void {
|
chord(code: number, modifiers: number[] = []): void {
|
||||||
modifiers.forEach((key: number) => this.key(key, true)); this.key(code, true); this.key(code, false);
|
modifiers.forEach((key: number) => this.key(key, true)); this.key(code, true); this.key(code, false);
|
||||||
modifiers.slice().reverse().forEach((key: number) => this.key(key, false));
|
modifiers.slice().reverse().forEach((key: number) => this.key(key, false));
|
||||||
@@ -352,11 +353,17 @@ export class MobileController {
|
|||||||
this.state.documentTitle = title; this.state.documentBack = back; this.state.page = 'document'; this.state.modal = '';
|
this.state.documentTitle = title; this.state.documentBack = back; this.state.page = 'document'; this.state.modal = '';
|
||||||
} catch { this.toast('无法读取文档'); }
|
} catch { this.toast('无法读取文档'); }
|
||||||
}
|
}
|
||||||
togglePrivacy(): void {
|
dismissPrivacyUnlock(): void {
|
||||||
|
this.privacyScreen.unlockNotice = false;
|
||||||
|
if (this.state.modal === 'privacyUnlock') this.state.modal = '';
|
||||||
|
}
|
||||||
|
lockPrivacy(): void { this.sendPrivacy(true); }
|
||||||
|
togglePrivacy(): void { this.sendPrivacy(false); }
|
||||||
|
private sendPrivacy(lock: boolean): void {
|
||||||
if (!this.state.ready || !this.state.consent) return;
|
if (!this.state.ready || !this.state.consent) return;
|
||||||
const enable = !this.privacyScreen.active(), request = this.privacyScreen.begin(this.now());
|
const enable = !this.privacyScreen.active(), request = this.privacyScreen.begin(this.now(), lock);
|
||||||
if (request < 0) return;
|
if (request < 0) return;
|
||||||
this.send({ op: 'privacy', flag: enable ? 1 : 2, request: request,
|
this.send({ op: 'privacy', flag: lock ? 3 : enable ? 1 : 2, request: request,
|
||||||
blockLocalInput: true });
|
blockLocalInput: true });
|
||||||
this.syncPrivacy();
|
this.syncPrivacy();
|
||||||
}
|
}
|
||||||
@@ -365,6 +372,13 @@ export class MobileController {
|
|||||||
this.state.privacyStatus = this.privacyScreen.status(now); this.state.privacyFeedback = this.privacyScreen.feedback(now);
|
this.state.privacyStatus = this.privacyScreen.status(now); this.state.privacyFeedback = this.privacyScreen.feedback(now);
|
||||||
this.state.privacyButtonTitle = this.privacyScreen.buttonTitle(now);
|
this.state.privacyButtonTitle = this.privacyScreen.buttonTitle(now);
|
||||||
this.state.privacyVisible = this.privacyScreen.desktopAvailable;
|
this.state.privacyVisible = this.privacyScreen.desktopAvailable;
|
||||||
|
this.state.privacyCanLock = this.privacyScreen.canLock(now);
|
||||||
|
if (this.foreground && this.state.ready && this.privacyScreen.unlockNotice && !this.state.modal) {
|
||||||
|
this.release();
|
||||||
|
this.state.modal = 'privacyUnlock';
|
||||||
|
} else if (!this.privacyScreen.unlockNotice && this.state.modal === 'privacyUnlock') {
|
||||||
|
this.state.modal = '';
|
||||||
|
}
|
||||||
this.state.privacyEnabled = this.privacyScreen.enabled(now);
|
this.state.privacyEnabled = this.privacyScreen.enabled(now);
|
||||||
this.state.privacyActive = this.privacyScreen.active(); this.state.privacyCanToggle = this.privacyScreen.canToggle(now);
|
this.state.privacyActive = this.privacyScreen.active(); this.state.privacyCanToggle = this.privacyScreen.canToggle(now);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
export interface PrivacyStatus {
|
export interface PrivacyStatus {
|
||||||
state: number; supported: boolean; input_block_supported: boolean;
|
state: number; supported: boolean; input_block_supported: boolean;
|
||||||
overlay_active: boolean; input_blocked: boolean; remote_paused: boolean;
|
overlay_active: boolean; input_blocked: boolean; remote_paused: boolean;
|
||||||
revision: number; reason: string;
|
revision: number; reason: string; local_unlock_revision?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RemotePrivacyScreen {
|
export class RemotePrivacyScreen {
|
||||||
desktopAvailable: boolean = true;
|
desktopAvailable: boolean = true;
|
||||||
snapshot?: PrivacyStatus;
|
snapshot?: PrivacyStatus;
|
||||||
pending: boolean = false;
|
pending: boolean = false;
|
||||||
|
unlockNotice: boolean = false;
|
||||||
requestId: number = 0;
|
requestId: number = 0;
|
||||||
private requestRevision: number = 0;
|
private requestRevision: number = 0;
|
||||||
private receivedAt: number = 0;
|
private receivedAt: number = 0;
|
||||||
@@ -26,6 +27,10 @@ export class RemotePrivacyScreen {
|
|||||||
typeof s.remote_paused !== 'boolean' || typeof s.reason !== 'string' || s.reason.includes('\0')) return;
|
typeof s.remote_paused !== 'boolean' || typeof s.reason !== 'string' || s.reason.includes('\0')) return;
|
||||||
if (s.state === 3 && (!s.supported || !s.overlay_active || s.remote_paused)) return;
|
if (s.state === 3 && (!s.supported || !s.overlay_active || s.remote_paused)) return;
|
||||||
if (this.snapshot && ((s.revision - this.snapshot.revision) | 0) < 0) return;
|
if (this.snapshot && ((s.revision - this.snapshot.revision) | 0) < 0) return;
|
||||||
|
const unlock = s.local_unlock_revision ?? 0;
|
||||||
|
if (!Number.isInteger(unlock) || unlock < 0 || unlock > 0xffffffff) return;
|
||||||
|
if (this.snapshot && ((unlock - (this.snapshot.local_unlock_revision ?? 0)) | 0) > 0) this.unlockNotice = true;
|
||||||
|
if (s.state === 3) this.unlockNotice = false;
|
||||||
this.snapshot = s; this.receivedAt = now; this.failure = '';
|
this.snapshot = s; this.receivedAt = now; this.failure = '';
|
||||||
if (this.pending && ((s.revision - this.requestRevision) | 0) > 0 && s.state !== 2 && s.state !== 4) this.pending = false;
|
if (this.pending && ((s.revision - this.requestRevision) | 0) > 0 && s.state !== 2 && s.state !== 4) this.pending = false;
|
||||||
}
|
}
|
||||||
@@ -57,9 +62,12 @@ export class RemotePrivacyScreen {
|
|||||||
const s = this.snapshot;
|
const s = this.snapshot;
|
||||||
return this.desktopAvailable && 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 {
|
canLock(now: number): boolean {
|
||||||
|
return this.desktopAvailable && !this.pending && this.fresh(now) && this.snapshot?.supported === true;
|
||||||
|
}
|
||||||
|
begin(now: number, lock: boolean = false): number {
|
||||||
this.tick(now);
|
this.tick(now);
|
||||||
if (!this.canToggle(now) || !this.snapshot) return -1;
|
if (!(lock ? this.canLock(now) : this.canToggle(now)) || !this.snapshot) return -1;
|
||||||
this.pending = true; this.requestedAt = now; this.requestRevision = this.snapshot.revision; this.failure = '';
|
this.pending = true; this.requestedAt = now; this.requestRevision = this.snapshot.revision; this.failure = '';
|
||||||
this.requestId = (this.requestId + 1) >>> 0;
|
this.requestId = (this.requestId + 1) >>> 0;
|
||||||
return this.requestId;
|
return this.requestId;
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ struct Mobile {
|
|||||||
return saved;
|
return saved;
|
||||||
}
|
}
|
||||||
private dismissModal(): void {
|
private dismissModal(): void {
|
||||||
|
if (this.state.modal === 'privacyUnlock') this.controller?.dismissPrivacyUnlock();
|
||||||
if (this.state.modal === 'password') { this.state.password = ''; this.showPassword = false; }
|
if (this.state.modal === 'password') { this.state.password = ''; this.showPassword = false; }
|
||||||
this.previewToggle = this.state.previews; this.state.modal = '';
|
this.previewToggle = this.state.previews; this.state.modal = '';
|
||||||
}
|
}
|
||||||
@@ -537,6 +538,21 @@ struct Mobile {
|
|||||||
}.padding({ left: 26, right: 26, top: 24, bottom: 20 }).width('100%')
|
}.padding({ left: 26, right: 26, top: 24, bottom: 20 }).width('100%')
|
||||||
this.Rule()
|
this.Rule()
|
||||||
this.Action('取消连接', () => this.controller?.end(''), true)
|
this.Action('取消连接', () => this.controller?.end(''), true)
|
||||||
|
} else if (this.state.modal === 'privacyUnlock') {
|
||||||
|
Column({ space: 12 }) {
|
||||||
|
Text('隐私屏已被解锁').fontSize(20).fontWeight(600).fontColor(MobileTheme.ink)
|
||||||
|
Text('被控端已通过组合键解除隐私屏。点击“锁定”将重新开启隐私屏,并禁止本机使用组合键解锁。')
|
||||||
|
.fontSize(14).lineHeight(21).fontColor(MobileTheme.secondary).width('100%')
|
||||||
|
if (this.state.privacyFeedback) { Text(this.state.privacyFeedback).fontSize(12).fontColor(MobileTheme.secondary) }
|
||||||
|
}.padding(24).width('100%')
|
||||||
|
this.Rule()
|
||||||
|
Row() {
|
||||||
|
Button('确认', { type: ButtonType.Normal }).fontSize(16).fontColor(MobileTheme.blue).height(50).layoutWeight(1)
|
||||||
|
.backgroundColor(Color.White).onClick(() => this.dismissModal())
|
||||||
|
Divider().vertical(true).height(50).strokeWidth(0.5).color(MobileTheme.line)
|
||||||
|
Button('锁定', { type: ButtonType.Normal }).fontSize(16).fontWeight(600).fontColor(MobileTheme.blue).height(50).layoutWeight(1)
|
||||||
|
.backgroundColor(Color.White).enabled(this.state.privacyCanLock).onClick(() => this.controller?.lockPrivacy())
|
||||||
|
}.width('100%')
|
||||||
} else if (this.state.modal === 'clipboard') {
|
} else if (this.state.modal === 'clipboard') {
|
||||||
Column({ space: 16 }) {
|
Column({ space: 16 }) {
|
||||||
this.SheetHeader('剪贴板')
|
this.SheetHeader('剪贴板')
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ typedef struct {
|
|||||||
- (void)sendFileAtURL:(NSURL *)fileURL;
|
- (void)sendFileAtURL:(NSURL *)fileURL;
|
||||||
- (void)sendSecureAttentionSequence;
|
- (void)sendSecureAttentionSequence;
|
||||||
- (void)sendPrivacyCommand:(BOOL)enable
|
- (void)sendPrivacyCommand:(BOOL)enable
|
||||||
|
lock:(BOOL)lock
|
||||||
blockLocalInput:(BOOL)blockLocalInput
|
blockLocalInput:(BOOL)blockLocalInput
|
||||||
requestID:(uint32_t)requestID;
|
requestID:(uint32_t)requestID;
|
||||||
|
|
||||||
|
|||||||
@@ -1044,6 +1044,7 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)sendPrivacyCommand:(BOOL)enable
|
- (void)sendPrivacyCommand:(BOOL)enable
|
||||||
|
lock:(BOOL)lock
|
||||||
blockLocalInput:(BOOL)blockLocalInput
|
blockLocalInput:(BOOL)blockLocalInput
|
||||||
requestID:(uint32_t)requestID {
|
requestID:(uint32_t)requestID {
|
||||||
const uint64_t generation = _activeControllerGeneration.load();
|
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;
|
if (![self isControllerGenerationActive:generation]) return;
|
||||||
RemoteAction action{};
|
RemoteAction action{};
|
||||||
action.type = ControlType::privacy_command;
|
action.type = ControlType::privacy_command;
|
||||||
action.pc = {enable ? crossdesk::PrivacyCommandFlag::enable
|
action.pc = {lock ? crossdesk::PrivacyCommandFlag::lock
|
||||||
|
: enable ? crossdesk::PrivacyCommandFlag::enable
|
||||||
: crossdesk::PrivacyCommandFlag::disable,
|
: crossdesk::PrivacyCommandFlag::disable,
|
||||||
static_cast<bool>(blockLocalInput)};
|
static_cast<bool>(blockLocalInput)};
|
||||||
const auto message = action.to_json();
|
const auto message = action.to_json();
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ struct RemotePrivacyStatus: Decodable {
|
|||||||
let remotePaused: Bool
|
let remotePaused: Bool
|
||||||
let revision: UInt32
|
let revision: UInt32
|
||||||
let reason: String
|
let reason: String
|
||||||
|
let localUnlockRevision: UInt32?
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RemotePrivacyScreen {
|
struct RemotePrivacyScreen {
|
||||||
private(set) var desktopAvailable = true
|
private(set) var desktopAvailable = true
|
||||||
private(set) var snapshot: RemotePrivacyStatus?
|
private(set) var snapshot: RemotePrivacyStatus?
|
||||||
|
var unlockNotice = false
|
||||||
private(set) var pending = false
|
private(set) var pending = false
|
||||||
private(set) var requestID: UInt32 = 0
|
private(set) var requestID: UInt32 = 0
|
||||||
private var requestRevision: UInt32 = 0
|
private var requestRevision: UInt32 = 0
|
||||||
@@ -64,14 +66,22 @@ struct RemotePrivacyScreen {
|
|||||||
!s.reason.contains("\0"),
|
!s.reason.contains("\0"),
|
||||||
s.state != 3 || (s.supported && s.overlayActive && !s.remotePaused) else { return }
|
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.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 = ""
|
snapshot = s; receivedAt = now; failure = ""
|
||||||
if pending && Int32(bitPattern: s.revision &- requestRevision) > 0 && ![2, 4].contains(s.state) {
|
if pending && Int32(bitPattern: s.revision &- requestRevision) > 0 && ![2, 4].contains(s.state) {
|
||||||
pending = false
|
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)
|
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 = ""
|
pending = true; requestedAt = now; requestRevision = s.revision; failure = ""
|
||||||
requestID &+= 1
|
requestID &+= 1
|
||||||
return requestID
|
return requestID
|
||||||
|
|||||||
@@ -1106,12 +1106,14 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
|||||||
privacyTimer = timer
|
privacyTimer = timer
|
||||||
}
|
}
|
||||||
|
|
||||||
func togglePrivacyScreen() {
|
func lockPrivacyScreen() { sendPrivacyScreen(lock: true) }
|
||||||
|
func togglePrivacyScreen() { sendPrivacyScreen(lock: false) }
|
||||||
|
private func sendPrivacyScreen(lock: Bool) {
|
||||||
guard hasNetworkConsent, isConnected else { return }
|
guard hasNetworkConsent, isConnected else { return }
|
||||||
privacyClock = ProcessInfo.processInfo.systemUptime
|
privacyClock = ProcessInfo.processInfo.systemUptime
|
||||||
let enable = !privacyScreen.active
|
let enable = !privacyScreen.active
|
||||||
guard let request = privacyScreen.begin(at: privacyClock) else { return }
|
guard let request = privacyScreen.begin(at: privacyClock, lock: lock) else { return }
|
||||||
bridge.sendPrivacyCommand(enable, blockLocalInput: true, requestID: request)
|
bridge.sendPrivacyCommand(enable, lock: lock, blockLocalInput: true, requestID: request)
|
||||||
}
|
}
|
||||||
|
|
||||||
func rtcBridge(_ bridge: CrossDeskRTCBridge, didReceivePrivacyStatus data: Data) {
|
func rtcBridge(_ bridge: CrossDeskRTCBridge, didReceivePrivacyStatus data: Data) {
|
||||||
|
|||||||
@@ -181,19 +181,28 @@ struct RemoteSessionView: View {
|
|||||||
floatingStatusControls
|
floatingStatusControls
|
||||||
.zIndex(30)
|
.zIndex(30)
|
||||||
|
|
||||||
|
if session.isConnected && session.privacyScreen.unlockNotice {
|
||||||
|
privacyUnlockOverlay.zIndex(60)
|
||||||
|
}
|
||||||
if disconnectConfirmationVisible {
|
if disconnectConfirmationVisible {
|
||||||
disconnectConfirmationOverlay
|
disconnectConfirmationOverlay
|
||||||
.zIndex(50)
|
.zIndex(50)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alert("升级提示", isPresented: Binding(
|
.alert("升级提示", isPresented: Binding(
|
||||||
get: { session.remoteUpdateMessage != nil },
|
get: { !session.privacyScreen.unlockNotice && session.remoteUpdateMessage != nil },
|
||||||
set: { if !$0 { session.dismissRemoteUpdate() } }
|
set: { if !$0 && !session.privacyScreen.unlockNotice { session.dismissRemoteUpdate() } }
|
||||||
)) {
|
)) {
|
||||||
Button("知道了", role: .cancel) { session.dismissRemoteUpdate() }
|
Button("知道了", role: .cancel) { session.dismissRemoteUpdate() }
|
||||||
} message: {
|
} message: {
|
||||||
Text(session.remoteUpdateMessage ?? "")
|
Text(session.remoteUpdateMessage ?? "")
|
||||||
}
|
}
|
||||||
|
.onChange(of: session.privacyScreen.unlockNotice) { visible in
|
||||||
|
if visible {
|
||||||
|
mouseInput.releaseAll()
|
||||||
|
keyboardInputVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
mouseInput.send = { x, y, flag, wheel in
|
mouseInput.send = { x, y, flag, wheel in
|
||||||
cursorPosition = CGPoint(x: CGFloat(x), y: CGFloat(y))
|
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 {
|
private var disconnectConfirmationOverlay: some View {
|
||||||
GeometryReader { proxy in
|
GeometryReader { proxy in
|
||||||
ZStack {
|
ZStack {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
namespace crossdesk::mobile_controller {
|
namespace crossdesk::mobile_controller {
|
||||||
inline std::string PrivacyMessage(int flag, bool block_local_input) {
|
inline std::string PrivacyMessage(int flag, bool block_local_input) {
|
||||||
if (flag < 0 || flag > 2) return {};
|
if (flag < 0 || flag > 3) return {};
|
||||||
RemoteAction action{};
|
RemoteAction action{};
|
||||||
action.type = privacy_command;
|
action.type = privacy_command;
|
||||||
action.pc = {static_cast<PrivacyCommandFlag>(flag), block_local_input};
|
action.pc = {static_cast<PrivacyCommandFlag>(flag), block_local_input};
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ struct ServiceCommand {
|
|||||||
};
|
};
|
||||||
|
|
||||||
enum class PrivacyState { unsupported, off, starting, on, stopping, failed };
|
enum class PrivacyState { unsupported, off, starting, on, stopping, failed };
|
||||||
enum class PrivacyCommandFlag { query, enable, disable };
|
enum class PrivacyCommandFlag { query, enable, disable, lock };
|
||||||
struct PrivacyCommand {
|
struct PrivacyCommand {
|
||||||
PrivacyCommandFlag flag;
|
PrivacyCommandFlag flag;
|
||||||
bool block_local_input;
|
bool block_local_input;
|
||||||
@@ -171,6 +171,8 @@ struct PrivacyStatus {
|
|||||||
bool remote_paused;
|
bool remote_paused;
|
||||||
uint32_t revision;
|
uint32_t revision;
|
||||||
char reason[256];
|
char reason[256];
|
||||||
|
// Monotonic event counter retained across status heartbeats and recovery.
|
||||||
|
uint32_t local_unlock_revision;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct VideoSettings {
|
struct VideoSettings {
|
||||||
|
|||||||
@@ -224,7 +224,8 @@ std::string RemoteAction::ToJson(const RemoteAction& action) {
|
|||||||
{"overlay_active", action.ps.overlay_active},
|
{"overlay_active", action.ps.overlay_active},
|
||||||
{"input_blocked", action.ps.input_blocked},
|
{"input_blocked", action.ps.input_blocked},
|
||||||
{"remote_paused", action.ps.remote_paused},
|
{"remote_paused", action.ps.remote_paused},
|
||||||
{"revision", action.ps.revision}, {"reason", action.ps.reason}};
|
{"revision", action.ps.revision}, {"reason", action.ps.reason},
|
||||||
|
{"local_unlock_revision", action.ps.local_unlock_revision}};
|
||||||
break;
|
break;
|
||||||
case ControlType::host_infomation: {
|
case ControlType::host_infomation: {
|
||||||
json displays = json::array();
|
json displays = json::array();
|
||||||
@@ -409,7 +410,7 @@ bool RemoteAction::FromJson(const std::string& json_string,
|
|||||||
case ControlType::privacy_command: {
|
case ControlType::privacy_command: {
|
||||||
const auto& command = object.at("privacy_command");
|
const auto& command = object.at("privacy_command");
|
||||||
const int flag = command.at("flag").get<int>();
|
const int flag = command.at("flag").get<int>();
|
||||||
if (flag < 0 || flag > 2) return false;
|
if (flag < 0 || flag > 3) return false;
|
||||||
output.pc.flag = static_cast<PrivacyCommandFlag>(flag);
|
output.pc.flag = static_cast<PrivacyCommandFlag>(flag);
|
||||||
output.pc.block_local_input = command.at("block_local_input").get<bool>();
|
output.pc.block_local_input = command.at("block_local_input").get<bool>();
|
||||||
break;
|
break;
|
||||||
@@ -427,6 +428,12 @@ bool RemoteAction::FromJson(const std::string& json_string,
|
|||||||
output.ps.input_blocked = status.at("input_blocked").get<bool>();
|
output.ps.input_blocked = status.at("input_blocked").get<bool>();
|
||||||
output.ps.remote_paused = status.at("remote_paused").get<bool>();
|
output.ps.remote_paused = status.at("remote_paused").get<bool>();
|
||||||
output.ps.revision = status.at("revision").get<uint32_t>();
|
output.ps.revision = status.at("revision").get<uint32_t>();
|
||||||
|
output.ps.local_unlock_revision = 0;
|
||||||
|
if (const auto event = status.find("local_unlock_revision"); event != status.end()) {
|
||||||
|
if (!event->is_number_integer() || event->get<int64_t>() < 0 ||
|
||||||
|
event->get<uint64_t>() > UINT32_MAX) return false;
|
||||||
|
output.ps.local_unlock_revision = event->get<uint32_t>();
|
||||||
|
}
|
||||||
std::memcpy(output.ps.reason, reason.c_str(), reason.size() + 1);
|
std::memcpy(output.ps.reason, reason.c_str(), reason.size() + 1);
|
||||||
// Never render a contradictory success from a malformed peer.
|
// Never render a contradictory success from a malformed peer.
|
||||||
if (output.ps.state == PrivacyState::on &&
|
if (output.ps.state == PrivacyState::on &&
|
||||||
|
|||||||
Reference in New Issue
Block a user