use frus_widgets::{Accessibility, Brightness, Locale};
use crate::app::PlatformSettings;
use jni::objects::{JObject, JString, JValue};
use jni::{JNIEnv, JavaVM};
use winit::platform::android::activity::AndroidApp;
const UI_MODE_NIGHT_MASK: i32 = 0x30;
const UI_MODE_NIGHT_YES: i32 = 0x20;
fn clear_pending(env: &mut JNIEnv) {
if env.exception_check().unwrap_or(false) {
let _ = env.exception_clear();
}
}
pub(crate) fn read(app: &AndroidApp) -> PlatformSettings {
match try_read(app) {
Ok(settings) => settings,
Err(err) => {
log::warn!("platform settings unavailable ({err}) — using the neutral defaults");
if let Ok(vm) = unsafe { JavaVM::from_raw(app.vm_as_ptr() as *mut jni::sys::JavaVM) } {
if let Ok(mut env) = vm.attach_current_thread_permanently() {
clear_pending(&mut env);
}
}
PlatformSettings::default()
}
}
}
fn sdk_int(env: &mut JNIEnv) -> i32 {
match env
.get_static_field("android/os/Build$VERSION", "SDK_INT", "I")
.and_then(|v| v.i())
{
Ok(level) => level,
Err(_) => {
clear_pending(env);
0
}
}
}
fn try_read(app: &AndroidApp) -> Result<PlatformSettings, jni::errors::Error> {
let vm = unsafe { JavaVM::from_raw(app.vm_as_ptr() as *mut jni::sys::JavaVM) }?;
let mut env = vm.attach_current_thread_permanently()?;
let activity = unsafe { JObject::from_raw(app.activity_as_ptr() as jni::sys::jobject) };
let resources = env
.call_method(
&activity,
"getResources",
"()Landroid/content/res/Resources;",
&[],
)?
.l()?;
let config = env
.call_method(
&resources,
"getConfiguration",
"()Landroid/content/res/Configuration;",
&[],
)?
.l()?;
let text_scaler = match env.get_field(&config, "fontScale", "F").and_then(|v| v.f()) {
Ok(scale) if scale > 0.0 => scale,
Ok(_) => 1.0,
Err(_) => {
clear_pending(&mut env);
1.0
}
};
let ui_mode = env.get_field(&config, "uiMode", "I").and_then(|v| v.i())?;
let brightness = if ui_mode & UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES {
Brightness::Dark
} else {
Brightness::Light
};
let bold_text = if sdk_int(&mut env) >= 31 {
match env
.get_field(&config, "fontWeightAdjustment", "I")
.and_then(|v| v.i())
{
Ok(adjustment) => adjustment != 0 && adjustment != i32::MIN,
Err(_) => {
clear_pending(&mut env);
false
}
}
} else {
false
};
let accessibility = Accessibility {
bold_text,
high_contrast: secure_flag(&mut env, &activity, "high_text_contrast_enabled")
.unwrap_or(false),
disable_animations: animations_off(&mut env, &activity).unwrap_or(false),
invert_colors: secure_flag(
&mut env,
&activity,
"accessibility_display_inversion_enabled",
)
.unwrap_or(false),
accessible_navigation: touch_exploration(&mut env, &activity).unwrap_or(false),
always_use_24_hour_format: is_24_hour(&mut env, &activity).unwrap_or(false),
};
Ok(PlatformSettings {
text_scaler,
brightness,
accessibility,
locales: locales(&mut env, &config),
})
}
fn locales(env: &mut JNIEnv, config: &JObject) -> Vec<Locale> {
let read = if sdk_int(env) >= 24 {
try_locale_list(env, config)
} else {
try_single_locale(env, config)
};
match read {
Ok(locales) => locales,
Err(_) => {
clear_pending(env);
Vec::new()
}
}
}
fn try_locale_list(env: &mut JNIEnv, config: &JObject) -> Result<Vec<Locale>, jni::errors::Error> {
let list = env
.call_method(config, "getLocales", "()Landroid/os/LocaleList;", &[])?
.l()?;
let size = env.call_method(&list, "size", "()I", &[])?.i()?;
let mut out = Vec::with_capacity(size.max(0) as usize);
for index in 0..size {
let locale = env
.call_method(&list, "get", "(I)Ljava/util/Locale;", &[JValue::Int(index)])?
.l()?;
if let Some(parsed) = language_tag(env, &locale)? {
out.push(parsed);
}
}
Ok(out)
}
fn try_single_locale(
env: &mut JNIEnv,
config: &JObject,
) -> Result<Vec<Locale>, jni::errors::Error> {
let locale = env.get_field(config, "locale", "Ljava/util/Locale;")?.l()?;
Ok(language_tag(env, &locale)?.into_iter().collect())
}
fn language_tag(env: &mut JNIEnv, locale: &JObject) -> Result<Option<Locale>, jni::errors::Error> {
let tag = env
.call_method(locale, "toLanguageTag", "()Ljava/lang/String;", &[])?
.l()?;
let tag: String = env.get_string(&JString::from(tag))?.into();
Ok(Locale::parse(&tag))
}
fn content_resolver<'a>(
env: &mut JNIEnv<'a>,
activity: &JObject,
) -> Result<JObject<'a>, jni::errors::Error> {
env.call_method(
activity,
"getContentResolver",
"()Landroid/content/ContentResolver;",
&[],
)?
.l()
}
fn secure_flag(env: &mut JNIEnv, activity: &JObject, key: &str) -> Option<bool> {
let read = try_secure_flag(env, activity, key);
if read.is_err() {
clear_pending(env);
}
read.ok()
}
fn try_secure_flag(
env: &mut JNIEnv,
activity: &JObject,
key: &str,
) -> Result<bool, jni::errors::Error> {
let resolver = content_resolver(env, activity)?;
let name = env.new_string(key)?;
let value = env
.call_static_method(
"android/provider/Settings$Secure",
"getInt",
"(Landroid/content/ContentResolver;Ljava/lang/String;I)I",
&[
JValue::Object(&resolver),
JValue::Object(&name),
JValue::Int(0),
],
)?
.i()?;
Ok(value != 0)
}
fn animations_off(env: &mut JNIEnv, activity: &JObject) -> Option<bool> {
let read = try_animations_off(env, activity);
if read.is_err() {
clear_pending(env);
}
read.ok()
}
fn try_animations_off(env: &mut JNIEnv, activity: &JObject) -> Result<bool, jni::errors::Error> {
let resolver = content_resolver(env, activity)?;
let name = env.new_string("animator_duration_scale")?;
let scale = env
.call_static_method(
"android/provider/Settings$Global",
"getFloat",
"(Landroid/content/ContentResolver;Ljava/lang/String;F)F",
&[
JValue::Object(&resolver),
JValue::Object(&name),
JValue::Float(1.0),
],
)?
.f()?;
Ok(scale == 0.0)
}
fn touch_exploration(env: &mut JNIEnv, activity: &JObject) -> Option<bool> {
let read = try_touch_exploration(env, activity);
if read.is_err() {
clear_pending(env);
}
read.ok().flatten()
}
fn try_touch_exploration(
env: &mut JNIEnv,
activity: &JObject,
) -> Result<Option<bool>, jni::errors::Error> {
let name = env.new_string("accessibility")?;
let manager = env
.call_method(
activity,
"getSystemService",
"(Ljava/lang/String;)Ljava/lang/Object;",
&[JValue::Object(&name)],
)?
.l()?;
if manager.is_null() {
return Ok(None);
}
Ok(Some(
env.call_method(&manager, "isTouchExplorationEnabled", "()Z", &[])?
.z()?,
))
}
fn is_24_hour(env: &mut JNIEnv, activity: &JObject) -> Option<bool> {
let read = env
.call_static_method(
"android/text/format/DateFormat",
"is24HourFormat",
"(Landroid/content/Context;)Z",
&[JValue::Object(activity)],
)
.and_then(|v| v.z());
match read {
Ok(value) => Some(value),
Err(_) => {
clear_pending(env);
None
}
}
}