iris/android: fix background-thread redraw crash, add missing INTERNET permission
Two real bugs found bringing up I5's Android transcript client, neither
specific to that screen -- any future caller of Tasks::redraw_handle()
from a background thread would hit the first one.
AndroidRedrawHandle::request_redraw called View::post_frame_callback from
a tokio worker thread; its Java side calls Choreographer.getInstance(),
which throws IllegalStateException unless the *calling* thread already
has a Looper, and a JNI-attached background thread has none. That crashed
the whole process (SIGABRT, unwrap() on a JavaException) the first time a
background fetch asked for a second frame. Fixed by routing through
View::post_delayed(0) instead, Android's own thread-safe way to queue
work onto a View's UI thread, landing on a new
IrisViewPeer::delayed_callback override that drains tasks and renders --
same body as do_frame, now running safely on the UI thread.
iris-android-app's manifest never needed INTERNET before (the tabs demo
makes no network call); its absence read as EPERM ("Operation not
permitted") from UreqTransport::new's connect, not the
ECONNREFUSED/ENETUNREACH a dead server would give.
Full account in RUST.md's I5 box and IRIS.md's Tasks::redraw_handle entry.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
aa3d11471f
commit
bf5087a598
3 files changed
+40
-4
No files matched your search
@@ -1,6 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!-- Only needed by the transcript-screen feature (RUST.md's I5),
|
||||
which talks to a real ai-server; the plain tabs demo (I2/I4) makes
|
||||
no network call and never noticed this was missing. Absent,
|
||||
UreqTransport::new's connect failed with EPERM (Operation not
|
||||
permitted), not the ECONNREFUSED/ENETUNREACH a firewall or a dead
|
||||
server would give: a seccomp-level socket denial reads nothing
|
||||
like a network problem, which is what made it worth a comment. -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:label="iris android-view demo"
|
||||
|
||||
@@ -169,9 +169,22 @@ impl AndroidRenderer {
|
||||
|
||||
/// `Tasks`' redraw handle on Android: a background task finishes on the
|
||||
/// tokio thread `Tasks::init` spawned, which is not attached to the JVM, so
|
||||
/// asking for a frame means attaching first. `post_frame_callback` needs a
|
||||
/// live `View` reference; the global ref is what survives past the JNI call
|
||||
/// that handed it to us.
|
||||
/// asking for a frame means attaching first. The global ref is what
|
||||
/// survives past the JNI call that handed the `View` to us.
|
||||
///
|
||||
/// **Goes through `View::post_delayed`, not `post_frame_callback`
|
||||
/// directly** -- found the hard way (RUST.md's I5 Android integration):
|
||||
/// `post_frame_callback`'s Java side calls `Choreographer.getInstance()`,
|
||||
/// which throws `IllegalStateException` unless the *calling* thread already
|
||||
/// has a `Looper` (`Choreographer.getInstance()`'s own contract). A tokio
|
||||
/// worker thread, even freshly attached to the JVM, has none -- the crash
|
||||
/// was a `JavaException` inside `View::post_frame_callback`'s `.unwrap()`,
|
||||
/// aborting the process on the second `redraw.request_redraw()` any
|
||||
/// android transcript-screen fetch made. `View.postDelayed(Runnable, 0)`
|
||||
/// is the ordinary Android answer to "queue work onto a View's own UI
|
||||
/// thread from any thread" and needs no Looper of its own; `delayed_callback`
|
||||
/// below is what that Runnable resolves to on the UI thread, where a real
|
||||
/// `post_frame_callback` is safe again.
|
||||
pub struct AndroidRedrawHandle {
|
||||
vm: JavaVM,
|
||||
view: GlobalRef,
|
||||
@@ -189,6 +202,6 @@ impl RequestRedraw for AndroidRedrawHandle {
|
||||
return;
|
||||
};
|
||||
let local = env.new_local_ref(&self.view).unwrap();
|
||||
View(local).post_frame_callback(&mut env);
|
||||
View(local).post_delayed(&mut env, 0);
|
||||
}
|
||||
}
|
||||
@@ -432,6 +432,20 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
|
||||
self.render(ctx);
|
||||
}
|
||||
|
||||
/// Where `AndroidRedrawHandle::request_redraw` (`android/render.rs`)
|
||||
/// actually lands: `View.postDelayed`'s Runnable resolves to this, on
|
||||
/// the UI thread, which is what makes it safe to call from a background
|
||||
/// task's own thread when `post_frame_callback`'s `Choreographer`
|
||||
/// requirement (a `Looper` on the *calling* thread) is not. Same body
|
||||
/// as `do_frame` -- draining tasks and rendering immediately is a
|
||||
/// perfectly good answer to "a background fetch has new state," and
|
||||
/// avoids a second frame-scheduling path to keep in sync with the real
|
||||
/// one.
|
||||
fn delayed_callback(&mut self, ctx: &mut CallbackCtx) {
|
||||
self.drain_tasks();
|
||||
self.render(ctx);
|
||||
}
|
||||
|
||||
fn as_input_connection(&mut self) -> Option<&mut dyn InputConnection> {
|
||||
Some(self)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user