fix(android): wrap sync HTTP tasks in per-call Tokio runtime
reqwest/hyper-util's GaiResolver calls tokio::runtime::Handle::current() which panics with "no reactor running" when driven by Bevy's AsyncComputeTaskPool (async-executor, not Tokio). Fixed all three spawn sites in sync_plugin.rs (start_pull, handle_manual_sync_request, push_replay_on_win) and the push_on_exit fallback by wrapping each HTTP future in tokio::runtime::Builder::new_current_thread().enable_all(). Also fixes a clippy type_complexity warning in hud_plugin.rs by extracting HudScoreFont / HudMovesFont / HudTimeFont type aliases for the update_hud_typography query parameters. Closes P4 AVD JNI bridge test: keystore JNI verified working on Android 14 x86_64 AVD (load_access_token returned NotFound correctly); clipboard JNI compiled and linked, runtime test deferred to a real-device session with a won game and active sync server. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -186,11 +186,38 @@ rewrites required.
|
||||
`card_plugin.rs` is explicit child-only teardown (parent kept alive)
|
||||
and is correct. No gameplay bugs attributed to these warnings over 2+
|
||||
min AVD runtime.
|
||||
- [ ] **AVD functional tests for JNI bridges.** Clipboard (`2c822ba`)
|
||||
and Keystore (`f281425`) shipped but never tested on real device
|
||||
or AVD. Requires hardware: connect Pixel 7 AVD (Android 14, x86_64),
|
||||
install the signed APK, and exercise the stats share-link button
|
||||
(clipboard) and the login flow (keystore).
|
||||
- [x] **AVD functional tests for JNI bridges.** *Partially closed
|
||||
2026-05-11.* Pixel 7 AVD (Android 14, x86_64) confirmed running;
|
||||
APK installs and runs stable. Key findings:
|
||||
|
||||
**Keystore JNI — verified working.** Forced `SolitaireServerClient`
|
||||
by writing a `solitaire_server` settings file, triggering
|
||||
`android_keystore::load_access_token()` at startup via `start_pull`.
|
||||
Logcat confirmed: `sync pull failed: authentication error: token
|
||||
not found for user avd_test` — the JNI call to `AndroidKeyStore`
|
||||
completed, correctly returned `NotFound`, and the sync system
|
||||
handled the error gracefully. No panic, no crash from the JNI layer.
|
||||
|
||||
**Clipboard JNI — not yet exercised at runtime.** The
|
||||
`android_clipboard::set_text()` path is gated behind
|
||||
`Interaction::Pressed` on the share button AND requires a non-null
|
||||
`share_url` (only present after a won game is uploaded to a sync
|
||||
server). Both conditions are unreachable in a headless AVD session.
|
||||
The code compiled and linked correctly on `x86_64-linux-android`.
|
||||
|
||||
**Side-finding fixed:** `reqwest`/`hyper-util`'s `GaiResolver`
|
||||
calls `tokio::runtime::Handle::current()` which panics with "no
|
||||
reactor running" when driven by Bevy's `AsyncComputeTaskPool`
|
||||
(async-executor, not Tokio). Fixed in `sync_plugin.rs`: all three
|
||||
`AsyncComputeTaskPool::spawn` sites and the `push_on_exit` fallback
|
||||
now wrap HTTP futures in a temporary
|
||||
`tokio::runtime::Builder::new_current_thread().enable_all()` runtime.
|
||||
|
||||
**Touch input limitation:** `adb shell input tap` does not deliver
|
||||
touch events to Bevy/winit on Android 14 + android-activity 0.6.1
|
||||
in headless AVD mode. Keyboard events (`KEYCODE_*`) work normally.
|
||||
Full clipboard test requires a real device or a GUI-accessible AVD
|
||||
session with a won game and active sync server.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -2016,20 +2016,18 @@ pub fn challenge_time_color(remaining: u64) -> Color {
|
||||
/// all three items remain on one row inside the 50 %-wide HUD column
|
||||
/// (≈ 180 dp on a 360 dp phone). At ≥ 480 px the original sizes are
|
||||
/// restored so desktop/tablet layouts are unaffected.
|
||||
type HudScoreFont<'w, 's> =
|
||||
Query<'w, 's, &'static mut TextFont, (With<HudScore>, Without<HudMoves>, Without<HudTime>)>;
|
||||
type HudMovesFont<'w, 's> =
|
||||
Query<'w, 's, &'static mut TextFont, (With<HudMoves>, Without<HudScore>, Without<HudTime>)>;
|
||||
type HudTimeFont<'w, 's> =
|
||||
Query<'w, 's, &'static mut TextFont, (With<HudTime>, Without<HudScore>, Without<HudMoves>)>;
|
||||
|
||||
fn update_hud_typography(
|
||||
mut events: MessageReader<WindowResized>,
|
||||
mut score_q: Query<
|
||||
&mut TextFont,
|
||||
(With<HudScore>, Without<HudMoves>, Without<HudTime>),
|
||||
>,
|
||||
mut moves_q: Query<
|
||||
&mut TextFont,
|
||||
(With<HudMoves>, Without<HudScore>, Without<HudTime>),
|
||||
>,
|
||||
mut time_q: Query<
|
||||
&mut TextFont,
|
||||
(With<HudTime>, Without<HudScore>, Without<HudMoves>),
|
||||
>,
|
||||
mut score_q: HudScoreFont,
|
||||
mut moves_q: HudMovesFont,
|
||||
mut time_q: HudTimeFont,
|
||||
) {
|
||||
let Some(ev) = events.read().last() else {
|
||||
return;
|
||||
|
||||
@@ -130,7 +130,14 @@ fn start_pull(
|
||||
) {
|
||||
let provider = provider.0.clone();
|
||||
let task = AsyncComputeTaskPool::get().spawn(async move {
|
||||
provider.pull().await
|
||||
// Bevy's AsyncComputeTaskPool uses async-executor (not Tokio), but
|
||||
// reqwest/hyper require a Tokio reactor for DNS and HTTP I/O. Provide
|
||||
// a short-lived single-threaded runtime for this network round-trip.
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.map_err(|e| SyncError::Network(format!("tokio rt: {e}")))?
|
||||
.block_on(provider.pull())
|
||||
});
|
||||
task_res.0 = Some(task);
|
||||
status.0 = SyncStatus::Syncing;
|
||||
@@ -153,7 +160,11 @@ fn handle_manual_sync_request(
|
||||
}
|
||||
let provider = provider.0.clone();
|
||||
let task = AsyncComputeTaskPool::get().spawn(async move {
|
||||
provider.pull().await
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.map_err(|e| SyncError::Network(format!("tokio rt: {e}")))?
|
||||
.block_on(provider.pull())
|
||||
});
|
||||
task_res.0 = Some(task);
|
||||
status.0 = SyncStatus::Syncing;
|
||||
@@ -259,11 +270,18 @@ fn push_on_exit(
|
||||
let payload = build_payload(&stats.0, &achievements.0, &progress.0);
|
||||
let provider = provider.0.clone();
|
||||
|
||||
// Prefer an existing tokio runtime; fall back to futures_lite block_on
|
||||
// for environments (e.g. tests) that don't have one.
|
||||
// Prefer an existing tokio runtime; fall back to a temporary one for
|
||||
// environments (e.g. tests, Android's non-Tokio async executor) where
|
||||
// reqwest/hyper would otherwise panic with "no reactor running".
|
||||
let result = match tokio::runtime::Handle::try_current() {
|
||||
Ok(handle) => handle.block_on(provider.push(&payload)),
|
||||
Err(_) => future::block_on(provider.push(&payload)),
|
||||
Err(_) => match tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
{
|
||||
Ok(rt) => rt.block_on(provider.push(&payload)),
|
||||
Err(e) => Err(SyncError::Network(format!("tokio rt on exit: {e}"))),
|
||||
},
|
||||
};
|
||||
match result {
|
||||
Ok(_) => {}
|
||||
@@ -314,8 +332,13 @@ fn push_replay_on_win(
|
||||
recording.moves.clone(),
|
||||
);
|
||||
let provider = provider.0.clone();
|
||||
let task = AsyncComputeTaskPool::get()
|
||||
.spawn(async move { provider.push_replay(&replay).await });
|
||||
let task = AsyncComputeTaskPool::get().spawn(async move {
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.map_err(|e| SyncError::Network(format!("tokio rt: {e}")))?
|
||||
.block_on(provider.push_replay(&replay))
|
||||
});
|
||||
// If a previous upload is still in flight, drop it — the most
|
||||
// recent win is the one whose share link the player will care
|
||||
// about. Bevy's `Task` Drop cancels cooperatively.
|
||||
|
||||
Reference in New Issue
Block a user