| 1 | //! [`Phone`] over JNI: calls into the app's `Platform` object (`Platform.kt`) from the worker's |
| 2 | //! thread. The thread attaches to the VM on its first call and stays attached until it ends; |
| 3 | //! each call gets a JNI frame of its own, so no local reference outlives it. |
| 4 | |
| 5 | use std::time::Duration; |
| 6 | |
| 7 | use budctl_core::{Error, Result}; |
| 8 | use jni::objects::{JByteArray, JObject, JString}; |
| 9 | use jni::refs::Global; |
| 10 | use jni::strings::JNIStr; |
| 11 | use jni::vm::JavaVM; |
| 12 | use jni::{Env, JValue, jni_sig, jni_str}; |
| 13 | |
| 14 | use crate::json; |
| 15 | use crate::phone::{Phone, app_error}; |
| 16 | |
| 17 | pub struct Kotlin { |
| 18 | platform: Global<JObject<'static>>, |
| 19 | } |
| 20 | |
| 21 | impl Kotlin { |
| 22 | pub fn new(platform: Global<JObject<'static>>) -> Self { |
| 23 | Kotlin { platform } |
| 24 | } |
| 25 | |
| 26 | /// Runs `call` on this thread's JNI environment. A Java exception or a JNI failure becomes an |
| 27 | /// app error about `what`. |
| 28 | fn call<T>( |
| 29 | &self, |
| 30 | what: &'static str, |
| 31 | call: impl FnOnce(&mut Env, &JObject<'static>) -> jni::errors::Result<T>, |
| 32 | ) -> Result<T> { |
| 33 | let vm = JavaVM::singleton().map_err(|e| app_error(what, e))?; |
| 34 | vm.attach_current_thread(|env| call(env, &self.platform)) |
| 35 | .map_err(|e| app_error(what, e)) |
| 36 | } |
| 37 | |
| 38 | /// Calls a `(String) -> Unit` method, for reports: a failure has nowhere to go. |
| 39 | fn tell(&self, method: &'static JNIStr, text: &str) { |
| 40 | let _ = self.call("reporting to the app", |env, platform| { |
| 41 | let text = env.new_string(text)?; |
| 42 | env.call_method( |
| 43 | platform, |
| 44 | method, |
| 45 | jni_sig!((text: java.lang.String) -> void), |
| 46 | &[JValue::Object(&text)], |
| 47 | )? |
| 48 | .v() |
| 49 | }); |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | /// A Kotlin `String?` as a Rust string. |
| 54 | fn string(env: &mut Env, object: JObject) -> jni::errors::Result<Option<String>> { |
| 55 | if object.is_null() { |
| 56 | return Ok(None); |
| 57 | } |
| 58 | let string = env.cast_local::<JString>(object)?; |
| 59 | Ok(Some(string.try_to_string(env)?)) |
| 60 | } |
| 61 | |
| 62 | impl Phone for Kotlin { |
| 63 | fn paired_devices(&self) -> Result<String> { |
| 64 | let list = self.call("listing the paired devices", |env, platform| { |
| 65 | let list = env |
| 66 | .call_method( |
| 67 | platform, |
| 68 | jni_str!("pairedDevices"), |
| 69 | jni_sig!(() -> java.lang.String), |
| 70 | &[], |
| 71 | )? |
| 72 | .l()?; |
| 73 | string(env, list) |
| 74 | })?; |
| 75 | Ok(list.unwrap_or_default()) |
| 76 | } |
| 77 | |
| 78 | fn connect(&self, address: &str, service: &str) -> Result<()> { |
| 79 | let failure = self.call("connecting to the control channel", |env, platform| { |
| 80 | let address = env.new_string(address)?; |
| 81 | let service = env.new_string(service)?; |
| 82 | let failure = env |
| 83 | .call_method( |
| 84 | platform, |
| 85 | jni_str!("connect"), |
| 86 | jni_sig!((address: java.lang.String, service: java.lang.String) -> java.lang.String), |
| 87 | &[JValue::Object(&address), JValue::Object(&service)], |
| 88 | )? |
| 89 | .l()?; |
| 90 | string(env, failure) |
| 91 | })?; |
| 92 | match failure { |
| 93 | None => Ok(()), |
| 94 | Some(json) => Err(json::connect_failure(&json)), |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | fn send(&self, bytes: &[u8]) -> Result<()> { |
| 99 | let failure = self.call("sending to the buds", |env, platform| { |
| 100 | let bytes = env.byte_array_from_slice(bytes)?; |
| 101 | let failure = env |
| 102 | .call_method( |
| 103 | platform, |
| 104 | jni_str!("send"), |
| 105 | jni_sig!((bytes: jbyte[]) -> java.lang.String), |
| 106 | &[JValue::Object(&bytes)], |
| 107 | )? |
| 108 | .l()?; |
| 109 | string(env, failure) |
| 110 | })?; |
| 111 | match failure { |
| 112 | None => Ok(()), |
| 113 | Some(message) => Err(Error::NotConnected(format!("sending to the buds: {message}"))), |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | fn recv(&self, timeout: Duration) -> Result<Option<Vec<u8>>> { |
| 118 | // Rounded up, so a wait shorter than a millisecond still waits. |
| 119 | let millis = i64::try_from(timeout.as_micros().div_ceil(1000)).unwrap_or(i64::MAX); |
| 120 | self.call("reading from the buds", |env, platform| { |
| 121 | let bytes = env |
| 122 | .call_method( |
| 123 | platform, |
| 124 | jni_str!("recv"), |
| 125 | jni_sig!((timeout_ms: jlong) -> jbyte[]), |
| 126 | &[JValue::Long(millis)], |
| 127 | )? |
| 128 | .l()?; |
| 129 | if bytes.is_null() { |
| 130 | return Ok(None); |
| 131 | } |
| 132 | let bytes = env.cast_local::<JByteArray>(bytes)?; |
| 133 | Ok(Some(env.convert_byte_array(&bytes)?)) |
| 134 | }) |
| 135 | } |
| 136 | |
| 137 | fn close(&self) { |
| 138 | let _ = self.call("closing the link", |env, platform| { |
| 139 | env.call_method(platform, jni_str!("close"), jni_sig!(() -> void), &[])? |
| 140 | .v() |
| 141 | }); |
| 142 | } |
| 143 | |
| 144 | fn event(&self, json: &str) { |
| 145 | self.tell(jni_str!("event"), json); |
| 146 | } |
| 147 | |
| 148 | fn log(&self, line: &str) { |
| 149 | self.tell(jni_str!("log"), line); |
| 150 | } |
| 151 | } |