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)
|
`card_plugin.rs` is explicit child-only teardown (parent kept alive)
|
||||||
and is correct. No gameplay bugs attributed to these warnings over 2+
|
and is correct. No gameplay bugs attributed to these warnings over 2+
|
||||||
min AVD runtime.
|
min AVD runtime.
|
||||||
- [ ] **AVD functional tests for JNI bridges.** Clipboard (`2c822ba`)
|
- [x] **AVD functional tests for JNI bridges.** *Partially closed
|
||||||
and Keystore (`f281425`) shipped but never tested on real device
|
2026-05-11.* Pixel 7 AVD (Android 14, x86_64) confirmed running;
|
||||||
or AVD. Requires hardware: connect Pixel 7 AVD (Android 14, x86_64),
|
APK installs and runs stable. Key findings:
|
||||||
install the signed APK, and exercise the stats share-link button
|
|
||||||
(clipboard) and the login flow (keystore).
|
**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
|
/// 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
|
/// (≈ 180 dp on a 360 dp phone). At ≥ 480 px the original sizes are
|
||||||
/// restored so desktop/tablet layouts are unaffected.
|
/// 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(
|
fn update_hud_typography(
|
||||||
mut events: MessageReader<WindowResized>,
|
mut events: MessageReader<WindowResized>,
|
||||||
mut score_q: Query<
|
mut score_q: HudScoreFont,
|
||||||
&mut TextFont,
|
mut moves_q: HudMovesFont,
|
||||||
(With<HudScore>, Without<HudMoves>, Without<HudTime>),
|
mut time_q: HudTimeFont,
|
||||||
>,
|
|
||||||
mut moves_q: Query<
|
|
||||||
&mut TextFont,
|
|
||||||
(With<HudMoves>, Without<HudScore>, Without<HudTime>),
|
|
||||||
>,
|
|
||||||
mut time_q: Query<
|
|
||||||
&mut TextFont,
|
|
||||||
(With<HudTime>, Without<HudScore>, Without<HudMoves>),
|
|
||||||
>,
|
|
||||||
) {
|
) {
|
||||||
let Some(ev) = events.read().last() else {
|
let Some(ev) = events.read().last() else {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -130,7 +130,14 @@ fn start_pull(
|
|||||||
) {
|
) {
|
||||||
let provider = provider.0.clone();
|
let provider = provider.0.clone();
|
||||||
let task = AsyncComputeTaskPool::get().spawn(async move {
|
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);
|
task_res.0 = Some(task);
|
||||||
status.0 = SyncStatus::Syncing;
|
status.0 = SyncStatus::Syncing;
|
||||||
@@ -153,7 +160,11 @@ fn handle_manual_sync_request(
|
|||||||
}
|
}
|
||||||
let provider = provider.0.clone();
|
let provider = provider.0.clone();
|
||||||
let task = AsyncComputeTaskPool::get().spawn(async move {
|
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);
|
task_res.0 = Some(task);
|
||||||
status.0 = SyncStatus::Syncing;
|
status.0 = SyncStatus::Syncing;
|
||||||
@@ -259,11 +270,18 @@ fn push_on_exit(
|
|||||||
let payload = build_payload(&stats.0, &achievements.0, &progress.0);
|
let payload = build_payload(&stats.0, &achievements.0, &progress.0);
|
||||||
let provider = provider.0.clone();
|
let provider = provider.0.clone();
|
||||||
|
|
||||||
// Prefer an existing tokio runtime; fall back to futures_lite block_on
|
// Prefer an existing tokio runtime; fall back to a temporary one for
|
||||||
// for environments (e.g. tests) that don't have one.
|
// 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() {
|
let result = match tokio::runtime::Handle::try_current() {
|
||||||
Ok(handle) => handle.block_on(provider.push(&payload)),
|
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 {
|
match result {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
@@ -314,8 +332,13 @@ fn push_replay_on_win(
|
|||||||
recording.moves.clone(),
|
recording.moves.clone(),
|
||||||
);
|
);
|
||||||
let provider = provider.0.clone();
|
let provider = provider.0.clone();
|
||||||
let task = AsyncComputeTaskPool::get()
|
let task = AsyncComputeTaskPool::get().spawn(async move {
|
||||||
.spawn(async move { provider.push_replay(&replay).await });
|
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
|
// If a previous upload is still in flight, drop it — the most
|
||||||
// recent win is the one whose share link the player will care
|
// recent win is the one whose share link the player will care
|
||||||
// about. Bevy's `Task` Drop cancels cooperatively.
|
// about. Bevy's `Task` Drop cancels cooperatively.
|
||||||
|
|||||||
Reference in New Issue
Block a user