use std::ffi::c_void;
use std::sync::OnceLock;
use jni::objects::{GlobalRef, JObject, JObjectArray, JString};
use jni::{JNIEnv, JavaVM};
#[cfg(feature = "pac-android-native")]
use super::proxy_info::SelectedProxy;
use super::proxy_info::{ProxyInfo, mode_from_proxy_info};
use crate::config::{ProxyConfig, ProxyConfigSource};
use crate::error::Error;
use crate::watch::WatchOptions;
mod receiver;
pub(crate) use receiver::Watch;
pub(crate) fn read_config(_options: &WatchOptions) -> Result<ProxyConfig, Error> {
let info = read_proxy_info()?;
let mode = mode_from_proxy_info(info.as_ref())?;
Ok(ProxyConfig::from_source(
ProxyConfigSource::ConnectivityManager,
mode,
))
}
fn read_proxy_info() -> Result<Option<ProxyInfo>, Error> {
with_framework(READING, read_in)
}
const READING: &str = "reading ConnectivityManager.getDefaultProxy() through JNI";
static HOST: OnceLock<Host> = OnceLock::new();
struct Host {
vm: JavaVM,
app: GlobalRef,
}
const INITIALIZING: &str = "proxy_watch::android::init";
pub unsafe fn init(vm: *mut c_void, context: *mut c_void) -> Result<(), Error> {
let raw = vm.cast::<jni::sys::JavaVM>();
if let Some(host) = HOST.get() {
return same_vm(host, raw);
}
let vm = unsafe { JavaVM::from_raw(raw) }.map_err(|e| jni_error(INITIALIZING, e))?;
let app = call_in(INITIALIZING, &vm, |env| {
let given = unsafe { JObject::from_raw(context.cast()) };
let app = application_context(env, &given)?;
if app.is_null() {
return Err(jni::errors::Error::NullPtr(
"getApplicationContext() result",
));
}
env.new_global_ref(app)
})?;
match HOST.set(Host { vm, app }) {
Ok(()) => Ok(()),
Err(_) => same_vm(HOST.get().expect("HOST is set"), raw),
}
}
fn same_vm(host: &Host, raw: *mut jni::sys::JavaVM) -> Result<(), Error> {
if host.vm.get_java_vm_pointer() == raw {
Ok(())
} else {
Err(jni_error(
INITIALIZING,
"called again with a different JavaVM than the first call",
))
}
}
fn with_framework<T>(
what: &'static str,
body: impl FnOnce(&mut JNIEnv, &JObject) -> jni::errors::Result<T>,
) -> Result<T, Error> {
if let Some(host) = HOST.get() {
return call_in(what, &host.vm, |env| body(env, host.app.as_obj()));
}
let context = std::panic::catch_unwind(ndk_context::android_context).map_err(|_| {
jni_error(
what,
"the host app has neither called proxy_watch::android::init nor registered its \
JavaVM and Context with ndk_context::initialize_android_context",
)
})?;
let vm = unsafe { JavaVM::from_raw(context.vm().cast()) }.map_err(|e| jni_error(what, e))?;
call_in(what, &vm, |env| {
let registered = unsafe { JObject::from_raw(context.context().cast()) };
let app = application_context(env, ®istered)?;
let app = if app.is_null() { registered } else { app };
body(env, &app)
})
}
fn application_context<'local>(
env: &mut JNIEnv<'local>,
context: &JObject,
) -> jni::errors::Result<JObject<'local>> {
env.call_method(
context,
"getApplicationContext",
"()Landroid/content/Context;",
&[],
)?
.l()
}
fn call_in<T>(
what: &'static str,
vm: &JavaVM,
body: impl FnOnce(&mut JNIEnv) -> jni::errors::Result<T>,
) -> Result<T, Error> {
let mut env = vm.attach_current_thread().map_err(|e| jni_error(what, e))?;
let result = env.with_local_frame(16, |env| body(env));
result.map_err(|e| match take_exception(&mut env) {
Some(thrown) => jni_error(what, format!("{e}: {thrown}")),
None => jni_error(what, e),
})
}
fn take_exception(env: &mut JNIEnv) -> Option<String> {
if !env.exception_check().unwrap_or(false) {
return None;
}
let thrown = env.exception_occurred();
let _ = env.exception_clear();
let thrown = thrown.ok()?;
let text = env
.with_local_frame(4, |env| string_of(env, &thrown, "toString"))
.ok()
.flatten();
if env.exception_check().unwrap_or(false) {
let _ = env.exception_clear();
}
let _ = env.delete_local_ref(thrown);
text
}
fn read_in(env: &mut JNIEnv, app: &JObject) -> jni::errors::Result<Option<ProxyInfo>> {
let service = env
.get_static_field(
"android/content/Context",
"CONNECTIVITY_SERVICE",
"Ljava/lang/String;",
)?
.l()?;
let manager = env
.call_method(
app,
"getSystemService",
"(Ljava/lang/String;)Ljava/lang/Object;",
&[(&service).into()],
)?
.l()?;
let info = env
.call_method(
&manager,
"getDefaultProxy",
"()Landroid/net/ProxyInfo;",
&[],
)?
.l()?;
if info.is_null() {
return Ok(None);
}
let host = string_of(env, &info, "getHost")?;
let port = env.call_method(&info, "getPort", "()I", &[])?.i()?;
let list = JObjectArray::from(
env.call_method(&info, "getExclusionList", "()[Ljava/lang/String;", &[])?
.l()?,
);
let mut exclusions = Vec::new();
if !list.is_null() {
for index in 0..env.get_array_length(&list)? {
let entry = JString::from(env.get_object_array_element(&list, index)?);
if !entry.is_null() {
exclusions.push(env.get_string(&entry)?.into());
}
env.delete_local_ref(entry)?;
}
}
let uri = env
.call_method(&info, "getPacFileUrl", "()Landroid/net/Uri;", &[])?
.l()?;
let pac_url = if uri.is_null() {
None
} else {
string_of(env, &uri, "toString")?
};
Ok(Some(ProxyInfo {
host,
port,
exclusions,
pac_url,
}))
}
#[cfg(feature = "pac-android-native")]
pub(crate) fn select(uri: &str) -> Result<(Option<String>, Option<Vec<SelectedProxy>>), Error> {
with_framework(READING, |env, app| {
let pac_url = read_in(env, app)?.and_then(|info| info.pac_url);
Ok((pac_url, select_in(env, uri)?))
})
}
#[cfg(feature = "pac-android-native")]
const PAC_SELECTOR: &str = "android.net.PacProxySelector";
#[cfg(feature = "pac-android-native")]
fn select_in(env: &mut JNIEnv, uri: &str) -> jni::errors::Result<Option<Vec<SelectedProxy>>> {
let selector = env
.call_static_method(
"java/net/ProxySelector",
"getDefault",
"()Ljava/net/ProxySelector;",
&[],
)?
.l()?;
if selector.is_null() {
return Ok(None);
}
let class = env
.call_method(&selector, "getClass", "()Ljava/lang/Class;", &[])?
.l()?;
if string_of(env, &class, "getName")?.as_deref() != Some(PAC_SELECTOR) {
return Ok(None);
}
let text = env.new_string(uri)?;
let uri = env
.call_static_method(
"java/net/URI",
"create",
"(Ljava/lang/String;)Ljava/net/URI;",
&[(&text).into()],
)?
.l()?;
let list = env
.call_method(
&selector,
"select",
"(Ljava/net/URI;)Ljava/util/List;",
&[(&uri).into()],
)?
.l()?;
let mut selected = Vec::new();
for index in 0..env.call_method(&list, "size", "()I", &[])?.i()? {
selected.push(env.with_local_frame(8, |env| {
let proxy = env
.call_method(&list, "get", "(I)Ljava/lang/Object;", &[index.into()])?
.l()?;
let kind = env
.call_method(&proxy, "type", "()Ljava/net/Proxy$Type;", &[])?
.l()?;
let kind = string_of(env, &kind, "name")?.unwrap_or_default();
let address = env
.call_method(&proxy, "address", "()Ljava/net/SocketAddress;", &[])?
.l()?;
let (host, port) = if address.is_null() {
(None, 0)
} else {
let host = string_of(env, &address, "getHostString")?;
(host, env.call_method(&address, "getPort", "()I", &[])?.i()?)
};
Ok::<_, jni::errors::Error>(SelectedProxy { kind, host, port })
})?);
}
Ok(Some(selected))
}
fn string_of(
env: &mut JNIEnv,
object: &JObject,
getter: &str,
) -> jni::errors::Result<Option<String>> {
let value = JString::from(
env.call_method(object, getter, "()Ljava/lang/String;", &[])?
.l()?,
);
if value.is_null() {
return Ok(None);
}
Ok(Some(env.get_string(&value)?.into()))
}
fn jni_error(what: &'static str, source: impl std::fmt::Display) -> Error {
Error::io(what, std::io::Error::other(source.to_string()))
}