From 5838392f506c20232e3e932a8461e4b90aa06466 Mon Sep 17 00:00:00 2001 From: Caleb Chong Date: Mon, 15 Jun 2026 18:34:32 +0800 Subject: [PATCH 1/4] fix: detect non-default mic activity --- tauri/src-tauri/bin/mic_check | Bin 52264 -> 57224 bytes .../bin/mic_check-aarch64-apple-darwin | Bin 52264 -> 57224 bytes tauri/src-tauri/build.rs | 37 +++++ tauri/src-tauri/src/call_detect.rs | 53 ++++--- tauri/src-tauri/src/mic_check.swift | 134 ++++++++++++++---- 5 files changed, 175 insertions(+), 49 deletions(-) diff --git a/tauri/src-tauri/bin/mic_check b/tauri/src-tauri/bin/mic_check index 8419e31ac7c827a647547fff56e1d7505509d945..06b262378ff65c1759cc166d0501c2f547f24534 100755 GIT binary patch literal 57224 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 M-Pgb6MqCE{AHeqFlK=n! literal 52264 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 M-Pgb6MqCE{AHeqFlK=n! literal 52264 zcmeI5e{3AZ702i9oa7urVi6O5G$n^IsX+*X69Z`rz4NccB*d<5f(ule_1)UuqD65UP-ulHRVj7)OR8Fm+d@O2lpj(n_@gNZ(WDdss#>)rrG(LuW#?^qSoo+J5O(#VNsk0OrU9f?frezyM6!o^fsaFd1xk`uevqOHMYCTKv5p= zG2~A1UZurA$W~R^P$fKBk)N8Db#nvRLQ{FXeF{!mjUc-;UR1?HvoD&Kw5@D0!SZne*iN1u^+O@G)o|d>eq$qtU zDMNDveYBx-q*}I>Vxe$*tsT+0!$~{kG04}0aQ16D4ELBQlPgivmvq~KPwX^grMq3O@ zX*TAvPGi~}XiVkPDcB?*gHj&%U;U4@Z?i1@ zyrx3WE)n9QP=)v%cFw}#MY!1}Xl*R^BQE71FNu$;FNHnuuU*;=ze2f}E}z!z#o^-pbwRnfnge@Q0g>sTaisFX*W?%=jA@t?5{|1`{*N1K_= z=x%bj5MidjItRg7p3b4OYrW|foaOqba}6)WPs|r#+|6*ZQ;@ICE zIARnD`A^#izje;}wU(Tjc1=qgOxt?tLRP|$zHg5`DMS?I1T)}YySA;js@eBPld*C74F$rC&n6t*Rwk;CXmM-aa+gawJ3ksBF4TN z7Lz|)C?>4mYCivu}UuN>Sxqd26_K zYhs@0qa35>$0t^V#7)upUQcx1?l0q)OY07RC-nv|k6gtSHH44eAsx$ftpFzj!*7LU_FPvNj{e#$C?X~V|azEpH@QeBpzsDe9v!%c%k>Ey;(T zuW^*5^tS>@-2W_o&@*}wW~had><^0&+NykmKbQa$U;<2l2`~XBzyz286JP>NfC(@G zCcp%k025#WOn?b60Vco%m;e)C0!)AjFaajO1egF5U;<2l2`~XBzyz286JP>NfC(@G zCcp%kzzhO4p8VZQjVFJRQsdEYRPbvxPyQ&ShJO0$+w)bqK$ZB-5TwS#&tCj;pNfC(@GCcp%k025#WOn?b60Vco%m;e)C z0!)AjFaajO1egF5U;<2l2`~XBzyz286JP>NfC(@GCcp%k025#WOn?b60Vco%m;e)C z0!)AjFaaj;ktb01#i^;kPs#t+@*|%tC&&bt025#WOn?b60Vco%m;e)C0!)AjFaajO z1egF5U;<2l2`~XBzyz286JP>NfC(@GCcp%k025#WOn?b60Vco%m;e)C0!)AjFaajO z1egF5U;<2l2`~XBzyz286JP>N;QyFF3~TF#?u4EK4M78$7gSK5SzeFN7b0G4B)?Et zi11<|syd*3(EFf&g4Tva)hD4D=-!Z6@ES&I#&**cS|XP+?XH0>M$%ntxZRGOF&sDF zrn|b(>a)GQ8>z@PtIDZ>%fBf4pcQw0X>_qV14_cQnJ<%VR{qHDyv$7i^vVU0FpHp^5+4m@WyRsiwwx{ej zmAzZpORHqOJ<4t+d!Z);{!xCzWaH2wCEp%Xc8#*1Q}%LYzp3n)vLm2C{`JaUsq6-2 ze_q+Em5t?!C7~L8%K4W0Hh5M`d%0|1K>Z|_Ly;$Dm2y>?t(Dn#OPi+g>_L>yU2 zc|qFv;z&6r?Py@1fypo)6s3Gx+Q?4IjnZBi*tba=1f)DDZJY)v&-%8H)^GTwvw9{qSC#jE|HF|p4N74Lsz0rtCvl!wqy8?Sctr6W*^gG=d6_Onl>)4IS00x z*^KTchbs&@p*Wpg>*=DMa~^rWnWIK)g+cw@m3p<7oSAk_OB+nvI_8pB`~T{y4VG$J zSy#tHLw60_!%4?*4Z5N`9DAhXlE$JH8pjw+<1vI?(MAm?>+eoBKQdsa&14LtGz|}u zwXq5hqY40vTZO|iVkWiZu#wza!C^Yn6Lc!>}A)gM{{QSz$-F%IZ32aPOsf` zS;_Sfyt7ajULH1$mievDnl~GBMqOo;ybP3it}(McImj1Q0cGKI)^EG%b6rMS%eU2jhs_F<8 z_=}e}yb?X@Cm$?-$eo&aU Vec { .collect() } -/// Check if the default audio input device is currently being used. +/// Check if any audio input is currently being used. /// -/// Uses a pre-compiled Swift helper that calls CoreAudio -/// `kAudioDevicePropertyDeviceIsRunningSomewhere` on the default input device. -/// Works on both Intel and Apple Silicon Macs. +/// Uses a pre-compiled Swift helper that queries CoreAudio process input +/// activity and falls back to scanning input-capable devices. This catches +/// call apps that capture from a non-default input route. /// /// Falls back to an inline `swift` invocation if the helper binary is missing. fn is_mic_in_use() -> bool { @@ -1343,22 +1343,9 @@ fn is_mic_in_use() -> bool { } // Fallback: inline swift (slower: ~200ms, but always works) - let script = r#" -import CoreAudio -var id = AudioObjectID(kAudioObjectSystemObject) -var pa = AudioObjectPropertyAddress(mSelector: kAudioHardwarePropertyDefaultInputDevice, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain) -var sz = UInt32(MemoryLayout.size) -guard AudioObjectGetPropertyData(AudioObjectID(kAudioObjectSystemObject), &pa, 0, nil, &sz, &id) == noErr else { print("0"); exit(0) } -var r: UInt32 = 0 -var ra = AudioObjectPropertyAddress(mSelector: kAudioDevicePropertyDeviceIsRunningSomewhere, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain) -sz = UInt32(MemoryLayout.size) -guard AudioObjectGetPropertyData(id, &ra, 0, nil, &sz, &r) == noErr else { print("0"); exit(0) } -print(r > 0 ? "1" : "0") -"#; - let output = std::process::Command::new("swift") .arg("-e") - .arg(script) + .arg(include_str!("mic_check.swift")) .output(); match output { @@ -1798,6 +1785,32 @@ mod tests { let _result = is_mic_in_use(); } + #[test] + fn bundled_mic_check_detects_non_default_input_activity() { + let swift_source = include_str!("mic_check.swift"); + + assert!( + swift_source.contains("kAudioHardwarePropertyProcessObjectList"), + "mic_check should query CoreAudio process objects for input activity" + ); + assert!( + swift_source.contains("kAudioProcessPropertyIsRunningInput"), + "mic_check should prefer process-level input activity over device-global running state" + ); + assert!( + swift_source.contains("kAudioHardwarePropertyDevices"), + "mic_check must still enumerate devices as a fallback for non-default inputs" + ); + assert!( + swift_source.contains("kAudioDevicePropertyScopeInput"), + "mic_check must limit activity checks to input-capable devices" + ); + assert!( + !swift_source.contains("kAudioHardwarePropertyDefaultInputDevice"), + "mic_check must not regress to default-input-only detection" + ); + } + #[test] fn call_end_fires_once_per_session() { let detector = CallDetector::new(test_call_detection_config(vec!["zoom.us".into()])); diff --git a/tauri/src-tauri/src/mic_check.swift b/tauri/src-tauri/src/mic_check.swift index 0849fe5b..2b9c0d1c 100644 --- a/tauri/src-tauri/src/mic_check.swift +++ b/tauri/src-tauri/src/mic_check.swift @@ -1,37 +1,113 @@ -// Minimal Swift helper to check if the default audio input device is in use. -// Outputs "1" if mic is active, "0" if idle. -// Uses CoreAudio kAudioDevicePropertyDeviceIsRunningSomewhere which works -// on both Intel and Apple Silicon Macs. +// Minimal Swift helper to check if any audio input is active. +// Outputs "1" if mic/input capture is active, "0" if idle. import CoreAudio import Foundation -var defaultInputID = AudioObjectID(kAudioObjectSystemObject) -var propAddr = AudioObjectPropertyAddress( - mSelector: kAudioHardwarePropertyDefaultInputDevice, - mScope: kAudioObjectPropertyScopeGlobal, - mElement: kAudioObjectPropertyElementMain -) -var size = UInt32(MemoryLayout.size) -let err = AudioObjectGetPropertyData( - AudioObjectID(kAudioObjectSystemObject), &propAddr, 0, nil, &size, &defaultInputID -) -guard err == noErr else { - print("0") - exit(0) +let systemObject = AudioObjectID(kAudioObjectSystemObject) + +func objectIDs( + for selector: AudioObjectPropertySelector, + on objectID: AudioObjectID = systemObject, + scope: AudioObjectPropertyScope = kAudioObjectPropertyScopeGlobal +) -> [AudioObjectID]? { + var address = AudioObjectPropertyAddress( + mSelector: selector, + mScope: scope, + mElement: kAudioObjectPropertyElementMain + ) + var size: UInt32 = 0 + guard AudioObjectGetPropertyDataSize(objectID, &address, 0, nil, &size) == noErr else { + return nil + } + let count = Int(size) / MemoryLayout.size + guard count > 0 else { + return [] + } + + var ids = [AudioObjectID](repeating: AudioObjectID(kAudioObjectUnknown), count: count) + let status = ids.withUnsafeMutableBufferPointer { buffer in + guard let baseAddress = buffer.baseAddress else { + return kAudioHardwareBadObjectError + } + return AudioObjectGetPropertyData(objectID, &address, 0, nil, &size, baseAddress) + } + guard status == noErr else { + return nil + } + return ids +} + +func uint32Property( + _ selector: AudioObjectPropertySelector, + on objectID: AudioObjectID, + scope: AudioObjectPropertyScope = kAudioObjectPropertyScopeGlobal +) -> UInt32? { + var value: UInt32 = 0 + var size = UInt32(MemoryLayout.size) + var address = AudioObjectPropertyAddress( + mSelector: selector, + mScope: scope, + mElement: kAudioObjectPropertyElementMain + ) + guard AudioObjectGetPropertyData(objectID, &address, 0, nil, &size, &value) == noErr else { + return nil + } + return value +} + +func anyProcessRunningInput() -> Bool? { + guard let processes = objectIDs(for: kAudioHardwarePropertyProcessObjectList) else { + return nil + } + var sawReadableProcess = false + for processID in processes { + guard let isRunning = uint32Property(kAudioProcessPropertyIsRunningInput, on: processID) else { + continue + } + sawReadableProcess = true + if isRunning > 0 { + return true + } + } + return sawReadableProcess ? false : nil +} + +func inputChannelCount(for deviceID: AudioObjectID) -> UInt32 { + var address = AudioObjectPropertyAddress( + mSelector: kAudioDevicePropertyStreamConfiguration, + mScope: kAudioDevicePropertyScopeInput, + mElement: kAudioObjectPropertyElementMain + ) + var size: UInt32 = 0 + guard AudioObjectGetPropertyDataSize(deviceID, &address, 0, nil, &size) == noErr, size > 0 else { + return 0 + } + + let bufferList = UnsafeMutableRawPointer.allocate( + byteCount: Int(size), + alignment: MemoryLayout.alignment + ) + defer { bufferList.deallocate() } + + guard AudioObjectGetPropertyData(deviceID, &address, 0, nil, &size, bufferList) == noErr else { + return 0 + } + + let audioBufferList = bufferList.assumingMemoryBound(to: AudioBufferList.self) + return UnsafeMutableAudioBufferListPointer(audioBufferList) + .reduce(UInt32(0)) { total, buffer in total + buffer.mNumberChannels } } -var isRunning: UInt32 = 0 -var runAddr = AudioObjectPropertyAddress( - mSelector: kAudioDevicePropertyDeviceIsRunningSomewhere, - mScope: kAudioObjectPropertyScopeGlobal, - mElement: kAudioObjectPropertyElementMain -) -size = UInt32(MemoryLayout.size) -let err2 = AudioObjectGetPropertyData(defaultInputID, &runAddr, 0, nil, &size, &isRunning) -guard err2 == noErr else { - print("0") - exit(0) +func anyInputDeviceRunning() -> Bool { + guard let devices = objectIDs(for: kAudioHardwarePropertyDevices) else { + return false + } + return devices.contains { deviceID in + inputChannelCount(for: deviceID) > 0 + && (uint32Property(kAudioDevicePropertyDeviceIsRunningSomewhere, on: deviceID) ?? 0) > 0 + } } -print(isRunning > 0 ? "1" : "0") +let micActive = anyProcessRunningInput() ?? anyInputDeviceRunning() +print(micActive ? "1" : "0") From 7c9d590dbe0da1c9aba5435d882ac63cc895db3e Mon Sep 17 00:00:00 2001 From: Caleb Chong Date: Mon, 15 Jun 2026 18:49:26 +0800 Subject: [PATCH 2/4] fix: attribute call detection to audio processes --- tauri/src-tauri/bin/mic_check | Bin 57224 -> 58904 bytes .../bin/mic_check-aarch64-apple-darwin | Bin 57224 -> 58904 bytes tauri/src-tauri/src/call_detect.rs | 374 +++++++++++++++++- tauri/src-tauri/src/mic_check.swift | 35 +- 4 files changed, 387 insertions(+), 22 deletions(-) diff --git a/tauri/src-tauri/bin/mic_check b/tauri/src-tauri/bin/mic_check index 06b262378ff65c1759cc166d0501c2f547f24534..c74032e1bf86c4d7ae5ed295ff840e7886749133 100755 GIT binary patch delta 9016 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 delta 5264 zcmai23sh9c8UF9R3of`km-mV+f(n9)Xd)`&Rg7Xx6py$Y&k1OnC<7M1+R!El89(3|AVq3nx#q^!L0-J<~qHVS_iUfDI+v5 zMsK!=W*29%z82OkSwqW(HdtsmS6o_EwuWvJTBFc%F1xf_psf+wcA@25*LKR*;H6f5 z+#;b}YGuL7W=UlYibb-pKPyS0>SWzS6(*ePgGU℘bcLH#q0I`)n@*D>f8M3GAF< zI{wCb70Mf8Shd%n84=z@qlu~l;WFYlQ3`6A%Ro8!Le!045Y-0|^@p4d@WSo-Y*V?+ zSLs!Ef+*8I$IPtW34wdGH3h3vGQO;xzj)!N>pw9y=XuvDL$bsmiRcsv@IiRN9=Uk- z1)?-eJ86ooH&Z&Tk!SHQp{vGy2fVR-#;qD1>vD z-98%U1AY>A2u5&UTc3TsM7e&@bAO$RaaRI-8&|8E6ifDV2KZKDoLNM+EyjK7xD7;O zx$)iJ-tapBrn#4l6YRY6Wmo5_99g2^ZRpr(Caq!ZKqmqf;En+sEqwRr?4v_BW@eVrmltYHw8v&-7+0C-2Pe-_%oKLU0xPAA|g#|DIKsxNbCQMSgSu>#&7%jaId;B+~ez zI+IkbGlsN!;n2*?pS znU8)(OulP2Ik?}h*{Y*!tS-BJsKN)?GVHU^xlxf$9ze$eKHyl@(WKQI82$@)aW@S(qrd>#~qz z0CX-Ht38fX`wM`mI=aHi!TYR4pRRD#(Ma?t_rv{mnaN>*oS7vES8&H&W^@c7M+%Qb z0Avin4PADSU3M1L(Npn6)1BEfksAt|8#dS{pYJO|T)XFt-iWcrwIi52ab%>Bs*VzO5hRpX}u?cFp6_Y@|8 z^>(x5fGW8|W_tpXya;oYnQYl1*T`&_<$Y;7dvZX`@LWDSBfniDoaK-kl{{yI@{E%t zRXd1uYFL}+d>_1iZKP9W?+#cl?PWgE6Ql4*ztwL{uRB-{|3)lEWD>; zT2t>4Ruc2P^bR{6GhXUuf!3)$yP>W>64|nn6<9N+b*#ZU)MpcK%M5ABWhbp8rCCgh zO$aO+Y?l!*SBMd$X>45Vs<3XOkE$6lD;$)67KsZzm$k)?m6DhiTPxjW_CaRpCfhz} z$JB14-~W%C^kZ-BLy=2SwOfcLk6hvcVsFMxHU+_^r($x`oqkiCtTjAB{wuMI0Rvfn zcp!_94-BjHjDC%(wGrDIK8%&c$4eX8yYUvOiG3WO9u+&$-Zd0AHWJA=d4j!B8Q^ru zucx#AgSSgF*uKH$=#lXEVQ+6OzYF;m@Vh)0ZTzC1%`Oj~WW#=U2fhcb9COAczvU)x zb(6!~@@-9^odBcat+z+k=6pxGAQ&$&b6qv)tr)Zt}Bk@(XUVzndKF zCVThE&tUbx>?4pX-Q;RFdA*zb8#j5gF5{%!FDW#N%WO$P&1m`6Sfc49Pek>~+y$@5 z#hg#qZ998=dwn2YL5;eeO8v~pN#(R??7bpR?0=Iy`eq=}n#se#WZ9q5h ztTB`f8;s<&0|-Q1s(q-R$(LBu=`^;;Ij%A;nnkE&$bv2Al`f6SC zi6gSq+;Ox=VyBNkV!I*N8Jv=uoNO|kFjPrp^ zyF+zu7raIA6M`QN)Ajs3aEB?8I?q7bbDk&o8-ni^{3tk|@TMr2y^Ssj1H!KQQz;lYl2q^eK7W#>o*8Kf^%Q&>14fsq2O79*9e|1_%XrrILG*OPc#$>gG3|` zceqsWxq`0{+$MOr;2&{rs=^aqpTJ?kje=hgJWz0N z*9xA@KL1;-4VM&;6Rsz{)OzqwJ@|PKe#L`#d+;AUI3CvSgR(H;+e)Pr|>@Lmt@hoRjk5TbKTQ!jX+bG^=a#$@Xa zXw%CA4_@lQ*Somt)n|{37Y=@^77EntHLIC+u<5@iUJ9nV>h|=<20N z=Qw(Lsnj`6j$U@U_=6B1_TU{JT*lPgBQo{@f8OPB)`JEY$M=u_lY$-r9tH5BqjA7^ zU;@BDZj%5Nm<&t-GJ&bUG+;U~6POL~uUrnB({8cN;Sw+x=+kiTV}buQ!bG%Z06#a< zW6<@DmIZkR@HoInegb$Bm<8km1$dCmZx0IMs~Q9FmGM}&Wow;(IuTd}eE}B}AwLD;bnHqvNq_I4RWTcqvO6i5r=jlBNH`Rq~ zt4t>RuV{$P93h&e`^%s=K8*Fg^@v@5!?9!8^px=f%XfXWf7^U%SLxT5#_M0Sjd(XK z#nKyT-nuhm$@ZwjrEMchYb$#X?)S?m{rpnG-O?N9cm6)vY6-ad+L4y`LhpT-Ib`17 z=dM}T^lkpd88Z)jaJT)Bw%z;lmLI?Ne5-*qd>d*#y`%l50Ke?T$-fTSJh474<@O8a zdr$8em(<*P{MgxZp?lbwZ#$>>O6-TW^lHn+*662fev2m8?#j6Ik6EeL&vxwWt()bn ouSnm!cTI!;g@TRCrcC(0W!T}ouV31B*z~f)5w-D1emb`NzdK67i~s-t diff --git a/tauri/src-tauri/bin/mic_check-aarch64-apple-darwin b/tauri/src-tauri/bin/mic_check-aarch64-apple-darwin index 06b262378ff65c1759cc166d0501c2f547f24534..c74032e1bf86c4d7ae5ed295ff840e7886749133 100755 GIT binary patch delta 9016 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 delta 5264 zcmai23sh9c8UF9R3of`km-mV+f(n9)Xd)`&Rg7Xx6py$Y&k1OnC<7M1+R!El89(3|AVq3nx#q^!L0-J<~qHVS_iUfDI+v5 zMsK!=W*29%z82OkSwqW(HdtsmS6o_EwuWvJTBFc%F1xf_psf+wcA@25*LKR*;H6f5 z+#;b}YGuL7W=UlYibb-pKPyS0>SWzS6(*ePgGU℘bcLH#q0I`)n@*D>f8M3GAF< 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 diff --git a/tauri/src-tauri/src/call_detect.rs b/tauri/src-tauri/src/call_detect.rs index 18f947bd..90ab136c 100644 --- a/tauri/src-tauri/src/call_detect.rs +++ b/tauri/src-tauri/src/call_detect.rs @@ -8,7 +8,7 @@ //! `is_mic_in_use`) use CoreAudio and `ps`. Windows/Linux would need //! alternative implementations behind `cfg(target_os)` gates. -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; @@ -221,17 +221,78 @@ fn is_low_confidence_native_call_app(app: &str) -> bool { matches!(app.to_ascii_lowercase().as_str(), "slack") } -fn native_app_matches_running_process(config_app: &str, running: &[String]) -> bool { +#[derive(Debug, Clone, PartialEq, Eq)] +struct RunningProcess { + pid: u32, + ppid: u32, + name: String, +} + +fn process_name_matches_config_app(config_app: &str, process_name: &str) -> bool { let config_lower = config_app.to_lowercase(); - running.iter().any(|p| { - let p_lower = p.to_lowercase(); - // Exact match (most common) or the config name is a prefix/suffix of - // the binary name (e.g. "zoom.us" matches "zoom.us"), but NOT a mere - // substring of a longer daemon name. - p_lower == config_lower - || p_lower.starts_with(&format!("{}.", config_lower)) - || p_lower.starts_with(&format!("{} ", config_lower)) - }) + let process_lower = process_name.to_lowercase(); + // Exact match (most common) or the config name is a prefix/suffix of + // the binary name (e.g. "zoom.us" matches "zoom.us"), but NOT a mere + // substring of a longer daemon name. + process_lower == config_lower + || process_lower.starts_with(&format!("{}.", config_lower)) + || process_lower.starts_with(&format!("{} ", config_lower)) +} + +fn native_app_matches_running_process(config_app: &str, running: &[String]) -> bool { + running + .iter() + .any(|p| process_name_matches_config_app(config_app, p)) +} + +fn zoom_audio_helper_process_name(process_name: &str) -> bool { + matches!( + process_name, + "ZoomHybridConf" | "CptHost" | "caphost" | "aomhost" + ) +} + +fn native_app_candidate_process_pids( + config_app: &str, + processes: &[RunningProcess], +) -> HashSet { + let mut candidates: HashSet = processes + .iter() + .filter(|process| process_name_matches_config_app(config_app, &process.name)) + .map(|process| process.pid) + .collect(); + + if config_app == "zoom.us" { + candidates.extend( + processes + .iter() + .filter(|process| zoom_audio_helper_process_name(&process.name)) + .map(|process| process.pid), + ); + } + + let mut changed = true; + while changed { + changed = false; + for process in processes { + if candidates.contains(&process.ppid) && candidates.insert(process.pid) { + changed = true; + } + } + } + + candidates +} + +fn native_app_has_active_input( + config_app: &str, + processes: &[RunningProcess], + active_input_pids: &HashSet, +) -> bool { + let candidate_pids = native_app_candidate_process_pids(config_app, processes); + candidate_pids + .iter() + .any(|pid| active_input_pids.contains(pid)) } enum BrowserMeetProbe { @@ -700,14 +761,19 @@ impl CallDetector { /// Check if any configured call app is active. fn detect_active_call(&self, config: &CallDetectionConfig) -> DetectActiveCallResult { - let mic_live = is_mic_in_use(); + let processes = running_processes(); + let active_input_pids = active_input_process_pids(); + let mic_live = active_input_pids + .as_ref() + .map(|pids| !pids.is_empty()) + .unwrap_or_else(is_mic_in_use); let mic_just_activated = self.note_mic_state(mic_live); - let running = running_process_names(); - self.detect_active_call_from_snapshot( + self.detect_active_call_from_process_snapshot( config, mic_live, - &running, + &processes, + active_input_pids.as_ref(), mic_just_activated, |detector, running, has_google_meet, has_teams_web| { if has_google_meet || has_teams_web { @@ -720,12 +786,61 @@ impl CallDetector { ) } + #[cfg(test)] fn detect_active_call_from_snapshot( &self, config: &CallDetectionConfig, mic_live: bool, running: &[String], force_browser_probe: bool, + browser_probe: F, + ) -> DetectActiveCallResult + where + F: FnMut(&Self, &[String], bool, bool) -> Option, + { + self.detect_active_call_from_snapshot_with_processes( + config, + mic_live, + running, + None, + None, + force_browser_probe, + browser_probe, + ) + } + + fn detect_active_call_from_process_snapshot( + &self, + config: &CallDetectionConfig, + mic_live: bool, + processes: &[RunningProcess], + active_input_pids: Option<&HashSet>, + force_browser_probe: bool, + browser_probe: F, + ) -> DetectActiveCallResult + where + F: FnMut(&Self, &[String], bool, bool) -> Option, + { + let running = process_names_from_snapshot(processes); + self.detect_active_call_from_snapshot_with_processes( + config, + mic_live, + &running, + Some(processes), + active_input_pids, + force_browser_probe, + browser_probe, + ) + } + + fn detect_active_call_from_snapshot_with_processes( + &self, + config: &CallDetectionConfig, + mic_live: bool, + running: &[String], + processes: Option<&[RunningProcess]>, + active_input_pids: Option<&HashSet>, + force_browser_probe: bool, mut browser_probe: F, ) -> DetectActiveCallResult where @@ -787,7 +902,13 @@ impl CallDetector { // e.g. "FaceTime" should match the "FaceTime" binary, NOT // "com.apple.FaceTime.FTConversationService" (a system daemon // that runs permanently and caused false positives). - if native_app_matches_running_process(config_app, running) { + let native_active = match (processes, active_input_pids) { + (Some(processes), Some(active_input_pids)) => { + native_app_has_active_input(config_app, processes, active_input_pids) + } + _ => native_app_matches_running_process(config_app, running), + }; + if native_active { let display = display_name_for(config_app); return DetectActiveCallResult::Detected { display_name: display, @@ -797,7 +918,13 @@ impl CallDetector { } for config_app in low_confidence_native_apps { - if native_app_matches_running_process(config_app, running) { + let native_active = match (processes, active_input_pids) { + (Some(processes), Some(active_input_pids)) => { + native_app_has_active_input(config_app, processes, active_input_pids) + } + _ => native_app_matches_running_process(config_app, running), + }; + if native_active { let display = display_name_for(config_app); return DetectActiveCallResult::Detected { display_name: display, @@ -1293,8 +1420,14 @@ fn looks_like_teams_meeting_url(url: &str) -> bool { // ── macOS-specific detection ────────────────────────────────── +fn binary_name_from_command(command: &str) -> String { + let trimmed = command.trim(); + trimmed.rsplit('/').next().unwrap_or(trimmed).to_string() +} + /// Get list of running process names via `ps`. Fast (~2ms), no permissions /// needed, no osascript overhead. +#[cfg(test)] fn running_process_names() -> Vec { let output = std::process::Command::new("ps") .args(["-eo", "comm="]) @@ -1309,6 +1442,28 @@ fn running_process_names() -> Vec { } } +fn running_processes() -> Vec { + let output = std::process::Command::new("ps") + .args(["-axo", "pid=,ppid=,comm="]) + .output(); + + match output { + Ok(out) if out.status.success() => { + let text = String::from_utf8_lossy(&out.stdout); + process_snapshots_from_ps_output(&text) + } + _ => Vec::new(), + } +} + +fn process_names_from_snapshot(processes: &[RunningProcess]) -> Vec { + processes + .iter() + .map(|process| process.name.clone()) + .collect() +} + +#[cfg(test)] fn process_names_from_ps_output(text: &str) -> Vec { text.lines() .filter_map(|line| { @@ -1318,7 +1473,33 @@ fn process_names_from_ps_output(text: &str) -> Vec { if trimmed.is_empty() { return None; } - Some(trimmed.rsplit('/').next().unwrap_or(trimmed).to_string()) + Some(binary_name_from_command(trimmed)) + }) + .collect() +} + +fn split_first_field(text: &str) -> Option<(&str, &str)> { + let trimmed = text.trim_start(); + let end = trimmed.find(char::is_whitespace)?; + Some((&trimmed[..end], &trimmed[end..])) +} + +fn process_snapshots_from_ps_output(text: &str) -> Vec { + text.lines() + .filter_map(|line| { + let trimmed = line.trim(); + if trimmed.is_empty() { + return None; + } + let (pid_text, rest) = split_first_field(trimmed)?; + let (ppid_text, command) = split_first_field(rest)?; + let pid = pid_text.parse().ok()?; + let ppid = ppid_text.parse().ok()?; + Some(RunningProcess { + pid, + ppid, + name: binary_name_from_command(command), + }) }) .collect() } @@ -1354,6 +1535,29 @@ fn is_mic_in_use() -> bool { } } +fn active_input_process_pids() -> Option> { + let helper = find_mic_check_binary()?; + let out = std::process::Command::new(helper) + .arg("--active-input-pids") + .output() + .ok()?; + if !out.status.success() { + return None; + } + + let text = String::from_utf8_lossy(&out.stdout); + let mut pids = HashSet::new(); + for line in text.lines() { + let trimmed = line.trim(); + if trimmed.is_empty() { + continue; + } + let pid = trimmed.parse().ok()?; + pids.insert(pid); + } + Some(pids) +} + /// Find the pre-compiled mic_check binary. /// Checks next to the app binary first, then the source tree location. fn find_mic_check_binary() -> Option { @@ -1672,6 +1876,109 @@ mod tests { ); } + #[test] + fn idle_zoom_does_not_detect_when_another_app_has_input() { + let detector = CallDetector::new(test_call_detection_config(vec!["zoom.us".into()])); + let config = detector.current_config(); + let processes = vec![ + RunningProcess { + pid: 100, + ppid: 1, + name: "zoom.us".into(), + }, + RunningProcess { + pid: 200, + ppid: 1, + name: "superwhisper".into(), + }, + ]; + let active_input_pids = HashSet::from([200]); + + let result = detector.detect_active_call_from_process_snapshot( + &config, + true, + &processes, + Some(&active_input_pids), + false, + |_detector, _running, _want_meet, _want_teams| None, + ); + + assert_eq!(result, DetectActiveCallResult::None); + } + + #[test] + fn zoom_helper_input_detects_zoom_call() { + let detector = CallDetector::new(test_call_detection_config(vec!["zoom.us".into()])); + let config = detector.current_config(); + let processes = vec![ + RunningProcess { + pid: 100, + ppid: 1, + name: "zoom.us".into(), + }, + RunningProcess { + pid: 110, + ppid: 100, + name: "ZoomHybridConf".into(), + }, + ]; + let active_input_pids = HashSet::from([110]); + + let result = detector.detect_active_call_from_process_snapshot( + &config, + true, + &processes, + Some(&active_input_pids), + false, + |_detector, _running, _want_meet, _want_teams| None, + ); + + assert_eq!( + result, + DetectActiveCallResult::Detected { + display_name: "Zoom".into(), + process_name: "zoom.us".into(), + } + ); + } + + #[test] + fn child_process_input_detects_native_call_app() { + let detector = + CallDetector::new(test_call_detection_config(vec!["Microsoft Teams".into()])); + let config = detector.current_config(); + let processes = vec![ + RunningProcess { + pid: 300, + ppid: 1, + name: "Microsoft Teams".into(), + }, + RunningProcess { + pid: 301, + ppid: 300, + name: "Microsoft Teams Helper".into(), + }, + ]; + let active_input_pids = HashSet::from([301]); + + let result = detector.detect_active_call_from_process_snapshot( + &config, + true, + &processes, + Some(&active_input_pids), + false, + |_detector, _running, _want_meet, _want_teams| None, + ); + + assert_eq!( + result, + DetectActiveCallResult::Detected { + display_name: "Teams".into(), + process_name: "Microsoft Teams".into(), + } + ); + } + #[test] fn mic_activation_forces_browser_probe_before_slack_even_when_rate_limited() { let detector = CallDetector::new(test_call_detection_config(vec![ @@ -1773,6 +2080,29 @@ mod tests { assert_eq!(procs, vec!["zoom.us", "Safari"]); } + #[test] + fn process_snapshot_parser_preserves_paths_with_spaces() { + let procs = process_snapshots_from_ps_output( + "\n 100 1 /Applications/zoom.us.app/Contents/MacOS/zoom.us\n 200 100 /Applications/Google Chrome.app/Contents/MacOS/Google Chrome\n", + ); + + assert_eq!( + procs, + vec![ + RunningProcess { + pid: 100, + ppid: 1, + name: "zoom.us".into(), + }, + RunningProcess { + pid: 200, + ppid: 100, + name: "Google Chrome".into(), + }, + ] + ); + } + #[test] fn process_list_probe_does_not_panic() { let _procs = running_process_names(); @@ -1797,6 +2127,14 @@ mod tests { swift_source.contains("kAudioProcessPropertyIsRunningInput"), "mic_check should prefer process-level input activity over device-global running state" ); + assert!( + swift_source.contains("kAudioProcessPropertyPID"), + "mic_check should expose active input PIDs for call-app attribution" + ); + assert!( + swift_source.contains("--active-input-pids"), + "mic_check should support attributed active-input PID output" + ); assert!( swift_source.contains("kAudioHardwarePropertyDevices"), "mic_check must still enumerate devices as a fallback for non-default inputs" diff --git a/tauri/src-tauri/src/mic_check.swift b/tauri/src-tauri/src/mic_check.swift index 2b9c0d1c..deb90996 100644 --- a/tauri/src-tauri/src/mic_check.swift +++ b/tauri/src-tauri/src/mic_check.swift @@ -56,10 +56,25 @@ func uint32Property( return value } -func anyProcessRunningInput() -> Bool? { +func pidProperty(on objectID: AudioObjectID) -> pid_t? { + var value = pid_t(0) + var size = UInt32(MemoryLayout.size) + var address = AudioObjectPropertyAddress( + mSelector: kAudioProcessPropertyPID, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMain + ) + guard AudioObjectGetPropertyData(objectID, &address, 0, nil, &size, &value) == noErr else { + return nil + } + return value +} + +func activeInputProcessIDs() -> [pid_t]? { guard let processes = objectIDs(for: kAudioHardwarePropertyProcessObjectList) else { return nil } + var pids: [pid_t] = [] var sawReadableProcess = false for processID in processes { guard let isRunning = uint32Property(kAudioProcessPropertyIsRunningInput, on: processID) else { @@ -67,10 +82,12 @@ func anyProcessRunningInput() -> Bool? { } sawReadableProcess = true if isRunning > 0 { - return true + if let pid = pidProperty(on: processID), pid > 0 { + pids.append(pid) + } } } - return sawReadableProcess ? false : nil + return sawReadableProcess ? pids : nil } func inputChannelCount(for deviceID: AudioObjectID) -> UInt32 { @@ -109,5 +126,15 @@ func anyInputDeviceRunning() -> Bool { } } -let micActive = anyProcessRunningInput() ?? anyInputDeviceRunning() +if CommandLine.arguments.contains("--active-input-pids") { + guard let pids = activeInputProcessIDs() else { + exit(2) + } + for pid in pids { + print(pid) + } + exit(0) +} + +let micActive = activeInputProcessIDs().map { !$0.isEmpty } ?? anyInputDeviceRunning() print(micActive ? "1" : "0") From e14d3dc3ce40afdf85dc7321967380e7883934b3 Mon Sep 17 00:00:00 2001 From: Caleb Chong Date: Mon, 15 Jun 2026 21:28:49 +0800 Subject: [PATCH 3/4] fix: require attributed input for native call detection --- tauri/src-tauri/src/call_detect.rs | 117 +++++++++++++++++++++-------- 1 file changed, 87 insertions(+), 30 deletions(-) diff --git a/tauri/src-tauri/src/call_detect.rs b/tauri/src-tauri/src/call_detect.rs index 90ab136c..986d778f 100644 --- a/tauri/src-tauri/src/call_detect.rs +++ b/tauri/src-tauri/src/call_detect.rs @@ -239,19 +239,6 @@ fn process_name_matches_config_app(config_app: &str, process_name: &str) -> bool || process_lower.starts_with(&format!("{} ", config_lower)) } -fn native_app_matches_running_process(config_app: &str, running: &[String]) -> bool { - running - .iter() - .any(|p| process_name_matches_config_app(config_app, p)) -} - -fn zoom_audio_helper_process_name(process_name: &str) -> bool { - matches!( - process_name, - "ZoomHybridConf" | "CptHost" | "caphost" | "aomhost" - ) -} - fn native_app_candidate_process_pids( config_app: &str, processes: &[RunningProcess], @@ -262,15 +249,6 @@ fn native_app_candidate_process_pids( .map(|process| process.pid) .collect(); - if config_app == "zoom.us" { - candidates.extend( - processes - .iter() - .filter(|process| zoom_audio_helper_process_name(&process.name)) - .map(|process| process.pid), - ); - } - let mut changed = true; while changed { changed = false; @@ -833,6 +811,7 @@ impl CallDetector { ) } + #[allow(clippy::too_many_arguments)] fn detect_active_call_from_snapshot_with_processes( &self, config: &CallDetectionConfig, @@ -906,7 +885,7 @@ impl CallDetector { (Some(processes), Some(active_input_pids)) => { native_app_has_active_input(config_app, processes, active_input_pids) } - _ => native_app_matches_running_process(config_app, running), + _ => false, }; if native_active { let display = display_name_for(config_app); @@ -922,7 +901,7 @@ impl CallDetector { (Some(processes), Some(active_input_pids)) => { native_app_has_active_input(config_app, processes, active_input_pids) } - _ => native_app_matches_running_process(config_app, running), + _ => false, }; if native_active { let display = display_name_for(config_app); @@ -1831,12 +1810,25 @@ mod tests { "google-meet".into(), ])); let config = detector.current_config(); - let running = vec!["zoom.us".into(), "Google Chrome".into()]; + let processes = vec![ + RunningProcess { + pid: 100, + ppid: 1, + name: "zoom.us".into(), + }, + RunningProcess { + pid: 200, + ppid: 1, + name: "Google Chrome".into(), + }, + ]; + let active_input_pids = HashSet::from([100]); - let result = detector.detect_active_call_from_snapshot( + let result = detector.detect_active_call_from_process_snapshot( &config, true, - &running, + &processes, + Some(&active_input_pids), false, |_detector, _running, _want_meet, _want_teams| Some(BrowserMeetProbe::NoMatch), ); @@ -1857,12 +1849,25 @@ mod tests { "google-meet".into(), ])); let config = detector.current_config(); - let running = vec!["Slack".into(), "Google Chrome".into()]; + let processes = vec![ + RunningProcess { + pid: 100, + ppid: 1, + name: "Slack".into(), + }, + RunningProcess { + pid: 200, + ppid: 1, + name: "Google Chrome".into(), + }, + ]; + let active_input_pids = HashSet::from([100]); - let result = detector.detect_active_call_from_snapshot( + let result = detector.detect_active_call_from_process_snapshot( &config, true, - &running, + &processes, + Some(&active_input_pids), false, |_detector, _running, _want_meet, _want_teams| Some(BrowserMeetProbe::NoMatch), ); @@ -1906,6 +1911,35 @@ mod tests { assert_eq!(result, DetectActiveCallResult::None); } + #[test] + fn idle_zoom_does_not_detect_when_active_input_attribution_is_unavailable() { + let detector = CallDetector::new(test_call_detection_config(vec!["zoom.us".into()])); + let config = detector.current_config(); + let processes = vec![ + RunningProcess { + pid: 100, + ppid: 1, + name: "zoom.us".into(), + }, + RunningProcess { + pid: 200, + ppid: 1, + name: "superwhisper".into(), + }, + ]; + + let result = detector.detect_active_call_from_process_snapshot( + &config, + true, + &processes, + None, + false, + |_detector, _running, _want_meet, _want_teams| None, + ); + + assert_eq!(result, DetectActiveCallResult::None); + } + #[test] fn zoom_helper_input_detects_zoom_call() { let detector = CallDetector::new(test_call_detection_config(vec!["zoom.us".into()])); @@ -1942,6 +1976,29 @@ mod tests { ); } + #[test] + fn unrooted_zoom_helper_input_does_not_detect_zoom_call() { + let detector = CallDetector::new(test_call_detection_config(vec!["zoom.us".into()])); + let config = detector.current_config(); + let processes = vec![RunningProcess { + pid: 110, + ppid: 1, + name: "ZoomHybridConf".into(), + }]; + let active_input_pids = HashSet::from([110]); + + let result = detector.detect_active_call_from_process_snapshot( + &config, + true, + &processes, + Some(&active_input_pids), + false, + |_detector, _running, _want_meet, _want_teams| None, + ); + + assert_eq!(result, DetectActiveCallResult::None); + } + #[test] fn child_process_input_detects_native_call_app() { let detector = From d16acd2de1e53ff2aa06c1293a1ecf3f9fe3b227 Mon Sep 17 00:00:00 2001 From: Caleb Chong Date: Mon, 15 Jun 2026 21:37:12 +0800 Subject: [PATCH 4/4] test: cover native attribution unavailable after browser probe --- tauri/src-tauri/src/call_detect.rs | 54 ++++++++++++++---------------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/tauri/src-tauri/src/call_detect.rs b/tauri/src-tauri/src/call_detect.rs index 986d778f..856d541b 100644 --- a/tauri/src-tauri/src/call_detect.rs +++ b/tauri/src-tauri/src/call_detect.rs @@ -1714,41 +1714,39 @@ mod tests { } #[test] - fn native_app_detection_wins_before_browser_meet_probe() { + fn native_app_does_not_fall_back_to_process_match_after_browser_no_match() { let detector = CallDetector::new(test_call_detection_config(vec![ "zoom.us".into(), "google-meet".into(), ])); - - let running = ["zoom.us".into(), "Safari".into()]; - let mic_live = true; - - // Reproduce the decision ordering from detect_active_call without relying - // on live browser automation in tests. let config = detector.current_config(); - let native_apps: Vec<&String> = config - .apps - .iter() - .filter(|app| app.as_str() != "google-meet") - .collect(); + let processes = vec![ + RunningProcess { + pid: 100, + ppid: 1, + name: "zoom.us".into(), + }, + RunningProcess { + pid: 200, + ppid: 1, + name: "Google Chrome".into(), + }, + ]; - let mut detected = None; - if mic_live { - for config_app in native_apps { - let config_lower = config_app.to_lowercase(); - if running.iter().any(|p: &String| { - let p_lower = p.to_lowercase(); - p_lower == config_lower - || p_lower.starts_with(&format!("{}.", config_lower)) - || p_lower.starts_with(&format!("{} ", config_lower)) - }) { - detected = Some(config_app.clone()); - break; - } - } - } + let result = detector.detect_active_call_from_process_snapshot( + &config, + true, + &processes, + None, + false, + |_detector, _running, want_meet, want_teams| { + assert!(want_meet); + assert!(!want_teams); + Some(BrowserMeetProbe::NoMatch) + }, + ); - assert_eq!(detected.as_deref(), Some("zoom.us")); + assert_eq!(result, DetectActiveCallResult::None); } #[test]