Finding
subscribe_events only appends a Box<dyn EventListener> to a Vec. There is no removal API, no capacity cap, and no weak-reference mechanism. Because AndroidEngine outlives any Android Activity, a listener registered on onResume is never dropped — the list grows on every rotation/recreation and is never compacted.
Evidence
crates/akouo-android/src/lib.rs:142-148 — push-only, no remove:
pub fn subscribe_events(&self, listener: Box<dyn EventListener>) {
let mut guard = self
.event_listeners
.lock()
.unwrap_or_else(|e| e.into_inner());
guard.push(listener);
}
There is no unsubscribe_events, no handle returned to identify a listener, and no bound on event_listeners.
Why this matters
Each retained listener holds a strong reference to its Kotlin object across the UniFFI trait boundary, preventing garbage collection. After N rotations, N copies of the listener receive every audio event, multiplying callback overhead and keeping event payloads alive longer than intended. Unbounded growth driven by ordinary UI churn is an availability hazard on a constrained sovereign device: a long-running or repeatedly-recreated session accumulates memory and CPU with no ceiling and no operator-visible recovery path short of process restart.
Desired correction
Return an opaque handle (e.g. a u64 id) from subscribe_events, and add unsubscribe_events(id: u64) that removes the matching listener. Alternatively store listeners behind a weak reference so they drop automatically when the Kotlin side is collected. Done when: repeated subscribe-on-onResume across Activity rotations leaves the listener count bounded (a subscribe paired with an unsubscribe returns the count to its prior value).
Finding
subscribe_eventsonly appends aBox<dyn EventListener>to aVec. There is no removal API, no capacity cap, and no weak-reference mechanism. BecauseAndroidEngineoutlives any AndroidActivity, a listener registered ononResumeis never dropped — the list grows on every rotation/recreation and is never compacted.Evidence
crates/akouo-android/src/lib.rs:142-148— push-only, no remove:There is no
unsubscribe_events, no handle returned to identify a listener, and no bound onevent_listeners.Why this matters
Each retained listener holds a strong reference to its Kotlin object across the UniFFI trait boundary, preventing garbage collection. After N rotations, N copies of the listener receive every audio event, multiplying callback overhead and keeping event payloads alive longer than intended. Unbounded growth driven by ordinary UI churn is an availability hazard on a constrained sovereign device: a long-running or repeatedly-recreated session accumulates memory and CPU with no ceiling and no operator-visible recovery path short of process restart.
Desired correction
Return an opaque handle (e.g. a
u64id) fromsubscribe_events, and addunsubscribe_events(id: u64)that removes the matching listener. Alternatively store listeners behind a weak reference so they drop automatically when the Kotlin side is collected. Done when: repeated subscribe-on-onResumeacross Activity rotations leaves the listener count bounded (a subscribe paired with an unsubscribe returns the count to its prior value).