Skip to main content

proxy_watch/sys/
android.rs

1//! Android: `ConnectivityManager.getDefaultProxy()` through JNI, watched through the
2//! `PROXY_CHANGE_ACTION` broadcast.
3//!
4//! The `JavaVM` and application `Context` come from [`crate::android::init`] when the host
5//! called it, and otherwise from `ndk-context`, which `android-activity` and `tao` 0.36 and
6//! later register themselves. `tao` before 0.36 (Tauri before 2.12) registers neither, so a
7//! host built on it calls `init`. Until one of the two holds a `JavaVM`, every read is an
8//! [`Error::Io`] saying so.
9//!
10//! Only a Java `BroadcastReceiver` subclass can receive the broadcast, so the crate carries
11//! one as a compiled dex and loads it with `InMemoryDexClassLoader` (API 26+); see
12//! [`receiver`]. Each broadcast wakes the [`super::poll`] thread, which re-reads. Below
13//! API 26, or where the platform refuses in-memory code loading, the watcher falls back to
14//! [`WatchOptions::poll_interval`], and fails to construct without one.
15//!
16//! A host that would rather not load code at run time can receive the broadcast itself and
17//! call [`ProxyWatcher::poll_now`](crate::ProxyWatcher::poll_now) from its receiver, with a
18//! long `poll_interval` as the backstop:
19//!
20//! ```kotlin
21//! context.registerReceiver(object : BroadcastReceiver() {
22//!     override fun onReceive(context: Context, intent: Intent) = nativePollNow()
23//! }, IntentFilter(Proxy.PROXY_CHANGE_ACTION))
24//! ```
25//!
26//! Chromium receives proxy changes this way, in
27//! `net/android/java/src/org/chromium/net/ProxyChangeListener.java`.
28
29use std::ffi::c_void;
30use std::sync::OnceLock;
31
32use jni::objects::{GlobalRef, JObject, JObjectArray, JString};
33use jni::{JNIEnv, JavaVM};
34
35#[cfg(feature = "pac-android-native")]
36use super::proxy_info::SelectedProxy;
37use super::proxy_info::{ProxyInfo, mode_from_proxy_info};
38use crate::config::{ProxyConfig, ProxyConfigSource};
39use crate::error::Error;
40use crate::watch::WatchOptions;
41
42mod receiver;
43
44pub(crate) use receiver::Watch;
45
46pub(crate) fn read_config(_options: &WatchOptions) -> Result<ProxyConfig, Error> {
47    let info = read_proxy_info()?;
48    let mode = mode_from_proxy_info(info.as_ref())?;
49    Ok(ProxyConfig::from_source(
50        ProxyConfigSource::ConnectivityManager,
51        mode,
52    ))
53}
54
55fn read_proxy_info() -> Result<Option<ProxyInfo>, Error> {
56    with_framework(READING, read_in)
57}
58
59const READING: &str = "reading ConnectivityManager.getDefaultProxy() through JNI";
60
61// The JavaVM and application Context [`init`] was given. Held for the life of the process,
62// which is the life of both: dropping the reference would need an attached thread, and
63// the receiver's unregistration needs the Context it was registered on.
64static HOST: OnceLock<Host> = OnceLock::new();
65
66struct Host {
67    vm: JavaVM,
68    app: GlobalRef,
69}
70
71const INITIALIZING: &str = "proxy_watch::android::init";
72
73/// Hands the crate the process's `JavaVM` and a `Context`, from which it keeps the
74/// application `Context` for the life of the process.
75///
76/// Once `init` has succeeded, the crate uses what it was given and ignores `ndk-context`.
77/// A second call with the same `JavaVM` returns `Ok` and changes nothing, so a host whose
78/// set-up code runs more than once needs no guard of its own.
79///
80/// # Errors
81///
82/// [`Error::Io`] when the thread cannot attach to `vm`, when `context` has no application
83/// yet (`getApplicationContext()` is `null`), or when an earlier call succeeded with a
84/// different `JavaVM`.
85///
86/// # Safety
87///
88/// `vm` is a valid `JavaVM*`, and `context` is a valid JNI reference (local or global) to
89/// an `android.content.Context` (an `Activity`, a `Service` or the `Application`) alive for
90/// the duration of the call. The crate only borrows `context`; the caller keeps ownership.
91pub unsafe fn init(vm: *mut c_void, context: *mut c_void) -> Result<(), Error> {
92    let raw = vm.cast::<jni::sys::JavaVM>();
93    if let Some(host) = HOST.get() {
94        return same_vm(host, raw);
95    }
96    // SAFETY: the caller passes a valid `JavaVM*`.
97    let vm = unsafe { JavaVM::from_raw(raw) }.map_err(|e| jni_error(INITIALIZING, e))?;
98    let app = call_in(INITIALIZING, &vm, |env| {
99        // SAFETY: the caller passes a valid reference to a `Context`; it is only borrowed.
100        let given = unsafe { JObject::from_raw(context.cast()) };
101        let app = application_context(env, &given)?;
102        if app.is_null() {
103            // Only a Context whose application does not exist yet answers `null`.
104            return Err(jni::errors::Error::NullPtr(
105                "getApplicationContext() result",
106            ));
107        }
108        env.new_global_ref(app)
109    })?;
110    match HOST.set(Host { vm, app }) {
111        Ok(()) => Ok(()),
112        // Another thread's `init` finished first.
113        Err(_) => same_vm(HOST.get().expect("HOST is set"), raw),
114    }
115}
116
117// A process has one JavaVM, so a second `init` with it is a repeat, not a conflict.
118fn same_vm(host: &Host, raw: *mut jni::sys::JavaVM) -> Result<(), Error> {
119    if host.vm.get_java_vm_pointer() == raw {
120        Ok(())
121    } else {
122        Err(jni_error(
123            INITIALIZING,
124            "called again with a different JavaVM than the first call",
125        ))
126    }
127}
128
129// Run `body` on this thread, attached to the host's JavaVM, with the application Context:
130// the one [`init`] holds, or else the one `ndk-context` holds.
131fn with_framework<T>(
132    what: &'static str,
133    body: impl FnOnce(&mut JNIEnv, &JObject) -> jni::errors::Result<T>,
134) -> Result<T, Error> {
135    if let Some(host) = HOST.get() {
136        return call_in(what, &host.vm, |env| body(env, host.app.as_obj()));
137    }
138    // Limitation: `android_context` panics when the host never registered one, and has no
139    // fallible twin, so the panic is caught; the panic hook still runs, and under
140    // `panic = "abort"` it aborts instead. A host that calls [`init`] never reaches it.
141    let context = std::panic::catch_unwind(ndk_context::android_context).map_err(|_| {
142        jni_error(
143            what,
144            "the host app has neither called proxy_watch::android::init nor registered its \
145             JavaVM and Context with ndk_context::initialize_android_context",
146        )
147    })?;
148    // SAFETY: `ndk-context` holds a `JavaVM*` the host registered and never releases while
149    // the app runs.
150    let vm = unsafe { JavaVM::from_raw(context.vm().cast()) }.map_err(|e| jni_error(what, e))?;
151    call_in(what, &vm, |env| {
152        // SAFETY: a global reference to the `Context` the host registered, valid for the process.
153        let registered = unsafe { JObject::from_raw(context.context().cast()) };
154        let app = application_context(env, &registered)?;
155        let app = if app.is_null() { registered } else { app };
156        body(env, &app)
157    })
158}
159
160// The host may hold an Activity, and a receiver registered on an Activity is removed when
161// it is destroyed. The application `Context` lives as long as the process; it is `null`
162// only before the application exists.
163fn application_context<'local>(
164    env: &mut JNIEnv<'local>,
165    context: &JObject,
166) -> jni::errors::Result<JObject<'local>> {
167    env.call_method(
168        context,
169        "getApplicationContext",
170        "()Landroid/content/Context;",
171        &[],
172    )?
173    .l()
174}
175
176fn call_in<T>(
177    what: &'static str,
178    vm: &JavaVM,
179    body: impl FnOnce(&mut JNIEnv) -> jni::errors::Result<T>,
180) -> Result<T, Error> {
181    let mut env = vm.attach_current_thread().map_err(|e| jni_error(what, e))?;
182    // A frame, so the local references die here even on a thread Java already owns.
183    let result = env.with_local_frame(16, |env| body(env));
184    result.map_err(|e| match take_exception(&mut env) {
185        Some(thrown) => jni_error(what, format!("{e}: {thrown}")),
186        None => jni_error(what, e),
187    })
188}
189
190// Clear the exception pending on this thread and return its `toString()`, which names the
191// class and the message: jni reports every thrown exception as the same `JavaException`.
192fn take_exception(env: &mut JNIEnv) -> Option<String> {
193    if !env.exception_check().unwrap_or(false) {
194        return None;
195    }
196    let thrown = env.exception_occurred();
197    let _ = env.exception_clear();
198    let thrown = thrown.ok()?;
199    let text = env
200        .with_local_frame(4, |env| string_of(env, &thrown, "toString"))
201        .ok()
202        .flatten();
203    // `toString()` can throw in turn; that one is dropped.
204    if env.exception_check().unwrap_or(false) {
205        let _ = env.exception_clear();
206    }
207    let _ = env.delete_local_ref(thrown);
208    text
209}
210
211fn read_in(env: &mut JNIEnv, app: &JObject) -> jni::errors::Result<Option<ProxyInfo>> {
212    let service = env
213        .get_static_field(
214            "android/content/Context",
215            "CONNECTIVITY_SERVICE",
216            "Ljava/lang/String;",
217        )?
218        .l()?;
219    let manager = env
220        .call_method(
221            app,
222            "getSystemService",
223            "(Ljava/lang/String;)Ljava/lang/Object;",
224            &[(&service).into()],
225        )?
226        .l()?;
227    let info = env
228        .call_method(
229            &manager,
230            "getDefaultProxy",
231            "()Landroid/net/ProxyInfo;",
232            &[],
233        )?
234        .l()?;
235    if info.is_null() {
236        return Ok(None);
237    }
238    let host = string_of(env, &info, "getHost")?;
239    let port = env.call_method(&info, "getPort", "()I", &[])?.i()?;
240    let list = JObjectArray::from(
241        env.call_method(&info, "getExclusionList", "()[Ljava/lang/String;", &[])?
242            .l()?,
243    );
244    let mut exclusions = Vec::new();
245    if !list.is_null() {
246        for index in 0..env.get_array_length(&list)? {
247            let entry = JString::from(env.get_object_array_element(&list, index)?);
248            if !entry.is_null() {
249                exclusions.push(env.get_string(&entry)?.into());
250            }
251            // Freed per entry, so a long list does not outgrow the caller's frame.
252            env.delete_local_ref(entry)?;
253        }
254    }
255    let uri = env
256        .call_method(&info, "getPacFileUrl", "()Landroid/net/Uri;", &[])?
257        .l()?;
258    let pac_url = if uri.is_null() {
259        None
260    } else {
261        string_of(env, &uri, "toString")?
262    };
263    Ok(Some(ProxyInfo {
264        host,
265        port,
266        exclusions,
267        pac_url,
268    }))
269}
270
271/// Ask `ProxySelector.getDefault()` where `uri` goes, the way `java.net` clients in this
272/// process are routed. Under a PAC configuration the framework installs `PacProxySelector`,
273/// which has the system PAC service evaluate the script for this URL.
274///
275/// Also returns the PAC URL `getDefaultProxy()` names now, read under the same attach, so
276/// the caller can tell whether the selector answered for the configuration it holds.
277/// `None` in place of the answer when the default selector is not `PacProxySelector`.
278#[cfg(feature = "pac-android-native")]
279pub(crate) fn select(uri: &str) -> Result<(Option<String>, Option<Vec<SelectedProxy>>), Error> {
280    with_framework(READING, |env, app| {
281        let pac_url = read_in(env, app)?.and_then(|info| info.pac_url);
282        Ok((pac_url, select_in(env, uri)?))
283    })
284}
285
286#[cfg(feature = "pac-android-native")]
287const PAC_SELECTOR: &str = "android.net.PacProxySelector";
288
289#[cfg(feature = "pac-android-native")]
290fn select_in(env: &mut JNIEnv, uri: &str) -> jni::errors::Result<Option<Vec<SelectedProxy>>> {
291    let selector = env
292        .call_static_method(
293            "java/net/ProxySelector",
294            "getDefault",
295            "()Ljava/net/ProxySelector;",
296            &[],
297        )?
298        .l()?;
299    if selector.is_null() {
300        return Ok(None);
301    }
302    // The system installs `PacProxySelector` in each process from a message that is not
303    // ordered with the `PROXY_CHANGE_ACTION` broadcast, so just after a switch to PAC the
304    // process can still hold the previous selector, which answers for the old settings. A
305    // selector the app installed answers for settings of its own.
306    let class = env
307        .call_method(&selector, "getClass", "()Ljava/lang/Class;", &[])?
308        .l()?;
309    if string_of(env, &class, "getName")?.as_deref() != Some(PAC_SELECTOR) {
310        return Ok(None);
311    }
312    let text = env.new_string(uri)?;
313    let uri = env
314        .call_static_method(
315            "java/net/URI",
316            "create",
317            "(Ljava/lang/String;)Ljava/net/URI;",
318            &[(&text).into()],
319        )?
320        .l()?;
321    let list = env
322        .call_method(
323            &selector,
324            "select",
325            "(Ljava/net/URI;)Ljava/util/List;",
326            &[(&uri).into()],
327        )?
328        .l()?;
329    let mut selected = Vec::new();
330    for index in 0..env.call_method(&list, "size", "()I", &[])?.i()? {
331        // A frame per entry, so a long list does not outgrow the caller's.
332        selected.push(env.with_local_frame(8, |env| {
333            let proxy = env
334                .call_method(&list, "get", "(I)Ljava/lang/Object;", &[index.into()])?
335                .l()?;
336            let kind = env
337                .call_method(&proxy, "type", "()Ljava/net/Proxy$Type;", &[])?
338                .l()?;
339            let kind = string_of(env, &kind, "name")?.unwrap_or_default();
340            let address = env
341                .call_method(&proxy, "address", "()Ljava/net/SocketAddress;", &[])?
342                .l()?;
343            let (host, port) = if address.is_null() {
344                (None, 0)
345            } else {
346                let host = string_of(env, &address, "getHostString")?;
347                (host, env.call_method(&address, "getPort", "()I", &[])?.i()?)
348            };
349            Ok::<_, jni::errors::Error>(SelectedProxy { kind, host, port })
350        })?);
351    }
352    Ok(Some(selected))
353}
354
355// A `String`-returning getter, `None` for `null`.
356fn string_of(
357    env: &mut JNIEnv,
358    object: &JObject,
359    getter: &str,
360) -> jni::errors::Result<Option<String>> {
361    let value = JString::from(
362        env.call_method(object, getter, "()Ljava/lang/String;", &[])?
363            .l()?,
364    );
365    if value.is_null() {
366        return Ok(None);
367    }
368    Ok(Some(env.get_string(&value)?.into()))
369}
370
371fn jni_error(what: &'static str, source: impl std::fmt::Display) -> Error {
372    Error::io(what, std::io::Error::other(source.to_string()))
373}