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//! `ActivityManager::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::android::dex;
30    use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd, OwnedFd};
31    use std::os::raw::{c_char, c_void};
32    use std::sync::Mutex;
33    use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU32, AtomicUsize, Ordering};
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 {
103        std::ptr::null_mut()
104    }
105    unsafe extern "C" fn am_on_destroy(_: *mut c_void) {}
106    unsafe extern "C" fn am_on_transact(
107        _: *mut AIBinder,
108        _: u32,
109        _: *const AParcel,
110        _: *mut AParcel,
111    ) -> BinderStatus {
112        STATUS_UNKNOWN_TRANSACTION
113    }
114
115    // IProcessObserver server callbacks
116    unsafe extern "C" fn obs_on_create(_: *mut c_void) -> *mut c_void {
117        std::ptr::null_mut()
118    }
119    unsafe extern "C" fn obs_on_destroy(_: *mut c_void) {}
120    unsafe extern "C" fn obs_on_transact(
121        _: *mut AIBinder,
122        code: u32,
123        _: *const AParcel,
124        _: *mut AParcel,
125    ) -> BinderStatus {
126        if code == OBS_FG_CODE.load(Ordering::Relaxed) {
127            // Write while holding the lock: the fd can only be closed while
128            // we hold it, so a revoke can never race us into a stale number.
129            if let Some(fd) = obs_eventfd_guard().as_ref() {
130                let val: u64 = 1;
131                unsafe { libc::write(fd.as_raw_fd(), &val as *const u64 as *const c_void, 8) };
132            }
133        }
134        STATUS_OK
135    }
136
137    // IForegroundProcessObserver server callbacks. Two parcel layouts, selected
138    // by FGPROC_IPROC_MODE:
139    //  - mode 0 (stock): `onForegroundProcessChanged(int pid)` — single int32.
140    //  - mode 1 (custom ROMs without IForegroundProcessObserver): the ROM
141    //    repurposes `IProcessObserver.onForegroundActivitiesChanged` to deliver
142    //    `(int pid, int uid, int fg)`. The callback stores the pid and only
143    //    signals the eventfd when fg != 0 (a foreground transition), so
144    //    background transitions never cause the daemon to react.
145    unsafe extern "C" fn fgproc_on_create(_: *mut c_void) -> *mut c_void {
146        std::ptr::null_mut()
147    }
148    unsafe extern "C" fn fgproc_on_destroy(_: *mut c_void) {}
149    unsafe extern "C" fn fgproc_on_transact(
150        _: *mut AIBinder,
151        code: u32,
152        in_parcel: *const AParcel,
153        _: *mut AParcel,
154    ) -> BinderStatus {
155        if code != FGPROC_FG_CODE.load(Ordering::Relaxed) {
156            return STATUS_UNKNOWN_TRANSACTION;
157        }
158        // Read fn is published non-zero before the code/eventfd, so a matching
159        // code is never paired with an unset reader.
160        let read_addr = FGPROC_READ_I32.load(Ordering::Relaxed);
161        if read_addr != 0 {
162            let read_fn: unsafe extern "C" fn(*const AParcel, *mut i32) -> BinderStatus =
163                unsafe { std::mem::transmute(read_addr) };
164            if FGPROC_IPROC_MODE.load(Ordering::Relaxed) == 1 {
165                // IProcessObserver.onForegroundActivitiesChanged(pid, uid, fg)
166                let mut pid: i32 = 0;
167                let mut _uid: i32 = 0;
168                let mut fg: i32 = 0;
169                let mut ok = unsafe { read_fn(in_parcel, &mut pid) } == STATUS_OK;
170                ok &= unsafe { read_fn(in_parcel, &mut _uid) } == STATUS_OK;
171                ok &= unsafe { read_fn(in_parcel, &mut fg) } == STATUS_OK;
172                if ok {
173                    FGPROC_PID.store(pid, Ordering::Relaxed);
174                    // Only foreground transitions are actionable; suppress the
175                    // background transition entirely (fg == 0).
176                    if fg == 0 {
177                        return STATUS_OK;
178                    }
179                } else {
180                    return STATUS_OK;
181                }
182            } else {
183                let mut pid: i32 = 0;
184                if unsafe { read_fn(in_parcel, &mut pid) } == STATUS_OK {
185                    FGPROC_PID.store(pid, Ordering::Relaxed);
186                }
187            }
188        }
189        // Write while holding the lock: the fd can only be closed while we
190        // hold it, so a revoke can never race us into a stale number.
191        if let Some(fd) = fgproc_eventfd_guard().as_ref() {
192            let val: u64 = 1;
193            unsafe { libc::write(fd.as_raw_fd(), &val as *const u64 as *const c_void, 8) };
194        }
195        STATUS_OK
196    }
197
198    /// The PID captured by the most recent `onForegroundProcessChanged`
199    /// callback (requires `open_with_fgproc_observer`).
200    pub fn last_foreground_pid() -> i32 {
201        FGPROC_PID.load(Ordering::Relaxed)
202    }
203
204    // ── String allocator ─────────────────────────────────────────────────────
205
206    /// Ceiling on a single parcel string regardless of the length the peer
207    /// advertises. Component names are at most a few hundred bytes; this bounds
208    /// the allocation so a malformed advertised length cannot drive a giant
209    /// `reserve_exact` (which would abort on OOM).
210    const MAX_BINDER_STRING_LEN: usize = 1024 * 1024;
211
212    unsafe extern "C" fn string_alloc(
213        cookie: *mut c_void,
214        length: i32,
215        buffer: *mut *mut c_char,
216    ) -> bool {
217        // length == -1 is the Java null-string marker: AParcel_readString
218        // calls the allocator with -1 (and a null buffer) when the parcel
219        // holds a null string, so returning true there decodes it to None
220        // instead of a hard STATUS_UNEXPECTED_NULL failure. Any other
221        // negative length is corruption and an allocation failure: returning
222        // true with no usable buffer would hand the reader a dangling pointer,
223        // and an oversized reserve_exact would abort on OOM.
224        if length == -1 {
225            return true;
226        }
227        if length < 0 {
228            return false;
229        }
230        let len = length as usize;
231        if len > MAX_BINDER_STRING_LEN {
232            return false;
233        }
234        let s = unsafe { &mut *(cookie as *mut StringBuf) };
235        s.0.reserve_exact(len + 1);
236        unsafe { s.0.as_mut_vec().resize(len + 1, 0) };
237        unsafe { *buffer = s.0.as_mut_ptr() as *mut c_char };
238        true
239    }
240
241    struct StringBuf(String);
242    impl StringBuf {
243        fn new() -> Self {
244            Self(String::new())
245        }
246        fn finish(mut self) -> Option<String> {
247            if let Some(pos) = self.0.as_bytes().iter().position(|&b| b == 0) {
248                unsafe { self.0.as_mut_vec().truncate(pos) };
249            }
250            if self.0.is_empty() {
251                None
252            } else {
253                Some(self.0)
254            }
255        }
256    }
257
258    // ── Vtable ────────────────────────────────────────────────────────────────
259
260    struct Vtable {
261        get_service: unsafe extern "C" fn(*const c_char) -> *mut AIBinder,
262        class_define: unsafe extern "C" fn(
263            *const c_char,
264            unsafe extern "C" fn(*mut c_void) -> *mut c_void,
265            unsafe extern "C" fn(*mut c_void),
266            unsafe extern "C" fn(*mut AIBinder, u32, *const AParcel, *mut AParcel) -> BinderStatus,
267        ) -> *mut AIBinder_Class,
268        associate_class: unsafe extern "C" fn(*mut AIBinder, *mut AIBinder_Class) -> bool,
269        new_binder: unsafe extern "C" fn(*const AIBinder_Class, *mut c_void) -> *mut AIBinder,
270        prepare_transaction: unsafe extern "C" fn(*mut AIBinder, *mut *mut AParcel) -> BinderStatus,
271        transact: unsafe extern "C" fn(
272            *mut AIBinder,
273            u32,
274            *mut *mut AParcel,
275            *mut *mut AParcel,
276            u32,
277        ) -> BinderStatus,
278        dec_strong: unsafe extern "C" fn(*mut AIBinder),
279        parcel_delete: unsafe extern "C" fn(*mut AParcel),
280        read_int32: unsafe extern "C" fn(*const AParcel, *mut i32) -> BinderStatus,
281        read_string:
282            unsafe extern "C" fn(*const AParcel, *mut c_void, StringAllocator) -> BinderStatus,
283        write_strong_binder: unsafe extern "C" fn(*mut AParcel, *mut AIBinder) -> BinderStatus,
284        set_thread_pool_max: unsafe extern "C" fn(u32),
285        join_thread_pool: unsafe extern "C" fn(),
286        get_user_data: unsafe extern "C" fn(*const AIBinder) -> *mut c_void,
287        write_int32: unsafe extern "C" fn(*mut AParcel, i32) -> BinderStatus,
288        read_float: Option<unsafe extern "C" fn(*const AParcel, *mut f32) -> BinderStatus>,
289        read_int64: Option<unsafe extern "C" fn(*const AParcel, *mut i64) -> BinderStatus>,
290        // Optional: only present on API 29+, but all modern Android has this
291        read_bool: Option<unsafe extern "C" fn(*const AParcel, *mut bool) -> BinderStatus>,
292    }
293
294    // ── RAII wrappers ─────────────────────────────────────────────────────────
295
296    struct DlHandle;
297    unsafe impl Send for DlHandle {}
298    impl Drop for DlHandle {
299        fn drop(&mut self) {
300            // Intentionally no dlclose: the binder thread pool spawned in
301            // open_with_observer() keeps executing library code until process
302            // exit. Unloading the library while that thread runs causes
303            // use-after-free. libbinder_ndk.so is never unloaded during the
304            // daemon lifetime; the OS reclaims it on exit.
305        }
306    }
307
308    struct OwnedParcel {
309        ptr: *mut AParcel,
310        delete: unsafe extern "C" fn(*mut AParcel),
311    }
312    impl Drop for OwnedParcel {
313        fn drop(&mut self) {
314            if !self.ptr.is_null() {
315                unsafe { (self.delete)(self.ptr) };
316            }
317        }
318    }
319
320    struct OwnedBinder {
321        ptr: *mut AIBinder,
322        dec_strong: unsafe extern "C" fn(*mut AIBinder),
323    }
324    unsafe impl Send for OwnedBinder {}
325    impl Drop for OwnedBinder {
326        fn drop(&mut self) {
327            if !self.ptr.is_null() {
328                unsafe { (self.dec_strong)(self.ptr) };
329            }
330        }
331    }
332
333    // ── dlsym helper ─────────────────────────────────────────────────────────
334
335    macro_rules! dlsym_fn {
336        ($handle:expr, $name:literal, $ty:ty) => {{
337            let sym =
338                unsafe { libc::dlsym($handle, concat!($name, "\0").as_ptr() as *const c_char) };
339            if sym.is_null() {
340                return Err(CoreError::binder(-1, concat!("dlsym:", $name)));
341            }
342            unsafe { std::mem::transmute::<*mut c_void, $ty>(sym) }
343        }};
344    }
345
346    macro_rules! dlsym_opt {
347        ($handle:expr, $name:literal, $ty:ty) => {{
348            let sym =
349                unsafe { libc::dlsym($handle, concat!($name, "\0").as_ptr() as *const c_char) };
350            if sym.is_null() {
351                None
352            } else {
353                Some(unsafe { std::mem::transmute::<*mut c_void, $ty>(sym) })
354            }
355        }};
356    }
357
358    fn load_vtable(handle: *mut c_void) -> Result<Vtable, CoreError> {
359        Ok(Vtable {
360            get_service: dlsym_fn!(
361                handle,
362                "AServiceManager_getService",
363                unsafe extern "C" fn(*const c_char) -> *mut AIBinder
364            ),
365            class_define: dlsym_fn!(
366                handle,
367                "AIBinder_Class_define",
368                unsafe extern "C" fn(
369                    *const c_char,
370                    unsafe extern "C" fn(*mut c_void) -> *mut c_void,
371                    unsafe extern "C" fn(*mut c_void),
372                    unsafe extern "C" fn(
373                        *mut AIBinder,
374                        u32,
375                        *const AParcel,
376                        *mut AParcel,
377                    ) -> BinderStatus,
378                ) -> *mut AIBinder_Class
379            ),
380            associate_class: dlsym_fn!(
381                handle,
382                "AIBinder_associateClass",
383                unsafe extern "C" fn(*mut AIBinder, *mut AIBinder_Class) -> bool
384            ),
385            new_binder: dlsym_fn!(
386                handle,
387                "AIBinder_new",
388                unsafe extern "C" fn(*const AIBinder_Class, *mut c_void) -> *mut AIBinder
389            ),
390            prepare_transaction: dlsym_fn!(
391                handle,
392                "AIBinder_prepareTransaction",
393                unsafe extern "C" fn(*mut AIBinder, *mut *mut AParcel) -> BinderStatus
394            ),
395            transact: dlsym_fn!(
396                handle,
397                "AIBinder_transact",
398                unsafe extern "C" fn(
399                    *mut AIBinder,
400                    u32,
401                    *mut *mut AParcel,
402                    *mut *mut AParcel,
403                    u32,
404                ) -> BinderStatus
405            ),
406            dec_strong: dlsym_fn!(
407                handle,
408                "AIBinder_decStrong",
409                unsafe extern "C" fn(*mut AIBinder)
410            ),
411            parcel_delete: dlsym_fn!(handle, "AParcel_delete", unsafe extern "C" fn(*mut AParcel)),
412            read_int32: dlsym_fn!(
413                handle,
414                "AParcel_readInt32",
415                unsafe extern "C" fn(*const AParcel, *mut i32) -> BinderStatus
416            ),
417            read_string: dlsym_fn!(
418                handle,
419                "AParcel_readString",
420                unsafe extern "C" fn(*const AParcel, *mut c_void, StringAllocator) -> BinderStatus
421            ),
422            write_strong_binder: dlsym_fn!(
423                handle,
424                "AParcel_writeStrongBinder",
425                unsafe extern "C" fn(*mut AParcel, *mut AIBinder) -> BinderStatus
426            ),
427            set_thread_pool_max: dlsym_fn!(
428                handle,
429                "ABinderProcess_setThreadPoolMaxThreadCount",
430                unsafe extern "C" fn(u32)
431            ),
432            join_thread_pool: dlsym_fn!(
433                handle,
434                "ABinderProcess_joinThreadPool",
435                unsafe extern "C" fn()
436            ),
437            get_user_data: dlsym_fn!(
438                handle,
439                "AIBinder_getUserData",
440                unsafe extern "C" fn(*const AIBinder) -> *mut c_void
441            ),
442            write_int32: dlsym_fn!(
443                handle,
444                "AParcel_writeInt32",
445                unsafe extern "C" fn(*mut AParcel, i32) -> BinderStatus
446            ),
447            read_bool: dlsym_opt!(
448                handle,
449                "AParcel_readBool",
450                unsafe extern "C" fn(*const AParcel, *mut bool) -> BinderStatus
451            ),
452            read_float: dlsym_opt!(
453                handle,
454                "AParcel_readFloat",
455                unsafe extern "C" fn(*const AParcel, *mut f32) -> BinderStatus
456            ),
457            read_int64: dlsym_opt!(
458                handle,
459                "AParcel_readInt64",
460                unsafe extern "C" fn(*const AParcel, *mut i64) -> BinderStatus
461            ),
462        })
463    }
464
465    // ── ParcelReader ──────────────────────────────────────────────────────────
466
467    struct ParcelReader<'a> {
468        vt: &'a Vtable,
469        parcel: &'a OwnedParcel,
470    }
471
472    impl<'a> ParcelReader<'a> {
473        fn read_i32(&self) -> Result<i32, CoreError> {
474            let mut v = 0i32;
475            let s = unsafe { (self.vt.read_int32)(self.parcel.ptr, &mut v) };
476            if s != STATUS_OK {
477                return Err(CoreError::binder(s, "AParcel_readInt32"));
478            }
479            Ok(v)
480        }
481        /// Read a String16 (Java `writeString`) as an owned `String`. Returns
482        /// `None` for a null string (`-1` marker); `string_alloc` accepts the
483        /// `-1` length so `AParcel_readString` returns `STATUS_OK` instead of
484        /// `STATUS_UNEXPECTED_NULL`. Strings are bound-capped (1 MiB) by
485        /// `string_alloc`.
486        fn read_string(&self) -> Result<Option<String>, CoreError> {
487            let mut buf = StringBuf::new();
488            let s = unsafe {
489                (self.vt.read_string)(
490                    self.parcel.ptr,
491                    &mut buf as *mut StringBuf as *mut c_void,
492                    string_alloc,
493                )
494            };
495            if s != STATUS_OK {
496                return Err(CoreError::binder(s, "AParcel_readString"));
497            }
498            Ok(buf.finish())
499        }
500        fn read_float(&self) -> Result<f32, CoreError> {
501            let r = self
502                .vt
503                .read_float
504                .ok_or_else(|| CoreError::binder(-1, "AParcel_readFloat:unavailable"))?;
505            let mut v = 0f32;
506            let s = unsafe { r(self.parcel.ptr, &mut v) };
507            if s != STATUS_OK {
508                return Err(CoreError::binder(s, "AParcel_readFloat"));
509            }
510            Ok(v)
511        }
512        fn read_int64(&self) -> Result<i64, CoreError> {
513            let r = self
514                .vt
515                .read_int64
516                .ok_or_else(|| CoreError::binder(-1, "AParcel_readInt64:unavailable"))?;
517            let mut v = 0i64;
518            let s = unsafe { r(self.parcel.ptr, &mut v) };
519            if s != STATUS_OK {
520                return Err(CoreError::binder(s, "AParcel_readInt64"));
521            }
522            Ok(v)
523        }
524        fn read_bool(&self) -> Result<bool, CoreError> {
525            if let Some(rb) = self.vt.read_bool {
526                let mut v = false;
527                let s = unsafe { rb(self.parcel.ptr, &mut v) };
528                if s != STATUS_OK {
529                    return Err(CoreError::binder(s, "AParcel_readBool"));
530                }
531                Ok(v)
532            } else {
533                Ok(self.read_i32()? != 0)
534            }
535        }
536        fn skip_i32s(&self, n: usize) -> Result<(), CoreError> {
537            for _ in 0..n {
538                self.read_i32()?;
539            }
540            Ok(())
541        }
542        fn skip_int_array(&self) -> Result<(), CoreError> {
543            let count = self.read_i32()?.max(0) as usize;
544            self.skip_i32s(count)
545        }
546        /// Skip `n` bytes, consuming 4-byte words. Parcel payloads are always
547        /// 4-byte aligned (Parcel::pad_size), so `n` is a multiple of 4 for
548        /// every skip we perform; the ceiling rounding is defensive.
549        fn skip_bytes(&self, n: usize) -> Result<(), CoreError> {
550            self.skip_i32s(n.div_ceil(4))
551        }
552        /// Skip a String16 (Java `writeString`) payload — `writeInt32(len)`
553        /// then `(len+1)*2` UTF-16 bytes padded to 4. Returns `Ok(true)` if a
554        /// non-null string was consumed, `Ok(false)` for the null marker
555        /// (`-1`). This never touches the decoded value, so it is immune to
556        /// the UTF-16→UTF-8 allocation path.
557        fn skip_string16(&self) -> Result<bool, CoreError> {
558            let len = self.read_i32()?;
559            if len < 0 {
560                return Ok(false);
561            }
562            // saturating: a hostile `len` could otherwise wrap the byte math
563            // on 32-bit targets and silently desync the walk.
564            let bytes = ((len as usize).saturating_add(1)).saturating_mul(2);
565            self.skip_bytes(bytes)?;
566            Ok(true)
567        }
568        /// Skip a String8 (Java `writeString8`) payload — `writeInt32(len)`
569        /// then `len+1` UTF-8 bytes padded to 4. Used for
570        /// `DisplayInfo.{name,ownerPackageName,uniqueId}` which the framework
571        /// writes with `writeString8`; `AParcel_readString` only decodes
572        /// String16 and would desync the walk.
573        fn skip_string8(&self) -> Result<bool, CoreError> {
574            let len = self.read_i32()?;
575            if len < 0 {
576                return Ok(false);
577            }
578            let bytes = (len as usize).saturating_add(1);
579            self.skip_bytes(bytes)?;
580            Ok(true)
581        }
582        /// Skip a `Parcel.readValue()` encoded value (Java `writeValue`).
583        /// Wire: an i32 type tag, optionally followed by a length prefix and a
584        /// payload per tag. Only the tags present in `DeviceProductInfo`
585        /// (String/Integer/Parcelable/null) and its nested `ManufactureDate`
586        /// must be handled; everything else is defensively rejected so a
587        /// layout drift surfaces as a `display_info` error instead of a
588        /// misaligned parse.
589        fn skip_value(&self) -> Result<(), CoreError> {
590            let tag = self.read_i32()?;
591            match tag {
592                // VAL_NULL (-1): no payload.
593                -1 => Ok(()),
594                // VAL_STRING (0): writeString → String16.
595                0 => self.skip_string16().map(|_| ()),
596                // VAL_INTEGER (1), VAL_SHORT (5), VAL_BOOLEAN (9), VAL_BYTE (20),
597                // VAL_CHAR (29): 4-byte scalar.
598                1 | 5 | 9 | 20 | 29 => self.read_i32().map(|_| ()),
599                // VAL_LONG (6): 8-byte scalar.
600                6 => self.read_int64().map(|_| ()),
601                // VAL_FLOAT (7): 4-byte scalar.
602                7 => self.read_float().map(|_| ()),
603                // VAL_DOUBLE (8): 8-byte scalar.
604                8 => {
605                    self.read_i32()?;
606                    self.read_i32()?;
607                    Ok(())
608                }
609                // VAL_SIZE (26): 2×i32.
610                26 => self.skip_i32s(2),
611                // VAL_SIZEF (27): 2×f32.
612                27 => {
613                    self.read_float()?;
614                    self.read_float()?;
615                    Ok(())
616                }
617                // VAL_PARCELABLE (4): length-prefixed — i32 tag, i32 body
618                // length, then the parcelable (class name + fields). Lengths
619                // are 4-byte aligned in practice; the ceiling is defensive.
620                4 => {
621                    let len = self.read_i32()?;
622                    if len < 0 {
623                        return Ok(());
624                    }
625                    self.skip_bytes(len as usize)
626                }
627                _ => Err(CoreError::binder(
628                    tag,
629                    "display_info:unsupported readValue tag",
630                )),
631            }
632        }
633        /// Skip a `Parcel.writeTypedObject` Rect: an i32 marker (0 = null,
634        /// 1 = non-null) followed by 4×i32 bounds when non-null. Anything else
635        /// is a layout drift and rejected.
636        fn skip_typed_rect(&self) -> Result<(), CoreError> {
637            match self.read_i32()? {
638                0 => Ok(()),
639                1 => self.skip_i32s(4),
640                m => Err(CoreError::binder(m, "display_info:bad typed Rect marker")),
641            }
642        }
643        fn read_first_package_from_names(&self) -> Result<Option<String>, CoreError> {
644            let count = self.read_i32()?.max(0) as usize;
645            let mut first: Option<String> = None;
646            for _ in 0..count {
647                let s = self.read_string()?;
648                if first.is_none() {
649                    first = s.and_then(|c| c.split('/').next().map(str::to_owned));
650                }
651            }
652            Ok(first)
653        }
654    }
655
656    // ── ParcelWriter / transact helper ────────────────────────────────────────
657
658    struct ParcelWriter<'a> {
659        vt: &'a Vtable,
660        parcel: &'a OwnedParcel,
661    }
662
663    impl<'a> ParcelWriter<'a> {
664        fn write_i32(&self, v: i32) -> Result<(), CoreError> {
665            let s = unsafe { (self.vt.write_int32)(self.parcel.ptr, v) };
666            if s != STATUS_OK {
667                return Err(CoreError::binder(s, "AParcel_writeInt32"));
668            }
669            Ok(())
670        }
671        fn write_strong_binder(&self, b: *mut AIBinder) -> Result<(), CoreError> {
672            let s = unsafe { (self.vt.write_strong_binder)(self.parcel.ptr, b) };
673            if s != STATUS_OK {
674                return Err(CoreError::binder(s, "AParcel_writeStrongBinder"));
675            }
676            Ok(())
677        }
678    }
679
680    /// Prepare an input parcel, run `writes`, then transact. RAII on both
681    /// ends: a write error drops the input parcel (previously it leaked on the
682    /// write-error path, never reaching transact), and the reply parcel is
683    /// returned owned. The input parcel is transferred to `AIBinder_transact`
684    /// (the framework deletes it even on failure), so the wrapper records that
685    /// by nulling its pointer — never a double-delete.
686    fn transact_write(
687        vt: &Vtable,
688        binder: *mut AIBinder,
689        code: u32,
690        writes: impl FnOnce(&ParcelWriter<'_>) -> Result<(), CoreError>,
691    ) -> Result<OwnedParcel, CoreError> {
692        let mut in_ptr: *mut AParcel = std::ptr::null_mut();
693        let s = unsafe { (vt.prepare_transaction)(binder, &mut in_ptr) };
694        if s != STATUS_OK {
695            return Err(CoreError::binder(s, "AIBinder_prepareTransaction"));
696        }
697        let mut inp = OwnedParcel {
698            ptr: in_ptr,
699            delete: vt.parcel_delete,
700        };
701        {
702            let writer = ParcelWriter { vt, parcel: &inp };
703            writes(&writer)?;
704        }
705        let mut out_ptr: *mut AParcel = std::ptr::null_mut();
706        let s = unsafe { (vt.transact)(binder, code, &mut inp.ptr, &mut out_ptr, 0) };
707        // The framework owns (and deletes) the input parcel from here.
708        inp.ptr = std::ptr::null_mut();
709        let out = OwnedParcel {
710            ptr: out_ptr,
711            delete: vt.parcel_delete,
712        };
713        if s != STATUS_OK {
714            return Err(CoreError::binder(s, "AIBinder_transact"));
715        }
716        Ok(out)
717    }
718
719    // ── Response parsers ──────────────────────────────────────────────────────
720
721    fn parse_stack_info_body(r: &ParcelReader<'_>) -> Result<Option<String>, CoreError> {
722        r.skip_i32s(5)?;
723        r.skip_int_array()?;
724        r.read_first_package_from_names()
725    }
726
727    // RootTaskInfo → (taskId, first childTaskName package). Walks the parcel
728    // once: prefix (bounds, childTaskIds), captures the first package from
729    // childTaskNames, then skips childTaskBounds / childTaskUserIds / visible /
730    // position / TaskInfo.userId to reach taskId — never touching the
731    // Intent/TaskInfo tail. taskId and pkg come from the same transaction, so
732    // callers pair them without a second (racy) round-trip.
733    fn parse_root_task_info_task(r: &ParcelReader<'_>) -> Result<(i32, Option<String>), CoreError> {
734        let scratch = r.read_i32()?;
735        if scratch != 0 {
736            r.skip_i32s(4)?;
737        }
738        r.skip_int_array()?; // childTaskIds
739        let pkg = r.read_first_package_from_names()?; // childTaskNames → pkg
740        // childTaskBounds: typed Rect array (nullable) — read count, skip 4 per entry
741        let bounds_count = r.read_i32()?;
742        let n = if bounds_count < 0 {
743            0
744        } else {
745            bounds_count as usize
746        };
747        for _ in 0..n {
748            let entry = r.read_i32()?;
749            if entry != 0 {
750                r.skip_i32s(4)?;
751            }
752        }
753        r.skip_int_array()?; // childTaskUserIds
754        r.skip_i32s(2)?; // visible, position
755        r.skip_i32s(1)?; // TaskInfo.userId
756        let task_id = r.read_i32()?;
757        Ok((task_id, pkg))
758    }
759
760    /// Decoded subset of the Android 14 `android.view.DisplayInfo` parcel used
761    /// for FPS normalization.
762    struct ParsedDisplayInfo {
763        active_mode_id: i32,
764        vsync_rate: f32,
765        peak_refresh_rate: f32,
766        render_frame_rate: f32,
767    }
768
769    /// Walk the `IDisplayManager.getDisplayInfo` reply parcel following the
770    /// Android 14 `android.view.DisplayInfo.writeToParcel` field order.
771    ///
772    /// Field order (Android 14):
773    /// layerStack, flags, type, displayId, displayGroupId (5×i32), then a
774    /// nullable `DisplayAddress` parcelable (`writeParcelable` → a String16
775    /// class name, or a single `-1` marker for null), a nullable
776    /// `DeviceProductInfo` parcelable, then `name` as a **String8** (Java
777    /// `writeString8`), 8 logical-dimension i32s, the `DisplayCutout`
778    /// ParcelableWrapper, rotation/modeId/renderFrameRate/defaultModeId, and
779    /// `supportedModes`. Only fields up to `supportedModes` are consumed — the
780    /// tail (color modes, HDR, rounded corners, …) is never touched.
781    ///
782    /// `AParcel_readString` decodes String16 only; `name` is String8, so it is
783    /// consumed with `skip_string8`. Null strings and null parcelables carry a
784    /// `-1` length marker, which `string_alloc` now accepts so they decode to
785    /// `None` rather than a hard `STATUS_UNEXPECTED_NULL`.
786    ///
787    /// Validity gate: any value shape outside the expected one returns a
788    /// `CoreError` tagged `display_info` so on-device logs identify the layout
789    /// mismatch.
790    fn parse_display_info(r: &ParcelReader<'_>) -> Result<ParsedDisplayInfo, CoreError> {
791        // 1. layerStack/type/displayId prefix: layerStack, flags, type,
792        //    displayId, displayGroupId.
793        r.skip_i32s(5)?;
794
795        // 2. address: nullable DisplayAddress parcelable. Non-null writes the
796        //    class name as a String16 then the body; null writes a -1 marker.
797        //    Body differs by subtype: Physical → 1×i64 display id, Network →
798        //    String16 mac address. Dispatch on the decoded class name so a
799        //    network display cannot desync the walk.
800        if let Some(addr_class) = r.read_string()? {
801            if addr_class.ends_with("$Physical") {
802                r.read_int64()?;
803            } else if addr_class.ends_with("$Network") {
804                r.skip_string16()?;
805            } else {
806                return Err(CoreError::binder(
807                    -1,
808                    "display_info:unsupported DisplayAddress",
809                ));
810            }
811        }
812
813        // 3. deviceProductInfo: nullable parcelable (same null probe). Body:
814        //    mName (String16), mManufacturerPnpId (String16), then three
815        //    readValue() fields — mProductId (String), mModelYear (Integer),
816        //    mManufactureDate (nullable ManufactureDate parcelable) — and a
817        //    final mConnectionToSinkType i32. The ManufactureDate parcelable is
818        //    length-prefixed, so skip_value's VAL_PARCELABLE case consumes it
819        //    wholesale including its internal writeValue fields.
820        if let Some(_dpi_class) = r.read_string()? {
821            r.skip_string16()?; // mName
822            r.skip_string16()?; // mManufacturerPnpId
823            r.skip_value()?; // mProductId
824            r.skip_value()?; // mModelYear
825            r.skip_value()?; // mManufactureDate
826            r.read_i32()?; // mConnectionToSinkType
827        }
828
829        // 4. name — Java `writeString8`, a String8 (byte length + UTF-8), may
830        //    be null. Must NOT be read with read_string() (String16 only).
831        r.skip_string8()?;
832
833        // 5. Logical dimensions: appWidth, appHeight, smallestNominalAppWidth,
834        //    smallestNominalAppHeight, largestNominalAppWidth,
835        //    largestNominalAppHeight, logicalWidth, logicalHeight (8×i32).
836        r.skip_i32s(8)?;
837
838        // 6. displayCutout: ParcelableWrapper. writeCutoutToParcel writes -1
839        //    (null), 0 (NO_CUTOUT), or 1 + body. Body: safeInsets (typed
840        //    Rect), bounds (typed Rect array), waterfallInsets (typed Rect),
841        //    4×i32 cutout path parser dims, density f32, cutoutSpec String16,
842        //    rotation i32, scale f32, physicalPixelDisplaySizeRatio f32.
843        let cutout_marker = r.read_i32()?;
844        if cutout_marker == 1 {
845            r.skip_typed_rect()?; // mSafeInsets
846            let bounds = r.read_i32()?; // mBounds Rect[]
847            if bounds >= 0 {
848                for _ in 0..bounds {
849                    r.skip_typed_rect()?;
850                }
851            }
852            r.skip_typed_rect()?; // mWaterfallInsets
853            r.skip_i32s(4)?; // cutout path parser info (display/phys dims)
854            r.read_float()?; // density
855            r.skip_string16()?; // cutoutSpec
856            r.read_i32()?; // rotation
857            r.read_float()?; // scale
858            r.read_float()?; // physicalPixelDisplaySizeRatio
859        }
860
861        // 7. rotation (i32), modeId (i32), renderFrameRate (f32),
862        //    defaultModeId (i32), then nModes (i32).
863        r.read_i32()?; // rotation
864        let active_mode_id = r.read_i32()?;
865        let render_frame_rate = {
866            let v = r.read_float()?;
867            if v.is_finite() && v >= 0.0 { v } else { 0.0 }
868        };
869        r.read_i32()?; // defaultModeId
870        let n_modes = r.read_i32()?.max(0) as usize;
871
872        // 8. supportedModes: each Display.Mode = { modeId, width, height,
873        //    refreshRate (×4 primitive) + alternativeRefreshRates float[],
874        //    supportedHdrTypes int[] }.
875        let mut vsync_rate: f32 = 0.0;
876        let mut peak_refresh_rate: f32 = 0.0;
877        for _ in 0..n_modes {
878            let mode_id = r.read_i32()?;
879            r.read_i32()?; // width
880            r.read_i32()?; // height
881            let refresh_rate = {
882                let v = r.read_float()?;
883                // collapse NaN/±Inf/negative like the FPS path; otherwise a
884                // hostile peer could leak +Inf into the public DisplayInfo.
885                if v.is_finite() && v >= 0.0 { v } else { 0.0 }
886            };
887            // alternativeRefreshRates: float[] (count + values)
888            let alt = r.read_i32()?.max(0) as usize;
889            for _ in 0..alt {
890                let _f = r.read_float()?;
891            }
892            // supportedHdrTypes: int[]
893            let hdr = r.read_i32()?.max(0) as usize;
894            for _ in 0..hdr {
895                r.read_i32()?;
896            }
897            peak_refresh_rate = peak_refresh_rate.max(refresh_rate);
898            if mode_id == active_mode_id {
899                vsync_rate = refresh_rate.max(vsync_rate);
900            }
901        }
902
903        Ok(ParsedDisplayInfo {
904            active_mode_id,
905            vsync_rate,
906            peak_refresh_rate,
907            render_frame_rate,
908        })
909    }
910
911    // ── Tx code resolution ────────────────────────────────────────────────────
912
913    pub struct TxCodes {
914        pub observer_code: u32,
915        pub query_code: u32,
916        pub api_mode: u8, // 1 = RootTaskInfo, 2 = StackInfo
917        pub fg_code: u32,
918    }
919
920    pub fn resolve_tx_codes() -> Result<TxCodes, CoreError> {
921        let (obs, query, api, fg) = dex::resolve_tx_codes_from_dex()
922            .ok_or_else(|| CoreError::binder(-1, "tx_code_resolution:dex_parse_failed"))?;
923        Ok(TxCodes {
924            observer_code: obs,
925            query_code: query,
926            api_mode: api,
927            fg_code: fg,
928        })
929    }
930
931    // ── ActivityManager ─────────────────────────────────────────────────
932
933    pub struct ActivityManager {
934        _lib: DlHandle,
935        vt: Vtable,
936        _class: *mut AIBinder_Class,
937        service: OwnedBinder,
938        tx_code: u32,
939        legacy: bool,
940    }
941    unsafe impl Send for ActivityManager {}
942
943    impl ActivityManager {
944        fn open_inner(
945            handle: *mut c_void,
946        ) -> Result<(DlHandle, Vtable, *mut AIBinder_Class, OwnedBinder), CoreError> {
947            let lib = DlHandle;
948            let vt = load_vtable(handle)?;
949
950            let am_class = unsafe {
951                (vt.class_define)(
952                    AM_DESCRIPTOR.as_ptr() as *const c_char,
953                    am_on_create,
954                    am_on_destroy,
955                    am_on_transact,
956                )
957            };
958            if am_class.is_null() {
959                return Err(CoreError::binder(-1, "AIBinder_Class_define:AM"));
960            }
961
962            let raw = unsafe { (vt.get_service)(ACTIVITY_SERVICE.as_ptr() as *const c_char) };
963            if raw.is_null() {
964                return Err(CoreError::binder(-1, "AServiceManager_getService:activity"));
965            }
966            unsafe { (vt.associate_class)(raw, am_class) };
967
968            let service = OwnedBinder {
969                ptr: raw,
970                dec_strong: vt.dec_strong,
971            };
972            Ok((lib, vt, am_class, service))
973        }
974
975        fn dlopen_libbinder() -> Result<*mut c_void, CoreError> {
976            use std::os::raw::c_char;
977            let handle = unsafe {
978                libc::dlopen(
979                    LIBBINDER_PATH.as_ptr() as *const c_char,
980                    libc::RTLD_NOW | libc::RTLD_LOCAL,
981                )
982            };
983            if handle.is_null() {
984                return Err(CoreError::binder(-1, "dlopen:libbinder_ndk.so"));
985            }
986            Ok(handle)
987        }
988
989        /// Open ActivityManager binder (polling mode — no observer).
990        /// Resolves the query tx code from cache or DEX.
991        pub fn open() -> Result<Self, CoreError> {
992            let handle = Self::dlopen_libbinder()?;
993            let (lib, vt, class, service) = Self::open_inner(handle)?;
994            let codes = resolve_tx_codes()?;
995            let legacy = codes.api_mode == 2;
996            Ok(Self {
997                _lib: lib,
998                vt,
999                _class: class,
1000                service,
1001                tx_code: codes.query_code,
1002                legacy,
1003            })
1004        }
1005
1006        /// Open ActivityManager binder and register as IProcessObserver.
1007        ///
1008        /// Returns `(Self, OwnedFd)` where the eventfd is a dup of the core's
1009        /// callback fd. It becomes readable whenever `onForegroundActivitiesChanged`
1010        /// fires. Caller must add it to epoll and may close it at any time — the
1011        /// callback keeps writing to the core's copy, so closing the returned
1012        /// fd never invalidates the notification path (C2). After the event
1013        /// fires, call `get_focused_package`.
1014        pub fn open_with_observer() -> Result<(Self, OwnedFd), CoreError> {
1015            let handle = Self::dlopen_libbinder()?;
1016            let (lib, vt, am_class, service) = Self::open_inner(handle)?;
1017            let codes = resolve_tx_codes()?;
1018            let legacy = codes.api_mode == 2;
1019
1020            // Create eventfd for callback → epoll bridge. Ownership stays in the
1021            // core for the observer lifetime; the consumer receives a dup below.
1022            let owned = unsafe {
1023                let raw = libc::eventfd(0, libc::EFD_NONBLOCK | libc::EFD_CLOEXEC);
1024                if raw < 0 {
1025                    return Err(CoreError::sys(*libc::__errno(), "eventfd"));
1026                }
1027                OwnedFd::from_raw_fd(raw)
1028            };
1029
1030            // Define IProcessObserver class (we're the server)
1031            let obs_class = unsafe {
1032                (vt.class_define)(
1033                    OBS_DESCRIPTOR.as_ptr() as *const c_char,
1034                    obs_on_create,
1035                    obs_on_destroy,
1036                    obs_on_transact,
1037                )
1038            };
1039            if obs_class.is_null() {
1040                return Err(CoreError::binder(-1, "AIBinder_Class_define:Observer"));
1041            }
1042
1043            // Instantiate our observer binder object
1044            let obs_binder = unsafe { (vt.new_binder)(obs_class, std::ptr::null_mut()) };
1045            if obs_binder.is_null() {
1046                return Err(CoreError::binder(-1, "AIBinder_new:Observer"));
1047            }
1048            unsafe { (vt.associate_class)(obs_binder, obs_class) };
1049
1050            // Call registerProcessObserver(observer)
1051            let _ = transact_write(&vt, service.ptr, codes.observer_code, |w| {
1052                w.write_strong_binder(obs_binder)
1053            })?;
1054
1055            // Consumer dup — made before publishing, so an error path drops the
1056            // owned fd without ever leaving a stale handle for the callback.
1057            let consumer = owned
1058                .try_clone()
1059                .map_err(|e| CoreError::sys(e.raw_os_error().unwrap_or(-1), "dup:observer"))?;
1060
1061            // Publish fg_code and the core-owned eventfd for the callback
1062            OBS_FG_CODE.store(codes.fg_code, Ordering::Relaxed);
1063            *obs_eventfd_guard() = Some(owned);
1064
1065            // Start binder thread pool — blocks forever in background thread
1066            unsafe { (vt.set_thread_pool_max)(0) };
1067            let join_fn = vt.join_thread_pool;
1068            std::thread::spawn(move || unsafe { join_fn() });
1069
1070            let binder = Self {
1071                _lib: lib,
1072                vt,
1073                _class: am_class,
1074                service,
1075                tx_code: codes.query_code,
1076                legacy,
1077            };
1078            Ok((binder, consumer))
1079        }
1080
1081        /// Open ActivityManager binder and register as the foreground process
1082        /// observer.
1083        ///
1084        /// The authoritative foreground PID is delivered in the callback; this
1085        /// is the low-noise foreground source. Two ROM variants are supported
1086        /// and selected automatically:
1087        ///
1088        /// - Stock: `IForegroundProcessObserver.onForegroundProcessChanged`
1089        ///   delivers a single `int pid`.
1090        /// - Custom ROMs that dropped that interface instead deliver `(int pid,
1091        ///   int uid, int fg)` through the repurposed
1092        ///   `IProcessObserver.onForegroundActivitiesChanged`; this registers
1093        ///   via `registerProcessObserver` and only signals on `fg != 0`.
1094        ///
1095        /// The callback stores the PID (readable via [`last_foreground_pid`])
1096        /// and signals the returned eventfd.
1097        ///
1098        /// Returns `(Self, OwnedFd)` where the eventfd is a dup of the core's
1099        /// callback fd. It becomes readable whenever a foreground process
1100        /// change fires. Same lifetime contract as
1101        /// [`ActivityManager::open_with_observer`] (C2): the core owns
1102        /// the eventfd and the callback only ever writes to that copy, so
1103        /// closing the returned dup never invalidates the notification path.
1104        pub fn open_with_fgproc_observer() -> Result<(Self, OwnedFd), CoreError> {
1105            let handle = Self::dlopen_libbinder()?;
1106            let (lib, vt, am_class, service) = Self::open_inner(handle)?;
1107
1108            // Resolve the foreground-observer tx codes. Prefer the stock
1109            // IForegroundProcessObserver path; fall back to the custom
1110            // IProcessObserver pid-carrying form on ROMs that dropped it.
1111            // mode 0 = stock single-int callback, mode 1 = (pid, uid, fg).
1112            let (register_code, fgproc_code, mode, descriptor): (u32, u32, u32, &[u8]) =
1113                match crate::android::dex::resolve_fgproc_codes() {
1114                    Some((r, c)) => (r, c, 0, FGPROC_DESCRIPTOR),
1115                    None => match crate::android::dex::resolve_fgproc_codes_fallback() {
1116                        Some((r, c)) => (r, c, 1, OBS_DESCRIPTOR),
1117                        None => {
1118                            return Err(CoreError::binder(
1119                                -1,
1120                                "tx_code_resolution:fgproc_dex_parse_failed",
1121                            ));
1122                        }
1123                    },
1124                };
1125
1126            // Create eventfd for callback → epoll bridge. Ownership stays in the
1127            // core for the observer lifetime; the consumer receives a dup below.
1128            let owned = unsafe {
1129                let raw = libc::eventfd(0, libc::EFD_NONBLOCK | libc::EFD_CLOEXEC);
1130                if raw < 0 {
1131                    return Err(CoreError::sys(*libc::__errno(), "eventfd"));
1132                }
1133                OwnedFd::from_raw_fd(raw)
1134            };
1135
1136            // Define our observer class (we're the server). The descriptor must
1137            // match whichever interface we actually register as.
1138            let obs_class = unsafe {
1139                (vt.class_define)(
1140                    descriptor.as_ptr() as *const c_char,
1141                    fgproc_on_create,
1142                    fgproc_on_destroy,
1143                    fgproc_on_transact,
1144                )
1145            };
1146            if obs_class.is_null() {
1147                return Err(CoreError::binder(
1148                    -1,
1149                    "AIBinder_Class_define:FGProcessObserver",
1150                ));
1151            }
1152
1153            // Instantiate our observer binder object
1154            let obs_binder = unsafe { (vt.new_binder)(obs_class, std::ptr::null_mut()) };
1155            if obs_binder.is_null() {
1156                return Err(CoreError::binder(-1, "AIBinder_new:FGProcessObserver"));
1157            }
1158            unsafe { (vt.associate_class)(obs_binder, obs_class) };
1159
1160            // Call registerForegroundProcessObserver(observer) or the fallback
1161            // registerProcessObserver(observer) depending on resolved mode.
1162            let _ = transact_write(&vt, service.ptr, register_code, |w| {
1163                w.write_strong_binder(obs_binder)
1164            })?;
1165
1166            // Consumer dup — made before publishing, so an error path drops the
1167            // owned fd without ever leaving a stale handle for the callback.
1168            let consumer = owned.try_clone().map_err(|e| {
1169                CoreError::sys(e.raw_os_error().unwrap_or(-1), "dup:fgproc_observer")
1170            })?;
1171
1172            // Publish reader fn, mode, fg code, pid base, and the core-owned
1173            // eventfd for the callback. Reader and mode are published first so
1174            // the callback never sees a matching code with an unset reader or
1175            // mode (C2-adjacent init order).
1176            FGPROC_READ_I32.store(vt.read_int32 as usize, Ordering::Relaxed);
1177            FGPROC_IPROC_MODE.store(mode, Ordering::Relaxed);
1178            FGPROC_FG_CODE.store(fgproc_code, Ordering::Relaxed);
1179            FGPROC_PID.store(0, Ordering::Relaxed);
1180            *fgproc_eventfd_guard() = Some(owned);
1181
1182            // Start binder thread pool — blocks forever in background thread
1183            unsafe { (vt.set_thread_pool_max)(0) };
1184            let join_fn = vt.join_thread_pool;
1185            std::thread::spawn(move || unsafe { join_fn() });
1186
1187            let binder = Self {
1188                _lib: lib,
1189                vt,
1190                _class: am_class,
1191                service,
1192                tx_code: 0,
1193                legacy: false,
1194            };
1195            Ok((binder, consumer))
1196        }
1197
1198        fn do_transact(&self) -> Result<OwnedParcel, CoreError> {
1199            transact_write(&self.vt, self.service.ptr, self.tx_code, |_| Ok(()))
1200        }
1201
1202        /// The focused root task's `(taskId, topActivity package)` from a single
1203        /// txn-31 transaction. Outer `None` = no focused root task (or legacy
1204        /// API 29, where the reply is `StackInfo` and carries no taskId); inner
1205        /// `None` = task known but no package in `childTaskNames`. Both values
1206        /// come from the same parcel, so the registration key and the report
1207        /// tag can never diverge.
1208        pub fn get_focused_task(&self) -> Result<Option<(i32, Option<String>)>, CoreError> {
1209            if self.legacy {
1210                // StackInfo has no taskId — report None rather than a wrong id.
1211                return Ok(None);
1212            }
1213            let out = self.do_transact()?;
1214            let r = ParcelReader {
1215                vt: &self.vt,
1216                parcel: &out,
1217            };
1218            let ex = r.read_i32()?;
1219            if ex != EX_NONE {
1220                return Err(CoreError::binder(ex, "getFocusedTask:exception"));
1221            }
1222            let present = r.read_i32()?;
1223            if present == 0 {
1224                return Ok(None);
1225            }
1226            Ok(Some(parse_root_task_info_task(&r)?))
1227        }
1228
1229        /// The `topActivity` package of the focused root task (legacy API 29
1230        /// builds use `StackInfo` and still resolve the package). Thin wrapper
1231        /// over [`ActivityManager::get_focused_task`].
1232        pub fn get_focused_package(&self) -> Result<Option<String>, CoreError> {
1233            if self.legacy {
1234                let out = self.do_transact()?;
1235                let r = ParcelReader {
1236                    vt: &self.vt,
1237                    parcel: &out,
1238                };
1239                let ex = r.read_i32()?;
1240                if ex != EX_NONE {
1241                    return Err(CoreError::binder(ex, "getFocusedTask:exception"));
1242                }
1243                let present = r.read_i32()?;
1244                if present == 0 {
1245                    return Ok(None);
1246                }
1247                return parse_stack_info_body(&r);
1248            }
1249            Ok(self.get_focused_task()?.and_then(|(_, pkg)| pkg))
1250        }
1251
1252        /// The `taskId` of the currently focused root task. Thin wrapper over
1253        /// [`ActivityManager::get_focused_task`]; returns `None` when there
1254        /// is no focused root task or on legacy API 29 builds.
1255        pub fn get_focused_task_id(&self) -> Result<Option<i32>, CoreError> {
1256            Ok(self.get_focused_task()?.map(|(task_id, _)| task_id))
1257        }
1258    }
1259
1260    // ── DisplayManager ─────────────────────────────────────────────────
1261
1262    const DISPLAY_SERVICE: &[u8] = b"display\0";
1263    const DISPLAY_DESCRIPTOR: &[u8] = b"android.hardware.display.IDisplayManager\0";
1264    const CALLBACK_DESCRIPTOR: &[u8] = b"android.hardware.display.IDisplayManagerCallback\0";
1265    const POWER_SERVICE: &[u8] = b"power\0";
1266    const POWER_DESCRIPTOR: &[u8] = b"android.os.IPowerManager\0";
1267
1268    const TX_DISPLAY_REGISTER_CALLBACK: u32 = 4;
1269
1270    // Core owns the callback eventfd; the consumer gets a dup and may close it
1271    // freely. Same lifetime discipline as the ActivityManager observer (C2).
1272    static DISP_EVENTFD: Mutex<Option<OwnedFd>> = Mutex::new(None);
1273
1274    fn disp_eventfd_guard() -> std::sync::MutexGuard<'static, Option<OwnedFd>> {
1275        DISP_EVENTFD.lock().unwrap_or_else(|p| p.into_inner())
1276    }
1277
1278    unsafe extern "C" fn disp_cb_on_create(_: *mut c_void) -> *mut c_void {
1279        std::ptr::null_mut()
1280    }
1281    unsafe extern "C" fn disp_cb_on_destroy(_: *mut c_void) {}
1282    unsafe extern "C" fn disp_cb_on_transact(
1283        _: *mut AIBinder,
1284        code: u32,
1285        _: *const AParcel,
1286        _: *mut AParcel,
1287    ) -> BinderStatus {
1288        if code == 1 {
1289            if let Some(fd) = disp_eventfd_guard().as_ref() {
1290                let val: u64 = 1;
1291                unsafe { libc::write(fd.as_raw_fd(), &val as *const u64 as *const c_void, 8) };
1292            }
1293        }
1294        STATUS_OK
1295    }
1296    // Client-only classes: the NDK requires a class on ANY binder that
1297    // participates in a transaction (AIBinder_prepareTransaction fails with
1298    // STATUS_INVALID_OPERATION when getClass() == null) and uses the class
1299    // descriptor to write the interface token. These stubs are never invoked
1300    // for a client (remote) binder — onCreate/onTransact only fire on local
1301    // AIBinder_new instances — so they mirror the ActivityManager client
1302    // pattern exactly.
1303    unsafe extern "C" fn disp_client_on_create(_: *mut c_void) -> *mut c_void {
1304        std::ptr::null_mut()
1305    }
1306    unsafe extern "C" fn disp_client_on_destroy(_: *mut c_void) {}
1307    unsafe extern "C" fn disp_client_on_transact(
1308        _: *mut AIBinder,
1309        _: u32,
1310        _: *const AParcel,
1311        _: *mut AParcel,
1312    ) -> BinderStatus {
1313        STATUS_UNKNOWN_TRANSACTION
1314    }
1315    /// Snapshot of the active display state used for FPS normalization.
1316    ///
1317    /// Populated from `IDisplayManager.getDisplayInfo` (tx resolved from the
1318    /// installed ROM's framework.jar). The parcel layout is Android-version
1319    /// specific; this parse targets Android 14 and must be validated on-device
1320    /// before consumers rely on the values for fps/R normalization.
1321    #[derive(Clone, Copy, Debug, PartialEq, Default)]
1322    pub struct DisplayInfo {
1323        /// Active display mode id (`DisplayInfo.modeId`).
1324        pub active_mode_id: i32,
1325        /// Refresh rate of the active mode, in Hz.
1326        pub vsync_rate: f32,
1327        /// Highest refresh rate across supported modes, in Hz.
1328        pub peak_refresh_rate: f32,
1329        /// `DisplayInfo.renderFrameRate` (Android 13+), in Hz.
1330        pub render_frame_rate: f32,
1331        /// Interactive state reported by the power service.
1332        pub is_interactive: bool,
1333    }
1334
1335    pub struct DisplayManager {
1336        _lib: DlHandle,
1337        vt: Vtable,
1338        display: Option<OwnedBinder>,
1339        display_info_tx: Option<u32>,
1340        power: Option<OwnedBinder>,
1341        is_interactive_tx: u32,
1342    }
1343    unsafe impl Send for DisplayManager {}
1344
1345    impl DisplayManager {
1346        pub fn open_with_callback() -> Result<(Self, crate::fd::Fd), CoreError> {
1347            let handle = unsafe {
1348                libc::dlopen(
1349                    LIBBINDER_PATH.as_ptr() as *const c_char,
1350                    libc::RTLD_NOW | libc::RTLD_LOCAL,
1351                )
1352            };
1353            if handle.is_null() {
1354                return Err(CoreError::binder(-1, "dlopen:libbinder_ndk.so"));
1355            }
1356            let lib = DlHandle;
1357            let vt = load_vtable(handle)?;
1358
1359            // Blocking eventfd (no EFD_NONBLOCK) — callback writes, caller's
1360            // read_u64_blocking() waits. The core owns it for the callback's
1361            // lifetime; the consumer receives a dup below (C2).
1362            let owned = unsafe {
1363                let raw = libc::eventfd(0, libc::EFD_CLOEXEC);
1364                if raw < 0 {
1365                    return Err(CoreError::sys(*libc::__errno(), "eventfd"));
1366                }
1367                OwnedFd::from_raw_fd(raw)
1368            };
1369
1370            // Get display service. A client-only binder still needs a class
1371            // (AIBinder_prepareTransaction requires getClass() != null and
1372            // writes the interface token from the class descriptor).
1373            let raw_display =
1374                unsafe { (vt.get_service)(DISPLAY_SERVICE.as_ptr() as *const c_char) };
1375            if raw_display.is_null() {
1376                return Err(CoreError::binder(-1, "AServiceManager_getService:display"));
1377            }
1378            let display_class = unsafe {
1379                (vt.class_define)(
1380                    DISPLAY_DESCRIPTOR.as_ptr() as *const c_char,
1381                    disp_client_on_create,
1382                    disp_client_on_destroy,
1383                    disp_client_on_transact,
1384                )
1385            };
1386            if display_class.is_null() {
1387                return Err(CoreError::binder(
1388                    -1,
1389                    "AIBinder_Class_define:IDisplayManager",
1390                ));
1391            }
1392            unsafe { (vt.associate_class)(raw_display, display_class) };
1393            let display = OwnedBinder {
1394                ptr: raw_display,
1395                dec_strong: vt.dec_strong,
1396            };
1397
1398            // Define IDisplayManagerCallback (we're the server receiving callbacks)
1399            let cb_class = unsafe {
1400                (vt.class_define)(
1401                    CALLBACK_DESCRIPTOR.as_ptr() as *const c_char,
1402                    disp_cb_on_create,
1403                    disp_cb_on_destroy,
1404                    disp_cb_on_transact,
1405                )
1406            };
1407            if cb_class.is_null() {
1408                return Err(CoreError::binder(
1409                    -1,
1410                    "AIBinder_Class_define:DisplayCallback",
1411                ));
1412            }
1413
1414            let cb_binder = unsafe { (vt.new_binder)(cb_class, std::ptr::null_mut()) };
1415            if cb_binder.is_null() {
1416                return Err(CoreError::binder(-1, "AIBinder_new:DisplayCallback"));
1417            }
1418
1419            // registerCallback(callback) — tx 4
1420            let _ = transact_write(&vt, display.ptr, TX_DISPLAY_REGISTER_CALLBACK, |w| {
1421                w.write_strong_binder(cb_binder)
1422            })?;
1423
1424            // Optional: grab power service for is_interactive(). Same client
1425            // class requirement as the display binder above.
1426            let power_class = unsafe {
1427                (vt.class_define)(
1428                    POWER_DESCRIPTOR.as_ptr() as *const c_char,
1429                    disp_client_on_create,
1430                    disp_client_on_destroy,
1431                    disp_client_on_transact,
1432                )
1433            };
1434            let power = if power_class.is_null() {
1435                None
1436            } else {
1437                let raw = unsafe { (vt.get_service)(POWER_SERVICE.as_ptr() as *const c_char) };
1438                if raw.is_null() {
1439                    None
1440                } else {
1441                    unsafe { (vt.associate_class)(raw, power_class) };
1442                    Some(OwnedBinder {
1443                        ptr: raw,
1444                        dec_strong: vt.dec_strong,
1445                    })
1446                }
1447            };
1448
1449            // Resolve isInteractive tx code from DEX at open time
1450            let is_interactive_tx = crate::android::dex::resolve_is_interactive_tx()
1451                .ok_or_else(|| CoreError::binder(-1, "dex:TRANSACTION_isInteractive not found"))?;
1452
1453            // Resolve getDisplayInfo tx from DEX (best-effort, optional). ROMs
1454            // that drop the field skip the info() API rather than failing open.
1455            let display_info_tx = crate::android::dex::resolve_display_info_tx();
1456
1457            // Consumer dup — made before publishing, so an error path drops the
1458            // owned fd without ever leaving a stale handle for the callback.
1459            let efd_owned = owned
1460                .try_clone()
1461                .map_err(|e| CoreError::sys(e.raw_os_error().unwrap_or(-1), "dup:display"))
1462                .and_then(|dup| unsafe {
1463                    crate::fd::Fd::from_owned_raw_fd(dup.into_raw_fd(), "display.efd")
1464                        .map_err(|_| CoreError::binder(-1, "Fd::from_owned_raw_fd:display.efd"))
1465                })?;
1466
1467            // Publish the core-owned eventfd for the callback
1468            *disp_eventfd_guard() = Some(owned);
1469
1470            // Join binder thread pool so callbacks can fire
1471            unsafe { (vt.set_thread_pool_max)(0) };
1472            let join_fn = vt.join_thread_pool;
1473            std::thread::spawn(move || unsafe { join_fn() });
1474
1475            Ok((
1476                Self {
1477                    _lib: lib,
1478                    vt,
1479                    display: Some(display),
1480                    display_info_tx,
1481                    power,
1482                    is_interactive_tx,
1483                },
1484                efd_owned,
1485            ))
1486        }
1487
1488        pub fn is_interactive(&self) -> Result<bool, CoreError> {
1489            let power = self
1490                .power
1491                .as_ref()
1492                .ok_or_else(|| CoreError::binder(-1, "power:unavailable"))?;
1493            let out = transact_write(&self.vt, power.ptr, self.is_interactive_tx, |_| Ok(()))?;
1494            let r = ParcelReader {
1495                vt: &self.vt,
1496                parcel: &out,
1497            };
1498            let ex = r.read_i32()?;
1499            if ex != EX_NONE {
1500                return Err(CoreError::binder(ex, "isInteractive:exception"));
1501            }
1502            r.read_bool()
1503        }
1504
1505        /// Query the display's current render state for FPS normalization.
1506        ///
1507        /// Transacts `IDisplayManager.getDisplayInfo(display_id)` and decodes
1508        /// the reply parcel (Android 14 `android.view.DisplayInfo` layout):
1509        /// active mode id, active-mode refresh rate, peak refresh rate across
1510        /// supported modes, and `renderFrameRate`, plus the interactive state
1511        /// from the power service.
1512        ///
1513        /// # Best-effort / on-device caveat
1514        /// The parcel walk below follows the Android 14 field ordering. ROMs or
1515        /// future Android releases that reorder `DisplayInfo` fields will fail
1516        /// the walk with a `CoreError` (never garbage) — tune
1517        /// `parse_display_info` against the installed framework.jar, which is
1518        /// why the tx code is resolved at open time via DEX.
1519        pub fn info(&self, display_id: i32) -> Result<DisplayInfo, CoreError> {
1520            let display = self
1521                .display
1522                .as_ref()
1523                .ok_or_else(|| CoreError::binder(-1, "display:unavailable"))?;
1524            let tx = self
1525                .display_info_tx
1526                .ok_or_else(|| CoreError::binder(-1, "display_info:tx unavailable"))?;
1527            let out = transact_write(&self.vt, display.ptr, tx, |w| {
1528                w.write_i32(display_id)
1529            })?;
1530            let r = ParcelReader {
1531                vt: &self.vt,
1532                parcel: &out,
1533            };
1534
1535            let ex = r.read_i32()?;
1536            if ex != EX_NONE {
1537                return Err(CoreError::binder(ex, "getDisplayInfo:exception"));
1538            }
1539            // The reply parcel carries the parcelable off the framework's own
1540            // writeToParcel, which writes no class-name header for a top-level
1541            // reply value in native code — it is the raw DisplayInfo fields.
1542            // The first field read must be layerStack. If a future framework
1543            // writes a class name first, the first read becomes a string length
1544            // (small positive int) and `layer_stack` is a plausible-looking
1545            // wrong value; guard by sanity-checking later fields instead.
1546            let parsed = parse_display_info(&r)?;
1547
1548            let interactive = self.is_interactive().unwrap_or(false);
1549            Ok(DisplayInfo {
1550                active_mode_id: parsed.active_mode_id,
1551                vsync_rate: parsed.vsync_rate,
1552                peak_refresh_rate: parsed.peak_refresh_rate,
1553                render_frame_rate: parsed.render_frame_rate,
1554                is_interactive: interactive,
1555            })
1556        }
1557    }
1558
1559    // ── FpsListener (task FPS callback) ───────────────────────────────────────
1560
1561    const WINDOW_SERVICE: &[u8] = b"window\0";
1562    const WM_DESCRIPTOR: &[u8] = b"android.view.IWindowManager\0";
1563    const FPS_DESCRIPTOR: &[u8] = b"android.window.ITaskFpsCallback\0";
1564    // The last reported FPS (bit pattern of the f32) is published before the
1565    // eventfd is signalled, so the consumer never reads a stale value. The wake
1566    // eventfd is per-instance (same pattern as TaskStackListener): it is handed
1567    // to AIBinder_new as the callback binder's userdata, so a second
1568    // FpsListener can never rewire an earlier registration's wake into its own
1569    // fd (the callback resolves its own binder's fd via AIBinder_getUserData).
1570    static FPS_VALUE: AtomicU32 = AtomicU32::new(0);
1571    // Distinct from FPS_VALUE's bits: `0.0f32` has bit pattern 0, so a "not
1572    // seen" sentinel of 0 would misread a genuine idle (0-FPS) report as "no
1573    // report yet" — swallowing the sample and (downstream) leaving the first-
1574    // report-after-swap drop armed. The seen flag disambiguates.
1575    static FPS_SEEN: AtomicBool = AtomicBool::new(false);
1576    static FPS_CODE: AtomicU32 = AtomicU32::new(0);
1577    static FPS_READ_I32: AtomicUsize = AtomicUsize::new(0);
1578
1579    // No-op callbacks for the client-only IWindowManager class (we never serve
1580    // transactions on the `window` binder — the class exists only to satisfy
1581    // AIBinder_prepareTransaction's remote-transaction contract).
1582    unsafe extern "C" fn wm_on_create(_: *mut c_void) -> *mut c_void {
1583        std::ptr::null_mut()
1584    }
1585    unsafe extern "C" fn wm_on_destroy(_: *mut c_void) {}
1586    unsafe extern "C" fn wm_on_transact(
1587        _: *mut AIBinder,
1588        _: u32,
1589        _: *const AParcel,
1590        _: *mut AParcel,
1591    ) -> BinderStatus {
1592        STATUS_OK
1593    }
1594
1595    // The per-instance wake eventfd is this callback binder's userdata, so
1596    // onCreate must return the args passed to AIBinder_new — AIBinder_getUserData
1597    // returns exactly that value — and onDestroy must reclaim the box (same
1598    // pattern as TaskStackListener). Returning null here would make
1599    // AIBinder_getUserData return null, silently breaking the FPS wake.
1600    unsafe extern "C" fn fps_on_create(userdata: *mut c_void) -> *mut c_void {
1601        userdata
1602    }
1603    unsafe extern "C" fn fps_on_destroy(userdata: *mut c_void) {
1604        if !userdata.is_null() {
1605            unsafe { drop(Box::from_raw(userdata as *mut OwnedFd)) };
1606        }
1607    }
1608    unsafe extern "C" fn fps_on_transact(
1609        binder: *mut AIBinder,
1610        code: u32,
1611        in_parcel: *const AParcel,
1612        _: *mut AParcel,
1613    ) -> BinderStatus {
1614        if code != FPS_CODE.load(Ordering::Relaxed) {
1615            return STATUS_UNKNOWN_TRANSACTION;
1616        }
1617        // Reader is published non-zero before the code, so a matching code is
1618        // never paired with an unset reader.
1619        let read_addr = FPS_READ_I32.load(Ordering::Relaxed);
1620        if read_addr != 0 {
1621            let read_fn: unsafe extern "C" fn(*const AParcel, *mut i32) -> BinderStatus =
1622                unsafe { std::mem::transmute(read_addr) };
1623            let mut bits: i32 = 0;
1624            if unsafe { read_fn(in_parcel, &mut bits) } == STATUS_OK {
1625                // Publish the value before signalling so the consumer always
1626                // sees the value that triggered the wakeup. Non-finite/negative
1627                // reports are normalized to 0.0 first (L5) — the stream must
1628                // never carry "NaN"/"inf".
1629                FPS_VALUE.store(sanitize_fps(bits as u32), Ordering::Relaxed);
1630                FPS_SEEN.store(true, Ordering::Release);
1631                // Per-instance wake: the eventfd is this callback binder's
1632                // userdata (see FpsListener::open), so two FpsListeners never
1633                // cross-wire their wakes. A missing slot means the process-wide
1634                // AIBinder_getUserData symbol has not been cached — drop the
1635                // signal rather than risk a stale fd.
1636                let get_user_data = GET_USER_DATA.lock().unwrap_or_else(|p| p.into_inner());
1637                if let Some(get_user_data) = *get_user_data {
1638                    let userdata = unsafe { get_user_data(binder) };
1639                    if !userdata.is_null() {
1640                        let efd = userdata as *mut OwnedFd;
1641                        let val: u64 = 1;
1642                        unsafe {
1643                            libc::write((*efd).as_raw_fd(), &val as *const u64 as *const c_void, 8)
1644                        };
1645                    }
1646                }
1647            }
1648        }
1649        STATUS_OK
1650    }
1651
1652    /// Push-based per-task FPS listener registered with `WindowManager`.
1653    ///
1654    /// Uses `IWindowManager.registerTaskFpsCallback(taskId, callback)`; the
1655    /// daemon hosts the `ITaskFpsCallback` server object and receives
1656    /// `onFpsReported(float)` one-way transactions from the `FpsReporter` at
1657    /// most every ~500 ms.
1658    ///
1659    /// The registering UID must hold `ACCESS_FPS_COUNTER` (signature|privileged)
1660    /// — this process typically runs as shell (uid 2000) via `su`.
1661    ///
1662    /// Returns `(Self, OwnedFd)` where the eventfd is a dup of the core's
1663    /// callback fd. It becomes readable whenever `onFpsReported` fires; call
1664    /// [`FpsListener::last_fps`] after the event to read the value.
1665    pub struct FpsListener {
1666        _lib: DlHandle,
1667        vt: Vtable,
1668        window: OwnedBinder,
1669        cb_binder: *mut AIBinder,
1670        _wm_class: *mut AIBinder_Class,
1671        register_code: u32,
1672        unregister_code: u32,
1673        task_id: i32,
1674    }
1675    unsafe impl Send for FpsListener {}
1676
1677    impl FpsListener {
1678        /// Open WindowManager and define the `ITaskFpsCallback` server object.
1679        ///
1680        /// Resolves the three tx codes from DEX. Does **not** register a task
1681        /// yet — call [`FpsListener::register`] once a taskId is known. Starts
1682        /// the binder thread pool so `onFpsReported` can fire.
1683        pub fn open() -> Result<(Self, OwnedFd), CoreError> {
1684            let handle = unsafe {
1685                libc::dlopen(
1686                    LIBBINDER_PATH.as_ptr() as *const c_char,
1687                    libc::RTLD_NOW | libc::RTLD_LOCAL,
1688                )
1689            };
1690            if handle.is_null() {
1691                return Err(CoreError::binder(-1, "dlopen:libbinder_ndk.so"));
1692            }
1693            let lib = DlHandle;
1694            let vt = load_vtable(handle)?;
1695
1696            let (register_code, unregister_code, on_fps_code) =
1697                crate::android::dex::resolve_fps_codes().ok_or_else(|| {
1698                    CoreError::binder(-1, "dex:TRANSACTION_registerTaskFpsCallback not found")
1699                })?;
1700
1701            let window = {
1702                let raw = unsafe { (vt.get_service)(WINDOW_SERVICE.as_ptr() as *const c_char) };
1703                if raw.is_null() {
1704                    return Err(CoreError::binder(-1, "AServiceManager_getService:window"));
1705                }
1706                OwnedBinder {
1707                    ptr: raw,
1708                    dec_strong: vt.dec_strong,
1709                }
1710            };
1711
1712            // Remote transactions require a class on the binder (same
1713            // AIBinder_prepareTransaction contract as the AM service above).
1714            let wm_class = unsafe {
1715                (vt.class_define)(
1716                    WM_DESCRIPTOR.as_ptr() as *const c_char,
1717                    wm_on_create,
1718                    wm_on_destroy,
1719                    wm_on_transact,
1720                )
1721            };
1722            if wm_class.is_null() {
1723                return Err(CoreError::binder(
1724                    -1,
1725                    "AIBinder_Class_define:IWindowManager",
1726                ));
1727            }
1728            unsafe { (vt.associate_class)(window.ptr, wm_class) };
1729
1730            let cb_class = unsafe {
1731                (vt.class_define)(
1732                    FPS_DESCRIPTOR.as_ptr() as *const c_char,
1733                    fps_on_create,
1734                    fps_on_destroy,
1735                    fps_on_transact,
1736                )
1737            };
1738            if cb_class.is_null() {
1739                return Err(CoreError::binder(
1740                    -1,
1741                    "AIBinder_Class_define:ITaskFpsCallback",
1742                ));
1743            }
1744
1745            // Nonblocking eventfd — the callback only ever writes to it (an
1746            // eventfd write never blocks), while epoll-based consumers register
1747            // it edge-triggered and drain to EAGAIN, so a blocking fd would
1748            // wedge the consumer's drain loop. Matches the obs/fgproc observer
1749            // eventfds. Each instance owns its own fd; it is handed to the
1750            // callback binder as userdata (per-instance routing, no process-wide
1751            // static) and the consumer receives a dup below (C2).
1752            let owned = unsafe {
1753                let raw = libc::eventfd(0, libc::EFD_NONBLOCK | libc::EFD_CLOEXEC);
1754                if raw < 0 {
1755                    return Err(CoreError::sys(*libc::__errno(), "eventfd"));
1756                }
1757                OwnedFd::from_raw_fd(raw)
1758            };
1759
1760            let consumer = owned
1761                .try_clone()
1762                .map_err(|e| CoreError::sys(e.raw_os_error().unwrap_or(-1), "dup:fps"))?;
1763
1764            let userdata = Box::into_raw(Box::new(owned)) as *mut c_void;
1765            let cb_binder = unsafe { (vt.new_binder)(cb_class, userdata) };
1766            if cb_binder.is_null() {
1767                // Reclaim the userdata box handed to AIBinder_new before bailing.
1768                unsafe { drop(Box::from_raw(userdata as *mut OwnedFd)) };
1769                return Err(CoreError::binder(-1, "AIBinder_new:ITaskFpsCallback"));
1770            }
1771            unsafe { (vt.associate_class)(cb_binder, cb_class) };
1772
1773            // Publish the reader and code before the eventfd registration; a
1774            // matching code is never paired with an unset reader (C2-adjacent).
1775            FPS_READ_I32.store(vt.read_int32 as usize, Ordering::Relaxed);
1776            FPS_SEEN.store(false, Ordering::Relaxed);
1777            FPS_CODE.store(on_fps_code, Ordering::Relaxed);
1778            FPS_VALUE.store(0, Ordering::Relaxed);
1779            // The callback resolves AIBinder_getUserData from this vtable; the
1780            // symbol address is process-wide, so a cached static is safe.
1781            *GET_USER_DATA.lock().unwrap_or_else(|p| p.into_inner()) = Some(vt.get_user_data);
1782
1783            unsafe { (vt.set_thread_pool_max)(0) };
1784            let join_fn = vt.join_thread_pool;
1785            std::thread::spawn(move || unsafe { join_fn() });
1786
1787            Ok((
1788                Self {
1789                    _lib: lib,
1790                    vt,
1791                    window,
1792                    cb_binder,
1793                    _wm_class: wm_class,
1794                    register_code,
1795                    unregister_code,
1796                    task_id: -1,
1797                },
1798                consumer,
1799            ))
1800        }
1801
1802        /// Register the callback for `task_id`. If a task was already
1803        /// registered, it is unregistered first (WindowManager tracks one task
1804        /// per callback binder).
1805        pub fn register(&mut self, task_id: i32) -> Result<(), CoreError> {
1806            if self.task_id == task_id {
1807                return Ok(());
1808            }
1809            if self.task_id >= 0 {
1810                let _ = self.unregister();
1811            }
1812
1813            let _ = transact_write(&self.vt, self.window.ptr, self.register_code, |w| {
1814                w.write_i32(task_id)?;
1815                w.write_strong_binder(self.cb_binder)
1816            })?;
1817            self.task_id = task_id;
1818            Ok(())
1819        }
1820
1821        /// Unregister the callback from WindowManager. No-op if nothing is
1822        /// registered.
1823        pub fn unregister(&mut self) -> Result<(), CoreError> {
1824            if self.task_id < 0 {
1825                return Ok(());
1826            }
1827            let _ = transact_write(&self.vt, self.window.ptr, self.unregister_code, |w| {
1828                w.write_strong_binder(self.cb_binder)
1829            })?;
1830            self.task_id = -1;
1831            Ok(())
1832        }
1833
1834        /// The most recent `onFpsReported` value (f32), or `None` if no report
1835        /// has arrived yet. Safe to call at any time; the bit pattern is
1836        /// published atomically. A genuine 0.0 (idle) report reads as `Some`,
1837        /// distinguished from the no-report state by `FPS_SEEN`.
1838        pub fn last_fps(&self) -> Option<f32> {
1839            fps_from_state(
1840                FPS_SEEN.load(Ordering::Acquire),
1841                FPS_VALUE.load(Ordering::Relaxed),
1842            )
1843        }
1844
1845        /// The taskId currently registered, or `None` if none.
1846        pub fn task_id(&self) -> Option<i32> {
1847            (self.task_id >= 0).then_some(self.task_id)
1848        }
1849    }
1850
1851    impl Drop for FpsListener {
1852        /// Best-effort deregistration from WindowManager so a dropped listener
1853        /// does not leave the framework delivering `onFpsReported` forever. The
1854        /// callback binder's local strong ref is intentionally NOT released:
1855        /// keeping it alive guarantees the per-binder userdata (the OwnedFd)
1856        /// can never be reclaimed by `on_destroy` while a callback is in
1857        /// flight, and the framework-side registration has been dropped by the
1858        /// unregister, so no stale transaction targets this instance.
1859        fn drop(&mut self) {
1860            let _ = self.unregister();
1861        }
1862    }
1863
1864    /// Disambiguate "no report yet" from a genuine 0.0 (idle) report: `seen`
1865    /// tracks whether the callback published a value; `bits` is that value's
1866    /// bit pattern. `0.0f32` has bits 0, so the bits alone cannot tell a real
1867    /// idle sample from an unset slot.
1868    fn fps_from_state(seen: bool, bits: u32) -> Option<f32> {
1869        if seen {
1870            Some(f32::from_bits(bits))
1871        } else {
1872            None
1873        }
1874    }
1875
1876    /// Normalize an `onFpsReported` bit pattern for the stream. Real FPS is
1877    /// non-negative and finite; NaN/±Inf (garbage or corruption) and negative
1878    /// values collapse to 0.0 (idle) so the value stream never shows "NaN" or
1879    /// "inf".
1880    fn sanitize_fps(bits: u32) -> u32 {
1881        let v = f32::from_bits(bits);
1882        if v.is_finite() && v >= 0.0 {
1883            bits
1884        } else {
1885            0.0f32.to_bits()
1886        }
1887    }
1888
1889    // ── TaskStackListener (task-stack change wake-up) ────────────────────────
1890
1891    const TASK_SERVICE: &[u8] = b"activity_task\0";
1892    const ATM_DESCRIPTOR: &[u8] = b"android.app.IActivityTaskManager\0";
1893    const TASK_STACK_DESCRIPTOR: &[u8] = b"android.app.ITaskStackListener\0";
1894
1895    // No-op callbacks for the client-only IActivityTaskManager class (we never
1896    // serve transactions on the `activity_task` binder — the class exists only
1897    // to satisfy AIBinder_prepareTransaction's remote-transaction contract).
1898    unsafe extern "C" fn atm_on_create(_: *mut c_void) -> *mut c_void {
1899        std::ptr::null_mut()
1900    }
1901    unsafe extern "C" fn atm_on_destroy(_: *mut c_void) {}
1902    unsafe extern "C" fn atm_on_transact(
1903        _: *mut AIBinder,
1904        _: u32,
1905        _: *const AParcel,
1906        _: *mut AParcel,
1907    ) -> BinderStatus {
1908        STATUS_OK
1909    }
1910
1911    // Pure wake-up handler: any ITaskStackListener callback
1912    // (onTaskStackChanged, onTaskMovedToFront, …) just signals the eventfd.
1913    // The callback arguments are deliberately NOT parsed — the authoritative
1914    // (taskId, pkg) comes from re-querying getFocusedRootTaskInfo (txn 31) on
1915    // the event, so we never depend on a parcel layout (RunningTaskInfo places
1916    // taskId near the parcel tail).
1917    //
1918    // The wake eventfd is per-instance: the daemon hosts two listeners (the fg
1919    // task source and the fps channel), each with its own eventfd. It is handed
1920    // to AIBinder_new as the binder's userdata, so on_destroy must reclaim it.
1921    // No process-wide static — a shared fd would deliver every instance's
1922    // wake to whichever listener opened last.
1923    //
1924    // The callback resolves the per-binder eventfd through AIBinder_getUserData.
1925    // The symbol address is process-wide, so it is cached once in a static.
1926    static GET_USER_DATA: std::sync::Mutex<
1927        Option<unsafe extern "C" fn(*const AIBinder) -> *mut c_void>,
1928    > = std::sync::Mutex::new(None);
1929
1930    unsafe extern "C" fn task_stack_on_create(userdata: *mut c_void) -> *mut c_void {
1931        userdata
1932    }
1933    unsafe extern "C" fn task_stack_on_destroy(userdata: *mut c_void) {
1934        if !userdata.is_null() {
1935            unsafe { drop(Box::from_raw(userdata as *mut OwnedFd)) };
1936        }
1937    }
1938    unsafe extern "C" fn task_stack_on_transact(
1939        binder: *mut AIBinder,
1940        _code: u32,
1941        _in_parcel: *const AParcel,
1942        _reply: *mut AParcel,
1943    ) -> BinderStatus {
1944        let get_user_data = GET_USER_DATA.lock().unwrap_or_else(|p| p.into_inner());
1945        if let Some(get_user_data) = *get_user_data {
1946            let userdata = unsafe { get_user_data(binder) };
1947            if !userdata.is_null() {
1948                let efd = userdata as *mut OwnedFd;
1949                let val: u64 = 1;
1950                unsafe { libc::write((*efd).as_raw_fd(), &val as *const u64 as *const c_void, 8) };
1951            }
1952        }
1953        STATUS_OK
1954    }
1955
1956    /// Push-based task-stack change listener registered with
1957    /// `IActivityTaskManager`.
1958    ///
1959    /// Uses `IActivityTaskManager.registerTaskStackListener(listener)`; the
1960    /// daemon hosts the `ITaskStackListener` server object. The callback is a
1961    /// pure wake-up: on any task-stack change it signals the eventfd and
1962    /// parses nothing. Consumers re-query `getFocusedRootTaskInfo` (txn 31) on
1963    /// the event for the authoritative `(taskId, pkg)`.
1964    ///
1965    /// This target's ROM exposes the legacy `ITaskStackListener` /
1966    /// `registerTaskStackListener` pair; the newer `ITaskChangeListener` /
1967    /// `registerTaskChangeListener` interface is absent.
1968    ///
1969    /// The registering UID must hold `MANAGE_ACTIVITY_TASKS` /
1970    /// `MANAGE_ACTIVITY_STACKS` — the same gate as txn 31, which root passes
1971    /// empirically on the target ROM.
1972    ///
1973    /// Returns `(Self, OwnedFd)` where the eventfd is a dup of the core's
1974    /// callback fd. It becomes readable on any task-stack change.
1975    pub struct TaskStackListener {
1976        _lib: DlHandle,
1977        vt: Vtable,
1978        service: OwnedBinder,
1979        cb_binder: *mut AIBinder,
1980        _atm_class: *mut AIBinder_Class,
1981        register_code: u32,
1982        unregister_code: u32,
1983    }
1984    unsafe impl Send for TaskStackListener {}
1985
1986    impl TaskStackListener {
1987        /// Open `activity_task`, define the `ITaskStackListener` server object,
1988        /// and start the binder thread pool. Does **not** register yet — call
1989        /// [`TaskStackListener::register`] once consumers are active.
1990        pub fn open() -> Result<(Self, OwnedFd), CoreError> {
1991            let handle = unsafe {
1992                libc::dlopen(
1993                    LIBBINDER_PATH.as_ptr() as *const c_char,
1994                    libc::RTLD_NOW | libc::RTLD_LOCAL,
1995                )
1996            };
1997            if handle.is_null() {
1998                return Err(CoreError::binder(-1, "dlopen:libbinder_ndk.so"));
1999            }
2000            let lib = DlHandle;
2001            let vt = load_vtable(handle)?;
2002
2003            let (register_code, unregister_code) = crate::android::dex::resolve_task_stack_codes()
2004                .ok_or_else(|| {
2005                    CoreError::binder(-1, "dex:TRANSACTION_registerTaskStackListener not found")
2006                })?;
2007
2008            let service = {
2009                let raw = unsafe { (vt.get_service)(TASK_SERVICE.as_ptr() as *const c_char) };
2010                if raw.is_null() {
2011                    return Err(CoreError::binder(
2012                        -1,
2013                        "AServiceManager_getService:activity_task",
2014                    ));
2015                }
2016                OwnedBinder {
2017                    ptr: raw,
2018                    dec_strong: vt.dec_strong,
2019                }
2020            };
2021
2022            // Remote transactions require a class on the binder (same
2023            // AIBinder_prepareTransaction contract as the other services).
2024            let atm_class = unsafe {
2025                (vt.class_define)(
2026                    ATM_DESCRIPTOR.as_ptr() as *const c_char,
2027                    atm_on_create,
2028                    atm_on_destroy,
2029                    atm_on_transact,
2030                )
2031            };
2032            if atm_class.is_null() {
2033                return Err(CoreError::binder(
2034                    -1,
2035                    "AIBinder_Class_define:IActivityTaskManager",
2036                ));
2037            }
2038            unsafe { (vt.associate_class)(service.ptr, atm_class) };
2039
2040            let cb_class = unsafe {
2041                (vt.class_define)(
2042                    TASK_STACK_DESCRIPTOR.as_ptr() as *const c_char,
2043                    task_stack_on_create,
2044                    task_stack_on_destroy,
2045                    task_stack_on_transact,
2046                )
2047            };
2048            if cb_class.is_null() {
2049                return Err(CoreError::binder(
2050                    -1,
2051                    "AIBinder_Class_define:ITaskStackListener",
2052                ));
2053            }
2054
2055            // Blocking eventfd — callback writes, consumer waits/reads. Each
2056            // instance owns its own fd; it is handed to the callback binder as
2057            // userdata (per-instance routing, no process-wide static) and the
2058            // consumer receives a dup below (C2).
2059            let owned = unsafe {
2060                let raw = libc::eventfd(0, libc::EFD_CLOEXEC);
2061                if raw < 0 {
2062                    return Err(CoreError::sys(*libc::__errno(), "eventfd"));
2063                }
2064                OwnedFd::from_raw_fd(raw)
2065            };
2066
2067            let consumer = owned
2068                .try_clone()
2069                .map_err(|e| CoreError::sys(e.raw_os_error().unwrap_or(-1), "dup:task_stack"))?;
2070
2071            let userdata = Box::into_raw(Box::new(owned)) as *mut c_void;
2072            let cb_binder = unsafe { (vt.new_binder)(cb_class, userdata) };
2073            if cb_binder.is_null() {
2074                // Reclaim the userdata box handed to AIBinder_new before bailing.
2075                unsafe { drop(Box::from_raw(userdata as *mut OwnedFd)) };
2076                return Err(CoreError::binder(-1, "AIBinder_new:ITaskStackListener"));
2077            }
2078            unsafe { (vt.associate_class)(cb_binder, cb_class) };
2079
2080            // The callback resolves AIBinder_getUserData from this vtable; the
2081            // symbol address is process-wide, so a cached static is safe.
2082            *GET_USER_DATA.lock().unwrap_or_else(|p| p.into_inner()) = Some(vt.get_user_data);
2083
2084            unsafe { (vt.set_thread_pool_max)(0) };
2085            let join_fn = vt.join_thread_pool;
2086            std::thread::spawn(move || unsafe { join_fn() });
2087
2088            Ok((
2089                Self {
2090                    _lib: lib,
2091                    vt,
2092                    service,
2093                    cb_binder,
2094                    _atm_class: atm_class,
2095                    register_code,
2096                    unregister_code,
2097                },
2098                consumer,
2099            ))
2100        }
2101
2102        /// Register the task-stack listener with `activity_task` (one listener
2103        /// receives all task-stack events). Idempotent at the framework level;
2104        /// callers should register once and keep the object alive.
2105        pub fn register(&self) -> Result<(), CoreError> {
2106            let _ = transact_write(&self.vt, self.service.ptr, self.register_code, |w| {
2107                w.write_strong_binder(self.cb_binder)
2108            })?;
2109            Ok(())
2110        }
2111
2112        /// Unregister the task-stack listener from `activity_task`. No-op at
2113        /// the framework level if not registered; callers should unregister
2114        /// before dropping the object.
2115        pub fn unregister(&self) -> Result<(), CoreError> {
2116            let _ = transact_write(&self.vt, self.service.ptr, self.unregister_code, |w| {
2117                w.write_strong_binder(self.cb_binder)
2118            })?;
2119            Ok(())
2120        }
2121    }
2122
2123    impl Drop for TaskStackListener {
2124        /// Best-effort deregistration from `activity_task`. Same deliberate
2125        /// non-release of the local strong ref as [`FpsListener`] — the
2126        /// userdata OwnedFd stays valid for any in-flight wake, and the
2127        /// framework-side registration is gone after the unregister.
2128        fn drop(&mut self) {
2129            let _ = self.unregister();
2130        }
2131    }
2132
2133    // ── RawBinderService ──────────────────────────────────────────────────────
2134
2135    /// Generic binder client for any named Android service.
2136    ///
2137    /// Handles its own `dlopen` on `libbinder_ndk.so`. Callers provide raw
2138    /// transaction codes (resolved via [`crate::android::dex::find_transaction_code`])
2139    /// and use [`RawBinderService::transact_bool`] /
2140    /// [`RawBinderService::transact_i32`] for typed round-trips.
2141    pub struct RawBinderService {
2142        _lib: DlHandle,
2143        vt: Vtable,
2144        service: OwnedBinder,
2145    }
2146    unsafe impl Send for RawBinderService {}
2147
2148    impl RawBinderService {
2149        /// Open a connection to the named service (e.g. `"power"`, `"batterystats"`).
2150        pub fn open(service_name: &str) -> Result<Self, CoreError> {
2151            use std::ffi::CString;
2152            let handle = unsafe {
2153                libc::dlopen(
2154                    LIBBINDER_PATH.as_ptr() as *const c_char,
2155                    libc::RTLD_NOW | libc::RTLD_LOCAL,
2156                )
2157            };
2158            if handle.is_null() {
2159                return Err(CoreError::binder(-1, "dlopen:libbinder_ndk.so"));
2160            }
2161            let lib = DlHandle;
2162            let vt = load_vtable(handle)?;
2163            let cs = CString::new(service_name)
2164                .map_err(|_| CoreError::binder(-1, "service_name:nul_byte"))?;
2165            let raw = unsafe { (vt.get_service)(cs.as_ptr()) };
2166            if raw.is_null() {
2167                return Err(CoreError::binder(-1, "AServiceManager_getService:null"));
2168            }
2169            let service = OwnedBinder {
2170                ptr: raw,
2171                dec_strong: vt.dec_strong,
2172            };
2173            Ok(Self {
2174                _lib: lib,
2175                vt,
2176                service,
2177            })
2178        }
2179
2180        /// Send a no-argument transaction; read exception header then bool reply.
2181        pub fn transact_bool(&self, code: u32) -> Result<bool, CoreError> {
2182            let out = self.raw_noarg(code)?;
2183            let r = ParcelReader {
2184                vt: &self.vt,
2185                parcel: &out,
2186            };
2187            let ex = r.read_i32()?;
2188            if ex != EX_NONE {
2189                return Err(CoreError::binder(ex, "transact_bool:exception"));
2190            }
2191            if let Some(rb) = self.vt.read_bool {
2192                let mut v = false;
2193                let s = unsafe { rb(out.ptr as *const AParcel, &mut v) };
2194                if s != STATUS_OK {
2195                    return Err(CoreError::binder(s, "AParcel_readBool"));
2196                }
2197                Ok(v)
2198            } else {
2199                Ok(r.read_i32()? != 0)
2200            }
2201        }
2202
2203        /// Send a transaction with one i32 argument; discard reply.
2204        pub fn transact_i32(&self, code: u32, arg: i32) -> Result<(), CoreError> {
2205            let _ = transact_write(&self.vt, self.service.ptr, code, |w| w.write_i32(arg))?;
2206            Ok(())
2207        }
2208
2209        fn raw_noarg(&self, code: u32) -> Result<OwnedParcel, CoreError> {
2210            transact_write(&self.vt, self.service.ptr, code, |_| Ok(()))
2211        }
2212    }
2213
2214    #[cfg(test)]
2215    mod tests {
2216        use super::*;
2217
2218        fn alloc(length: i32) -> bool {
2219            let mut buf = StringBuf::new();
2220            let mut out: *mut c_char = std::ptr::null_mut();
2221            unsafe { string_alloc(&mut buf as *mut StringBuf as *mut c_void, length, &mut out) }
2222        }
2223
2224        #[test]
2225        fn string_alloc_rejects_oversized() {
2226            assert!(!alloc(MAX_BINDER_STRING_LEN as i32 + 1));
2227        }
2228
2229        #[test]
2230        fn string_alloc_accepts_null_marker_rejects_other_negative() {
2231            assert!(alloc(-1));
2232            assert!(!alloc(-2));
2233        }
2234
2235        #[test]
2236        fn string_alloc_accepts_valid_len_and_nul_terminates() {
2237            let mut buf = StringBuf::new();
2238            let mut out: *mut c_char = std::ptr::null_mut();
2239            let ok =
2240                unsafe { string_alloc(&mut buf as *mut StringBuf as *mut c_void, 4, &mut out) };
2241            assert!(ok);
2242            assert!(!out.is_null());
2243            {
2244                let vec = unsafe { buf.0.as_mut_vec() };
2245                b"ABCD".iter().enumerate().for_each(|(i, &b)| vec[i] = b);
2246                vec[4] = 0;
2247            }
2248            assert_eq!(buf.finish().as_deref(), Some("ABCD"));
2249        }
2250
2251        #[test]
2252        fn fps_zero_report_distinct_from_unseen() {
2253            assert_eq!(fps_from_state(false, 0), None);
2254            assert_eq!(fps_from_state(true, 0), Some(0.0));
2255            assert_eq!(fps_from_state(true, 60.0f32.to_bits()), Some(60.0));
2256            assert_eq!(fps_from_state(false, 60.0f32.to_bits()), None);
2257        }
2258
2259        #[test]
2260        fn sanitize_fps_rejects_non_finite_and_negative() {
2261            let de = |bits| f32::from_bits(sanitize_fps(bits));
2262            assert_eq!(de(0.0f32.to_bits()), 0.0);
2263            assert_eq!(de(60.0f32.to_bits()), 60.0);
2264            assert_eq!(de(f32::NAN.to_bits()), 0.0);
2265            assert_eq!(de(f32::INFINITY.to_bits()), 0.0);
2266            assert_eq!(de(f32::NEG_INFINITY.to_bits()), 0.0);
2267            assert_eq!(de((-5.0f32).to_bits()), 0.0);
2268        }
2269    }
2270}
2271
2272// ── Public re-exports ─────────────────────────────────────────────────────────
2273
2274#[cfg(target_os = "android")]
2275pub use imp::{
2276    ActivityManager, DisplayInfo, DisplayManager, FpsListener, RawBinderService, TaskStackListener,
2277    TxCodes, last_foreground_pid, resolve_tx_codes,
2278};
2279
2280// ── Non-Android stubs ─────────────────────────────────────────────────────────
2281
2282#[cfg(not(target_os = "android"))]
2283pub struct ActivityManager;
2284
2285#[cfg(not(target_os = "android"))]
2286impl ActivityManager {
2287    pub fn open() -> Result<Self, crate::CoreError> {
2288        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
2289    }
2290    pub fn open_with_observer() -> Result<(Self, std::os::fd::OwnedFd), crate::CoreError> {
2291        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
2292    }
2293    pub fn open_with_fgproc_observer() -> Result<(Self, std::os::fd::OwnedFd), crate::CoreError> {
2294        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
2295    }
2296    pub fn get_focused_task(&self) -> Result<Option<(i32, Option<String>)>, crate::CoreError> {
2297        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
2298    }
2299    pub fn get_focused_package(&self) -> Result<Option<String>, crate::CoreError> {
2300        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
2301    }
2302    pub fn get_focused_task_id(&self) -> Result<Option<i32>, crate::CoreError> {
2303        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
2304    }
2305}
2306
2307#[cfg(not(target_os = "android"))]
2308pub struct DisplayManager;
2309
2310#[cfg(not(target_os = "android"))]
2311impl DisplayManager {
2312    pub fn open_with_callback() -> Result<(Self, crate::fd::Fd), crate::CoreError> {
2313        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
2314    }
2315    pub fn is_interactive(&self) -> Result<bool, crate::CoreError> {
2316        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
2317    }
2318    pub fn info(&self, _display_id: i32) -> Result<DisplayInfo, crate::CoreError> {
2319        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
2320    }
2321}
2322
2323#[cfg(not(target_os = "android"))]
2324#[derive(Clone, Copy, Debug, PartialEq, Default)]
2325pub struct DisplayInfo {
2326    pub active_mode_id: i32,
2327    pub vsync_rate: f32,
2328    pub peak_refresh_rate: f32,
2329    pub render_frame_rate: f32,
2330    pub is_interactive: bool,
2331}
2332
2333#[cfg(not(target_os = "android"))]
2334pub struct RawBinderService;
2335
2336#[cfg(not(target_os = "android"))]
2337impl RawBinderService {
2338    pub fn open(_service_name: &str) -> Result<Self, crate::CoreError> {
2339        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
2340    }
2341    pub fn transact_bool(&self, _code: u32) -> Result<bool, crate::CoreError> {
2342        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
2343    }
2344    pub fn transact_i32(&self, _code: u32, _arg: i32) -> Result<(), crate::CoreError> {
2345        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
2346    }
2347}
2348
2349#[cfg(not(target_os = "android"))]
2350pub struct FpsListener;
2351
2352#[cfg(not(target_os = "android"))]
2353impl FpsListener {
2354    pub fn open() -> Result<(Self, std::os::fd::OwnedFd), crate::CoreError> {
2355        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
2356    }
2357    pub fn register(&mut self, _task_id: i32) -> Result<(), crate::CoreError> {
2358        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
2359    }
2360    pub fn unregister(&mut self) -> Result<(), crate::CoreError> {
2361        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
2362    }
2363    pub fn last_fps(&self) -> Option<f32> {
2364        None
2365    }
2366    pub fn task_id(&self) -> Option<i32> {
2367        None
2368    }
2369}
2370
2371#[cfg(not(target_os = "android"))]
2372pub struct TaskStackListener;
2373
2374#[cfg(not(target_os = "android"))]
2375impl TaskStackListener {
2376    pub fn open() -> Result<(Self, std::os::fd::OwnedFd), crate::CoreError> {
2377        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
2378    }
2379    pub fn register(&self) -> Result<(), crate::CoreError> {
2380        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
2381    }
2382    pub fn unregister(&self) -> Result<(), crate::CoreError> {
2383        Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
2384    }
2385}
2386
2387#[cfg(not(target_os = "android"))]
2388pub struct TxCodes {
2389    pub observer_code: u32,
2390    pub query_code: u32,
2391    pub api_mode: u8,
2392    pub fg_code: u32,
2393}
2394
2395#[cfg(not(target_os = "android"))]
2396pub fn resolve_tx_codes() -> Result<TxCodes, crate::CoreError> {
2397    Err(crate::CoreError::binder(-1, "binder:unsupported platform"))
2398}
2399
2400#[cfg(not(target_os = "android"))]
2401pub fn last_foreground_pid() -> i32 {
2402    0
2403}