Open the sign-in URL through an ACTION_VIEW Intent on Android
The previous commit made the missing launcher honest; this gives Android a real one, so Login Flow v2 can complete on device. Builds `new Intent(ACTION_VIEW, Uri.parse(url))` and hands it to `startActivity` over JNI. The JavaVM and Activity come from ndk_context, which android-activity's glue populates at startup — the same handle Slint's backend uses, so there is no second VM to reconcile. The login worker is a plain std::thread and therefore unknown to the JVM, where any JNI call would abort the process. jni 0.22 scopes attachment to a closure rather than returning a guard, so the whole Intent is built and dispatched inside `attach_current_thread` and the thread detaches on the way out. Names use `jni_str!` and signatures `jni_sig!`, both compile-time: a typo is a build error rather than a NoSuchMethodError on the device. A pending Java exception is checked and cleared before returning, since leaving one pending makes the next JNI call fail somewhere unrelated; in practice it means ActivityNotFoundException, i.e. no browser installed. jni is pinned to 0.22 to match Slint's Android backend. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Generated
+2
@@ -1469,7 +1469,9 @@ dependencies = [
|
|||||||
"dr-sync-nextcloud",
|
"dr-sync-nextcloud",
|
||||||
"dr-thumbs",
|
"dr-thumbs",
|
||||||
"dr-types",
|
"dr-types",
|
||||||
|
"jni 0.22.4",
|
||||||
"log",
|
"log",
|
||||||
|
"ndk-context",
|
||||||
"pollster",
|
"pollster",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ slint = { workspace = true, features = ["backend-winit"] }
|
|||||||
|
|
||||||
[target.'cfg(target_os = "android")'.dependencies]
|
[target.'cfg(target_os = "android")'.dependencies]
|
||||||
slint = { workspace = true, features = ["backend-android-activity-06"] }
|
slint = { workspace = true, features = ["backend-android-activity-06"] }
|
||||||
|
# Opening the sign-in URL needs an ACTION_VIEW Intent — Android has no
|
||||||
|
# xdg-open. Version-matched to Slint's Android backend so both halves of the
|
||||||
|
# process agree on the JavaVM types; ndk-context supplies the VM and activity
|
||||||
|
# that android-activity's glue already stashed.
|
||||||
|
jni = "0.22"
|
||||||
|
ndk-context = "0.1"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
slint-build.workspace = true
|
slint-build.workspace = true
|
||||||
|
|||||||
@@ -553,7 +553,14 @@ fn open_in_browser(url: &str) -> std::io::Result<()> {
|
|||||||
.spawn()
|
.spawn()
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
}
|
}
|
||||||
#[cfg(not(all(unix, not(target_os = "android"), not(target_os = "macos"))))]
|
#[cfg(target_os = "android")]
|
||||||
|
{
|
||||||
|
android_open_url(url).map_err(|e| std::io::Error::other(e))
|
||||||
|
}
|
||||||
|
#[cfg(not(any(
|
||||||
|
all(unix, not(target_os = "android"), not(target_os = "macos")),
|
||||||
|
target_os = "android"
|
||||||
|
)))]
|
||||||
{
|
{
|
||||||
let _ = url;
|
let _ = url;
|
||||||
Err(std::io::Error::new(
|
Err(std::io::Error::new(
|
||||||
@@ -562,3 +569,76 @@ fn open_in_browser(url: &str) -> std::io::Result<()> {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Hand a URL to whatever the user has set as their browser, via
|
||||||
|
/// `startActivity(new Intent(ACTION_VIEW, Uri.parse(url)))`.
|
||||||
|
///
|
||||||
|
/// Called from the login worker, which is a plain `std::thread` and therefore
|
||||||
|
/// not known to the JVM — every JNI call from it would abort the process
|
||||||
|
/// without `attach_current_thread` first. The thread detaches when the returned
|
||||||
|
/// guard drops.
|
||||||
|
///
|
||||||
|
/// The VM and activity come from `ndk_context`, which android-activity's glue
|
||||||
|
/// populates at startup; that is the same handle Slint's backend uses, so there
|
||||||
|
/// is no second JavaVM to reconcile.
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
fn android_open_url(url: &str) -> Result<(), String> {
|
||||||
|
let ctx = ndk_context::android_context();
|
||||||
|
if ctx.vm().is_null() || ctx.context().is_null() {
|
||||||
|
return Err("no Android context available".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: the pointer comes from ndk_context, which android-activity fills
|
||||||
|
// in with the process's real JavaVM before any Rust runs.
|
||||||
|
let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) };
|
||||||
|
let raw_activity: jni::sys::jobject = ctx.context().cast();
|
||||||
|
|
||||||
|
// jni 0.22 scopes the attachment to a closure rather than handing back a
|
||||||
|
// guard, so all the JNI work happens in here and the thread is detached on
|
||||||
|
// the way out.
|
||||||
|
vm.attach_current_thread(|env| {
|
||||||
|
// SAFETY: valid for as long as this frame, which is all we need — the
|
||||||
|
// Intent is dispatched before the closure returns.
|
||||||
|
let activity = unsafe { jni::objects::JObject::from_raw(env, raw_activity) };
|
||||||
|
|
||||||
|
// Names go through `jni_str!` (UTF-8 literal to MUTF-8 `&'static
|
||||||
|
// JNIStr`) and signatures through `jni_sig!`, which parses and
|
||||||
|
// type-checks them at compile time — a typo in either is a build error
|
||||||
|
// rather than a NoSuchMethodError on the device.
|
||||||
|
let jurl = env.new_string(url)?;
|
||||||
|
let uri = env
|
||||||
|
.call_static_method(
|
||||||
|
jni::jni_str!("android/net/Uri"),
|
||||||
|
jni::jni_str!("parse"),
|
||||||
|
jni::jni_sig!("(Ljava/lang/String;)Landroid/net/Uri;"),
|
||||||
|
&[(&jurl).into()],
|
||||||
|
)?
|
||||||
|
.l()?;
|
||||||
|
|
||||||
|
let action = env.new_string("android.intent.action.VIEW")?;
|
||||||
|
let intent = env.new_object(
|
||||||
|
jni::jni_str!("android/content/Intent"),
|
||||||
|
jni::jni_sig!("(Ljava/lang/String;Landroid/net/Uri;)V"),
|
||||||
|
&[(&action).into(), (&uri).into()],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
env.call_method(
|
||||||
|
&activity,
|
||||||
|
jni::jni_str!("startActivity"),
|
||||||
|
jni::jni_sig!("(Landroid/content/Intent;)V"),
|
||||||
|
&[(&intent).into()],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// A pending Java exception leaves the JVM unusable for the next call,
|
||||||
|
// and ActivityNotFoundException here means the device has no browser at
|
||||||
|
// all — worth reporting rather than leaving for something unrelated to
|
||||||
|
// trip over.
|
||||||
|
if env.exception_check() {
|
||||||
|
env.exception_clear();
|
||||||
|
return Err(jni::errors::Error::JavaException);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.map_err(|e: jni::errors::Error| e.to_string())
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user