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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<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
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:label="iris android-view demo"
|
android:label="iris android-view demo"
|
||||||
|
|||||||
@@ -169,9 +169,22 @@ impl AndroidRenderer {
|
|||||||
|
|
||||||
/// `Tasks`' redraw handle on Android: a background task finishes on the
|
/// `Tasks`' redraw handle on Android: a background task finishes on the
|
||||||
/// tokio thread `Tasks::init` spawned, which is not attached to the JVM, so
|
/// 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
|
/// asking for a frame means attaching first. The global ref is what
|
||||||
/// live `View` reference; the global ref is what survives past the JNI call
|
/// survives past the JNI call that handed the `View` to us.
|
||||||
/// that handed it 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 {
|
pub struct AndroidRedrawHandle {
|
||||||
vm: JavaVM,
|
vm: JavaVM,
|
||||||
view: GlobalRef,
|
view: GlobalRef,
|
||||||
@@ -189,6 +202,6 @@ impl RequestRedraw for AndroidRedrawHandle {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let local = env.new_local_ref(&self.view).unwrap();
|
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);
|
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> {
|
fn as_input_connection(&mut self) -> Option<&mut dyn InputConnection> {
|
||||||
Some(self)
|
Some(self)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user