use std::sync::OnceLock;
use std::time::{Duration, Instant};
use jni::objects::{GlobalRef, JClass, JObject, JString, JValue};
use jni::{JNIEnv, JavaVM};
use crate::notification::{
left_the_shade, question, NotificationError, NotificationResponse, Question, SystemNotification,
};
struct Handle {
vm: JavaVM,
class: GlobalRef,
activity: GlobalRef,
}
unsafe impl Send for Handle {}
unsafe impl Sync for Handle {}
static HANDLE: OnceLock<Handle> = OnceLock::new();
const POLL: Duration = Duration::from_millis(250);
pub(crate) fn install(env: &mut JNIEnv, loader: &JObject, activity: &JObject) {
match try_install(env, loader, activity) {
Ok(handle) => {
let _ = HANDLE.set(handle);
}
Err(err) => {
clear_exception(env);
log::warn!("notifications unavailable ({err})");
}
}
}
fn try_install(
env: &mut JNIEnv,
loader: &JObject,
activity: &JObject,
) -> Result<Handle, jni::errors::Error> {
let name = env.new_string("dev.frus.input.FrusNotifications")?;
let class = env
.call_method(
loader,
"loadClass",
"(Ljava/lang/String;)Ljava/lang/Class;",
&[JValue::Object(&name)],
)?
.l()?;
Ok(Handle {
vm: env.get_java_vm()?,
class: env.new_global_ref(&class)?,
activity: env.new_global_ref(activity)?,
})
}
fn clear_exception(env: &mut JNIEnv) {
if env.exception_check().unwrap_or(false) {
let _ = env.exception_clear();
}
}
pub(crate) fn show(
notification: &SystemNotification,
) -> Result<NotificationResponse, NotificationError> {
let handle = HANDLE
.get()
.ok_or_else(|| failed("the notification bridge was not loaded"))?;
let mut env = handle
.vm
.attach_current_thread_permanently()
.map_err(|err| failed(&format!("no Java thread ({err})")))?;
let result = run(&mut env, handle, notification);
if result.is_err() {
clear_exception(&mut env);
}
result
}
fn failed(why: &str) -> NotificationError {
NotificationError(format!("the notification could not be shown: {why}"))
}
fn run(
env: &mut JNIEnv,
handle: &Handle,
notification: &SystemNotification,
) -> Result<NotificationResponse, NotificationError> {
let jni = |err: jni::errors::Error| failed(&err.to_string());
let class: &JClass = handle.class.as_obj().into();
let activity = handle.activity.as_obj();
let allowed = |env: &mut JNIEnv| {
env.call_static_method(
class,
"allowed",
"(Landroid/app/Activity;)I",
&[JValue::Object(activity)],
)
.and_then(|value| value.i())
};
let focused = |env: &mut JNIEnv| {
env.call_static_method(
class,
"focused",
"(Landroid/app/Activity;)Z",
&[JValue::Object(activity)],
)
.and_then(|value| value.z())
};
let mut unanswered = false;
if allowed(env).map_err(jni)? == 0 {
env.call_static_method(
class,
"ask",
"(Landroid/app/Activity;)V",
&[JValue::Object(activity)],
)
.map_err(jni)?;
let asked = Instant::now();
let mut was_away = false;
loop {
let has_focus = focused(env).map_err(jni)?;
was_away |= !has_focus;
match question(
allowed(env).map_err(jni)?,
has_focus,
was_away,
asked.elapsed(),
) {
Question::Open => std::thread::sleep(POLL),
Question::Settled => break,
Question::Unanswered => {
unanswered = true;
break;
}
}
}
}
match allowed(env).map_err(jni)? {
1 => {}
0 if unanswered => return Err(failed("the person did not answer whether it may")),
0 => return Err(failed("the person did not allow notifications")),
_ => {
return Err(failed(
"notifications are turned off for the application, or its manifest does not \
ask for android.permission.POST_NOTIFICATIONS",
))
}
}
let title = string(env, ¬ification.title).map_err(jni)?;
let body = string(env, ¬ification.body).map_err(jni)?;
let ids: Vec<&str> = notification
.buttons
.iter()
.map(|(id, _)| id.as_str())
.collect();
let labels: Vec<&str> = notification
.buttons
.iter()
.map(|(_, label)| label.as_str())
.collect();
let ids = strings(env, &ids).map_err(jni)?;
let labels = strings(env, &labels).map_err(jni)?;
let image = match ¬ification.image {
Some(path) => string(env, &path.to_string_lossy()).map_err(jni)?,
None => JObject::null(),
};
let id = env
.call_static_method(
class,
"post",
"(Landroid/app/Activity;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;\
[Ljava/lang/String;Ljava/lang/String;Z)I",
&[
JValue::Object(activity),
JValue::Object(&title),
JValue::Object(&body),
JValue::Object(&ids),
JValue::Object(&labels),
JValue::Object(&image),
JValue::Bool(notification.silent.into()),
],
)
.and_then(|value| value.i())
.map_err(jni)?;
if id == 0 {
return Err(failed("the system has no notification service"));
}
let answer = |env: &mut JNIEnv| -> Result<Option<String>, jni::errors::Error> {
let object = env
.call_static_method(class, "answer", "(I)Ljava/lang/String;", &[JValue::Int(id)])?
.l()?;
if object.is_null() {
return Ok(None);
}
Ok(Some(env.get_string(&JString::from(object))?.into()))
};
let active = |env: &mut JNIEnv| {
env.call_static_method(
class,
"active",
"(Landroid/app/Activity;I)Z",
&[JValue::Object(activity), JValue::Int(id)],
)
.and_then(|value| value.z())
};
loop {
if let Some(recorded) = answer(env).map_err(jni)? {
return Ok(left_the_shade(Some(&recorded)));
}
if !active(env).map_err(jni)? {
std::thread::sleep(POLL * 2);
return Ok(left_the_shade(answer(env).map_err(jni)?.as_deref()));
}
std::thread::sleep(POLL);
}
}
fn string<'local>(
env: &mut JNIEnv<'local>,
text: &str,
) -> Result<JObject<'local>, jni::errors::Error> {
env.new_string(text).map(JObject::from)
}
fn strings<'local>(
env: &mut JNIEnv<'local>,
texts: &[&str],
) -> Result<JObject<'local>, jni::errors::Error> {
let array = env.new_object_array(texts.len() as i32, "java/lang/String", JObject::null())?;
for (i, text) in texts.iter().enumerate() {
let text = env.new_string(text)?;
env.set_object_array_element(&array, i as i32, text)?;
}
Ok(array.into())
}