use crate::{Browser, BrowserOptions, Error, ErrorKind, Result, TargetType};
use jni::{objects::JString, refs::Global};
use std::process::{Command, Stdio};
pub(super) fn open_browser_internal(
browser: Browser,
target: &TargetType,
options: &BrowserOptions,
) -> Result<()> {
let url = target.get_http_url()?;
match browser {
Browser::Default => open_browser_default(url, options),
_ => Err(Error::new(
ErrorKind::NotFound,
"only default browser supported",
)),
}
}
jni::bind_java_type! {
AUri => "android.net.Uri",
methods {
static fn parse(uri: JString) -> AUri
}
}
jni::bind_java_type! {
AIntent => "android.content.Intent",
type_map {
AUri => "android.net.Uri"
},
constructors {
fn new_with_url(action: JString, uri: AUri)
},
fields {
#[allow(non_snake_case)]
static ACTION_VIEW: JString,
#[allow(non_snake_case)]
static FLAG_ACTIVITY_NEW_TASK: jint
},
methods {
fn add_flags(flags: jint) -> AIntent
}
}
jni::bind_java_type! {
AContext => "android.content.Context",
type_map {
AIntent => "android.content.Intent"
},
methods {
fn start_activity(intent: AIntent)
}
}
jni::bind_java_type! {
AActivity => "android.app.Activity",
type_map {
AContext => "android.content.Context"
},
is_instance_of {
activity: AContext
},
}
#[derive(Debug)]
enum OpenUrlJniError {
Jni(jni::errors::Error),
Other(crate::Error),
}
impl From<jni::errors::Error> for OpenUrlJniError {
fn from(err: jni::errors::Error) -> Self {
OpenUrlJniError::Jni(err)
}
}
impl From<crate::Error> for OpenUrlJniError {
fn from(err: crate::Error) -> Self {
OpenUrlJniError::Other(err)
}
}
impl std::fmt::Display for OpenUrlJniError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
OpenUrlJniError::Jni(err) => write!(f, "JNI error: {}", err),
OpenUrlJniError::Other(err) => write!(f, "{}", err),
}
}
}
fn open_browser_default(url: &str, options: &BrowserOptions) -> Result<()> {
if options.dry_run {
return Ok(());
}
if try_for_termux(url, options).is_ok() {
return Ok(());
}
let ctx = ndk_context::android_context();
if ctx.vm().is_null() || ctx.context().is_null() {
return Err(Error::new(
ErrorKind::NotFound,
"Expected to find JVM and context.context.Context reference via ndk_context crate",
));
}
let vm = unsafe { jni::JavaVM::from_raw(ctx.vm() as _) };
vm.attach_current_thread(|env| -> std::result::Result<(), OpenUrlJniError> {
let context: jni::sys::jobject = ctx.context() as _;
let context = unsafe { env.as_cast_raw::<Global<AContext>>(&context) }
.map_err(|e| Error::other(format!("Failed to cast context: {}", e)))?;
let context: &AContext = &context;
let action_view = AIntent::ACTION_VIEW(env).map_err(|e| {
Error::other(format!(
"Failed to lookup ACTION_VIEW field constant: {}",
e
))
})?;
let url = JString::new(env, url)
.map_err(|e| Error::other(format!("Failed to create JString for URL: {}", e)))?;
let uri = AUri::parse(env, url)
.map_err(|e| Error::other(format!("Failed to parse URL: {}", e)))?;
let intent = AIntent::new_with_url(env, &action_view, &uri)
.map_err(|e| Error::other(format!("Failed to create ACTION_VIEW intent: {}", e)))?;
let maybe_activity = env.as_cast::<AActivity>(&context);
match maybe_activity {
Ok(_activity) => {
context.start_activity(env, &intent)?;
}
Err(_) => {
let flag_activity_new_task = AIntent::FLAG_ACTIVITY_NEW_TASK(env)?;
let intent = intent.add_flags(env, flag_activity_new_task)?;
context.start_activity(env, &intent)?;
}
}
Ok(())
})
.map_err(|e| Error::other(format!("Failed to open URL via Android Intent: {}", e)))?;
Ok(())
}
fn try_for_termux(url: &str, options: &BrowserOptions) -> Result<()> {
use std::env;
if env::var("TERMUX_VERSION").is_ok() {
if options.dry_run {
return Ok(());
}
let mut cmd = Command::new("termux-open");
cmd.arg(url);
if options.suppress_output {
cmd.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
}
cmd.status().and_then(|status| {
if status.success() {
Ok(())
} else {
Err(Error::other("command present but exited unsuccessfully"))
}
})
} else {
Err(Error::other("Not a termux environment"))
}
}