Krolyxon
Commit c2b9d4b

Branches

Refs →

budctlcratesbudctl-androidsrclib.rs

114 lines · 4.2 KB · Rust

1//! budctl-core inside the Android app (docs/android.md).
2//!
3//! The app's `Core` object calls three functions: `start` the worker, queue a `command`, and
4//! `stop` it. The worker calls the app back on its own thread, through the `Platform` object
5//! `start` was given (`kotlin.rs`): to list the paired devices, to use the RFCOMM link, and to
6//! report events and log lines. Commands and events cross as JSON (`json.rs`). Nothing here knows
7//! the buds' protocol.
8//!
9//! The exports are the second exception to rule 5 (CLAUDE.md): the VM finds a native method by
10//! its exported name, and exporting one is an unsafe attribute. Nothing else here is unsafe.
11
12mod json;
13mod kotlin;
14mod phone;
15
16use std::sync::mpsc::Sender;
17use std::sync::{Arc, Mutex, PoisonError};
18use std::time::Duration;
19
20use budctl_core::debug;
21use budctl_core::worker::{self, Command};
22use jni::errors::ThrowRuntimeExAndDefault;
23use jni::objects::{JClass, JObject, JString};
24use jni::sys::{jboolean, jlong};
25use jni::{Env, EnvUnowned};
26
27use kotlin::Kotlin;
28use phone::{Phone, Reporter};
29
30/// The running worker's inbox. Dropping it ends the worker, which closes any session.
31static WORKER: Mutex<Option<Sender<Command>>> = Mutex::new(None);
32
33/// `Core.start(platform, idleMs, debug)`: starts the worker, in place of any running one. Returns
34/// null, or why it could not start.
35#[allow(unsafe_code)]
36#[unsafe(no_mangle)]
37pub extern "system" fn Java_io_github_krolyxon_budctl_Core_start<'caller>(
38 mut unowned: EnvUnowned<'caller>,
39 _class: JClass<'caller>,
40 platform: JObject<'caller>,
41 idle_ms: jlong,
42 debug: jboolean,
43) -> JString<'caller> {
44 unowned
45 .with_env(|env| -> jni::errors::Result<_> {
46 let phone: Arc<dyn Phone> = Arc::new(Kotlin::new(env.new_global_ref(&platform)?));
47 if debug {
48 debug::enable();
49 } else {
50 debug::disable();
51 }
52 let output = phone.clone();
53 debug::set_output(Box::new(move |line| output.log(line)));
54
55 let idle = Duration::from_millis(u64::try_from(idle_ms).unwrap_or(0));
56 let connect = phone::connect(phone.clone());
57 let failure = match worker::start(connect, Box::new(Reporter(phone)), idle) {
58 Ok(inbox) => {
59 *WORKER.lock().unwrap_or_else(PoisonError::into_inner) = Some(inbox);
60 None
61 }
62 Err(e) => Some(format!("could not start the Bluetooth thread: {e}")),
63 };
64 answer(env, failure)
65 })
66 .resolve::<ThrowRuntimeExAndDefault>()
67}
68
69/// `Core.command(json)`: queues one command for the worker; its event comes back through
70/// `Platform.event`. Returns null, or why the command was not queued.
71#[allow(unsafe_code)]
72#[unsafe(no_mangle)]
73pub extern "system" fn Java_io_github_krolyxon_budctl_Core_command<'caller>(
74 mut unowned: EnvUnowned<'caller>,
75 _class: JClass<'caller>,
76 json: JString<'caller>,
77) -> JString<'caller> {
78 unowned
79 .with_env(|env| -> jni::errors::Result<_> {
80 let failure = match json::command(&json.try_to_string(env)?) {
81 Ok(command) => queue(command).err(),
82 Err(e) => Some(e.to_string()),
83 };
84 answer(env, failure)
85 })
86 .resolve::<ThrowRuntimeExAndDefault>()
87}
88
89/// `Core.stop()`: ends the worker, which closes any session once the command at hand is done.
90#[allow(unsafe_code)]
91#[unsafe(no_mangle)]
92pub extern "system" fn Java_io_github_krolyxon_budctl_Core_stop<'caller>(
93 _unowned: EnvUnowned<'caller>,
94 _class: JClass<'caller>,
95) {
96 let inbox = WORKER.lock().unwrap_or_else(PoisonError::into_inner).take();
97 drop(inbox);
98}
99
100fn queue(command: Command) -> Result<(), String> {
101 let worker = WORKER.lock().unwrap_or_else(PoisonError::into_inner);
102 let inbox = worker.as_ref().ok_or("the Bluetooth thread is not running")?;
103 inbox
104 .send(command)
105 .map_err(|_| "the Bluetooth thread has stopped".to_owned())
106}
107
108/// Null for no failure, else the message, as a Java string.
109fn answer<'local>(env: &mut Env<'local>, failure: Option<String>) -> jni::errors::Result<JString<'local>> {
110 match failure {
111 None => Ok(JString::null()),
112 Some(message) => env.new_string(message),
113 }
114}