Skip to content

Commit 729bfce

Browse files
Merge pull request #477 from m1sterc001guy/version_fix
feat: Query Fedimint Observer
2 parents db0c42d + 8c5e39f commit 729bfce

7 files changed

Lines changed: 338 additions & 86 deletions

File tree

android/app/src/main/kotlin/app/ecash/MainActivity.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,19 @@ import io.flutter.plugin.common.MethodChannel
1414

1515
class MainActivity : FlutterActivity() {
1616

17+
companion object {
18+
init {
19+
// Load the Rust library through the JVM so its `JNI_OnLoad` runs and
20+
// initializes `ndk_context`. Native networking crates used by
21+
// fedimint's iroh transport (hickory-resolver for DNS, netdev) read
22+
// the Android system network config through `ndk_context`; without it
23+
// the first federation DNS lookup panics with
24+
// "android context was not initialized". Flutter later dlopen()s the
25+
// same .so from Dart, which just reuses this already-loaded library.
26+
System.loadLibrary("ecashapp")
27+
}
28+
}
29+
1730
private val hceComponent by lazy {
1831
ComponentName(this, EcashHceService::class.java)
1932
}

rust/ecashapp/Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust/ecashapp/Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ serde_json = "1.0.140"
5252
thiserror = "1.0"
5353
tokio = "1.45.1"
5454

55+
# Android-only. `iroh` (fedimint's guardian transport) pulls in native
56+
# networking crates — `hickory-resolver` (DNS) and `netdev` — that, since the
57+
# hickory 0.26 / netdev 0.44 upgrade, read the device network config through
58+
# `ndk_context`. Flutter loads our .so via Dart `dlopen`, which never runs
59+
# `JNI_OnLoad`, so nothing initializes that context. We do it ourselves in
60+
# src/android_init.rs using these crates. See that file for the full story.
61+
[target.'cfg(target_os = "android")'.dependencies]
62+
jni = "0.21"
63+
ndk-context = "0.1"
64+
5565
[profile.dev.package]
5666
librocksdb-sys = { opt-level = 3 }
5767
secp256k1 = { opt-level = 3}

rust/ecashapp/src/android_init.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
//! Android-only JNI bootstrap.
2+
//!
3+
//! Since the hickory-resolver 0.26 / netdev 0.44 upgrade (pulled in transitively
4+
//! by `iroh`, fedimint's guardian transport), the crates that read the device's
5+
//! network configuration do so through the Android framework. They reach the
6+
//! `JavaVM` and the `Context` via the `ndk_context` crate's global
7+
//! `AndroidContext` — e.g. `hickory-resolver`'s `system_conf/android.rs` calls
8+
//! `ndk_context::android_context()` to read DNS servers from `ConnectivityManager`.
9+
//! Older hickory (0.25.2) used `resolv-conf` instead and never touched this.
10+
//!
11+
//! On a normal `ndk-glue` app `ndk_context` is initialized before `main`, but
12+
//! here Flutter owns the activity and loads our `.so` with `dlopen` from Dart —
13+
//! which never runs `JNI_OnLoad` — so nobody initializes it. The first DNS lookup
14+
//! then panics with "android context was not initialized", which surfaces on the
15+
//! Dart side as `PanicException` (e.g. when fetching a federation's metadata).
16+
//!
17+
//! Fix: `MainActivity` calls `System.loadLibrary("ecashapp")`, which DOES run
18+
//! `JNI_OnLoad` below. We grab the `JavaVM`, look up the process-wide
19+
//! `Application` via `ActivityThread.currentApplication()` (so we need no help
20+
//! from Kotlin and stay independent of the dev/prod application id), and hand
21+
//! both to `ndk_context`. This module is compiled only on Android; desktop reads
22+
//! `/etc/resolv.conf` and never touches `ndk_context`.
23+
24+
use std::ffi::c_void;
25+
26+
use jni::sys::{jint, JNI_VERSION_1_6};
27+
use jni::JavaVM;
28+
29+
/// Invoked by the JVM when `System.loadLibrary("ecashapp")` loads this library.
30+
#[no_mangle]
31+
pub extern "system" fn JNI_OnLoad(vm: JavaVM, _reserved: *mut c_void) -> jint {
32+
if let Err(e) = init_android_context(&vm) {
33+
// The Flutter event bus isn't wired up this early, so this only reaches
34+
// logcat — but a failure here means networking will panic later anyway.
35+
eprintln!("ecashapp: failed to initialize Android context for native networking: {e:?}");
36+
}
37+
JNI_VERSION_1_6
38+
}
39+
40+
fn init_android_context(vm: &JavaVM) -> anyhow::Result<()> {
41+
// `JNI_OnLoad` runs on a thread that is already attached to the JVM.
42+
let mut env = vm.get_env()?;
43+
44+
// android.app.ActivityThread.currentApplication() -> Application (a Context).
45+
let activity_thread = env.find_class("android/app/ActivityThread")?;
46+
let application = env
47+
.call_static_method(
48+
activity_thread,
49+
"currentApplication",
50+
"()Landroid/app/Application;",
51+
&[],
52+
)?
53+
.l()?;
54+
55+
if application.is_null() {
56+
anyhow::bail!("ActivityThread.currentApplication() returned null");
57+
}
58+
59+
// Promote to a global ref and leak it so the jobject stays valid for the
60+
// whole process lifetime — `ndk_context` only stores the raw pointer.
61+
let application = env.new_global_ref(&application)?;
62+
let context_ptr = application.as_obj().as_raw();
63+
std::mem::forget(application);
64+
65+
// SAFETY: the pointers are valid, and `JNI_OnLoad` runs once per library
66+
// load so `initialize_android_context` (which asserts it is called once) is
67+
// not invoked twice.
68+
unsafe {
69+
ndk_context::initialize_android_context(
70+
vm.get_java_vm_pointer().cast::<c_void>(),
71+
context_ptr.cast::<c_void>(),
72+
);
73+
}
74+
75+
Ok(())
76+
}

rust/ecashapp/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#![allow(unexpected_cfgs)]
22

3+
#[cfg(target_os = "android")]
4+
mod android_init;
35
mod app_error;
46
mod db;
57
mod event_bus;

rust/ecashapp/src/multimint.rs

Lines changed: 47 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1573,67 +1573,55 @@ impl Multimint {
15731573
// Get the connection status stream from the client
15741574
let status_stream = client.api().connection_status_stream();
15751575

1576-
// Fold the guardian version into each peer status so the UI reflects
1577-
// upgrades in real time without a separate timer. The version only needs
1578-
// (re)fetching when a peer comes online: a guardian upgrade restarts
1579-
// fedimintd, which surfaces here as an offline->online transition. We
1580-
// cache the last-known version per peer and refetch only on that
1581-
// transition (or if we never managed to fetch it) to avoid hitting every
1582-
// guardian on each unrelated connectivity change.
1583-
let mapped_stream = stream::unfold(
1584-
(
1585-
Box::pin(status_stream),
1586-
client,
1587-
peers,
1588-
BTreeMap::<u16, Option<String>>::new(),
1589-
BTreeMap::<u16, bool>::new(),
1590-
),
1591-
|(mut status_stream, client, peers, mut cached_versions, mut prev_online)| async move {
1592-
let status_map = status_stream.next().await?;
1593-
1594-
let mut peers_status = Vec::with_capacity(peers.len());
1595-
for (peer_id, (name, url)) in peers.iter() {
1596-
let (online, connectivity) = match status_map.get(&(*peer_id).into()) {
1597-
Some(FedimintPeerStatus::Connected(c)) => (true, (*c).into()),
1598-
Some(FedimintPeerStatus::Disconnected) | None => {
1599-
(false, PeerConnectivity::Unknown)
1600-
}
1601-
};
1576+
// Guardian versions are refreshed out-of-band by the periodic meta cache
1577+
// task (`spawn_cache_task` -> `cache_federation_meta`) and persisted under
1578+
// `FederationMetaKey`. We read them from that cache rather than fetching
1579+
// inline, so this stream stays a cheap, purely-local mapping of
1580+
// connectivity -> status: the sidebar's online indicator updates the
1581+
// instant fedimint reports a peer up/down, with no per-guardian network
1582+
// round-trips gating the emission.
1583+
let db = self.db.clone();
1584+
let federation_id = client.federation_id();
1585+
let peers = Arc::new(peers);
1586+
let mapped_stream = status_stream.then(move |status_map| {
1587+
let db = db.clone();
1588+
let peers = peers.clone();
1589+
async move {
1590+
// A single local read returns every guardian's last-known version.
1591+
let cached_versions: BTreeMap<u16, Option<String>> = {
1592+
let mut dbtx = db.begin_transaction_nc().await;
1593+
dbtx.get_value(&FederationMetaKey { federation_id })
1594+
.await
1595+
.map(|meta| {
1596+
meta.guardians
1597+
.into_iter()
1598+
.map(|g| (g.peer_id, g.version))
1599+
.collect()
1600+
})
1601+
.unwrap_or_default()
1602+
};
16021603

1603-
let version = if online {
1604-
let was_online = prev_online.get(peer_id).copied().unwrap_or(false);
1605-
let have_version =
1606-
cached_versions.get(peer_id).is_some_and(|v| v.is_some());
1607-
if was_online && have_version {
1608-
cached_versions.get(peer_id).cloned().flatten()
1609-
} else {
1610-
let fetched =
1611-
client.api().fedimintd_version((*peer_id).into()).await.ok();
1612-
cached_versions.insert(*peer_id, fetched.clone());
1613-
fetched
1604+
peers
1605+
.iter()
1606+
.map(|(peer_id, (name, url))| {
1607+
let (online, connectivity) = match status_map.get(&(*peer_id).into()) {
1608+
Some(FedimintPeerStatus::Connected(c)) => (true, (*c).into()),
1609+
Some(FedimintPeerStatus::Disconnected) | None => {
1610+
(false, PeerConnectivity::Unknown)
1611+
}
1612+
};
1613+
PeerStatus {
1614+
peer_id: *peer_id,
1615+
name: name.clone(),
1616+
online,
1617+
connectivity,
1618+
url: url.clone(),
1619+
version: cached_versions.get(peer_id).cloned().flatten(),
16141620
}
1615-
} else {
1616-
None
1617-
};
1618-
1619-
prev_online.insert(*peer_id, online);
1620-
1621-
peers_status.push(PeerStatus {
1622-
peer_id: *peer_id,
1623-
name: name.clone(),
1624-
online,
1625-
connectivity,
1626-
url: url.clone(),
1627-
version,
1628-
});
1629-
}
1630-
1631-
Some((
1632-
peers_status,
1633-
(status_stream, client, peers, cached_versions, prev_online),
1634-
))
1635-
},
1636-
);
1621+
})
1622+
.collect::<Vec<_>>()
1623+
}
1624+
});
16371625

16381626
Ok(mapped_stream.boxed())
16391627
}

0 commit comments

Comments
 (0)