Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions openless-all/app/src-tauri/src/permissions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,11 @@ mod platform {
/// Windows 的麦克风权限走系统设置 → 隐私 → 麦克风;
/// 这里用 cpal 建立一次短生命周期输入流,避免只查设备格式时误报已授权。
pub fn check_microphone() -> PermissionStatus {
if windows_microphone_registry_denied() {
log::warn!("[mic] Windows microphone privacy registry is denied");
return PermissionStatus::Denied;
}

let host = cpal::default_host();
let Some(device) = host.default_input_device() else {
log::warn!("[mic] no default input device");
Expand Down Expand Up @@ -334,6 +339,51 @@ mod platform {
drop(stream);
Ok(())
}

fn windows_microphone_registry_denied() -> bool {
candidate_microphone_registry_paths()
.into_iter()
.any(|path| registry_value_is_deny(&path))
}

fn candidate_microphone_registry_paths() -> Vec<String> {
let mut paths = vec![
r"HKCU\Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\microphone".to_string(),
r"HKCU\Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\microphone\NonPackaged".to_string(),
r"HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\microphone".to_string(),
];

if let Ok(exe) = std::env::current_exe() {
if let Some(encoded) = exe.to_str().map(|path| path.replace('\\', "#")) {
paths.push(format!(
r"HKCU\Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\microphone\NonPackaged\{encoded}"
));
}
}

paths
}

fn registry_value_is_deny(path: &str) -> bool {
let output = match std::process::Command::new("reg")
.args(["query", path, "/v", "Value"])
.output()
{
Ok(output) => output,
Err(err) => {
log::warn!("[mic] reg query failed for {path}: {err}");
return false;
}
};

if !output.status.success() {
return false;
}

String::from_utf8_lossy(&output.stdout)
.lines()
.any(|line| line.contains("REG_SZ") && line.split_whitespace().any(|part| part == "Deny"))
}
}

pub use platform::{
Expand Down
27 changes: 18 additions & 9 deletions openless-all/app/src/pages/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -502,45 +502,54 @@ function PermissionsSection() {
const [hotkey, setHotkey] = useState<HotkeyStatus | null>(null);
const { capability } = useHotkeySettings();

const refresh = async () => {
const refreshPermissions = async () => {
const [a, m] = await Promise.all([
checkAccessibilityPermission(),
checkMicrophonePermission(),
]);
setAccessibility(a);
setMicrophone(m);
};

const refreshHotkey = async () => {
setHotkey(await getHotkeyStatus());
};

useEffect(() => {
refresh();
const id = window.setInterval(refresh, 1000);
// 用户从系统设置切回来时立刻刷新(不等下一个 1s tick)
const onFocus = () => refresh();
refreshPermissions();
refreshHotkey();
const hotkeyId = window.setInterval(refreshHotkey, 1000);
// 麦克风检查会短暂打开输入流,避免每秒探测导致隐私指示器频繁闪烁。
const permissionId = window.setInterval(refreshPermissions, 10000);
const onFocus = () => {
refreshPermissions();
refreshHotkey();
};
window.addEventListener('focus', onFocus);
return () => {
window.clearInterval(id);
window.clearInterval(hotkeyId);
window.clearInterval(permissionId);
window.removeEventListener('focus', onFocus);
};
}, []);

const reRequestAccessibility = async () => {
await requestAccessibilityPermission();
refresh();
refreshPermissions();
};

const reRequestMicrophone = async () => {
if (microphone === 'denied' || microphone === 'restricted') {
await openSystemSettings('microphone');
refresh();
refreshPermissions();
return;
}
const status = await requestMicrophonePermission();
setMicrophone(status);
if (status === 'denied' || status === 'restricted') {
await openSystemSettings('microphone');
}
refresh();
refreshPermissions();
};

const desc = capability?.requiresAccessibilityPermission
Expand Down
Loading