Skip to main content

coreshift_core/binder/
mod.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/
4
5//! NDK binder primitives for querying Android system services.
6//!
7//! Uses `dlopen` on `libbinder_ndk.so` to avoid hard-linking against a library
8//! absent from older NDK toolchains or non-Android targets.
9//!
10//! ## Transaction code resolution
11//!
12//! Transaction codes are resolved fresh from `framework.jar` DEX on each
13//! startup; no persistent cache.
14//!
15//! ## Observer mode
16//!
17//! `ActivityManagerBinder::open_with_observer` registers this process as an
18//! `IProcessObserver` with ActivityManager. Callbacks fire when foreground
19//! activities change and signal an `eventfd` that callers can poll via epoll.
20//! After the eventfd fires, call `get_focused_package` to read the new value.
21
22// ─────────────────────────────────────────────────────────────────────────────
23// Android-only implementation
24// ─────────────────────────────────────────────────────────────────────────────
25
26#[cfg(target_os = "android")]
27mod imp {
28    use crate::CoreError;
29    use crate::dex;
30    use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd, OwnedFd};
31    use std::os::raw::{c_char, c_void};
32    use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU32, AtomicUsize, Ordering};
33    use std::sync::Mutex;
34
35    // ── NDK binder status codes ───────────────────────────────────────────────
36
37    const STATUS_OK: i32 = 0;
38    const STATUS_UNKNOWN_TRANSACTION: i32 = -2;
39    const EX_NONE: i32 = 0;
40
41    // ── Interface constants ───────────────────────────────────────────────────
42
43    const AM_DESCRIPTOR:    &[u8] = b"android.app.IActivityManager\0";
44    const OBS_DESCRIPTOR:   &[u8] = b"android.app.IProcessObserver\0";
45    const FGPROC_DESCRIPTOR: &[u8] = b"android.app.IForegroundProcessObserver\0";
46    const ACTIVITY_SERVICE: &[u8] = b"activity\0";
47    #[cfg(target_pointer_width = "64")]
48    const LIBBINDER_PATH: &[u8] = b"/system/lib64/libbinder_ndk.so\0";
49    #[cfg(target_pointer_width = "32")]
50    const LIBBINDER_PATH: &[u8] = b"/system/lib/libbinder_ndk.so\0";
51
52    // ── Tx code cache ─────────────────────────────────────────────────────────
53    // Format (watcher.c compatible): observer_code query_code api_mode fg_code
54    // api_mode: 1 = getFocusedRootTaskInfo, 2 = getFocusedStackInfo (API 29)
55
56    // ── Statics for observer callback (binder thread pool context) ────────────
57    // The core owns the eventfd for the observer lifetime; the consumer receives
58    // a dup and may close it freely. The callback only ever writes to the core's
59    // copy, so it can never touch a closed/recycled fd (C2). The mutex guards
60    // publication/revocation against a callback firing concurrently.
61
62    static OBS_FG_CODE: AtomicU32 = AtomicU32::new(0);
63    static OBS_EVENTFD:  Mutex<Option<OwnedFd>> = Mutex::new(None);
64
65    fn obs_eventfd_guard() -> std::sync::MutexGuard<'static, Option<OwnedFd>> {
66        OBS_EVENTFD.lock().unwrap_or_else(|p| p.into_inner())
67    }
68
69    // ── IForegroundProcessObserver statics ───────────────────────────────────
70    // Separate from the IProcessObserver pair; same C2 discipline (core owns
71    // the eventfd, consumer gets a dup). FGPROC_READ_I32 holds the vtable's
72    // AParcel_readInt32 fn pointer so the callback can decode the `int pid`
73    // argument without owning the Vtable; it is published (non-zero) before
74    // FGPROC_FG_CODE/eventfd, so the callback never races an unset reader.
75
76    static FGPROC_FG_CODE: AtomicU32 = AtomicU32::new(0);
77    static FGPROC_PID: AtomicI32 = AtomicI32::new(0);
78    static FGPROC_EVENTFD: Mutex<Option<OwnedFd>> = Mutex::new(None);
79    static FGPROC_READ_I32: AtomicUsize = AtomicUsize::new(0);
80    // Binder class mode: 0 = stock IForegroundProcessObserver (single int pid),
81    // 1 = custom IProcessObserver (pid, uid, fg triplets). Only one of the two
82    // is ever configured by open_with_fgproc_observer; the callback branches on
83    // this to decode the in-parcel layout it actually receives.
84    static FGPROC_IPROC_MODE: AtomicU32 = AtomicU32::new(0);
85
86    fn fgproc_eventfd_guard() -> std::sync::MutexGuard<'static, Option<OwnedFd>> {
87        FGPROC_EVENTFD.lock().unwrap_or_else(|p| p.into_inner())
88    }
89
90    // ── Raw NDK type aliases ──────────────────────────────────────────────────
91
92    type AIBinder = c_void;
93    #[allow(non_camel_case_types)]
94    type AIBinder_Class = c_void;
95    type AParcel = c_void;
96    type BinderStatus = i32;
97    type StringAllocator = unsafe extern "C" fn(*mut c_void, i32, *mut *mut c_char) -> bool;
98
99    // ── AIBinder_Class callbacks ──────────────────────────────────────────────
100
101    // AM client — no-op server side (we're a client only)
102    unsafe extern "C" fn am_on_create(_: *mut c_void) -> *mut c_void { std::ptr::null_mut() }
103    unsafe extern "C" fn am_on_destroy(_: *mut c_void) {}
104    unsafe extern "C" fn am_on_transact(
105        _: *mut AIBinder, _: u32, _: *const AParcel, _: *mut AParcel,
106    ) -> BinderStatus { STATUS_UNKNOWN_TRANSACTION }
107
108    // IProcessObserver server callbacks
109    unsafe extern "C" fn obs_on_create(_: *mut c_void) -> *mut c_void { std::ptr::null_mut() }
110    unsafe extern "C" fn obs_on_destroy(_: *mut c_void) {}
111    unsafe extern "C" fn obs_on_transact(
112        _: *mut AIBinder, code: u32, _: *const AParcel, _: *mut AParcel,
113    ) -> BinderStatus {
114        if code == OBS_FG_CODE.load(Ordering::Relaxed) {
115            // Write while holding the lock: the fd can only be closed while
116            // we hold it, so a revoke can never race us into a stale number.
117            if let Some(fd) = obs_eventfd_guard().as_ref() {
118                let val: u64 = 1;
119                unsafe { libc::write(fd.as_raw_fd(), &val as *const u64 as *const c_void, 8) };
120            }
121        }
122        STATUS_OK
123    }
124
125    // IForegroundProcessObserver server callbacks. Two parcel layouts, selected
126    // by FGPROC_IPROC_MODE:
127    //  - mode 0 (stock): `onForegroundProcessChanged(int pid)` — single int32.
128    //  - mode 1 (custom ROMs without IForegroundProcessObserver): the ROM
129    //    repurposes `IProcessObserver.onForegroundActivitiesChanged` to deliver
130    //    `(int pid, int uid, int fg)`. The callback stores the pid and only
131    //    signals the eventfd when fg != 0 (a foreground transition), so
132    //    background transitions never cause the daemon to react.
133    unsafe extern "C" fn fgproc_on_create(_: *mut c_void) -> *mut c_void { std::ptr::null_mut() }
134    unsafe extern "C" fn fgproc_on_destroy(_: *mut c_void) {}
135    unsafe extern "C" fn fgproc_on_transact(
136        _: *mut AIBinder, code: u32, in_parcel: *const AParcel, _: *mut AParcel,
137    ) -> BinderStatus {
138        if code != FGPROC_FG_CODE.load(Ordering::Relaxed) {
139            return STATUS_UNKNOWN_TRANSACTION;
140        }
141        // Read fn is published non-zero before the code/eventfd, so a matching
142        // code is never paired with an unset reader.
143        let read_addr = FGPROC_READ_I32.load(Ordering::Relaxed);
144        if read_addr != 0 {
145            let read_fn: unsafe extern "C" fn(*const AParcel, *mut i32) -> BinderStatus =
146                unsafe { std::mem::transmute(read_addr) };
147            if FGPROC_IPROC_MODE.load(Ordering::Relaxed) == 1 {
148                // IProcessObserver.onForegroundActivitiesChanged(pid, uid, fg)
149                let mut pid: i32 = 0;
150                let mut _uid: i32 = 0;
151                let mut fg: i32 = 0;
152                let mut ok = unsafe { read_fn(in_parcel, &mut pid) } == STATUS_OK;
153                ok &= unsafe { read_fn(in_parcel, &mut _uid) } == STATUS_OK;
154                ok &= unsafe { read_fn(in_parcel, &mut fg) } == STATUS_OK;
155                if ok {
156                    FGPROC_PID.store(pid, Ordering::Relaxed);
157                    // Only foreground transitions are actionable; suppress the
158                    // background transition entirely (fg == 0).
159                    if fg == 0 {
160                        return STATUS_OK;
161                    }
162                } else {
163                    return STATUS_OK;
164                }
165            } else {
166                let mut pid: i32 = 0;
167                if unsafe { read_fn(in_parcel, &mut pid) } == STATUS_OK {
168                    FGPROC_PID.store(pid, Ordering::Relaxed);
169                }
170            }
171        }
172        // Write while holding the lock: the fd can only be closed while we
173        // hold it, so a revoke can never race us into a stale number.
174        if let Some(fd) = fgproc_eventfd_guard().as_ref() {
175            let val: u64 = 1;
176            unsafe { libc::write(fd.as_raw_fd(), &val as *const u64 as *const c_void, 8) };
177        }
178        STATUS_OK
179    }
180
181    /// The PID captured by the most recent `onForegroundProcessChanged`
182    /// callback (requires `open_with_fgproc_observer`).
183    pub fn last_foreground_pid() -> i32 {
184        FGPROC_PID.load(Ordering::Relaxed)
185    }
186
187    // ── String allocator ─────────────────────────────────────────────────────
188
189    /// Ceiling on a single parcel string regardless of the length the peer
190    /// advertises. Component names are at most a few hundred bytes; this bounds
191    /// the allocation so a malformed advertised length cannot drive a giant
192    /// `reserve_exact` (which would abort on OOM).
193    const MAX_BINDER_STRING_LEN: usize = 1024 * 1024;
194
195    unsafe extern "C" fn string_alloc(
196        cookie: *mut c_void, length: i32, buffer: *mut *mut c_char,
197    ) -> bool {
198        // Negative or oversized lengths are allocation failures: returning
199        // true with no usable buffer would hand the reader a dangling pointer,
200        // and an oversized reserve_exact would abort on OOM.
201        if length < 0 { return false; }
202        let len = length as usize;
203        if len > MAX_BINDER_STRING_LEN { return false; }
204        let s = unsafe { &mut *(cookie as *mut StringBuf) };
205        s.0.reserve_exact(len + 1);
206        unsafe { s.0.as_mut_vec().resize(len + 1, 0) };
207        unsafe { *buffer = s.0.as_mut_ptr() as *mut c_char };
208        true
209    }
210
211    struct StringBuf(String);
212    impl StringBuf {
213        fn new() -> Self { Self(String::new()) }
214        fn finish(mut self) -> Option<String> {
215            if let Some(pos) = self.0.as_bytes().iter().position(|&b| b == 0) {
216                unsafe { self.0.as_mut_vec().truncate(pos) };
217            }
218            if self.0.is_empty() { None } else { Some(self.0) }
219        }
220    }
221
222    // ── Vtable ────────────────────────────────────────────────────────────────
223
224    struct Vtable {
225        get_service:         unsafe extern "C" fn(*const c_char) -> *mut AIBinder,
226        class_define:        unsafe extern "C" fn(
227                                 *const c_char,
228                                 unsafe extern "C" fn(*mut c_void) -> *mut c_void,
229                                 unsafe extern "C" fn(*mut c_void),
230                                 unsafe extern "C" fn(*mut AIBinder, u32, *const AParcel, *mut AParcel) -> BinderStatus,
231                             ) -> *mut AIBinder_Class,
232        associate_class:     unsafe extern "C" fn(*mut AIBinder, *mut AIBinder_Class) -> bool,
233        new_binder:          unsafe extern "C" fn(*const AIBinder_Class, *mut c_void) -> *mut AIBinder,
234        prepare_transaction: unsafe extern "C" fn(*mut AIBinder, *mut *mut AParcel) -> BinderStatus,
235        transact:            unsafe extern "C" fn(*mut AIBinder, u32, *mut *mut AParcel, *mut *mut AParcel, u32) -> BinderStatus,
236        dec_strong:          unsafe extern "C" fn(*mut AIBinder),
237        parcel_delete:       unsafe extern "C" fn(*mut AParcel),
238        read_int32:          unsafe extern "C" fn(*const AParcel, *mut i32) -> BinderStatus,
239        read_string:         unsafe extern "C" fn(*const AParcel, *mut c_void, StringAllocator) -> BinderStatus,
240        write_strong_binder: unsafe extern "C" fn(*mut AParcel, *mut AIBinder) -> BinderStatus,
241        set_thread_pool_max: unsafe extern "C" fn(u32),
242        join_thread_pool:    unsafe extern "C" fn(),
243        get_user_data:       unsafe extern "C" fn(*const AIBinder) -> *mut c_void,
244        write_int32:         unsafe extern "C" fn(*mut AParcel, i32) -> BinderStatus,
245        // Optional: only present on API 29+, but all modern Android has this
246        read_bool:           Option<unsafe extern "C" fn(*const AParcel, *mut bool) -> BinderStatus>,
247    }
248
249    // ── RAII wrappers ─────────────────────────────────────────────────────────
250
251    struct DlHandle(*mut c_void);
252    unsafe impl Send for DlHandle {}
253    impl Drop for DlHandle {
254        fn drop(&mut self) {
255            // Intentionally no dlclose: the binder thread pool spawned in
256            // open_with_observer() keeps executing library code until process
257            // exit. Unloading the library while that thread runs causes
258            // use-after-free. libbinder_ndk.so is never unloaded during the
259            // daemon lifetime; the OS reclaims it on exit.
260        }
261    }
262
263    struct OwnedParcel { ptr: *mut AParcel, delete: unsafe extern "C" fn(*mut AParcel) }
264    impl Drop for OwnedParcel {
265        fn drop(&mut self) { if !self.ptr.is_null() { unsafe { (self.delete)(self.ptr) }; } }
266    }
267
268    struct OwnedBinder { ptr: *mut AIBinder, dec_strong: unsafe extern "C" fn(*mut AIBinder) }
269    unsafe impl Send for OwnedBinder {}
270    impl Drop for OwnedBinder {
271        fn drop(&mut self) { if !self.ptr.is_null() { unsafe { (self.dec_strong)(self.ptr) }; } }
272    }
273
274    // ── dlsym helper ─────────────────────────────────────────────────────────
275
276    macro_rules! dlsym_fn {
277        ($handle:expr, $name:literal, $ty:ty) => {{
278            let sym = unsafe {
279                libc::dlsym($handle, concat!($name, "\0").as_ptr() as *const c_char)
280            };
281            if sym.is_null() {
282                return Err(CoreError::binder(-1, concat!("dlsym:", $name)));
283            }
284            unsafe { std::mem::transmute::<*mut c_void, $ty>(sym) }
285        }};
286    }
287
288    macro_rules! dlsym_opt {
289        ($handle:expr, $name:literal, $ty:ty) => {{
290            let sym = unsafe {
291                libc::dlsym($handle, concat!($name, "\0").as_ptr() as *const c_char)
292            };
293            if sym.is_null() { None }
294            else { Some(unsafe { std::mem::transmute::<*mut c_void, $ty>(sym) }) }
295        }};
296    }
297
298    fn load_vtable(handle: *mut c_void) -> Result<Vtable, CoreError> {
299        Ok(Vtable {
300            get_service: dlsym_fn!(handle, "AServiceManager_getService",
301                unsafe extern "C" fn(*const c_char) -> *mut AIBinder),
302            class_define: dlsym_fn!(handle, "AIBinder_Class_define",
303                unsafe extern "C" fn(
304                    *const c_char,
305                    unsafe extern "C" fn(*mut c_void) -> *mut c_void,
306                    unsafe extern "C" fn(*mut c_void),
307                    unsafe extern "C" fn(*mut AIBinder, u32, *const AParcel, *mut AParcel) -> BinderStatus,
308                ) -> *mut AIBinder_Class),
309            associate_class: dlsym_fn!(handle, "AIBinder_associateClass",
310                unsafe extern "C" fn(*mut AIBinder, *mut AIBinder_Class) -> bool),
311            new_binder: dlsym_fn!(handle, "AIBinder_new",
312                unsafe extern "C" fn(*const AIBinder_Class, *mut c_void) -> *mut AIBinder),
313            prepare_transaction: dlsym_fn!(handle, "AIBinder_prepareTransaction",
314                unsafe extern "C" fn(*mut AIBinder, *mut *mut AParcel) -> BinderStatus),
315            transact: dlsym_fn!(handle, "AIBinder_transact",
316                unsafe extern "C" fn(*mut AIBinder, u32, *mut *mut AParcel, *mut *mut AParcel, u32) -> BinderStatus),
317            dec_strong: dlsym_fn!(handle, "AIBinder_decStrong",
318                unsafe extern "C" fn(*mut AIBinder)),
319            parcel_delete: dlsym_fn!(handle, "AParcel_delete",
320                unsafe extern "C" fn(*mut AParcel)),
321            read_int32: dlsym_fn!(handle, "AParcel_readInt32",
322                unsafe extern "C" fn(*const AParcel, *mut i32) -> BinderStatus),
323            read_string: dlsym_fn!(handle, "AParcel_readString",
324                unsafe extern "C" fn(*const AParcel, *mut c_void, StringAllocator) -> BinderStatus),
325            write_strong_binder: dlsym_fn!(handle, "AParcel_writeStrongBinder",
326                unsafe extern "C" fn(*mut AParcel, *mut AIBinder) -> BinderStatus),
327            set_thread_pool_max: dlsym_fn!(handle, "ABinderProcess_setThreadPoolMaxThreadCount",
328                unsafe extern "C" fn(u32)),
329            join_thread_pool: dlsym_fn!(handle, "ABinderProcess_joinThreadPool",
330                unsafe extern "C" fn()),
331            get_user_data: dlsym_fn!(handle, "AIBinder_getUserData",
332                unsafe extern "C" fn(*const AIBinder) -> *mut c_void),
333            write_int32: dlsym_fn!(handle, "AParcel_writeInt32",
334                unsafe extern "C" fn(*mut AParcel, i32) -> BinderStatus),
335            read_bool: dlsym_opt!(handle, "AParcel_readBool",
336                unsafe extern "C" fn(*const AParcel, *mut bool) -> BinderStatus),
337        })
338    }
339
340    // ── ParcelReader ──────────────────────────────────────────────────────────
341
342    struct ParcelReader<'a> { vt: &'a Vtable, parcel: &'a OwnedParcel }
343
344    impl<'a> ParcelReader<'a> {
345        fn read_i32(&self) -> Result<i32, CoreError> {
346            let mut v = 0i32;
347            let s = unsafe { (self.vt.read_int32)(self.parcel.ptr, &mut v) };
348            if s != STATUS_OK { return Err(CoreError::binder(s, "AParcel_readInt32")); }
349            Ok(v)
350        }
351        fn read_string(&self) -> Result<Option<String>, CoreError> {
352            let mut buf = StringBuf::new();
353            let s = unsafe {
354                (self.vt.read_string)(self.parcel.ptr, &mut buf as *mut StringBuf as *mut c_void, string_alloc)
355            };
356            if s != STATUS_OK { return Err(CoreError::binder(s, "AParcel_readString")); }
357            Ok(buf.finish())
358        }
359        fn skip_i32s(&self, n: usize) -> Result<(), CoreError> {
360            for _ in 0..n { self.read_i32()?; }
361            Ok(())
362        }
363        fn skip_int_array(&self) -> Result<(), CoreError> {
364            let count = self.read_i32()?.max(0) as usize;
365            self.skip_i32s(count)
366        }
367        fn read_first_package_from_names(&self) -> Result<Option<String>, CoreError> {
368            let count = self.read_i32()?.max(0) as usize;
369            let mut first: Option<String> = None;
370            for _ in 0..count {
371                let s = self.read_string()?;
372                if first.is_none() {
373                    first = s.and_then(|c| c.split('/').next().map(str::to_owned));
374                }
375            }
376            Ok(first)
377        }
378    }
379
380    // ── ParcelWriter / transact helper ────────────────────────────────────────
381
382    struct ParcelWriter<'a> { vt: &'a Vtable, parcel: &'a OwnedParcel }
383
384    impl<'a> ParcelWriter<'a> {
385        fn write_i32(&self, v: i32) -> Result<(), CoreError> {
386            let s = unsafe { (self.vt.write_int32)(self.parcel.ptr, v) };
387            if s != STATUS_OK { return Err(CoreError::binder(s, "AParcel_writeInt32")); }
388            Ok(())
389        }
390        fn write_strong_binder(&self, b: *mut AIBinder) -> Result<(), CoreError> {
391            let s = unsafe { (self.vt.write_strong_binder)(self.parcel.ptr, b) };
392            if s != STATUS_OK { return Err(CoreError::binder(s, "AParcel_writeStrongBinder")); }
393            Ok(())
394        }
395    }
396
397    /// Prepare an input parcel, run `writes`, then transact. RAII on both
398    /// ends: a write error drops the input parcel (previously it leaked on the
399    /// write-error path, never reaching transact), and the reply parcel is
400    /// returned owned. The input parcel is transferred to `AIBinder_transact`
401    /// (the framework deletes it even on failure), so the wrapper records that
402    /// by nulling its pointer — never a double-delete.
403    fn transact_write(
404        vt: &Vtable,
405        binder: *mut AIBinder,
406        code: u32,
407        writes: impl FnOnce(&ParcelWriter<'_>) -> Result<(), CoreError>,
408    ) -> Result<OwnedParcel, CoreError> {
409        let mut in_ptr: *mut AParcel = std::ptr::null_mut();
410        let s = unsafe { (vt.prepare_transaction)(binder, &mut in_ptr) };
411        if s != STATUS_OK {
412            return Err(CoreError::binder(s, "AIBinder_prepareTransaction"));
413        }
414        let mut inp = OwnedParcel { ptr: in_ptr, delete: vt.parcel_delete };
415        {
416            let writer = ParcelWriter { vt, parcel: &inp };
417            writes(&writer)?;
418        }
419        let mut out_ptr: *mut AParcel = std::ptr::null_mut();
420        let s = unsafe { (vt.transact)(binder, code, &mut inp.ptr, &mut out_ptr, 0) };
421        // The framework owns (and deletes) the input parcel from here.
422        inp.ptr = std::ptr::null_mut();
423        let out = OwnedParcel { ptr: out_ptr, delete: vt.parcel_delete };
424        if s != STATUS_OK {
425            return Err(CoreError::binder(s, "AIBinder_transact"));
426        }
427        Ok(out)
428    }
429
430    // ── Response parsers ──────────────────────────────────────────────────────
431
432    fn parse_stack_info_body(r: &ParcelReader<'_>) -> Result<Option<String>, CoreError> {
433        r.skip_i32s(5)?;
434        r.skip_int_array()?;
435        r.read_first_package_from_names()
436    }
437
438    // RootTaskInfo → (taskId, first childTaskName package). Walks the parcel
439    // once: prefix (bounds, childTaskIds), captures the first package from
440    // childTaskNames, then skips childTaskBounds / childTaskUserIds / visible /
441    // position / TaskInfo.userId to reach taskId — never touching the
442    // Intent/TaskInfo tail. taskId and pkg come from the same transaction, so
443    // callers pair them without a second (racy) round-trip.
444    fn parse_root_task_info_task(r: &ParcelReader<'_>) -> Result<(i32, Option<String>), CoreError> {
445        let scratch = r.read_i32()?;
446        if scratch != 0 { r.skip_i32s(4)?; }
447        r.skip_int_array()?; // childTaskIds
448        let pkg = r.read_first_package_from_names()?; // childTaskNames → pkg
449        // childTaskBounds: typed Rect array (nullable) — read count, skip 4 per entry
450        let bounds_count = r.read_i32()?;
451        let n = if bounds_count < 0 { 0 } else { bounds_count as usize };
452        for _ in 0..n {
453            let entry = r.read_i32()?;
454            if entry != 0 { r.skip_i32s(4)?; }
455        }
456        r.skip_int_array()?; // childTaskUserIds
457        r.skip_i32s(2)?;     // visible, position
458        r.skip_i32s(1)?;     // TaskInfo.userId
459        let task_id = r.read_i32()?;
460        Ok((task_id, pkg))
461    }
462
463    // ── Tx code resolution ────────────────────────────────────────────────────
464
465    pub struct TxCodes {
466        pub observer_code: u32,
467        pub query_code:    u32,
468        pub api_mode:      u8,  // 1 = RootTaskInfo, 2 = StackInfo
469        pub fg_code:       u32,
470    }
471
472    pub fn resolve_tx_codes() -> Result<TxCodes, CoreError> {
473        let (obs, query, api, fg) = dex::resolve_tx_codes_from_dex()
474            .ok_or_else(|| CoreError::binder(-1, "tx_code_resolution:dex_parse_failed"))?;
475        Ok(TxCodes { observer_code: obs, query_code: query, api_mode: api, fg_code: fg })
476    }
477
478    // ── ActivityManagerBinder ─────────────────────────────────────────────────
479
480    pub struct ActivityManagerBinder {
481        _lib:    DlHandle,
482        vt:      Vtable,
483        _class:  *mut AIBinder_Class,
484        service: OwnedBinder,
485        tx_code: u32,
486        legacy:  bool,
487    }
488    unsafe impl Send for ActivityManagerBinder {}
489
490    impl ActivityManagerBinder {
491        fn open_inner(handle: *mut c_void) -> Result<(DlHandle, Vtable, *mut AIBinder_Class, OwnedBinder), CoreError> {
492            let lib = DlHandle(handle);
493            let vt = load_vtable(handle)?;
494
495            let am_class = unsafe {
496                (vt.class_define)(
497                    AM_DESCRIPTOR.as_ptr() as *const c_char,
498                    am_on_create, am_on_destroy, am_on_transact,
499                )
500            };
501            if am_class.is_null() { return Err(CoreError::binder(-1, "AIBinder_Class_define:AM")); }
502
503            let raw = unsafe { (vt.get_service)(ACTIVITY_SERVICE.as_ptr() as *const c_char) };
504            if raw.is_null() { return Err(CoreError::binder(-1, "AServiceManager_getService:activity")); }
505            unsafe { (vt.associate_class)(raw, am_class) };
506
507            let service = OwnedBinder { ptr: raw, dec_strong: vt.dec_strong };
508            Ok((lib, vt, am_class, service))
509        }
510
511        fn dlopen_libbinder() -> Result<*mut c_void, CoreError> {
512            use std::os::raw::c_char;
513            let handle = unsafe {
514                libc::dlopen(LIBBINDER_PATH.as_ptr() as *const c_char, libc::RTLD_NOW | libc::RTLD_LOCAL)
515            };
516            if handle.is_null() { return Err(CoreError::binder(-1, "dlopen:libbinder_ndk.so")); }
517            Ok(handle)
518        }
519
520        /// Open ActivityManager binder (polling mode — no observer).
521        /// Resolves the query tx code from cache or DEX.
522        pub fn open() -> Result<Self, CoreError> {
523            let handle = Self::dlopen_libbinder()?;
524            let (lib, vt, class, service) = Self::open_inner(handle)?;
525            let codes = resolve_tx_codes()?;
526            let legacy = codes.api_mode == 2;
527            Ok(Self { _lib: lib, vt, _class: class, service, tx_code: codes.query_code, legacy })
528        }
529
530        /// Open ActivityManager binder and register as IProcessObserver.
531        ///
532        /// Returns `(Self, OwnedFd)` where the eventfd is a dup of the core's
533        /// callback fd. It becomes readable whenever `onForegroundActivitiesChanged`
534        /// fires. Caller must add it to epoll and may close it at any time — the
535        /// callback keeps writing to the core's copy, so closing the returned
536        /// fd never invalidates the notification path (C2). After the event
537        /// fires, call `get_focused_package`.
538        pub fn open_with_observer() -> Result<(Self, OwnedFd), CoreError> {
539            let handle = Self::dlopen_libbinder()?;
540            let (lib, vt, am_class, service) = Self::open_inner(handle)?;
541            let codes = resolve_tx_codes()?;
542            let legacy = codes.api_mode == 2;
543
544            // Create eventfd for callback → epoll bridge. Ownership stays in the
545            // core for the observer lifetime; the consumer receives a dup below.
546            let owned = unsafe {
547                let raw = libc::eventfd(0, libc::EFD_NONBLOCK | libc::EFD_CLOEXEC);
548                if raw < 0 { return Err(CoreError::sys(*libc::__errno(), "eventfd")); }
549                OwnedFd::from_raw_fd(raw)
550            };
551
552            // Define IProcessObserver class (we're the server)
553            let obs_class = unsafe {
554                (vt.class_define)(
555                    OBS_DESCRIPTOR.as_ptr() as *const c_char,
556                    obs_on_create, obs_on_destroy, obs_on_transact,
557                )
558            };
559            if obs_class.is_null() {
560                return Err(CoreError::binder(-1, "AIBinder_Class_define:Observer"));
561            }
562
563            // Instantiate our observer binder object
564            let obs_binder = unsafe { (vt.new_binder)(obs_class, std::ptr::null_mut()) };
565            if obs_binder.is_null() {
566                return Err(CoreError::binder(-1, "AIBinder_new:Observer"));
567            }
568            unsafe { (vt.associate_class)(obs_binder, obs_class) };
569
570            // Call registerProcessObserver(observer)
571            let _ = transact_write(&vt, service.ptr, codes.observer_code, |w| {
572                w.write_strong_binder(obs_binder)
573            })?;
574
575            // Consumer dup — made before publishing, so an error path drops the
576            // owned fd without ever leaving a stale handle for the callback.
577            let consumer = owned.try_clone()
578                .map_err(|e| CoreError::sys(e.raw_os_error().unwrap_or(-1), "dup:observer"))?;
579
580            // Publish fg_code and the core-owned eventfd for the callback
581            OBS_FG_CODE.store(codes.fg_code, Ordering::Relaxed);
582            *obs_eventfd_guard() = Some(owned);
583
584            // Start binder thread pool — blocks forever in background thread
585            unsafe { (vt.set_thread_pool_max)(0) };
586            let join_fn = vt.join_thread_pool;
587            std::thread::spawn(move || unsafe { join_fn() });
588
589            let binder = Self { _lib: lib, vt, _class: am_class, service, tx_code: codes.query_code, legacy };
590            Ok((binder, consumer))
591        }
592
593        /// Open ActivityManager binder and register as the foreground process
594        /// observer.
595        ///
596        /// The authoritative foreground PID is delivered in the callback; this
597        /// is the low-noise foreground source. Two ROM variants are supported
598        /// and selected automatically:
599        ///
600        /// - Stock: `IForegroundProcessObserver.onForegroundProcessChanged`
601        ///   delivers a single `int pid`.
602        /// - Custom ROMs that dropped that interface instead deliver `(int pid,
603        ///   int uid, int fg)` through the repurposed
604        ///   `IProcessObserver.onForegroundActivitiesChanged`; this registers
605        ///   via `registerProcessObserver` and only signals on `fg != 0`.
606        ///
607        /// The callback stores the PID (readable via [`last_foreground_pid`])
608        /// and signals the returned eventfd.
609        ///
610        /// Returns `(Self, OwnedFd)` where the eventfd is a dup of the core's
611        /// callback fd. It becomes readable whenever a foreground process
612        /// change fires. Same lifetime contract as
613        /// [`ActivityManagerBinder::open_with_observer`] (C2): the core owns
614        /// the eventfd and the callback only ever writes to that copy, so
615        /// closing the returned dup never invalidates the notification path.
616        pub fn open_with_fgproc_observer() -> Result<(Self, OwnedFd), CoreError> {
617            let handle = Self::dlopen_libbinder()?;
618            let (lib, vt, am_class, service) = Self::open_inner(handle)?;
619
620            // Resolve the foreground-observer tx codes. Prefer the stock
621            // IForegroundProcessObserver path; fall back to the custom
622            // IProcessObserver pid-carrying form on ROMs that dropped it.
623            // mode 0 = stock single-int callback, mode 1 = (pid, uid, fg).
624            let (register_code, fgproc_code, mode, descriptor): (u32, u32, u32, &[u8]) =
625                match crate::dex::resolve_fgproc_codes() {
626                    Some((r, c)) => (r, c, 0, FGPROC_DESCRIPTOR),
627                    None => match crate::dex::resolve_fgproc_codes_fallback() {
628                        Some((r, c)) => (r, c, 1, OBS_DESCRIPTOR),
629                        None => {
630                            return Err(CoreError::binder(-1, "tx_code_resolution:fgproc_dex_parse_failed"));
631                        }
632                    },
633                };
634
635            // Create eventfd for callback → epoll bridge. Ownership stays in the
636            // core for the observer lifetime; the consumer receives a dup below.
637            let owned = unsafe {
638                let raw = libc::eventfd(0, libc::EFD_NONBLOCK | libc::EFD_CLOEXEC);
639                if raw < 0 { return Err(CoreError::sys(*libc::__errno(), "eventfd")); }
640                OwnedFd::from_raw_fd(raw)
641            };
642
643            // Define our observer class (we're the server). The descriptor must
644            // match whichever interface we actually register as.
645            let obs_class = unsafe {
646                (vt.class_define)(
647                    descriptor.as_ptr() as *const c_char,
648                    fgproc_on_create, fgproc_on_destroy, fgproc_on_transact,
649                )
650            };
651            if obs_class.is_null() {
652                return Err(CoreError::binder(-1, "AIBinder_Class_define:FGProcessObserver"));
653            }
654
655            // Instantiate our observer binder object
656            let obs_binder = unsafe { (vt.new_binder)(obs_class, std::ptr::null_mut()) };
657            if obs_binder.is_null() {
658                return Err(CoreError::binder(-1, "AIBinder_new:FGProcessObserver"));
659            }
660            unsafe { (vt.associate_class)(obs_binder, obs_class) };
661
662            // Call registerForegroundProcessObserver(observer) or the fallback
663            // registerProcessObserver(observer) depending on resolved mode.
664            let _ = transact_write(&vt, service.ptr, register_code, |w| {
665                w.write_strong_binder(obs_binder)
666            })?;
667
668            // Consumer dup — made before publishing, so an error path drops the
669            // owned fd without ever leaving a stale handle for the callback.
670            let consumer = owned.try_clone()
671                .map_err(|e| CoreError::sys(e.raw_os_error().unwrap_or(-1), "dup:fgproc_observer"))?;
672
673            // Publish reader fn, mode, fg code, pid base, and the core-owned
674            // eventfd for the callback. Reader and mode are published first so
675            // the callback never sees a matching code with an unset reader or
676            // mode (C2-adjacent init order).
677            FGPROC_READ_I32.store(vt.read_int32 as usize, Ordering::Relaxed);
678            FGPROC_IPROC_MODE.store(mode, Ordering::Relaxed);
679            FGPROC_FG_CODE.store(fgproc_code, Ordering::Relaxed);
680            FGPROC_PID.store(0, Ordering::Relaxed);
681            *fgproc_eventfd_guard() = Some(owned);
682
683            // Start binder thread pool — blocks forever in background thread
684            unsafe { (vt.set_thread_pool_max)(0) };
685            let join_fn = vt.join_thread_pool;
686            std::thread::spawn(move || unsafe { join_fn() });
687
688            let binder = Self { _lib: lib, vt, _class: am_class, service, tx_code: 0, legacy: false };
689            Ok((binder, consumer))
690        }
691
692        fn do_transact(&self) -> Result<OwnedParcel, CoreError> {
693            transact_write(&self.vt, self.service.ptr, self.tx_code, |_| Ok(()))
694        }
695
696        /// The focused root task's `(taskId, topActivity package)` from a single
697        /// txn-31 transaction. Outer `None` = no focused root task (or legacy
698        /// API 29, where the reply is `StackInfo` and carries no taskId); inner
699        /// `None` = task known but no package in `childTaskNames`. Both values
700        /// come from the same parcel, so the registration key and the report
701        /// tag can never diverge.
702        pub fn get_focused_task(&self) -> Result<Option<(i32, Option<String>)>, CoreError> {
703            if self.legacy {
704                // StackInfo has no taskId — report None rather than a wrong id.
705                return Ok(None);
706            }
707            let out = self.do_transact()?;
708            let r = ParcelReader { vt: &self.vt, parcel: &out };
709            let ex = r.read_i32()?;
710            if ex != EX_NONE { return Err(CoreError::binder(ex, "getFocusedTask:exception")); }
711            let present = r.read_i32()?;
712            if present == 0 { return Ok(None); }
713            Ok(Some(parse_root_task_info_task(&r)?))
714        }
715
716        /// The `topActivity` package of the focused root task (legacy API 29
717        /// builds use `StackInfo` and still resolve the package). Thin wrapper
718        /// over [`ActivityManagerBinder::get_focused_task`].
719        pub fn get_focused_package(&self) -> Result<Option<String>, CoreError> {
720            if self.legacy {
721                let out = self.do_transact()?;
722                let r = ParcelReader { vt: &self.vt, parcel: &out };
723                let ex = r.read_i32()?;
724                if ex != EX_NONE { return Err(CoreError::binder(ex, "getFocusedTask:exception")); }
725                let present = r.read_i32()?;
726                if present == 0 { return Ok(None); }
727                return parse_stack_info_body(&r);
728            }
729            Ok(self.get_focused_task()?.map(|(_, pkg)| pkg).flatten())
730        }
731
732        /// The `taskId` of the currently focused root task. Thin wrapper over
733        /// [`ActivityManagerBinder::get_focused_task`]; returns `None` when there
734        /// is no focused root task or on legacy API 29 builds.
735        pub fn get_focused_task_id(&self) -> Result<Option<i32>, CoreError> {
736            Ok(self.get_focused_task()?.map(|(task_id, _)| task_id))
737        }
738    }
739
740    // ── DisplayManagerBinder ─────────────────────────────────────────────────
741
742    const DISPLAY_SERVICE:    &[u8] = b"display\0";
743    const DISPLAY_DESCRIPTOR: &[u8] = b"android.hardware.display.IDisplayManager\0";
744    const CALLBACK_DESCRIPTOR: &[u8] = b"android.hardware.display.IDisplayManagerCallback\0";
745    const POWER_SERVICE:      &[u8] = b"power\0";
746
747    const TX_DISPLAY_REGISTER_CALLBACK: u32 = 4;
748
749    // Core owns the callback eventfd; the consumer gets a dup and may close it
750    // freely. Same lifetime discipline as the ActivityManager observer (C2).
751    static DISP_EVENTFD: Mutex<Option<OwnedFd>> = Mutex::new(None);
752
753    fn disp_eventfd_guard() -> std::sync::MutexGuard<'static, Option<OwnedFd>> {
754        DISP_EVENTFD.lock().unwrap_or_else(|p| p.into_inner())
755    }
756
757    unsafe extern "C" fn disp_cb_on_create(_: *mut c_void) -> *mut c_void { std::ptr::null_mut() }
758    unsafe extern "C" fn disp_cb_on_destroy(_: *mut c_void) {}
759    unsafe extern "C" fn disp_cb_on_transact(
760        _: *mut AIBinder, code: u32, _: *const AParcel, _: *mut AParcel,
761    ) -> BinderStatus {
762        if code == 1 {
763            if let Some(fd) = disp_eventfd_guard().as_ref() {
764                let val: u64 = 1;
765                unsafe { libc::write(fd.as_raw_fd(), &val as *const u64 as *const c_void, 8) };
766            }
767        }
768        STATUS_OK
769    }
770
771    pub struct DisplayManagerBinder {
772        _lib:           DlHandle,
773        vt:             Vtable,
774        display:        OwnedBinder,
775        power:          Option<OwnedBinder>,
776        is_interactive_tx: u32,
777    }
778    unsafe impl Send for DisplayManagerBinder {}
779
780    impl DisplayManagerBinder {
781        pub fn open_with_callback() -> Result<(Self, crate::reactor::Fd), CoreError> {
782            let handle = unsafe {
783                libc::dlopen(LIBBINDER_PATH.as_ptr() as *const c_char, libc::RTLD_NOW | libc::RTLD_LOCAL)
784            };
785            if handle.is_null() { return Err(CoreError::binder(-1, "dlopen:libbinder_ndk.so")); }
786            let lib = DlHandle(handle);
787            let vt = load_vtable(handle)?;
788
789            // Blocking eventfd (no EFD_NONBLOCK) — callback writes, caller's
790            // read_u64_blocking() waits. The core owns it for the callback's
791            // lifetime; the consumer receives a dup below (C2).
792            let owned = unsafe {
793                let raw = libc::eventfd(0, libc::EFD_CLOEXEC);
794                if raw < 0 { return Err(CoreError::sys(*libc::__errno(), "eventfd")); }
795                OwnedFd::from_raw_fd(raw)
796            };
797
798            // Get display service (no class_define needed for client-only)
799            let raw_display = unsafe { (vt.get_service)(DISPLAY_SERVICE.as_ptr() as *const c_char) };
800            if raw_display.is_null() {
801                return Err(CoreError::binder(-1, "AServiceManager_getService:display"));
802            }
803            let display = OwnedBinder { ptr: raw_display, dec_strong: vt.dec_strong };
804
805            // Define IDisplayManagerCallback (we're the server receiving callbacks)
806            let cb_class = unsafe {
807                (vt.class_define)(
808                    CALLBACK_DESCRIPTOR.as_ptr() as *const c_char,
809                    disp_cb_on_create, disp_cb_on_destroy, disp_cb_on_transact,
810                )
811            };
812            if cb_class.is_null() {
813                return Err(CoreError::binder(-1, "AIBinder_Class_define:DisplayCallback"));
814            }
815
816            let cb_binder = unsafe { (vt.new_binder)(cb_class, std::ptr::null_mut()) };
817            if cb_binder.is_null() {
818                return Err(CoreError::binder(-1, "AIBinder_new:DisplayCallback"));
819            }
820
821            // registerCallback(callback) — tx 4
822            let _ = transact_write(
823                &vt,
824                display.ptr,
825                TX_DISPLAY_REGISTER_CALLBACK,
826                |w| w.write_strong_binder(cb_binder),
827            )?;
828
829            // Optional: grab power service for is_interactive()
830            let power = {
831                let raw = unsafe { (vt.get_service)(POWER_SERVICE.as_ptr() as *const c_char) };
832                if raw.is_null() { None } else { Some(OwnedBinder { ptr: raw, dec_strong: vt.dec_strong }) }
833            };
834
835            // Resolve isInteractive tx code from DEX at open time
836            let is_interactive_tx = crate::dex::resolve_is_interactive_tx()
837                .ok_or_else(|| CoreError::binder(-1, "dex:TRANSACTION_isInteractive not found"))?;
838
839            // Consumer dup — made before publishing, so an error path drops the
840            // owned fd without ever leaving a stale handle for the callback.
841            let efd_owned = owned.try_clone()
842                .map_err(|e| CoreError::sys(e.raw_os_error().unwrap_or(-1), "dup:display"))
843                .and_then(|dup| unsafe {
844                    crate::reactor::Fd::from_owned_raw_fd(dup.into_raw_fd(), "display.efd")
845                        .map_err(|_| CoreError::binder(-1, "Fd::from_owned_raw_fd:display.efd"))
846                })?;
847
848            // Publish the core-owned eventfd for the callback
849            *disp_eventfd_guard() = Some(owned);
850
851            // Join binder thread pool so callbacks can fire
852            unsafe { (vt.set_thread_pool_max)(0) };
853            let join_fn = vt.join_thread_pool;
854            std::thread::spawn(move || unsafe { join_fn() });
855
856            Ok((Self { _lib: lib, vt, display, power, is_interactive_tx }, efd_owned))
857        }
858
859        pub fn is_interactive(&self) -> Result<bool, CoreError> {
860            let power = self.power.as_ref()
861                .ok_or_else(|| CoreError::binder(-1, "power:unavailable"))?;
862            let out = transact_write(&self.vt, power.ptr, self.is_interactive_tx, |_| Ok(()))?;
863            let r = ParcelReader { vt: &self.vt, parcel: &out };
864            let ex = r.read_i32()?;
865            if ex != EX_NONE { return Err(CoreError::binder(ex, "isInteractive:exception")); }
866            if let Some(rb) = self.vt.read_bool {
867                let mut v = false;
868                let s = unsafe { rb(out.ptr as *const AParcel, &mut v) };
869                if s != STATUS_OK { return Err(CoreError::binder(s, "readBool:isInteractive")); }
870                Ok(v)
871            } else {
872                Ok(r.read_i32()? != 0)
873            }
874        }
875    }
876
877    // ── FpsListener (task FPS callback) ───────────────────────────────────────
878
879    const WINDOW_SERVICE:    &[u8] = b"window\0";
880    const WM_DESCRIPTOR:     &[u8] = b"android.view.IWindowManager\0";
881    const FPS_DESCRIPTOR:    &[u8] = b"android.window.ITaskFpsCallback\0";
882    // The last reported FPS (bit pattern of the f32) is published before the
883    // eventfd is signalled, so the consumer never reads a stale value. The wake
884    // eventfd is per-instance (same pattern as TaskStackListener): it is handed
885    // to AIBinder_new as the callback binder's userdata, so a second
886    // FpsListener can never rewire an earlier registration's wake into its own
887    // fd (the callback resolves its own binder's fd via AIBinder_getUserData).
888    static FPS_VALUE: AtomicU32 = AtomicU32::new(0);
889    // Distinct from FPS_VALUE's bits: `0.0f32` has bit pattern 0, so a "not
890    // seen" sentinel of 0 would misread a genuine idle (0-FPS) report as "no
891    // report yet" — swallowing the sample and (downstream) leaving the first-
892    // report-after-swap drop armed. The seen flag disambiguates.
893    static FPS_SEEN: AtomicBool = AtomicBool::new(false);
894    static FPS_CODE: AtomicU32 = AtomicU32::new(0);
895    static FPS_READ_I32: AtomicUsize = AtomicUsize::new(0);
896
897    // No-op callbacks for the client-only IWindowManager class (we never serve
898    // transactions on the `window` binder — the class exists only to satisfy
899    // AIBinder_prepareTransaction's remote-transaction contract).
900    unsafe extern "C" fn wm_on_create(_: *mut c_void) -> *mut c_void { std::ptr::null_mut() }
901    unsafe extern "C" fn wm_on_destroy(_: *mut c_void) {}
902    unsafe extern "C" fn wm_on_transact(
903        _: *mut AIBinder, _: u32, _: *const AParcel, _: *mut AParcel,
904    ) -> BinderStatus { STATUS_OK }
905
906    // The per-instance wake eventfd is this callback binder's userdata, so
907    // onCreate must return the args passed to AIBinder_new — AIBinder_getUserData
908    // returns exactly that value — and onDestroy must reclaim the box (same
909    // pattern as TaskStackListener). Returning null here would make
910    // AIBinder_getUserData return null, silently breaking the FPS wake.
911    unsafe extern "C" fn fps_on_create(userdata: *mut c_void) -> *mut c_void { userdata }
912    unsafe extern "C" fn fps_on_destroy(userdata: *mut c_void) {
913        if !userdata.is_null() {
914            unsafe { drop(Box::from_raw(userdata as *mut OwnedFd)) };
915        }
916    }
917    unsafe extern "C" fn fps_on_transact(
918        binder: *mut AIBinder, code: u32, in_parcel: *const AParcel, _: *mut AParcel,
919    ) -> BinderStatus {
920        if code != FPS_CODE.load(Ordering::Relaxed) {
921            return STATUS_UNKNOWN_TRANSACTION;
922        }
923        // Reader is published non-zero before the code, so a matching code is
924        // never paired with an unset reader.
925        let read_addr = FPS_READ_I32.load(Ordering::Relaxed);
926        if read_addr != 0 {
927            let read_fn: unsafe extern "C" fn(*const AParcel, *mut i32) -> BinderStatus =
928                unsafe { std::mem::transmute(read_addr) };
929            let mut bits: i32 = 0;
930            if unsafe { read_fn(in_parcel, &mut bits) } == STATUS_OK {
931                // Publish the value before signalling so the consumer always
932                // sees the value that triggered the wakeup. Non-finite/negative
933                // reports are normalized to 0.0 first (L5) — the stream must
934                // never carry "NaN"/"inf".
935                FPS_VALUE.store(sanitize_fps(bits as u32), Ordering::Relaxed);
936                FPS_SEEN.store(true, Ordering::Release);
937                // Per-instance wake: the eventfd is this callback binder's
938                // userdata (see FpsListener::open), so two FpsListeners never
939                // cross-wire their wakes. A missing slot means the process-wide
940                // AIBinder_getUserData symbol has not been cached — drop the
941                // signal rather than risk a stale fd.
942                let get_user_data = GET_USER_DATA
943                    .lock()
944                    .unwrap_or_else(|p| p.into_inner());
945                if let Some(get_user_data) = *get_user_data {
946                    let userdata = unsafe { get_user_data(binder) };
947                    if !userdata.is_null() {
948                        let efd = userdata as *mut OwnedFd;
949                        let val: u64 = 1;
950                        unsafe {
951                            libc::write((*efd).as_raw_fd(), &val as *const u64 as *const c_void, 8)
952                        };
953                    }
954                }
955            }
956        }
957        STATUS_OK
958    }
959
960    /// Push-based per-task FPS listener registered with `WindowManager`.
961    ///
962    /// Uses `IWindowManager.registerTaskFpsCallback(taskId, callback)`; the
963    /// daemon hosts the `ITaskFpsCallback` server object and receives
964    /// `onFpsReported(float)` one-way transactions from the `FpsReporter` at
965    /// most every ~500 ms.
966    ///
967    /// The registering UID must hold `ACCESS_FPS_COUNTER` (signature|privileged)
968    /// — this process typically runs as shell (uid 2000) via `su`.
969    ///
970    /// Returns `(Self, OwnedFd)` where the eventfd is a dup of the core's
971    /// callback fd. It becomes readable whenever `onFpsReported` fires; call
972    /// [`FpsListener::last_fps`] after the event to read the value.
973    pub struct FpsListener {
974        _lib:       DlHandle,
975        vt:         Vtable,
976        window:     OwnedBinder,
977        cb_binder:  *mut AIBinder,
978        _wm_class:  *mut AIBinder_Class,
979        register_code: u32,
980        unregister_code: u32,
981        task_id:    i32,
982    }
983    unsafe impl Send for FpsListener {}
984
985    impl FpsListener {
986        /// Open WindowManager and define the `ITaskFpsCallback` server object.
987        ///
988        /// Resolves the three tx codes from DEX. Does **not** register a task
989        /// yet — call [`FpsListener::register`] once a taskId is known. Starts
990        /// the binder thread pool so `onFpsReported` can fire.
991        pub fn open() -> Result<(Self, OwnedFd), CoreError> {
992            let handle = unsafe {
993                libc::dlopen(LIBBINDER_PATH.as_ptr() as *const c_char, libc::RTLD_NOW | libc::RTLD_LOCAL)
994            };
995            if handle.is_null() { return Err(CoreError::binder(-1, "dlopen:libbinder_ndk.so")); }
996            let lib = DlHandle(handle);
997            let vt = load_vtable(handle)?;
998
999            let (register_code, unregister_code, on_fps_code) =
1000                crate::dex::resolve_fps_codes()
1001                    .ok_or_else(|| CoreError::binder(-1, "dex:TRANSACTION_registerTaskFpsCallback not found"))?;
1002
1003            let window = {
1004                let raw = unsafe { (vt.get_service)(WINDOW_SERVICE.as_ptr() as *const c_char) };
1005                if raw.is_null() { return Err(CoreError::binder(-1, "AServiceManager_getService:window")); }
1006                OwnedBinder { ptr: raw, dec_strong: vt.dec_strong }
1007            };
1008
1009            // Remote transactions require a class on the binder (same
1010            // AIBinder_prepareTransaction contract as the AM service above).
1011            let wm_class = unsafe {
1012                (vt.class_define)(
1013                    WM_DESCRIPTOR.as_ptr() as *const c_char,
1014                    wm_on_create, wm_on_destroy, wm_on_transact,
1015                )
1016            };
1017            if wm_class.is_null() {
1018                return Err(CoreError::binder(-1, "AIBinder_Class_define:IWindowManager"));
1019            }
1020            unsafe { (vt.associate_class)(window.ptr, wm_class) };
1021
1022            let cb_class = unsafe {
1023                (vt.class_define)(
1024                    FPS_DESCRIPTOR.as_ptr() as *const c_char,
1025                    fps_on_create, fps_on_destroy, fps_on_transact,
1026                )
1027            };
1028            if cb_class.is_null() {
1029                return Err(CoreError::binder(-1, "AIBinder_Class_define:ITaskFpsCallback"));
1030            }
1031
1032            // Blocking eventfd — callback writes, consumer waits/reads. Each
1033            // instance owns its own fd; it is handed to the callback binder as
1034            // userdata (per-instance routing, no process-wide static) and the
1035            // consumer receives a dup below (C2).
1036            let owned = unsafe {
1037                let raw = libc::eventfd(0, libc::EFD_CLOEXEC);
1038                if raw < 0 { return Err(CoreError::sys(*libc::__errno(), "eventfd")); }
1039                OwnedFd::from_raw_fd(raw)
1040            };
1041
1042            let consumer = owned.try_clone()
1043                .map_err(|e| CoreError::sys(e.raw_os_error().unwrap_or(-1), "dup:fps"))?;
1044
1045            let userdata = Box::into_raw(Box::new(owned)) as *mut c_void;
1046            let cb_binder = unsafe { (vt.new_binder)(cb_class, userdata) };
1047            if cb_binder.is_null() {
1048                // Reclaim the userdata box handed to AIBinder_new before bailing.
1049                unsafe { drop(Box::from_raw(userdata as *mut OwnedFd)) };
1050                return Err(CoreError::binder(-1, "AIBinder_new:ITaskFpsCallback"));
1051            }
1052            unsafe { (vt.associate_class)(cb_binder, cb_class) };
1053
1054            // Publish the reader and code before the eventfd registration; a
1055            // matching code is never paired with an unset reader (C2-adjacent).
1056            FPS_READ_I32.store(vt.read_int32 as usize, Ordering::Relaxed);
1057            FPS_SEEN.store(false, Ordering::Relaxed);
1058            FPS_CODE.store(on_fps_code, Ordering::Relaxed);
1059            FPS_VALUE.store(0, Ordering::Relaxed);
1060            // The callback resolves AIBinder_getUserData from this vtable; the
1061            // symbol address is process-wide, so a cached static is safe.
1062            *GET_USER_DATA.lock().unwrap_or_else(|p| p.into_inner()) = Some(vt.get_user_data);
1063
1064            unsafe { (vt.set_thread_pool_max)(0) };
1065            let join_fn = vt.join_thread_pool;
1066            std::thread::spawn(move || unsafe { join_fn() });
1067
1068            Ok((Self {
1069                _lib: lib, vt, window, cb_binder, _wm_class: wm_class,
1070                register_code, unregister_code, task_id: -1,
1071            }, consumer))
1072        }
1073
1074        /// Register the callback for `task_id`. If a task was already
1075        /// registered, it is unregistered first (WindowManager tracks one task
1076        /// per callback binder).
1077        pub fn register(&mut self, task_id: i32) -> Result<(), CoreError> {
1078            if self.task_id == task_id {
1079                return Ok(());
1080            }
1081            if self.task_id >= 0 {
1082                let _ = self.unregister();
1083            }
1084
1085            let _ = transact_write(&self.vt, self.window.ptr, self.register_code, |w| {
1086                w.write_i32(task_id)?;
1087                w.write_strong_binder(self.cb_binder)
1088            })?;
1089            self.task_id = task_id;
1090            Ok(())
1091        }
1092
1093        /// Unregister the callback from WindowManager. No-op if nothing is
1094        /// registered.
1095        pub fn unregister(&mut self) -> Result<(), CoreError> {
1096            if self.task_id < 0 {
1097                return Ok(());
1098            }
1099            let _ = transact_write(&self.vt, self.window.ptr, self.unregister_code, |w| {
1100                w.write_strong_binder(self.cb_binder)
1101            })?;
1102            self.task_id = -1;
1103            Ok(())
1104        }
1105
1106        /// The most recent `onFpsReported` value (f32), or `None` if no report
1107        /// has arrived yet. Safe to call at any time; the bit pattern is
1108        /// published atomically. A genuine 0.0 (idle) report reads as `Some`,
1109        /// distinguished from the no-report state by `FPS_SEEN`.
1110        pub fn last_fps(&self) -> Option<f32> {
1111            fps_from_state(FPS_SEEN.load(Ordering::Acquire), FPS_VALUE.load(Ordering::Relaxed))
1112        }
1113
1114        /// The taskId currently registered, or `None` if none.
1115        pub fn task_id(&self) -> Option<i32> {
1116            (self.task_id >= 0).then_some(self.task_id)
1117        }
1118    }
1119
1120    impl Drop for FpsListener {
1121        /// Best-effort deregistration from WindowManager so a dropped listener
1122        /// does not leave the framework delivering `onFpsReported` forever. The
1123        /// callback binder's local strong ref is intentionally NOT released:
1124        /// keeping it alive guarantees the per-binder userdata (the OwnedFd)
1125        /// can never be reclaimed by `on_destroy` while a callback is in
1126        /// flight, and the framework-side registration has been dropped by the
1127        /// unregister, so no stale transaction targets this instance.
1128        fn drop(&mut self) {
1129            let _ = self.unregister();
1130        }
1131    }
1132
1133    /// Disambiguate "no report yet" from a genuine 0.0 (idle) report: `seen`
1134    /// tracks whether the callback published a value; `bits` is that value's
1135    /// bit pattern. `0.0f32` has bits 0, so the bits alone cannot tell a real
1136    /// idle sample from an unset slot.
1137    fn fps_from_state(seen: bool, bits: u32) -> Option<f32> {
1138        if seen {
1139            Some(f32::from_bits(bits))
1140        } else {
1141            None
1142        }
1143    }
1144
1145    /// Normalize an `onFpsReported` bit pattern for the stream. Real FPS is
1146    /// non-negative and finite; NaN/±Inf (garbage or corruption) and negative
1147    /// values collapse to 0.0 (idle) so the value stream never shows "NaN" or
1148    /// "inf".
1149    fn sanitize_fps(bits: u32) -> u32 {
1150        let v = f32::from_bits(bits);
1151        if v.is_finite() && v >= 0.0 {
1152            bits
1153        } else {
1154            0.0f32.to_bits()
1155        }
1156    }
1157
1158    // ── TaskStackListener (task-stack change wake-up) ────────────────────────
1159
1160    const TASK_SERVICE:     &[u8] = b"activity_task\0";
1161    const ATM_DESCRIPTOR:   &[u8] = b"android.app.IActivityTaskManager\0";
1162    const TASK_STACK_DESCRIPTOR: &[u8] = b"android.app.ITaskStackListener\0";
1163
1164    // No-op callbacks for the client-only IActivityTaskManager class (we never
1165    // serve transactions on the `activity_task` binder — the class exists only
1166    // to satisfy AIBinder_prepareTransaction's remote-transaction contract).
1167    unsafe extern "C" fn atm_on_create(_: *mut c_void) -> *mut c_void { std::ptr::null_mut() }
1168    unsafe extern "C" fn atm_on_destroy(_: *mut c_void) {}
1169    unsafe extern "C" fn atm_on_transact(
1170        _: *mut AIBinder, _: u32, _: *const AParcel, _: *mut AParcel,
1171    ) -> BinderStatus { STATUS_OK }
1172
1173    // Pure wake-up handler: any ITaskStackListener callback
1174    // (onTaskStackChanged, onTaskMovedToFront, …) just signals the eventfd.
1175    // The callback arguments are deliberately NOT parsed — the authoritative
1176    // (taskId, pkg) comes from re-querying getFocusedRootTaskInfo (txn 31) on
1177    // the event, so we never depend on a parcel layout (RunningTaskInfo places
1178    // taskId near the parcel tail).
1179    //
1180    // The wake eventfd is per-instance: the daemon hosts two listeners (the fg
1181    // task source and the fps channel), each with its own eventfd. It is handed
1182    // to AIBinder_new as the binder's userdata, so on_destroy must reclaim it.
1183    // No process-wide static — a shared fd would deliver every instance's
1184    // wake to whichever listener opened last.
1185    //
1186    // The callback resolves the per-binder eventfd through AIBinder_getUserData.
1187    // The symbol address is process-wide, so it is cached once in a static.
1188    static GET_USER_DATA: std::sync::Mutex<Option<unsafe extern "C" fn(*const AIBinder) -> *mut c_void>> =
1189        std::sync::Mutex::new(None);
1190
1191    unsafe extern "C" fn task_stack_on_create(userdata: *mut c_void) -> *mut c_void { userdata }
1192    unsafe extern "C" fn task_stack_on_destroy(userdata: *mut c_void) {
1193        if !userdata.is_null() {
1194            unsafe { drop(Box::from_raw(userdata as *mut OwnedFd)) };
1195        }
1196    }
1197    unsafe extern "C" fn task_stack_on_transact(
1198        binder: *mut AIBinder, _code: u32, _in_parcel: *const AParcel, _reply: *mut AParcel,
1199    ) -> BinderStatus {
1200        let get_user_data = GET_USER_DATA
1201            .lock()
1202            .unwrap_or_else(|p| p.into_inner());
1203        if let Some(get_user_data) = *get_user_data {
1204            let userdata = unsafe { get_user_data(binder) };
1205            if !userdata.is_null() {
1206                let efd = userdata as *mut OwnedFd;
1207                let val: u64 = 1;
1208                unsafe { libc::write((*efd).as_raw_fd(), &val as *const u64 as *const c_void, 8) };
1209            }
1210        }
1211        STATUS_OK
1212    }
1213
1214    /// Push-based task-stack change listener registered with
1215    /// `IActivityTaskManager`.
1216    ///
1217    /// Uses `IActivityTaskManager.registerTaskStackListener(listener)`; the
1218    /// daemon hosts the `ITaskStackListener` server object. The callback is a
1219    /// pure wake-up: on any task-stack change it signals the eventfd and
1220    /// parses nothing. Consumers re-query `getFocusedRootTaskInfo` (txn 31) on
1221    /// the event for the authoritative `(taskId, pkg)`.
1222    ///
1223    /// This target's ROM exposes the legacy `ITaskStackListener` /
1224    /// `registerTaskStackListener` pair; the newer `ITaskChangeListener` /
1225    /// `registerTaskChangeListener` interface is absent.
1226    ///
1227    /// The registering UID must hold `MANAGE_ACTIVITY_TASKS` /
1228    /// `MANAGE_ACTIVITY_STACKS` — the same gate as txn 31, which root passes
1229    /// empirically on the target ROM.
1230    ///
1231    /// Returns `(Self, OwnedFd)` where the eventfd is a dup of the core's
1232    /// callback fd. It becomes readable on any task-stack change.
1233    pub struct TaskStackListener {
1234        _lib:       DlHandle,
1235        vt:         Vtable,
1236        service:    OwnedBinder,
1237        cb_binder:  *mut AIBinder,
1238        _atm_class: *mut AIBinder_Class,
1239        register_code: u32,
1240        unregister_code: u32,
1241    }
1242    unsafe impl Send for TaskStackListener {}
1243
1244    impl TaskStackListener {
1245        /// Open `activity_task`, define the `ITaskStackListener` server object,
1246        /// and start the binder thread pool. Does **not** register yet — call
1247        /// [`TaskStackListener::register`] once consumers are active.
1248        pub fn open() -> Result<(Self, OwnedFd), CoreError> {
1249            let handle = unsafe {
1250                libc::dlopen(LIBBINDER_PATH.as_ptr() as *const c_char, libc::RTLD_NOW | libc::RTLD_LOCAL)
1251            };
1252            if handle.is_null() { return Err(CoreError::binder(-1, "dlopen:libbinder_ndk.so")); }
1253            let lib = DlHandle(handle);
1254            let vt = load_vtable(handle)?;
1255
1256            let (register_code, unregister_code) =
1257                crate::dex::resolve_task_stack_codes()
1258                    .ok_or_else(|| CoreError::binder(-1, "dex:TRANSACTION_registerTaskStackListener not found"))?;
1259
1260            let service = {
1261                let raw = unsafe { (vt.get_service)(TASK_SERVICE.as_ptr() as *const c_char) };
1262                if raw.is_null() { return Err(CoreError::binder(-1, "AServiceManager_getService:activity_task")); }
1263                OwnedBinder { ptr: raw, dec_strong: vt.dec_strong }
1264            };
1265
1266            // Remote transactions require a class on the binder (same
1267            // AIBinder_prepareTransaction contract as the other services).
1268            let atm_class = unsafe {
1269                (vt.class_define)(
1270                    ATM_DESCRIPTOR.as_ptr() as *const c_char,
1271                    atm_on_create, atm_on_destroy, atm_on_transact,
1272                )
1273            };
1274            if atm_class.is_null() {
1275                return Err(CoreError::binder(-1, "AIBinder_Class_define:IActivityTaskManager"));
1276            }
1277            unsafe { (vt.associate_class)(service.ptr, atm_class) };
1278
1279            let cb_class = unsafe {
1280                (vt.class_define)(
1281                    TASK_STACK_DESCRIPTOR.as_ptr() as *const c_char,
1282                    task_stack_on_create, task_stack_on_destroy, task_stack_on_transact,
1283                )
1284            };
1285            if cb_class.is_null() {
1286                return Err(CoreError::binder(-1, "AIBinder_Class_define:ITaskStackListener"));
1287            }
1288
1289            // Blocking eventfd — callback writes, consumer waits/reads. Each
1290            // instance owns its own fd; it is handed to the callback binder as
1291            // userdata (per-instance routing, no process-wide static) and the
1292            // consumer receives a dup below (C2).
1293            let owned = unsafe {
1294                let raw = libc::eventfd(0, libc::EFD_CLOEXEC);
1295                if raw < 0 { return Err(CoreError::sys(*libc::__errno(), "eventfd")); }
1296                OwnedFd::from_raw_fd(raw)
1297            };
1298
1299            let consumer = owned.try_clone()
1300                .map_err(|e| CoreError::sys(e.raw_os_error().unwrap_or(-1), "dup:task_stack"))?;
1301
1302            let userdata = Box::into_raw(Box::new(owned)) as *mut c_void;
1303            let cb_binder = unsafe { (vt.new_binder)(cb_class, userdata) };
1304            if cb_binder.is_null() {
1305                // Reclaim the userdata box handed to AIBinder_new before bailing.
1306                unsafe { drop(Box::from_raw(userdata as *mut OwnedFd)) };
1307                return Err(CoreError::binder(-1, "AIBinder_new:ITaskStackListener"));
1308            }
1309            unsafe { (vt.associate_class)(cb_binder, cb_class) };
1310
1311            // The callback resolves AIBinder_getUserData from this vtable; the
1312            // symbol address is process-wide, so a cached static is safe.
1313            *GET_USER_DATA.lock().unwrap_or_else(|p| p.into_inner()) = Some(vt.get_user_data);
1314
1315            unsafe { (vt.set_thread_pool_max)(0) };
1316            let join_fn = vt.join_thread_pool;
1317            std::thread::spawn(move || unsafe { join_fn() });
1318
1319            Ok((Self {
1320                _lib: lib, vt, service, cb_binder, _atm_class: atm_class,
1321                register_code, unregister_code,
1322            }, consumer))
1323        }
1324
1325        /// Register the task-stack listener with `activity_task` (one listener
1326        /// receives all task-stack events). Idempotent at the framework level;
1327        /// callers should register once and keep the object alive.
1328        pub fn register(&self) -> Result<(), CoreError> {
1329            let _ = transact_write(&self.vt, self.service.ptr, self.register_code, |w| {
1330                w.write_strong_binder(self.cb_binder)
1331            })?;
1332            Ok(())
1333        }
1334
1335        /// Unregister the task-stack listener from `activity_task`. No-op at
1336        /// the framework level if not registered; callers should unregister
1337        /// before dropping the object.
1338        pub fn unregister(&self) -> Result<(), CoreError> {
1339            let _ = transact_write(&self.vt, self.service.ptr, self.unregister_code, |w| {
1340                w.write_strong_binder(self.cb_binder)
1341            })?;
1342            Ok(())
1343        }
1344    }
1345
1346    impl Drop for TaskStackListener {
1347        /// Best-effort deregistration from `activity_task`. Same deliberate
1348        /// non-release of the local strong ref as [`FpsListener`] — the
1349        /// userdata OwnedFd stays valid for any in-flight wake, and the
1350        /// framework-side registration is gone after the unregister.
1351        fn drop(&mut self) {
1352            let _ = self.unregister();
1353        }
1354    }
1355
1356    // ── RawBinderService ──────────────────────────────────────────────────────
1357
1358    /// Generic binder client for any named Android service.
1359    ///
1360    /// Handles its own `dlopen` on `libbinder_ndk.so`. Callers provide raw
1361    /// transaction codes (resolved via [`crate::dex::find_transaction_code`])
1362    /// and use [`RawBinderService::transact_bool`] /
1363    /// [`RawBinderService::transact_i32`] for typed round-trips.
1364    pub struct RawBinderService {
1365        _lib:    DlHandle,
1366        vt:      Vtable,
1367        service: OwnedBinder,
1368    }
1369    unsafe impl Send for RawBinderService {}
1370
1371    impl RawBinderService {
1372        /// Open a connection to the named service (e.g. `"power"`, `"batterystats"`).
1373        pub fn open(service_name: &str) -> Result<Self, CoreError> {
1374            use std::ffi::CString;
1375            let handle = unsafe {
1376                libc::dlopen(LIBBINDER_PATH.as_ptr() as *const c_char, libc::RTLD_NOW | libc::RTLD_LOCAL)
1377            };
1378            if handle.is_null() {
1379                return Err(CoreError::binder(-1, "dlopen:libbinder_ndk.so"));
1380            }
1381            let lib = DlHandle(handle);
1382            let vt = load_vtable(handle)?;
1383            let cs = CString::new(service_name)
1384                .map_err(|_| CoreError::binder(-1, "service_name:nul_byte"))?;
1385            let raw = unsafe { (vt.get_service)(cs.as_ptr()) };
1386            if raw.is_null() {
1387                return Err(CoreError::binder(-1, "AServiceManager_getService:null"));
1388            }
1389            let service = OwnedBinder { ptr: raw, dec_strong: vt.dec_strong };
1390            Ok(Self { _lib: lib, vt, service })
1391        }
1392
1393        /// Send a no-argument transaction; read exception header then bool reply.
1394        pub fn transact_bool(&self, code: u32) -> Result<bool, CoreError> {
1395            let out = self.raw_noarg(code)?;
1396            let r = ParcelReader { vt: &self.vt, parcel: &out };
1397            let ex = r.read_i32()?;
1398            if ex != EX_NONE { return Err(CoreError::binder(ex, "transact_bool:exception")); }
1399            if let Some(rb) = self.vt.read_bool {
1400                let mut v = false;
1401                let s = unsafe { rb(out.ptr as *const AParcel, &mut v) };
1402                if s != STATUS_OK { return Err(CoreError::binder(s, "AParcel_readBool")); }
1403                Ok(v)
1404            } else {
1405                Ok(r.read_i32()? != 0)
1406            }
1407        }
1408
1409        /// Send a transaction with one i32 argument; discard reply.
1410        pub fn transact_i32(&self, code: u32, arg: i32) -> Result<(), CoreError> {
1411            let _ = transact_write(&self.vt, self.service.ptr, code, |w| w.write_i32(arg))?;
1412            Ok(())
1413        }
1414
1415        fn raw_noarg(&self, code: u32) -> Result<OwnedParcel, CoreError> {
1416            transact_write(&self.vt, self.service.ptr, code, |_| Ok(()))
1417        }
1418    }
1419
1420    #[cfg(test)]
1421    mod tests {
1422        use super::*;
1423
1424        fn alloc(length: i32) -> bool {
1425            let mut buf = StringBuf::new();
1426            let mut out: *mut c_char = std::ptr::null_mut();
1427            unsafe { string_alloc(&mut buf as *mut StringBuf as *mut c_void, length, &mut out) }
1428        }
1429
1430        #[test]
1431        fn string_alloc_rejects_oversized() {
1432            assert!(!alloc(MAX_BINDER_STRING_LEN as i32 + 1));
1433        }
1434
1435        #[test]
1436        fn string_alloc_rejects_negative() {
1437            assert!(!alloc(-1));
1438        }
1439
1440        #[test]
1441        fn string_alloc_accepts_valid_len_and_nul_terminates() {
1442            let mut buf = StringBuf::new();
1443            let mut out: *mut c_char = std::ptr::null_mut();
1444            let ok = unsafe {
1445                string_alloc(&mut buf as *mut StringBuf as *mut c_void, 4, &mut out)
1446            };
1447            assert!(ok);
1448            assert!(!out.is_null());
1449            {
1450                let vec = unsafe { buf.0.as_mut_vec() };
1451                b"ABCD".iter().enumerate().for_each(|(i, &b)| vec[i] = b);
1452                vec[4] = 0;
1453            }
1454            assert_eq!(buf.finish().as_deref(), Some("ABCD"));
1455        }
1456
1457        #[test]
1458        fn fps_zero_report_distinct_from_unseen() {
1459            assert_eq!(fps_from_state(false, 0), None);
1460            assert_eq!(fps_from_state(true, 0), Some(0.0));
1461            assert_eq!(fps_from_state(true, 60.0f32.to_bits()), Some(60.0));
1462            assert_eq!(fps_from_state(false, 60.0f32.to_bits()), None);
1463        }
1464
1465        #[test]
1466        fn sanitize_fps_rejects_non_finite_and_negative() {
1467            let de = |bits| f32::from_bits(sanitize_fps(bits));
1468            assert_eq!(de(0.0f32.to_bits()), 0.0);
1469            assert_eq!(de(60.0f32.to_bits()), 60.0);
1470            assert_eq!(de(f32::NAN.to_bits()), 0.0);
1471            assert_eq!(de(f32::INFINITY.to_bits()), 0.0);
1472            assert_eq!(de(f32::NEG_INFINITY.to_bits()), 0.0);
1473            assert_eq!(de((-5.0f32).to_bits()), 0.0);
1474        }
1475    }
1476}
1477
1478// ── Public re-exports ─────────────────────────────────────────────────────────
1479
1480#[cfg(target_os = "android")]
1481pub use imp::{ActivityManagerBinder, DisplayManagerBinder, FpsListener, RawBinderService, TaskStackListener, TxCodes, last_foreground_pid, resolve_tx_codes};
1482
1483// ── Non-Android stubs ─────────────────────────────────────────────────────────
1484
1485#[cfg(not(target_os = "android"))]
1486pub struct ActivityManagerBinder;
1487
1488#[cfg(not(target_os = "android"))]
1489impl ActivityManagerBinder {
1490    pub fn open() -> Result<Self, crate::CoreError> {
1491        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
1492    }
1493    pub fn open_with_observer() -> Result<(Self, std::os::fd::OwnedFd), crate::CoreError> {
1494        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
1495    }
1496    pub fn open_with_fgproc_observer() -> Result<(Self, std::os::fd::OwnedFd), crate::CoreError> {
1497        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
1498    }
1499    pub fn get_focused_task(&self) -> Result<Option<(i32, Option<String>)>, crate::CoreError> {
1500        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
1501    }
1502    pub fn get_focused_package(&self) -> Result<Option<String>, crate::CoreError> {
1503        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
1504    }
1505    pub fn get_focused_task_id(&self) -> Result<Option<i32>, crate::CoreError> {
1506        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
1507    }
1508}
1509
1510#[cfg(not(target_os = "android"))]
1511pub struct DisplayManagerBinder;
1512
1513#[cfg(not(target_os = "android"))]
1514impl DisplayManagerBinder {
1515    pub fn open_with_callback() -> Result<(Self, crate::reactor::Fd), crate::CoreError> {
1516        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
1517    }
1518    pub fn is_interactive(&self) -> Result<bool, crate::CoreError> {
1519        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
1520    }
1521}
1522
1523#[cfg(not(target_os = "android"))]
1524pub struct RawBinderService;
1525
1526#[cfg(not(target_os = "android"))]
1527impl RawBinderService {
1528    pub fn open(_service_name: &str) -> Result<Self, crate::CoreError> {
1529        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
1530    }
1531    pub fn transact_bool(&self, _code: u32) -> Result<bool, crate::CoreError> {
1532        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
1533    }
1534    pub fn transact_i32(&self, _code: u32, _arg: i32) -> Result<(), crate::CoreError> {
1535        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
1536    }
1537}
1538
1539#[cfg(not(target_os = "android"))]
1540pub struct FpsListener;
1541
1542#[cfg(not(target_os = "android"))]
1543impl FpsListener {
1544    pub fn open() -> Result<(Self, std::os::fd::OwnedFd), crate::CoreError> {
1545        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
1546    }
1547    pub fn register(&mut self, _task_id: i32) -> Result<(), crate::CoreError> {
1548        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
1549    }
1550    pub fn unregister(&mut self) -> Result<(), crate::CoreError> {
1551        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
1552    }
1553    pub fn last_fps(&self) -> Option<f32> {
1554        None
1555    }
1556    pub fn task_id(&self) -> Option<i32> {
1557        None
1558    }
1559}
1560
1561#[cfg(not(target_os = "android"))]
1562pub struct TaskStackListener;
1563
1564#[cfg(not(target_os = "android"))]
1565impl TaskStackListener {
1566    pub fn open() -> Result<(Self, std::os::fd::OwnedFd), crate::CoreError> {
1567        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
1568    }
1569    pub fn register(&self) -> Result<(), crate::CoreError> {
1570        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
1571    }
1572    pub fn unregister(&self) -> Result<(), crate::CoreError> {
1573        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
1574    }
1575}
1576
1577#[cfg(not(target_os = "android"))]
1578pub struct TxCodes { pub observer_code: u32, pub query_code: u32, pub api_mode: u8, pub fg_code: u32 }
1579
1580#[cfg(not(target_os = "android"))]
1581pub fn resolve_tx_codes() -> Result<TxCodes, crate::CoreError> {
1582    Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
1583}
1584
1585#[cfg(not(target_os = "android"))]
1586pub fn last_foreground_pid() -> i32 {
1587    0
1588}