use std::sync::{Mutex, OnceLock};
pub fn init_logging() {
static INSTALLED: std::sync::Once = std::sync::Once::new();
INSTALLED.call_once(|| {
let _ = log::set_logger(&LOGCAT_LOGGER);
log::set_max_level(log::LevelFilter::Info);
});
}
static LOGCAT_LOGGER: LogcatLogger = LogcatLogger;
struct LogcatLogger;
fn logcat_priority(level: log::Level) -> i32 {
match level {
log::Level::Error => 6,
log::Level::Warn => 5,
log::Level::Info => 4,
log::Level::Debug => 3,
log::Level::Trace => 2,
}
}
impl log::Log for LogcatLogger {
fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
metadata.level() <= log::Level::Info
}
fn log(&self, record: &log::Record<'_>) {
if !self.enabled(record.metadata()) {
return;
}
let tag = std::ffi::CString::new("rust_widgets").unwrap_or_default();
let message = std::ffi::CString::new(record.args().to_string()).unwrap_or_default();
unsafe {
let _ =
android_log_write(logcat_priority(record.level()), tag.as_ptr(), message.as_ptr());
}
}
fn flush(&self) {}
}
extern "C" {
fn __android_log_write(
prio: i32,
tag: *const core::ffi::c_char,
text: *const core::ffi::c_char,
) -> i32;
}
unsafe fn android_log_write(
prio: i32,
tag: *const core::ffi::c_char,
text: *const core::ffi::c_char,
) -> i32 {
unsafe { __android_log_write(prio, tag, text) }
}
static JAVA_VM: OnceLock<jni::JavaVM> = OnceLock::new();
fn activity_context_slot() -> &'static Mutex<Option<jni::objects::GlobalRef>> {
static SLOT: OnceLock<Mutex<Option<jni::objects::GlobalRef>>> = OnceLock::new();
SLOT.get_or_init(|| Mutex::new(None))
}
pub fn set_activity_context(
env: &mut jni::JNIEnv<'_>,
context: &jni::objects::JObject<'_>,
) -> bool {
let Ok(global) = env.new_global_ref(context) else {
log::error!("[android-jni] nativeAttachContext: failed to create a global reference");
return false;
};
let mut slot = activity_context_slot().lock().unwrap_or_else(|poisoned| poisoned.into_inner());
*slot = Some(global);
log::info!("[android-jni] Activity Context attached");
true
}
pub fn has_activity_context() -> bool {
activity_context_slot().lock().unwrap_or_else(|poisoned| poisoned.into_inner()).is_some()
}
pub fn native_view_creation_ready() -> bool {
is_initialized() && has_activity_context()
}
pub fn is_initialized() -> bool {
JAVA_VM.get().is_some()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IntegrationStatus {
pub jni_initialized: bool,
pub context_attached: bool,
pub native_methods_count: u32,
pub ready: bool,
}
pub fn android_integration_ready() -> IntegrationStatus {
let jni_initialized = is_initialized();
let context_attached = has_activity_context();
IntegrationStatus {
jni_initialized,
context_attached,
native_methods_count: NATIVE_METHOD_COUNT,
ready: jni_initialized && context_attached,
}
}
pub const NATIVE_METHOD_COUNT: u32 = 17;
pub const PICK_REQUEST_CODE: i32 = 21079;
pub fn with_jni_env<F, R>(f: F) -> Option<R>
where
F: FnOnce(&mut jni::JNIEnv<'_>) -> R,
{
let vm = JAVA_VM.get()?;
let mut env = vm.attach_current_thread().ok()?;
Some(f(&mut env))
}
#[cfg(feature = "android-jni")]
pub fn print_file(job_file: &std::path::Path) -> Result<(), String> {
if !job_file.is_file() {
return Err(format!("print job '{}' is not a readable file", job_file.display()));
}
let context = activity_context_slot()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.clone()
.ok_or_else(|| "printing needs an attached Activity context".to_string())?;
let path = job_file.to_string_lossy().into_owned();
with_jni_env(|env| {
let class = env
.find_class("rust_widgets/RustWidgets")
.map_err(|e| format!("cannot resolve rust_widgets.RustWidgets: {e}"))?;
let path = env
.new_string(path)
.map_err(|e| format!("cannot create the print path string: {e}"))?;
let result = env
.call_static_method(
class,
"printFile",
"(Landroid/content/Context;Ljava/lang/String;)I",
&[
jni::objects::JValue::Object(context.as_obj()),
jni::objects::JValue::Object(&path),
],
)
.map_err(|e| format!("printFile call failed: {e}"))?
.i()
.map_err(|e| format!("printFile returned a non-int: {e}"))?;
if result == 1 {
Ok(())
} else {
Err(format!("the system print manager refused '{}'", job_file.display()))
}
})
.unwrap_or_else(|| Err("no JavaVM is stored; nativeInit has not run".to_string()))
}
#[cfg(feature = "android-jni")]
pub fn print_available() -> bool {
is_initialized() && has_activity_context()
}
pub fn launch_file_dialog(mime_type: &str) -> bool {
let Some(result) = with_jni_env(|env| launch_document_picker(env, mime_type)) else {
log::warn!(
"[android-jni] {mime_type}: no JavaVM is stored, so the document picker cannot be \
launched; nativeInit has not run"
);
return false;
};
result
}
fn launch_document_picker(env: &mut jni::JNIEnv<'_>, mime_type: &str) -> bool {
let context = {
let slot = activity_context_slot().lock().unwrap_or_else(|poisoned| poisoned.into_inner());
match slot.as_ref() {
Some(global) => global.clone(),
None => {
log::warn!(
"[android-jni] {mime_type}: no Activity Context is attached, so the document \
picker cannot be launched; the host app must call nativeAttachContext"
);
return false;
}
}
};
let Ok(mime) = env.new_string(mime_type) else {
log::error!("[android-jni] failed to allocate the mime type string");
return false;
};
let result = (|| -> jni::errors::Result<()> {
let intent_class = env.find_class("android/content/Intent")?;
let action = env.new_string("android.intent.action.OPEN_DOCUMENT")?;
let intent = env.new_object(
&intent_class,
"(Ljava/lang/String;)V",
&[jni::objects::JValue::Object(&action)],
)?;
env.call_method(
&intent,
"setType",
"(Ljava/lang/String;)Landroid/content/Intent;",
&[jni::objects::JValue::Object(&mime)],
)?;
env.call_method(
context.as_obj(),
"startActivityForResult",
"(Landroid/content/Intent;I)V",
&[jni::objects::JValue::Object(&intent), jni::objects::JValue::Int(PICK_REQUEST_CODE)],
)?;
Ok(())
})();
match result {
Ok(()) => {
log::info!("[android-jni] document picker launched for {mime_type}");
true
}
Err(error) => {
log::error!("[android-jni] failed to launch the document picker: {error}");
false
}
}
}
const CLIPBOARD_SERVICE: &str = "clipboard";
pub fn clipboard_ready() -> bool {
is_initialized() && has_activity_context()
}
pub fn set_clipboard_text(text: &str) -> Option<bool> {
with_jni_env(|env| write_primary_clip(env, text)).flatten()
}
pub fn clipboard_text() -> Option<String> {
with_jni_env(read_primary_clip).flatten()
}
fn attached_context() -> Option<jni::objects::GlobalRef> {
let slot = activity_context_slot().lock().unwrap_or_else(|poisoned| poisoned.into_inner());
slot.clone()
}
fn clipboard_manager<'local>(
env: &mut jni::JNIEnv<'local>,
) -> Option<jni::objects::JObject<'local>> {
let context = attached_context()?;
let service = env.new_string(CLIPBOARD_SERVICE).ok()?;
let manager = env
.call_method(
context.as_obj(),
"getSystemService",
"(Ljava/lang/String;)Ljava/lang/Object;",
&[jni::objects::JValue::Object(&service)],
)
.ok()?
.l()
.ok()?;
if manager.is_null() {
log::warn!("[android-jni] getSystemService(\"clipboard\") returned null");
return None;
}
Some(manager)
}
fn primary_clip_item<'local>(
env: &mut jni::JNIEnv<'local>,
manager: &jni::objects::JObject<'local>,
) -> Option<jni::objects::JObject<'local>> {
let has_clip = env.call_method(manager, "hasPrimaryClip", "()Z", &[]).ok()?.z().ok()?;
if !has_clip {
return None;
}
let clip = env
.call_method(manager, "getPrimaryClip", "()Landroid/content/ClipData;", &[])
.ok()?
.l()
.ok()?;
if clip.is_null() {
return None;
}
let item = env
.call_method(
&clip,
"getItemAt",
"(I)Landroid/content/ClipData$Item;",
&[jni::objects::JValue::Int(0)],
)
.ok()?
.l()
.ok()?;
if item.is_null() {
log::warn!("[android-jni] the primary clip holds no item");
return None;
}
Some(item)
}
fn item_text(
env: &mut jni::JNIEnv<'_>,
item: &jni::objects::JObject<'_>,
method: &str,
) -> Option<String> {
let chars = env.call_method(item, method, "()Ljava/lang/CharSequence;", &[]).ok()?.l().ok()?;
if chars.is_null() {
return None;
}
let text = env.call_method(&chars, "toString", "()Ljava/lang/String;", &[]).ok()?.l().ok()?;
if text.is_null() {
return None;
}
env.get_string(&jni::objects::JString::from(text)).ok().map(|value| value.into())
}
fn read_primary_clip(env: &mut jni::JNIEnv<'_>) -> Option<String> {
let context = attached_context()?;
let manager = clipboard_manager(env)?;
let item = primary_clip_item(env, &manager)?;
let chars = env
.call_method(
&item,
"coerceToText",
"(Landroid/content/Context;)Ljava/lang/CharSequence;",
&[jni::objects::JValue::Object(context.as_obj())],
)
.ok()?
.l()
.ok()?;
if chars.is_null() {
return None;
}
let text = env.call_method(&chars, "toString", "()Ljava/lang/String;", &[]).ok()?.l().ok()?;
if text.is_null() {
return None;
}
env.get_string(&jni::objects::JString::from(text)).ok().map(|value| value.into())
}
fn read_primary_html(env: &mut jni::JNIEnv<'_>) -> Option<String> {
let manager = clipboard_manager(env)?;
let item = primary_clip_item(env, &manager)?;
item_text(env, &item, "getHtmlText")
}
fn primary_clip<'local>(
env: &mut jni::JNIEnv<'local>,
manager: &jni::objects::JObject<'local>,
) -> Option<jni::objects::JObject<'local>> {
let has_clip = env.call_method(manager, "hasPrimaryClip", "()Z", &[]).ok()?.z().ok()?;
if !has_clip {
return None;
}
let clip = env
.call_method(manager, "getPrimaryClip", "()Landroid/content/ClipData;", &[])
.ok()?
.l()
.ok()?;
(!clip.is_null()).then_some(clip)
}
fn primary_clip_types(env: &mut jni::JNIEnv<'_>) -> Option<Vec<String>> {
let manager = clipboard_manager(env)?;
let clip = primary_clip(env, &manager)?;
let description = env
.call_method(&clip, "getDescription", "()Landroid/content/ClipDescription;", &[])
.ok()?
.l()
.ok()?;
if description.is_null() {
return None;
}
let count = env.call_method(&description, "getMimeTypeCount", "()I", &[]).ok()?.i().ok()?;
let mut types = Vec::new();
for index in 0..count {
let mime = env
.call_method(
&description,
"getMimeType",
"(I)Ljava/lang/String;",
&[jni::objects::JValue::Int(index)],
)
.ok()?
.l()
.ok()?;
if mime.is_null() {
continue;
}
let text =
env.call_method(&mime, "toString", "()Ljava/lang/String;", &[]).ok()?.l().ok()?;
if let Ok(value) = env.get_string(&jni::objects::JString::from(text)) {
types.push(value.into());
}
}
Some(types)
}
fn primary_clip_has_type(env: &mut jni::JNIEnv<'_>, content_type: &str) -> bool {
primary_clip_types(env)
.is_some_and(|types| types.iter().any(|declared| declared == content_type))
}
fn primary_clip_custom_type(env: &mut jni::JNIEnv<'_>) -> Option<String> {
primary_clip_types(env)?
.into_iter()
.find(|declared| declared != "text/plain" && declared != "text/html")
}
fn bytes_to_latin1(data: &[u8]) -> String {
data.iter().map(|byte| char::from(*byte)).collect()
}
fn latin1_to_bytes(text: &str) -> Option<Vec<u8>> {
text.chars().map(|character| u8::try_from(u32::from(character)).ok()).collect()
}
fn build_custom_clip<'local>(
env: &mut jni::JNIEnv<'local>,
content_type: &str,
data: &[u8],
) -> Option<jni::objects::JObject<'local>> {
let label = env.new_string("rust-widgets").ok()?;
let mime = env.new_string(content_type).ok()?;
let string_class = env.find_class("java/lang/String").ok()?;
let types = env.new_object_array(1, string_class, &mime).ok()?;
let description_class = env.find_class("android/content/ClipDescription").ok()?;
let description = env
.new_object(
description_class,
"(Ljava/lang/CharSequence;[Ljava/lang/String;)V",
&[jni::objects::JValue::Object(&label), jni::objects::JValue::Object(&types)],
)
.ok()?;
let payload = env.new_string(bytes_to_latin1(data)).ok()?;
let item_class = env.find_class("android/content/ClipData$Item").ok()?;
let item = env
.new_object(
item_class,
"(Ljava/lang/CharSequence;)V",
&[jni::objects::JValue::Object(&payload)],
)
.ok()?;
let clip_class = env.find_class("android/content/ClipData").ok()?;
env.new_object(
clip_class,
"(Landroid/content/ClipDescription;Landroid/content/ClipData$Item;)V",
&[jni::objects::JValue::Object(&description), jni::objects::JValue::Object(&item)],
)
.ok()
.filter(|clip| !clip.is_null())
}
fn read_primary_custom(env: &mut jni::JNIEnv<'_>, content_type: &str) -> Option<Vec<u8>> {
if !primary_clip_has_type(env, content_type) {
log::warn!(
"[android-jni] custom read: the primary clip does not declare {content_type}; it \
declares {:?}",
primary_clip_types(env)
);
return None;
}
let manager = clipboard_manager(env)?;
let item = primary_clip_item(env, &manager)?;
let Some(text) = item_text(env, &item, "getText") else {
log::warn!("[android-jni] custom read: ClipData.Item.getText() returned null");
return None;
};
let bytes = latin1_to_bytes(&text);
if bytes.is_none() {
log::warn!(
"[android-jni] custom read: the item text is not in this crate's byte encoding \
({} chars, first non-Latin-1 char at {:?})",
text.chars().count(),
text.chars().position(|character| u32::from(character) > 0xFF)
);
}
bytes
}
fn publish_primary_custom_clip(
env: &mut jni::JNIEnv<'_>,
clip: &jni::objects::JObject<'_>,
content_type: &str,
data: &[u8],
) -> Option<bool> {
let manager = clipboard_manager(env)?;
if let Err(error) = env.call_method(
&manager,
"setPrimaryClip",
"(Landroid/content/ClipData;)V",
&[jni::objects::JValue::Object(clip)],
) {
log::error!("[android-jni] setPrimaryClip (custom type) failed: {error}");
return Some(false);
}
let written = read_primary_custom(env, content_type).is_some_and(|stored| stored == data);
if !written {
log::warn!(
"[android-jni] setPrimaryClip returned without error but the custom type {content_type} \
does not hold the bytes that were written (the platform refuses background clipboard \
writes, or another app replaced the clip); the clip now declares {:?}",
primary_clip_types(env)
);
}
Some(written)
}
fn write_primary_custom_clip(
env: &mut jni::JNIEnv<'_>,
content_type: &str,
data: &[u8],
) -> Option<bool> {
let clip = build_custom_clip(env, content_type, data)?;
publish_primary_custom_clip(env, &clip, content_type, data)
}
pub fn set_clipboard_type(content_type: &str, data: &[u8]) -> Option<bool> {
with_jni_env(|env| write_primary_custom_clip(env, content_type, data)).flatten()
}
pub fn clipboard_type(content_type: &str) -> Option<Vec<u8>> {
with_jni_env(|env| read_primary_custom(env, content_type)).flatten()
}
pub fn clipboard_has_type(content_type: &str) -> bool {
with_jni_env(|env| primary_clip_has_type(env, content_type)).unwrap_or(false)
}
pub fn clipboard_custom_type() -> Option<String> {
with_jni_env(primary_clip_custom_type).flatten()
}
fn input_method_manager<'local>(
env: &mut jni::JNIEnv<'local>,
) -> Option<jni::objects::JObject<'local>> {
let context = attached_context()?;
let service = env.new_string("input_method").ok()?;
let manager = env
.call_method(
context.as_obj(),
"getSystemService",
"(Ljava/lang/String;)Ljava/lang/Object;",
&[jni::objects::JValue::Object(&service)],
)
.ok()?
.l()
.ok()?;
if manager.is_null() {
log::warn!("[android-jni] getSystemService(\"input_method\") returned null");
return None;
}
Some(manager)
}
fn activity_decor_view<'local>(
env: &mut jni::JNIEnv<'local>,
) -> Option<jni::objects::JObject<'local>> {
let context = attached_context()?;
let window =
match env.call_method(context.as_obj(), "getWindow", "()Landroid/view/Window;", &[]) {
Ok(value) => value.l().ok()?,
Err(error) => {
log::debug!("[android-jni] Context.getWindow() is unavailable: {error}");
let _ = env.exception_clear();
return None;
}
};
if window.is_null() {
return None;
}
let view =
env.call_method(&window, "getDecorView", "()Landroid/view/View;", &[]).ok()?.l().ok()?;
(!view.is_null()).then_some(view)
}
fn show_soft_input_env(env: &mut jni::JNIEnv<'_>) -> bool {
let Some(manager) = input_method_manager(env) else {
return false;
};
let Some(view) = activity_decor_view(env) else {
return false;
};
match env.call_method(
&manager,
"showSoftInput",
"(Landroid/view/View;I)Z",
&[jni::objects::JValue::Object(&view), jni::objects::JValue::Int(0)],
) {
Ok(value) => value.z().unwrap_or(false),
Err(error) => {
log::warn!("[android-jni] showSoftInput failed: {error}");
let _ = env.exception_clear();
false
}
}
}
fn hide_soft_input_env(env: &mut jni::JNIEnv<'_>) -> bool {
let Some(manager) = input_method_manager(env) else {
return false;
};
let Some(view) = activity_decor_view(env) else {
return false;
};
let token = match env.call_method(&view, "getWindowToken", "()Landroid/os/IBinder;", &[]) {
Ok(value) => value.l().ok(),
Err(error) => {
log::warn!("[android-jni] View.getWindowToken failed: {error}");
let _ = env.exception_clear();
None
}
};
let Some(token) = token else {
return false;
};
match env.call_method(
&manager,
"hideSoftInputFromWindow",
"(Landroid/os/IBinder;I)Z",
&[jni::objects::JValue::Object(&token), jni::objects::JValue::Int(0)],
) {
Ok(value) => value.z().unwrap_or(false),
Err(error) => {
log::warn!("[android-jni] hideSoftInputFromWindow failed: {error}");
let _ = env.exception_clear();
false
}
}
}
pub fn show_soft_input() -> bool {
with_jni_env(show_soft_input_env).unwrap_or(false)
}
pub fn hide_soft_input() -> bool {
with_jni_env(hide_soft_input_env).unwrap_or(false)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AndroidDisplayInfo {
pub id: u64,
pub width: u32,
pub height: u32,
pub work_width: u32,
pub work_height: u32,
pub scale: f32,
pub is_primary: bool,
}
pub fn display_info() -> Option<AndroidDisplayInfo> {
macro_rules! need {
($env:expr, $expr:expr, $what:literal) => {
match $expr {
Ok(value) => value,
Err(error) => {
log::warn!("[android-jni] display_info: {} failed: {error}", $what);
let _ = $env.exception_clear();
return None;
}
}
};
}
with_jni_env(|env| {
let Some(context) = attached_context() else {
log::warn!("[android-jni] display_info: no Activity Context is attached");
return None;
};
let service = need!(env, env.new_string("window"), "new_string(\"window\")");
let manager = need!(
env,
need!(
env,
env.call_method(
context.as_obj(),
"getSystemService",
"(Ljava/lang/String;)Ljava/lang/Object;",
&[jni::objects::JValue::Object(&service)],
),
"getSystemService"
)
.l(),
"getSystemService -> object"
);
if manager.is_null() {
log::warn!("[android-jni] display_info: getSystemService(window) returned null");
return None;
}
let display = need!(
env,
need!(
env,
env.call_method(&manager, "getDefaultDisplay", "()Landroid/view/Display;", &[]),
"getDefaultDisplay"
)
.l(),
"getDefaultDisplay -> object"
);
if display.is_null() {
log::warn!("[android-jni] display_info: getDefaultDisplay returned null");
return None;
}
let id = need!(
env,
need!(env, env.call_method(&display, "getDisplayId", "()I", &[]), "getDisplayId").i(),
"getDisplayId -> int"
) as u64;
let metrics_class =
need!(env, env.find_class("android/util/DisplayMetrics"), "find_class(DisplayMetrics)");
let metrics = need!(env, env.new_object(metrics_class, "()V", &[]), "new DisplayMetrics()");
need!(
env,
env.call_method(
&display,
"getRealMetrics",
"(Landroid/util/DisplayMetrics;)V",
&[jni::objects::JValue::Object(&metrics)],
),
"getRealMetrics"
);
let width = need!(
env,
need!(env, env.get_field(&metrics, "widthPixels", "I"), "get widthPixels").i(),
"widthPixels -> int"
);
let height = need!(
env,
need!(env, env.get_field(&metrics, "heightPixels", "I"), "get heightPixels").i(),
"heightPixels -> int"
);
let density_dpi = need!(
env,
need!(env, env.get_field(&metrics, "densityDpi", "I"), "get densityDpi").i(),
"densityDpi -> int"
);
let work_width = width.max(0) as u32;
let work_height = height.max(0) as u32;
Some(AndroidDisplayInfo {
id,
width: width.max(0) as u32,
height: height.max(0) as u32,
work_width,
work_height,
scale: if density_dpi > 0 { density_dpi as f32 / 160.0 } else { 1.0 },
is_primary: true,
})
})
.flatten()
}
struct AndroidUiConfiguration {
night_mode: Option<bool>,
font_scale: f32,
}
fn ui_configuration() -> Option<AndroidUiConfiguration> {
const UI_MODE_NIGHT_MASK: i32 = 0x30;
const UI_MODE_NIGHT_NO: i32 = 0x10;
const UI_MODE_NIGHT_YES: i32 = 0x20;
with_jni_env(|env| {
let context = attached_context()?;
let read = (|| -> Option<AndroidUiConfiguration> {
let resources = env
.call_method(
context.as_obj(),
"getResources",
"()Landroid/content/res/Resources;",
&[],
)
.ok()?
.l()
.ok()?;
if resources.is_null() {
return None;
}
let configuration = env
.call_method(
&resources,
"getConfiguration",
"()Landroid/content/res/Configuration;",
&[],
)
.ok()?
.l()
.ok()?;
if configuration.is_null() {
return None;
}
let ui_mode = env.get_field(&configuration, "uiMode", "I").ok()?.i().ok()?;
let font_scale = env.get_field(&configuration, "fontScale", "F").ok()?.f().ok()?;
Some(AndroidUiConfiguration {
night_mode: match ui_mode & UI_MODE_NIGHT_MASK {
UI_MODE_NIGHT_YES => Some(true),
UI_MODE_NIGHT_NO => Some(false),
_ => None,
},
font_scale,
})
})();
if read.is_none() {
let _ = env.exception_clear();
}
read
})
.flatten()
}
pub fn system_theme() -> Option<bool> {
ui_configuration()?.night_mode
}
pub fn font_scale() -> Option<f32> {
let scale = ui_configuration()?.font_scale;
(scale.is_finite() && scale > 0.0).then_some(scale)
}
pub fn high_contrast() -> Option<bool> {
with_jni_env(|env| {
let context = attached_context()?;
let service = env.new_string("accessibility").ok()?;
let manager = env
.call_method(
context.as_obj(),
"getSystemService",
"(Ljava/lang/String;)Ljava/lang/Object;",
&[jni::objects::JValue::Object(&service)],
)
.ok()?
.l()
.ok()?;
if manager.is_null() {
log::warn!("[android-jni] getSystemService(\"accessibility\") returned null");
return None;
}
let answer =
env.call_method(&manager, "isHighTextContrastEnabled", "()Z", &[]).ok()?.z().ok()?;
Some(answer)
})
.flatten()
}
pub fn reduce_motion() -> Option<bool> {
with_jni_env(|env| {
let context = attached_context()?;
let resolver = env
.call_method(
context.as_obj(),
"getContentResolver",
"()Landroid/content/ContentResolver;",
&[],
)
.ok()?
.l()
.ok()?;
if resolver.is_null() {
return None;
}
let settings = env.find_class("android/provider/Settings$Global").ok()?;
let key = env.new_string("animator_duration_scale").ok()?;
let scale = env
.call_static_method(
settings,
"getFloat",
"(Landroid/content/ContentResolver;Ljava/lang/String;F)F",
&[
jni::objects::JValue::Object(&resolver),
jni::objects::JValue::Object(&key),
jni::objects::JValue::Float(1.0),
],
)
.ok()?
.f()
.ok()?;
Some(scale == 0.0)
})
.flatten()
}
pub fn system_locale() -> Option<String> {
with_jni_env(|env| {
let locale_class = env.find_class("java/util/Locale").ok()?;
let locale = env
.call_static_method(locale_class, "getDefault", "()Ljava/util/Locale;", &[])
.ok()?
.l()
.ok()?;
if locale.is_null() {
return None;
}
let tag = env
.call_method(&locale, "toLanguageTag", "()Ljava/lang/String;", &[])
.ok()?
.l()
.ok()?;
if tag.is_null() {
return None;
}
let tag: jni::objects::JString = tag.into();
env.get_string(&tag).ok().map(|value| value.into())
})
.flatten()
}
pub fn window_insets() -> Option<(u32, u32, u32, u32)> {
let scale = display_info().map(|info| info.scale).unwrap_or(1.0);
let scale = if scale.is_finite() && scale > 0.0 { scale } else { 1.0 };
let pixels = with_jni_env(|env| {
macro_rules! need {
($expr:expr, $what:literal) => {
match $expr {
Ok(value) => value,
Err(error) => {
log::warn!("[android-jni] window_insets: {} failed: {error}", $what);
let _ = env.exception_clear();
return None;
}
}
};
}
let context = attached_context()?;
let read = (|| -> Option<(i32, i32, i32, i32)> {
let service = env.new_string("window").ok()?;
let manager = need!(
env.call_method(
context.as_obj(),
"getSystemService",
"(Ljava/lang/String;)Ljava/lang/Object;",
&[jni::objects::JValue::Object(&service)],
),
"getSystemService(window)"
)
.l()
.ok()?;
if manager.is_null() {
log::warn!("[android-jni] window_insets: getSystemService(window) returned null");
return None;
}
let metrics = need!(
env.call_method(
&manager,
"getCurrentWindowMetrics",
"()Landroid/view/WindowMetrics;",
&[],
),
"getCurrentWindowMetrics"
)
.l()
.ok()?;
if metrics.is_null() {
log::warn!("[android-jni] window_insets: getCurrentWindowMetrics returned null");
return None;
}
let insets = need!(
env.call_method(&metrics, "getWindowInsets", "()Landroid/view/WindowInsets;", &[],),
"getWindowInsets"
)
.l()
.ok()?;
if insets.is_null() {
log::warn!("[android-jni] window_insets: getWindowInsets returned null");
return None;
}
let type_class = need!(
env.find_class("android/view/WindowInsets$Type"),
"find_class(WindowInsets$Type)"
);
let system_bars = need!(
env.call_static_method(&type_class, "systemBars", "()I", &[]),
"WindowInsets.Type.systemBars"
)
.i()
.ok()?;
let cutout = need!(
env.call_static_method(&type_class, "displayCutout", "()I", &[]),
"WindowInsets.Type.displayCutout"
)
.i()
.ok()?;
let edges = need!(
env.call_method(
&insets,
"getInsets",
"(I)Landroid/graphics/Insets;",
&[jni::objects::JValue::Int(system_bars | cutout)],
),
"getInsets(systemBars|displayCutout)"
)
.l()
.ok()?;
if edges.is_null() {
log::warn!("[android-jni] window_insets: getInsets returned null");
return None;
}
let top = need!(env.get_field(&edges, "top", "I"), "Insets.top").i().ok()?;
let bottom = need!(env.get_field(&edges, "bottom", "I"), "Insets.bottom").i().ok()?;
let left = need!(env.get_field(&edges, "left", "I"), "Insets.left").i().ok()?;
let right = need!(env.get_field(&edges, "right", "I"), "Insets.right").i().ok()?;
Some((top, bottom, left, right))
})();
if read.is_none() {
let _ = env.exception_clear();
}
read
})
.flatten()?;
let logical = |pixels: i32| (pixels.max(0) as f32 / scale).round() as u32;
Some((logical(pixels.0), logical(pixels.1), logical(pixels.2), logical(pixels.3)))
}
fn publish_primary_clip(
env: &mut jni::JNIEnv<'_>,
clip: &jni::objects::JObject<'_>,
expect: &str,
reader: fn(&mut jni::JNIEnv<'_>) -> Option<String>,
) -> Option<bool> {
let manager = clipboard_manager(env)?;
if let Err(error) = env.call_method(
&manager,
"setPrimaryClip",
"(Landroid/content/ClipData;)V",
&[jni::objects::JValue::Object(clip)],
) {
log::error!("[android-jni] setPrimaryClip failed: {error}");
return Some(false);
}
let written = reader(env).is_some_and(|stored| stored == expect);
if !written {
log::warn!(
"[android-jni] setPrimaryClip returned without error but the clipboard does not hold \
the value that was written (the platform refuses background clipboard writes)"
);
}
Some(written)
}
fn write_primary_clip(env: &mut jni::JNIEnv<'_>, text: &str) -> Option<bool> {
let clip = build_text_clip(env, text)?;
publish_primary_clip(env, &clip, text, read_primary_clip)
}
fn write_primary_html_clip(env: &mut jni::JNIEnv<'_>, html: &str, plain: &str) -> Option<bool> {
let clip = build_html_clip(env, html, plain)?;
publish_primary_clip(env, &clip, html, read_primary_html)
}
fn build_text_clip<'local>(
env: &mut jni::JNIEnv<'local>,
text: &str,
) -> Option<jni::objects::JObject<'local>> {
let label = env.new_string("rust-widgets").ok()?;
let value = env.new_string(text).ok()?;
let clip_data = env.find_class("android/content/ClipData").ok()?;
new_clip(
env,
&clip_data,
"newPlainText",
"(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Landroid/content/ClipData;",
&[jni::objects::JValue::Object(&label), jni::objects::JValue::Object(&value)],
)
}
fn build_html_clip<'local>(
env: &mut jni::JNIEnv<'local>,
html: &str,
plain: &str,
) -> Option<jni::objects::JObject<'local>> {
let label = env.new_string("rust-widgets").ok()?;
let text = env.new_string(plain).ok()?;
let html_text = env.new_string(html).ok()?;
let clip_data = env.find_class("android/content/ClipData").ok()?;
new_clip(
env,
&clip_data,
"newHtmlText",
"(Ljava/lang/CharSequence;Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Landroid/content/ClipData;",
&[
jni::objects::JValue::Object(&label),
jni::objects::JValue::Object(&text),
jni::objects::JValue::Object(&html_text),
],
)
}
fn new_clip<'local>(
env: &mut jni::JNIEnv<'local>,
clip_data: &jni::objects::JClass<'local>,
factory: &str,
signature: &str,
args: &[jni::objects::JValue<'_, '_>],
) -> Option<jni::objects::JObject<'local>> {
match env.call_static_method(clip_data, factory, signature, args) {
Ok(clip) => clip.l().ok().filter(|clip| !clip.is_null()),
Err(error) => {
log::error!("[android-jni] ClipData.{factory} failed: {error}");
None
}
}
}
pub struct AndroidClipboard;
impl AndroidClipboard {
pub fn new() -> Self {
Self
}
}
impl Default for AndroidClipboard {
fn default() -> Self {
Self::new()
}
}
impl crate::platform::clipboard::RichClipboardBackend for AndroidClipboard {
fn set_contents(&self, content: crate::platform::clipboard::ClipboardContent) -> bool {
use crate::platform::clipboard::ClipboardContent;
let written = match content {
ClipboardContent::Text(text) => {
with_jni_env(|env| write_primary_clip(env, &text)).flatten()
}
ClipboardContent::Html { html, plain } => {
with_jni_env(|env| write_primary_html_clip(env, &html, &plain)).flatten()
}
ClipboardContent::Custom { content_type, data } => {
with_jni_env(|env| write_primary_custom_clip(env, &content_type, &data)).flatten()
}
other => {
log::warn!(
"[android-jni] {} is not carried by this backend; the write is reported as \
refused rather than pretended",
other.content_type()
);
return false;
}
};
written.unwrap_or(false)
}
fn get_contents(&self) -> Option<crate::platform::clipboard::ClipboardContent> {
use crate::platform::clipboard::ClipboardContent;
if let Some(content_type) = clipboard_custom_type() {
if let Some(data) = clipboard_type(&content_type) {
return Some(ClipboardContent::Custom { content_type, data });
}
}
if let Some(html) = clipboard_text_of(read_primary_html) {
let plain = clipboard_text_of(read_primary_clip).unwrap_or_default();
return Some(ClipboardContent::Html { html, plain });
}
clipboard_text_of(read_primary_clip).map(ClipboardContent::Text)
}
fn has_format(&self, content_type: &str) -> bool {
match content_type {
"text/html" => clipboard_text_of(read_primary_html).is_some(),
"text/plain" => clipboard_text_of(read_primary_clip).is_some(),
other => clipboard_has_type(other),
}
}
fn set_contents_with_type(&self, content_type: &str, data: &[u8]) -> bool {
set_clipboard_type(content_type, data).unwrap_or(false)
}
fn get_contents_for_type(&self, content_type: &str) -> Option<Vec<u8>> {
match content_type {
"text/plain" => clipboard_text_of(read_primary_clip).map(String::into_bytes),
"text/html" => clipboard_text_of(read_primary_html).map(String::into_bytes),
other => clipboard_type(other),
}
}
}
fn clipboard_text_of(reader: fn(&mut jni::JNIEnv<'_>) -> Option<String>) -> Option<String> {
with_jni_env(reader).flatten()
}
#[no_mangle]
pub extern "system" fn Java_rust_1widgets_RustWidgets_nativeInit(
env: jni::JNIEnv,
_class: jni::objects::JClass,
) {
init_logging();
match env.get_java_vm() {
Ok(vm) => {
if JAVA_VM.set(vm).is_ok() {
log::info!("[android-jni] JavaVM stored, native library initialized");
} else {
log::info!("[android-jni] JavaVM already initialized (duplicate call)");
}
}
Err(error) => {
log::error!("[android-jni] failed to get JavaVM: {error}");
}
}
}
#[no_mangle]
pub extern "system" fn Java_rust_1widgets_RustWidgets_nativeAttachContext<'local>(
mut env: jni::JNIEnv<'local>,
_class: jni::objects::JClass<'local>,
context: jni::objects::JObject<'local>,
) -> jni::sys::jint {
if set_activity_context(&mut env, &context) {
1
} else {
0
}
}
#[no_mangle]
pub extern "system" fn Java_rust_1widgets_RustWidgets_nativeOpenDocument<'local>(
mut env: jni::JNIEnv<'local>,
_class: jni::objects::JClass<'local>,
mime_type: jni::objects::JString<'local>,
) -> jni::sys::jint {
let mime: String = match env.get_string(&mime_type) {
Ok(text) => text.into(),
Err(error) => {
log::error!("[android-jni] nativeOpenDocument: unreadable mime type: {error}");
return 0;
}
};
if launch_document_picker(&mut env, &mime) {
1
} else {
0
}
}
#[no_mangle]
pub extern "system" fn Java_rust_1widgets_RustWidgets_nativeIntegrationStatus(
_env: jni::JNIEnv,
_class: jni::objects::JClass,
) -> jni::sys::jint {
let mut status = 0;
if is_initialized() {
status |= 1;
}
if has_activity_context() {
status |= 2;
}
status
}
#[no_mangle]
pub extern "system" fn Java_rust_1widgets_RustWidgets_nativeMethodCount(
_env: jni::JNIEnv,
_class: jni::objects::JClass,
) -> jni::sys::jint {
NATIVE_METHOD_COUNT as jni::sys::jint
}
#[no_mangle]
pub extern "system" fn Java_rust_1widgets_RustWidgets_nativeInstallLogging(
_env: jni::JNIEnv,
_class: jni::objects::JClass,
) {
init_logging();
}
#[no_mangle]
pub extern "system" fn Java_rust_1widgets_RustWidgets_nativeWidgetSelfTest(
_env: jni::JNIEnv,
_class: jni::objects::JClass,
) -> jni::sys::jint {
widget_self_test()
}
#[cfg(not(alloc_frugal))]
fn widget_self_test() -> jni::sys::jint {
let mut passed = 0;
let window = crate::create_window("Self Test", 0, 0, 1080, 1920);
if window == 0 {
log::error!("[android-jni] widget self-test: create_window returned 0");
return passed;
}
passed |= 1;
let button = crate::create_button(window, "Self Test", 16, 16, 240, 64);
if button == 0 {
log::error!("[android-jni] widget self-test: create_button returned 0");
destroy(window);
return passed;
}
passed |= 1 << 1;
crate::set_widget_text(button, "Round Tripped");
let read_back = crate::get_widget_text(button);
if read_back == "Round Tripped" {
passed |= 1 << 2;
} else {
log::error!("[android-jni] widget self-test: text read back as {read_back:?}");
}
let (x, y, w, h) = (32, 48, 320, 80);
crate::set_widget_geometry(button, x, y, w, h);
match crate::widget_geometry(button) {
Some(rect) if rect == (x, y, w, h) => {
passed |= 1 << 3;
}
other => log::error!("[android-jni] widget self-test: geometry read back as {other:?}"),
}
crate::hide_widget(button);
let hidden = !crate::is_widget_visible(button);
crate::show_widget(button);
let shown = crate::is_widget_visible(button);
if hidden && shown {
passed |= 1 << 4;
} else {
log::error!("[android-jni] widget self-test: hidden={hidden} shown={shown}");
}
destroy(button);
destroy(window);
if !crate::widget::runtime::is_mounted(button) && !crate::widget::runtime::is_mounted(window) {
passed |= 1 << 5;
} else {
log::error!("[android-jni] widget self-test: a control survived destroy");
}
let caps = crate::platform::capabilities();
if !caps.native_menu {
passed |= 1 << 6;
} else {
log::error!("[android-jni] widget self-test: Android claims a native menu it has none of");
}
let backend = crate::platform::backend_name();
if !backend.is_empty() {
passed |= 1 << 7;
} else {
log::error!("[android-jni] widget self-test: the backend reported no name");
}
passed
}
static PATH_CHOICES: std::sync::Mutex<std::collections::VecDeque<crate::platform::PathChoice>> =
std::sync::Mutex::new(std::collections::VecDeque::new());
pub fn push_path_choice(choice: crate::platform::PathChoice) {
let mut queue = PATH_CHOICES.lock().unwrap_or_else(|error| error.into_inner());
queue.push_back(choice);
}
pub fn take_path_choice() -> Option<crate::platform::PathChoice> {
PATH_CHOICES.lock().unwrap_or_else(|error| error.into_inner()).pop_front()
}
pub fn picker_ready() -> bool {
JAVA_VM.get().is_some() && attached_context().is_some()
}
#[no_mangle]
#[cfg(not(alloc_frugal))]
pub extern "system" fn Java_rust_1widgets_RustWidgets_nativeInjectPathChoice(
mut env: jni::JNIEnv,
_class: jni::objects::JClass,
path: jni::objects::JString,
cancelled: jni::sys::jint,
) -> jni::sys::jint {
if cancelled != 0 {
push_path_choice(crate::platform::PathChoice::Cancelled);
return 1;
}
let value: String = match env.get_string(&path) {
Ok(value) => value.into(),
Err(error) => {
log::warn!("[android-jni] nativeInjectPathChoice: get_string failed: {error}");
push_path_choice(crate::platform::PathChoice::Failed);
return 0;
}
};
if value.is_empty() {
push_path_choice(crate::platform::PathChoice::Failed);
} else {
push_path_choice(crate::platform::PathChoice::Chosen(std::path::PathBuf::from(value)));
}
1
}
#[no_mangle]
pub extern "system" fn Java_rust_1widgets_RustWidgets_nativeOnConfigurationChanged(
_env: jni::JNIEnv,
_class: jni::objects::JClass,
rotation: jni::sys::jint,
axis: jni::sys::jint,
) -> jni::sys::jint {
let accepted = if (-1..=3).contains(&rotation) && rotation >= 0 {
crate::platform::lifecycle::set_rotation(rotation)
} else {
crate::platform::lifecycle::set_axis_orientation(axis)
};
i32::from(accepted)
}
#[no_mangle]
pub extern "system" fn Java_rust_1widgets_RustWidgets_nativeOnLifecycleChanged(
_env: jni::JNIEnv,
_class: jni::objects::JClass,
state: jni::sys::jint,
) -> jni::sys::jint {
let phase = match state {
0 => crate::platform::MobileLifecycleState::Active,
1 => crate::platform::MobileLifecycleState::Inactive,
2 => crate::platform::MobileLifecycleState::Background,
_ => return 0,
};
i32::from(crate::platform::lifecycle::set_state(phase))
}
#[no_mangle]
pub extern "system" fn Java_rust_1widgets_RustWidgets_nativePlatformProbe(
_env: jni::JNIEnv,
_class: jni::objects::JClass,
) -> jni::sys::jint {
platform_services_probe()
}
#[cfg(not(alloc_frugal))]
fn platform_services_probe() -> jni::sys::jint {
let diagnostics = crate::platform::diagnostics::run();
log::info!("[android-jni] platform diagnostics: {}", diagnostics.to_json());
for entry in &diagnostics.entries {
match entry.status {
crate::platform::diagnostics::DiagnosticStatus::Pass => {}
crate::platform::diagnostics::DiagnosticStatus::Skip => {
log::info!(
"[android-jni] platform probe: bit {} ({}) skipped: {}",
entry.bit,
entry.name,
entry.detail
);
}
crate::platform::diagnostics::DiagnosticStatus::Fail => {
log::error!(
"[android-jni] platform probe: bit {} ({}) failed: {}",
entry.bit,
entry.name,
entry.detail
);
}
}
}
diagnostics.mask() as jni::sys::jint
}
#[cfg(alloc_frugal)]
fn platform_services_probe() -> jni::sys::jint {
0
}
#[no_mangle]
pub extern "system" fn Java_rust_1widgets_RustWidgets_nativePlatformProbeFailures(
_env: jni::JNIEnv,
_class: jni::objects::JClass,
) -> jni::sys::jint {
platform_services_failures()
}
#[cfg(not(alloc_frugal))]
fn platform_services_failures() -> jni::sys::jint {
let diagnostics = crate::platform::diagnostics::run();
diagnostics
.entries
.iter()
.filter(|entry| entry.status == crate::platform::diagnostics::DiagnosticStatus::Fail)
.fold(0i32, |mask, entry| mask | (1i32 << entry.bit)) as jni::sys::jint
}
#[cfg(alloc_frugal)]
fn platform_services_failures() -> jni::sys::jint {
0
}
#[cfg(not(alloc_frugal))]
fn destroy(widget_id: crate::core::ObjectId) {
let _ = crate::control_backend::get_control_backend().destroy_widget(widget_id);
}
#[cfg(not(alloc_frugal))]
fn first_window() -> Option<crate::core::ObjectId> {
let found = core::cell::Cell::new(None);
crate::widget::runtime::for_each_mounted_widget(|id, widget| {
if found.get().is_none() && widget.kind() == crate::widget::WidgetKind::Window {
found.set(Some(id));
}
});
found.get()
}
#[no_mangle]
#[cfg(not(alloc_frugal))]
pub extern "system" fn Java_rust_1widgets_RustWidgets_nativeDispatchPointerEvent(
_env: jni::JNIEnv,
_class: jni::objects::JClass,
code: jni::sys::jint,
x: jni::sys::jint,
y: jni::sys::jint,
button: jni::sys::jint,
) -> jni::sys::jint {
let Some(window) = first_window() else {
log::warn!("[android-jni] pointer event before a widget window exists; ignored");
return 0;
};
let scale = crate::platform::get_platform().dpi_scale_factor();
let scale = if scale.is_finite() && scale > 0.0 { scale } else { 1.0 };
let logical_x = (x as f32 / scale).round() as i32;
let logical_y = (y as f32 / scale).round() as i32;
let point = crate::core::Point::new(logical_x, logical_y);
let event = match code {
1 => crate::event::Event::mouse_press(logical_x, logical_y, button as u32),
2 => crate::event::Event::mouse_release(logical_x, logical_y, button as u32),
3 => crate::event::Event::mouse_move(logical_x, logical_y),
4 => crate::event::Event::MouseEnter { pos: point },
5 => crate::event::Event::MouseLeave { pos: point },
_ => {
log::warn!("[android-jni] unknown pointer event code {code}");
return 0;
}
};
if crate::widget::runtime::dispatch_pointer_event(window, &event, point) {
1
} else {
0
}
}
#[no_mangle]
#[cfg(not(alloc_frugal))]
pub extern "system" fn Java_rust_1widgets_RustWidgets_nativeDispatchText(
mut env: jni::JNIEnv,
_class: jni::objects::JClass,
text: jni::objects::JString,
) -> jni::sys::jint {
let value: String = match env.get_string(&text) {
Ok(value) => value.into(),
Err(error) => {
log::warn!("[android-jni] nativeDispatchText: get_string failed: {error}");
return 0;
}
};
let Some(focused) = crate::widget::runtime::focused_widget() else {
return 0;
};
if crate::widget::runtime::dispatch_event(focused, &crate::event::Event::text_input(value)) {
1
} else {
0
}
}
#[no_mangle]
#[cfg(not(alloc_frugal))]
pub extern "system" fn Java_rust_1widgets_RustWidgets_nativeDispatchKeyEvent(
_env: jni::JNIEnv,
_class: jni::objects::JClass,
key_code: jni::sys::jint,
modifiers: jni::sys::jint,
pressed: jni::sys::jint,
) -> jni::sys::jint {
let Some(focused) = crate::widget::runtime::focused_widget() else {
return 0;
};
let event = if pressed != 0 {
crate::event::Event::key_press(key_code as u32, modifiers as u32)
} else {
crate::event::Event::key_release(key_code as u32, modifiers as u32)
};
if crate::widget::runtime::dispatch_event(focused, &event) {
1
} else {
0
}
}
#[cfg(alloc_frugal)]
fn widget_self_test() -> jni::sys::jint {
0
}
#[no_mangle]
pub extern "system" fn Java_rust_1widgets_RustWidgets_nativeDetachContext(
_env: jni::JNIEnv,
_class: jni::objects::JClass,
) {
let mut slot = activity_context_slot().lock().unwrap_or_else(|poisoned| poisoned.into_inner());
*slot = None;
log::info!("[android-jni] Activity Context detached");
}
#[no_mangle]
pub extern "system" fn Java_rust_1widgets_RustWidgets_nativeNotifyResize(
_env: jni::JNIEnv,
_class: jni::objects::JClass,
window_id: jni::sys::jlong,
width: jni::sys::jint,
height: jni::sys::jint,
) -> jni::sys::jint {
if width <= 0 || height <= 0 {
log::warn!("[android-jni] nativeNotifyResize: ignoring non-positive size {width}x{height}");
return 0;
}
if window_id <= 0 {
log::warn!("[android-jni] nativeNotifyResize: ignoring non-positive window id {window_id}");
return 0;
}
if accept_host_resize(window_id, width, height) {
1
} else {
log::warn!(
"[android-jni] nativeNotifyResize: {window_id} addresses no live window; \
the resize was refused"
);
0
}
}
fn accept_host_resize(window_id: i64, width: i32, height: i32) -> bool {
debug_assert!(window_id > 0, "a non-positive window id must be filtered by the caller");
debug_assert!(width > 0 && height > 0, "a non-positive size must be filtered by the caller");
crate::queue_resize_trigger(window_id as u64, width as u32, height as u32)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bridge_is_uninitialized_without_a_vm() {
assert!(!is_initialized());
assert!(!has_activity_context());
assert!(!native_view_creation_ready());
assert!(!launch_file_dialog("*/*"));
assert!(with_jni_env(|_| ()).is_none());
}
#[test]
fn integration_status_is_honest_before_init() {
let status = android_integration_ready();
assert!(!status.jni_initialized);
assert!(!status.context_attached);
assert!(!status.ready);
assert_eq!(status.native_methods_count, NATIVE_METHOD_COUNT);
}
#[test]
fn reported_entry_point_count_matches_the_exports() {
const PREFIX: &str = "pub extern \"system\" fn Java_rust_1widgets_RustWidgets_";
let source = include_str!("android_jni.rs");
let exported = source.matches(PREFIX).count();
assert_eq!(
exported as u32, NATIVE_METHOD_COUNT,
"NATIVE_METHOD_COUNT must be updated when a JNI entry point is added or removed",
);
}
#[test]
fn logcat_priorities_follow_the_android_scale() {
assert_eq!(logcat_priority(log::Level::Error), 6);
assert_eq!(logcat_priority(log::Level::Warn), 5);
assert_eq!(logcat_priority(log::Level::Info), 4);
assert_eq!(logcat_priority(log::Level::Debug), 3);
assert_eq!(logcat_priority(log::Level::Trace), 2);
}
#[test]
fn a_custom_payload_round_trips_through_the_text_channel() {
let data: Vec<u8> = (0..=255).collect();
let text = bytes_to_latin1(&data);
assert_eq!(text.chars().count(), data.len(), "one character per byte");
assert_eq!(latin1_to_bytes(&text), Some(data));
}
#[test]
fn text_outside_the_byte_range_is_not_decoded_as_a_payload() {
assert_eq!(latin1_to_bytes("你好"), None);
assert_eq!(latin1_to_bytes("abc"), Some(b"abc".to_vec()));
}
#[test]
fn a_resize_for_an_unknown_window_is_refused() {
assert!(
!accept_host_resize(0x0BAD_1DEA, 800, 600),
"an id this library never handed out must be refused"
);
}
#[test]
fn a_refused_resize_records_no_size() {
assert!(!accept_host_resize(0x0BAD_1DEA, 800, 600));
assert_eq!(
crate::window_client_size(0x0BAD_1DEA),
None,
"a refused resize must leave no size behind"
);
}
#[test]
fn the_entry_point_rejects_non_positive_inputs() {
let entry_point_accepts =
|window_id: i64, width: i32, height: i32| width > 0 && height > 0 && window_id > 0;
assert!(!entry_point_accepts(1, -1, 600), "a negative width must be refused");
assert!(!entry_point_accepts(1, 800, -5), "a negative height must be refused");
assert!(!entry_point_accepts(1, 0, 600), "a zero width must be refused");
assert!(!entry_point_accepts(1, 800, 0), "a zero height must be refused");
assert!(!entry_point_accepts(-1, 800, 600), "a negative id must be refused");
assert!(entry_point_accepts(1, 800, 600), "a well-formed report must be accepted");
}
}