use crate::core::ObjectId;
use std::collections::HashMap;
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);
});
}
struct LogcatLogger;
static LOGCAT_LOGGER: LogcatLogger = LogcatLogger;
fn logcat_priority(level: log::Level) -> i32 {
match level {
log::Level::Trace => 2,
log::Level::Debug => 3,
log::Level::Info => 4,
log::Level::Warn => 5,
log::Level::Error => 6,
}
}
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(format!("{}", record.args()))
.unwrap_or_else(|_| std::ffi::CString::new("(message contained NUL)").unwrap());
unsafe {
android_log_write(logcat_priority(record.level()), tag.as_ptr(), message.as_ptr());
}
}
fn flush(&self) {}
}
extern "C" {
#[link_name = "__android_log_write"]
fn android_log_write(
prio: i32,
tag: *const std::os::raw::c_char,
text: *const std::os::raw::c_char,
) -> i32;
}
static JAVA_VM: OnceLock<jni::JavaVM> = OnceLock::new();
static VIEW_REGISTRY: OnceLock<Mutex<HashMap<ObjectId, jni::objects::GlobalRef>>> = OnceLock::new();
fn view_registry() -> &'static Mutex<HashMap<ObjectId, jni::objects::GlobalRef>> {
VIEW_REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
}
static ACTIVITY_CONTEXT: OnceLock<Mutex<Option<jni::objects::GlobalRef>>> = OnceLock::new();
fn activity_context_slot() -> &'static Mutex<Option<jni::objects::GlobalRef>> {
ACTIVITY_CONTEXT.get_or_init(|| Mutex::new(None))
}
pub fn set_activity_context(
env: &mut jni::JNIEnv<'_>,
context: &jni::objects::JObject<'_>,
) -> bool {
let global = match env.new_global_ref(context) {
Ok(g) => g,
Err(e) => {
log::error!("[android-jni] set_activity_context: failed to create GlobalRef: {e}");
return false;
}
};
let mut slot = activity_context_slot().lock().expect("activity context lock poisoned");
*slot = Some(global);
log::info!("[android-jni] activity Context stored");
true
}
pub fn has_activity_context() -> bool {
activity_context_slot().lock().map(|slot| slot.is_some()).unwrap_or(false)
}
pub fn native_view_creation_ready() -> bool {
is_initialized() && has_activity_context()
}
static NEXT_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
fn allocate_id() -> ObjectId {
NEXT_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
}
pub fn is_initialized() -> bool {
JAVA_VM.get().is_some()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IntegrationStatus {
pub jni_initialized: bool,
pub native_methods_count: u32,
pub ready: bool,
}
pub fn android_integration_ready() -> IntegrationStatus {
let jni_initialized = JAVA_VM.get().is_some();
let native_methods_count = 13;
IntegrationStatus {
jni_initialized,
native_methods_count,
ready: jni_initialized && native_methods_count > 0,
}
}
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 guard = vm.attach_current_thread().ok()?;
Some(f(&mut guard))
}
pub fn register_view(id: ObjectId, global_ref: jni::objects::GlobalRef) {
view_registry().lock().expect("view registry lock poisoned").insert(id, global_ref);
}
pub fn lookup_view(id: ObjectId) -> Option<jni::objects::GlobalRef> {
view_registry().lock().expect("view registry lock poisoned").get(&id).cloned()
}
pub fn unregister_view(id: ObjectId) {
view_registry().lock().expect("view registry lock poisoned").remove(&id);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AndroidViewClass {
Button,
TextView,
EditText,
CheckBox,
RadioButton,
SeekBar,
ProgressBar,
Spinner,
ListView,
ScrollView,
NumberPicker,
FrameLayout,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AndroidLogicalKind {
Window,
Button,
CheckBox,
LineEdit,
Label,
RadioButton,
Slider,
ProgressBar,
ComboBox,
ListBox,
Panel,
MenuBar,
Menu,
MenuItem,
ToolBar,
StatusBar,
MessageBox,
FileDialog,
ColorDialog,
FontDialog,
SpinBox,
ListView,
ScrollArea,
}
pub fn view_class_for(kind: AndroidLogicalKind) -> Option<AndroidViewClass> {
use AndroidLogicalKind::*;
Some(match kind {
Button => AndroidViewClass::Button,
Label | StatusBar => AndroidViewClass::TextView,
LineEdit => AndroidViewClass::EditText,
CheckBox => AndroidViewClass::CheckBox,
RadioButton => AndroidViewClass::RadioButton,
Slider => AndroidViewClass::SeekBar,
ProgressBar => AndroidViewClass::ProgressBar,
ComboBox => AndroidViewClass::Spinner,
ListBox | ListView => AndroidViewClass::ListView,
ScrollArea => AndroidViewClass::ScrollView,
SpinBox => AndroidViewClass::NumberPicker,
Panel | Window => AndroidViewClass::FrameLayout,
MenuBar | Menu | MenuItem | ToolBar | MessageBox | FileDialog | ColorDialog
| FontDialog => return None,
})
}
impl AndroidViewClass {
fn jni_class_path(self) -> &'static str {
match self {
AndroidViewClass::Button => "android/widget/Button",
AndroidViewClass::TextView => "android/widget/TextView",
AndroidViewClass::EditText => "android/widget/EditText",
AndroidViewClass::CheckBox => "android/widget/CheckBox",
AndroidViewClass::RadioButton => "android/widget/RadioButton",
AndroidViewClass::SeekBar => "android/widget/SeekBar",
AndroidViewClass::ProgressBar => "android/widget/ProgressBar",
AndroidViewClass::Spinner => "android/widget/Spinner",
AndroidViewClass::ListView => "android/widget/ListView",
AndroidViewClass::ScrollView => "android/widget/ScrollView",
AndroidViewClass::NumberPicker => "android/widget/NumberPicker",
AndroidViewClass::FrameLayout => "android/widget/FrameLayout",
}
}
fn supports_text(self) -> bool {
matches!(
self,
AndroidViewClass::Button
| AndroidViewClass::TextView
| AndroidViewClass::EditText
| AndroidViewClass::CheckBox
| AndroidViewClass::RadioButton
)
}
}
pub fn create_native_view(
class: AndroidViewClass,
text: &str,
x: i32,
y: i32,
width: u32,
height: u32,
) -> Option<ObjectId> {
if !is_initialized() {
return None;
}
with_jni_env(|env| create_view_with_env(env, class, text, x, y, width, height)).flatten()
}
fn create_view_with_env(
env: &mut jni::JNIEnv<'_>,
class: AndroidViewClass,
text: &str,
x: i32,
y: i32,
width: u32,
height: u32,
) -> Option<ObjectId> {
let context = {
let slot = activity_context_slot().lock().expect("activity context lock poisoned");
slot.as_ref()?.clone()
};
let context_obj = context.as_obj();
let class_path = class.jni_class_path();
let view_class = env.find_class(class_path).ok()?;
let view = env
.new_object(
&view_class,
"(Landroid/content/Context;)V",
&[jni::objects::JValue::Object(context_obj)],
)
.ok()?;
if class.supports_text() && !text.is_empty() {
if let Ok(java_text) = env.new_string(text) {
if let Err(e) = env.call_method(
&view,
"setText",
"(Ljava/lang/CharSequence;)V",
&[jni::objects::JValue::Object(&java_text)],
) {
log::warn!("[android-jni] create_native_view({class_path}): setText failed: {e}");
}
}
}
apply_view_layout(env, &view, x, y, width as i32, height as i32);
let id = allocate_id();
let global = env.new_global_ref(&view).ok()?;
register_view(id, global);
log::info!("[android-jni] create_native_view({class_path}) -> id={id}");
Some(id)
}
pub fn destroy_native_view(id: ObjectId) -> bool {
if lookup_view(id).is_none() {
return false;
}
unregister_view(id);
true
}
pub fn set_native_view_text(id: ObjectId, text: &str) -> bool {
with_jni_env(|env| {
let global = match lookup_view(id) {
Some(g) => g,
None => return false,
};
let java_text = match env.new_string(text) {
Ok(t) => t,
Err(e) => {
log::error!("[android-jni] set_native_view_text({id}): new_string failed: {e}");
return false;
}
};
env.call_method(
global.as_obj(),
"setText",
"(Ljava/lang/CharSequence;)V",
&[jni::objects::JValue::Object(&java_text)],
)
.is_ok()
})
.unwrap_or(false)
}
pub fn set_native_view_bounds(id: ObjectId, x: i32, y: i32, width: u32, height: u32) -> bool {
with_jni_env(|env| {
let global = match lookup_view(id) {
Some(g) => g,
None => return false,
};
apply_view_layout(env, global.as_obj(), x, y, width as i32, height as i32);
true
})
.unwrap_or(false)
}
pub fn set_native_view_visibility(id: ObjectId, visible: bool) -> bool {
with_jni_env(|env| {
let global = match lookup_view(id) {
Some(g) => g,
None => return false,
};
let visibility = if visible { 0 } else { 8 };
env.call_method(
global.as_obj(),
"setVisibility",
"(I)V",
&[jni::objects::JValue::Int(visibility)],
)
.is_ok()
})
.unwrap_or(false)
}
pub fn set_native_view_enabled(id: ObjectId, enabled: bool) -> bool {
with_jni_env(|env| {
let global = match lookup_view(id) {
Some(g) => g,
None => return false,
};
env.call_method(
global.as_obj(),
"setEnabled",
"(Z)V",
&[jni::objects::JValue::Bool(if enabled {
jni::sys::JNI_TRUE
} else {
jni::sys::JNI_FALSE
})],
)
.is_ok()
})
.unwrap_or(false)
}
pub fn append_spinner_item(id: ObjectId, text: &str, set_selection: bool) -> bool {
with_jni_env(|env| {
let global = match lookup_view(id) {
Some(g) => g,
None => return false,
};
let spinner = global.as_obj();
let java_text = match env.new_string(text) {
Ok(t) => t,
Err(e) => {
log::error!("[android-jni] append_spinner_item({id}): new_string failed: {e}");
return false;
}
};
let adapter = match env.call_method(spinner, "getAdapter", "()Landroid/widget/SpinnerAdapter;", &[])
{
Ok(v) => v.l().ok().filter(|o| !o.is_null()),
Err(e) => {
log::error!("[android-jni] append_spinner_item({id}): getAdapter failed: {e}");
return false;
}
};
let added = match adapter {
Some(adapter_obj) => env
.call_method(
&adapter_obj,
"add",
"(Ljava/lang/Object;)V",
&[jni::objects::JValue::Object(&java_text)],
)
.is_ok(),
None => {
let array_adapter_class = match env.find_class("android/widget/ArrayAdapter") {
Ok(c) => c,
Err(e) => {
log::error!("[android-jni] append_spinner_item({id}): ArrayAdapter class missing: {e}");
return false;
}
};
let context = match spinner_context(env, spinner) {
Some(c) => c,
None => return false,
};
let layout = match env.get_static_field(
"android/R$layout",
"simple_spinner_item",
"I",
) {
Ok(v) => v.i().unwrap_or(0),
Err(_) => 0,
};
let adapter = match env.new_object(
&array_adapter_class,
"(Landroid/content/Context;I)V",
&[
jni::objects::JValue::Object(&context),
jni::objects::JValue::Int(layout),
],
) {
Ok(a) => a,
Err(e) => {
log::error!("[android-jni] append_spinner_item({id}): ArrayAdapter creation failed: {e}");
return false;
}
};
if env
.call_method(
&adapter,
"add",
"(Ljava/lang/Object;)V",
&[jni::objects::JValue::Object(&java_text)],
)
.is_err()
{
log::error!("[android-jni] append_spinner_item({id}): adapter.add failed");
return false;
}
env.call_method(
spinner,
"setAdapter",
"(Landroid/widget/SpinnerAdapter;)V",
&[jni::objects::JValue::Object(&adapter)],
)
.is_ok()
}
};
if !added {
return false;
}
if set_selection {
let _ = env.call_method(
spinner,
"setSelection",
"(I)V",
&[jni::objects::JValue::Int(0)],
);
}
true
})
.unwrap_or(false)
}
fn spinner_context<'local>(
env: &mut jni::JNIEnv<'local>,
view: &jni::objects::JObject<'local>,
) -> Option<jni::objects::JObject<'local>> {
env.call_method(view, "getContext", "()Landroid/content/Context;", &[])
.ok()
.and_then(|v| v.l().ok())
.filter(|o| !o.is_null())
}
pub fn append_list_item(id: ObjectId, texts: &[&str]) -> bool {
with_jni_env(|env| {
let global = match lookup_view(id) {
Some(g) => g,
None => return false,
};
let list_view = global.as_obj();
let existing = env
.call_method(list_view, "getAdapter", "()Landroid/widget/ListAdapter;", &[])
.ok()
.and_then(|v| v.l().ok())
.filter(|o| !o.is_null());
if let Some(adapter) = existing {
for text in texts {
let java_text = match env.new_string(*text) {
Ok(t) => t,
Err(e) => {
log::error!("[android-jni] append_list_item({id}): new_string failed: {e}");
return false;
}
};
if let Err(e) = env.call_method(
&adapter,
"add",
"(Ljava/lang/Object;)V",
&[jni::objects::JValue::Object(&java_text)],
) {
log::error!("[android-jni] append_list_item({id}): adapter.add failed: {e}");
return false;
}
}
return true;
}
let array_adapter_class = match env.find_class("android/widget/ArrayAdapter") {
Ok(c) => c,
Err(e) => {
log::error!(
"[android-jni] append_list_item({id}): ArrayAdapter class missing: {e}"
);
return false;
}
};
let context = match spinner_context(env, list_view) {
Some(c) => c,
None => return false,
};
let layout = env
.get_static_field("android/R$layout", "simple_list_item_1", "I")
.ok()
.and_then(|v| v.i().ok())
.unwrap_or(0);
let adapter = match env.new_object(
&array_adapter_class,
"(Landroid/content/Context;I)V",
&[jni::objects::JValue::Object(&context), jni::objects::JValue::Int(layout)],
) {
Ok(a) => a,
Err(e) => {
log::error!(
"[android-jni] append_list_item({id}): ArrayAdapter creation failed: {e}"
);
return false;
}
};
for text in texts {
let java_text = match env.new_string(*text) {
Ok(t) => t,
Err(e) => {
log::error!("[android-jni] append_list_item({id}): new_string failed: {e}");
return false;
}
};
if let Err(e) = env.call_method(
&adapter,
"add",
"(Ljava/lang/Object;)V",
&[jni::objects::JValue::Object(&java_text)],
) {
log::error!("[android-jni] append_list_item({id}): adapter.add failed: {e}");
return false;
}
}
if let Err(e) = env.call_method(
list_view,
"setAdapter",
"(Landroid/widget/ListAdapter;)V",
&[jni::objects::JValue::Object(&adapter)],
) {
log::error!("[android-jni] append_list_item({id}): setAdapter failed: {e}");
return false;
}
true
})
.unwrap_or(false)
}
#[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(e) => {
log::error!("[android-jni] failed to get JavaVM: {e}");
}
}
}
#[no_mangle]
pub extern "system" fn Java_rust_1widgets_RustWidgets_nativeCreateButton<'local>(
mut env: jni::JNIEnv<'local>,
_class: jni::objects::JClass<'local>,
context: jni::objects::JObject<'local>,
text: jni::objects::JString<'local>,
x: jni::sys::jint,
y: jni::sys::jint,
w: jni::sys::jint,
h: jni::sys::jint,
) -> jni::sys::jlong {
let text_str: String = match env.get_string(&text) {
Ok(s) => s.into(),
Err(e) => {
log::error!("[android-jni] nativeCreateButton: failed to get text string: {e}");
return 0;
}
};
log::info!("[android-jni] nativeCreateButton: text={text_str}, pos=({x},{y}), size=({w},{h})");
let button_class = match env.find_class("android/widget/Button") {
Ok(c) => c,
Err(e) => {
log::error!("[android-jni] nativeCreateButton: cannot find android/widget/Button: {e}");
return 0;
}
};
let button = match env.new_object(
&button_class,
"(Landroid/content/Context;)V",
&[jni::objects::JValue::Object(&context)],
) {
Ok(b) => b,
Err(e) => {
log::error!("[android-jni] nativeCreateButton: failed to create Button: {e}");
return 0;
}
};
if let Err(e) = env.call_method(
&button,
"setText",
"(Ljava/lang/CharSequence;)V",
&[jni::objects::JValue::Object(&text)],
) {
log::error!("[android-jni] nativeCreateButton: setText failed: {e}");
}
apply_view_layout(&mut env, &button, x, y, w, h);
let id = allocate_id();
match env.new_global_ref(&button) {
Ok(global_ref) => {
register_view(id, global_ref);
}
Err(e) => {
log::error!("[android-jni] nativeCreateButton: failed to create global ref: {e}");
return 0;
}
}
id as jni::sys::jlong
}
#[no_mangle]
pub extern "system" fn Java_rust_1widgets_RustWidgets_nativeCreateTextView<'local>(
mut env: jni::JNIEnv<'local>,
_class: jni::objects::JClass<'local>,
context: jni::objects::JObject<'local>,
text: jni::objects::JString<'local>,
x: jni::sys::jint,
y: jni::sys::jint,
w: jni::sys::jint,
h: jni::sys::jint,
) -> jni::sys::jlong {
let text_str: String = match env.get_string(&text) {
Ok(s) => s.into(),
Err(e) => {
log::error!("[android-jni] nativeCreateTextView: failed to get text string: {e}");
return 0;
}
};
log::info!(
"[android-jni] nativeCreateTextView: text={text_str}, pos=({x},{y}), size=({w},{h})"
);
let text_view_class = match env.find_class("android/widget/TextView") {
Ok(c) => c,
Err(e) => {
log::error!(
"[android-jni] nativeCreateTextView: cannot find android/widget/TextView: {e}"
);
return 0;
}
};
let text_view = match env.new_object(
&text_view_class,
"(Landroid/content/Context;)V",
&[jni::objects::JValue::Object(&context)],
) {
Ok(tv) => tv,
Err(e) => {
log::error!("[android-jni] nativeCreateTextView: failed to create TextView: {e}");
return 0;
}
};
if let Err(e) = env.call_method(
&text_view,
"setText",
"(Ljava/lang/CharSequence;)V",
&[jni::objects::JValue::Object(&text)],
) {
log::error!("[android-jni] nativeCreateTextView: setText failed: {e}");
}
apply_view_layout(&mut env, &text_view, x, y, w, h);
let id = allocate_id();
match env.new_global_ref(&text_view) {
Ok(global_ref) => {
register_view(id, global_ref);
}
Err(e) => {
log::error!("[android-jni] nativeCreateTextView: failed to create global ref: {e}");
return 0;
}
}
id as jni::sys::jlong
}
#[no_mangle]
pub extern "system" fn Java_rust_1widgets_RustWidgets_nativeCreateEditText<'local>(
mut env: jni::JNIEnv<'local>,
_class: jni::objects::JClass<'local>,
context: jni::objects::JObject<'local>,
text: jni::objects::JString<'local>,
x: jni::sys::jint,
y: jni::sys::jint,
w: jni::sys::jint,
h: jni::sys::jint,
) -> jni::sys::jlong {
let text_str: String = match env.get_string(&text) {
Ok(s) => s.into(),
Err(e) => {
log::error!("[android-jni] nativeCreateEditText: failed to get text string: {e}");
return 0;
}
};
log::info!(
"[android-jni] nativeCreateEditText: text={text_str}, pos=({x},{y}), size=({w},{h})"
);
let edit_text_class = match env.find_class("android/widget/EditText") {
Ok(c) => c,
Err(e) => {
log::error!(
"[android-jni] nativeCreateEditText: cannot find android/widget/EditText: {e}"
);
return 0;
}
};
let edit_text = match env.new_object(
&edit_text_class,
"(Landroid/content/Context;)V",
&[jni::objects::JValue::Object(&context)],
) {
Ok(et) => et,
Err(e) => {
log::error!("[android-jni] nativeCreateEditText: failed to create EditText: {e}");
return 0;
}
};
if let Err(e) = env.call_method(
&edit_text,
"setText",
"(Ljava/lang/CharSequence;)V",
&[jni::objects::JValue::Object(&text)],
) {
log::error!("[android-jni] nativeCreateEditText: setText failed: {e}");
}
apply_view_layout(&mut env, &edit_text, x, y, w, h);
let id = allocate_id();
match env.new_global_ref(&edit_text) {
Ok(global_ref) => {
register_view(id, global_ref);
}
Err(e) => {
log::error!("[android-jni] nativeCreateEditText: failed to create global ref: {e}");
return 0;
}
}
id as jni::sys::jlong
}
#[no_mangle]
pub extern "system" fn Java_rust_1widgets_RustWidgets_nativeCreateCheckBox<'local>(
mut env: jni::JNIEnv<'local>,
_class: jni::objects::JClass<'local>,
context: jni::objects::JObject<'local>,
text: jni::objects::JString<'local>,
x: jni::sys::jint,
y: jni::sys::jint,
w: jni::sys::jint,
h: jni::sys::jint,
) -> jni::sys::jlong {
let text_str: String = match env.get_string(&text) {
Ok(s) => s.into(),
Err(e) => {
log::error!("[android-jni] nativeCreateCheckBox: failed to get text string: {e}");
return 0;
}
};
log::info!(
"[android-jni] nativeCreateCheckBox: text={text_str}, pos=({x},{y}), size=({w},{h})"
);
let check_box_class = match env.find_class("android/widget/CheckBox") {
Ok(c) => c,
Err(e) => {
log::error!(
"[android-jni] nativeCreateCheckBox: cannot find android/widget/CheckBox: {e}"
);
return 0;
}
};
let check_box = match env.new_object(
&check_box_class,
"(Landroid/content/Context;)V",
&[jni::objects::JValue::Object(&context)],
) {
Ok(cb) => cb,
Err(e) => {
log::error!("[android-jni] nativeCreateCheckBox: failed to create CheckBox: {e}");
return 0;
}
};
if let Err(e) = env.call_method(
&check_box,
"setText",
"(Ljava/lang/CharSequence;)V",
&[jni::objects::JValue::Object(&text)],
) {
log::error!("[android-jni] nativeCreateCheckBox: setText failed: {e}");
}
apply_view_layout(&mut env, &check_box, x, y, w, h);
let id = allocate_id();
match env.new_global_ref(&check_box) {
Ok(global_ref) => {
register_view(id, global_ref);
}
Err(e) => {
log::error!("[android-jni] nativeCreateCheckBox: failed to create global ref: {e}");
return 0;
}
}
id as jni::sys::jlong
}
#[no_mangle]
pub extern "system" fn Java_rust_1widgets_RustWidgets_nativeCreateRadioButton<'local>(
mut env: jni::JNIEnv<'local>,
_class: jni::objects::JClass<'local>,
context: jni::objects::JObject<'local>,
text: jni::objects::JString<'local>,
x: jni::sys::jint,
y: jni::sys::jint,
w: jni::sys::jint,
h: jni::sys::jint,
) -> jni::sys::jlong {
let text_str: String = match env.get_string(&text) {
Ok(s) => s.into(),
Err(e) => {
log::error!("[android-jni] nativeCreateRadioButton: failed to get text string: {e}");
return 0;
}
};
log::info!(
"[android-jni] nativeCreateRadioButton: text={text_str}, pos=({x},{y}), size=({w},{h})"
);
let radio_button_class = match env.find_class("android/widget/RadioButton") {
Ok(c) => c,
Err(e) => {
log::error!(
"[android-jni] nativeCreateRadioButton: cannot find android/widget/RadioButton: {e}"
);
return 0;
}
};
let radio_button = match env.new_object(
&radio_button_class,
"(Landroid/content/Context;)V",
&[jni::objects::JValue::Object(&context)],
) {
Ok(rb) => rb,
Err(e) => {
log::error!("[android-jni] nativeCreateRadioButton: failed to create RadioButton: {e}");
return 0;
}
};
if let Err(e) = env.call_method(
&radio_button,
"setText",
"(Ljava/lang/CharSequence;)V",
&[jni::objects::JValue::Object(&text)],
) {
log::error!("[android-jni] nativeCreateRadioButton: setText failed: {e}");
}
apply_view_layout(&mut env, &radio_button, x, y, w, h);
let id = allocate_id();
match env.new_global_ref(&radio_button) {
Ok(global_ref) => {
register_view(id, global_ref);
}
Err(e) => {
log::error!("[android-jni] nativeCreateRadioButton: failed to create global ref: {e}");
return 0;
}
}
id as jni::sys::jlong
}
#[no_mangle]
pub extern "system" fn Java_rust_1widgets_RustWidgets_nativeCreateProgressBar<'local>(
mut env: jni::JNIEnv<'local>,
_class: jni::objects::JClass<'local>,
context: jni::objects::JObject<'local>,
x: jni::sys::jint,
y: jni::sys::jint,
w: jni::sys::jint,
h: jni::sys::jint,
) -> jni::sys::jlong {
log::info!("[android-jni] nativeCreateProgressBar: pos=({x},{y}), size=({w},{h})");
let progress_bar_class = match env.find_class("android/widget/ProgressBar") {
Ok(c) => c,
Err(e) => {
log::error!(
"[android-jni] nativeCreateProgressBar: cannot find android/widget/ProgressBar: {e}"
);
return 0;
}
};
let progress_bar = match env.new_object(
&progress_bar_class,
"(Landroid/content/Context;)V",
&[jni::objects::JValue::Object(&context)],
) {
Ok(pb) => pb,
Err(e) => {
log::error!("[android-jni] nativeCreateProgressBar: failed to create ProgressBar: {e}");
return 0;
}
};
apply_view_layout(&mut env, &progress_bar, x, y, w, h);
let id = allocate_id();
match env.new_global_ref(&progress_bar) {
Ok(global_ref) => {
register_view(id, global_ref);
}
Err(e) => {
log::error!("[android-jni] nativeCreateProgressBar: failed to create global ref: {e}");
return 0;
}
}
id as jni::sys::jlong
}
#[no_mangle]
pub extern "system" fn Java_rust_1widgets_RustWidgets_nativeCreateSeekBar<'local>(
mut env: jni::JNIEnv<'local>,
_class: jni::objects::JClass<'local>,
context: jni::objects::JObject<'local>,
x: jni::sys::jint,
y: jni::sys::jint,
w: jni::sys::jint,
h: jni::sys::jint,
) -> jni::sys::jlong {
log::info!("[android-jni] nativeCreateSeekBar: pos=({x},{y}), size=({w},{h})");
let seek_bar_class = match env.find_class("android/widget/SeekBar") {
Ok(c) => c,
Err(e) => {
log::error!(
"[android-jni] nativeCreateSeekBar: cannot find android/widget/SeekBar: {e}"
);
return 0;
}
};
let seek_bar = match env.new_object(
&seek_bar_class,
"(Landroid/content/Context;)V",
&[jni::objects::JValue::Object(&context)],
) {
Ok(sb) => sb,
Err(e) => {
log::error!("[android-jni] nativeCreateSeekBar: failed to create SeekBar: {e}");
return 0;
}
};
apply_view_layout(&mut env, &seek_bar, x, y, w, h);
let id = allocate_id();
match env.new_global_ref(&seek_bar) {
Ok(global_ref) => {
register_view(id, global_ref);
}
Err(e) => {
log::error!("[android-jni] nativeCreateSeekBar: failed to create global ref: {e}");
return 0;
}
}
id as jni::sys::jlong
}
fn register_dialog(
id: ObjectId,
env: &mut jni::JNIEnv<'_>,
dialog: &jni::objects::JObject<'_>,
) -> bool {
match env.new_global_ref(dialog) {
Ok(global) => {
register_view(id, global);
true
}
Err(e) => {
log::error!("[android-jni] register_dialog: failed to create GlobalRef: {e}");
false
}
}
}
pub fn create_native_dialog(title: &str, message: &str) -> Option<ObjectId> {
if !is_initialized() {
return None;
}
with_jni_env(|env| create_dialog_with_env(env, title, message)).flatten()
}
fn create_dialog_with_env(
env: &mut jni::JNIEnv<'_>,
title: &str,
message: &str,
) -> Option<ObjectId> {
let context = {
let slot = activity_context_slot().lock().expect("activity context lock poisoned");
slot.as_ref()?.clone()
};
let context_obj = context.as_obj();
let builder_class = env.find_class("android/app/AlertDialog$Builder").ok()?;
let builder = env
.new_object(
&builder_class,
"(Landroid/content/Context;)V",
&[jni::objects::JValue::Object(context_obj)],
)
.ok()?;
if !title.is_empty() {
let java_title = env.new_string(title).ok()?;
if let Err(e) = env.call_method(
&builder,
"setTitle",
"(Ljava/lang/CharSequence;)Landroid/app/AlertDialog$Builder;",
&[jni::objects::JValue::Object(&java_title)],
) {
log::warn!("[android-jni] create_native_dialog: setTitle failed: {e}");
}
}
if !message.is_empty() {
let java_message = env.new_string(message).ok()?;
if let Err(e) = env.call_method(
&builder,
"setMessage",
"(Ljava/lang/CharSequence;)Landroid/app/AlertDialog$Builder;",
&[jni::objects::JValue::Object(&java_message)],
) {
log::warn!("[android-jni] create_native_dialog: setMessage failed: {e}");
}
}
let dialog = env
.call_method(&builder, "create", "()Landroid/app/AlertDialog;", &[])
.ok()
.and_then(|v| v.l().ok())?;
if let Err(e) = env.call_method(&dialog, "show", "()V", &[]) {
log::warn!("[android-jni] create_native_dialog: show() failed: {e}");
}
let id = allocate_id();
if !register_dialog(id, env, &dialog) {
return None;
}
log::info!("[android-jni] create_native_dialog -> id={id}");
Some(id)
}
pub const FILE_DIALOG_REQUEST_CODE: i32 = 0x5257;
pub fn launch_file_dialog(mime_type: &str) -> bool {
if !is_initialized() {
log::info!("[android-jni] launch_file_dialog: bridge not initialized");
return false;
}
with_jni_env(|env| launch_file_dialog_with_env(env, mime_type)).flatten().unwrap_or(false)
}
fn launch_file_dialog_with_env(env: &mut jni::JNIEnv<'_>, mime_type: &str) -> Option<bool> {
let context = {
let slot = activity_context_slot().lock().expect("activity context lock poisoned");
slot.as_ref()?.clone()
};
let context_obj = context.as_obj();
let activity_class = match env.find_class("android/app/Activity") {
Ok(c) => c,
Err(e) => {
log::error!("[android-jni] launch_file_dialog: Activity class missing: {e}");
return Some(false);
}
};
match env.is_instance_of(context_obj, &activity_class) {
Ok(true) => {}
Ok(false) => {
log::warn!(
"[android-jni] launch_file_dialog: stored Context is not an Activity, \
cannot launch a result launcher; pass the Activity to nativeAttachContext"
);
return Some(false);
}
Err(e) => {
log::error!("[android-jni] launch_file_dialog: is_instance_of failed: {e}");
return Some(false);
}
}
let intent_class = match env.find_class("android/content/Intent") {
Ok(c) => c,
Err(e) => {
log::error!("[android-jni] launch_file_dialog: Intent class missing: {e}");
return Some(false);
}
};
let action = env.new_string("android.intent.action.OPEN_DOCUMENT").ok()?;
let intent = match env.new_object(
&intent_class,
"(Ljava/lang/String;)V",
&[jni::objects::JValue::Object(&action)],
) {
Ok(i) => i,
Err(e) => {
log::error!("[android-jni] launch_file_dialog: new Intent failed: {e}");
return Some(false);
}
};
let category = env.new_string("android.intent.category.OPENABLE").ok()?;
if let Err(e) = env.call_method(
&intent,
"addCategory",
"(Ljava/lang/String;)Landroid/content/Intent;",
&[jni::objects::JValue::Object(&category)],
) {
log::warn!("[android-jni] launch_file_dialog: addCategory failed: {e}");
}
let mime = if mime_type.is_empty() { "*/*" } else { mime_type };
let mime_str = env.new_string(mime).ok()?;
if let Err(e) = env.call_method(
&intent,
"setType",
"(Ljava/lang/String;)Landroid/content/Intent;",
&[jni::objects::JValue::Object(&mime_str)],
) {
log::warn!("[android-jni] launch_file_dialog: setType failed: {e}");
}
if let Err(e) = env.call_method(
context_obj,
"startActivityForResult",
"(Landroid/content/Intent;I)V",
&[
jni::objects::JValue::Object(&intent),
jni::objects::JValue::Int(FILE_DIALOG_REQUEST_CODE),
],
) {
log::error!("[android-jni] launch_file_dialog: startActivityForResult failed: {e}");
return Some(false);
}
log::info!(
"[android-jni] launch_file_dialog: ACTION_OPEN_DOCUMENT launched (mime={mime}, \
requestCode={FILE_DIALOG_REQUEST_CODE})"
);
Some(true)
}
pub fn set_native_dialog_message(id: ObjectId, message: &str) -> bool {
with_jni_env(|env| {
let global = match lookup_view(id) {
Some(g) => g,
None => return false,
};
let dialog = global.as_obj();
let java_message = match env.new_string(message) {
Ok(t) => t,
Err(e) => {
log::error!(
"[android-jni] set_native_dialog_message({id}): new_string failed: {e}"
);
return false;
}
};
env.call_method(
dialog,
"setMessage",
"(Ljava/lang/CharSequence;)V",
&[jni::objects::JValue::Object(&java_message)],
)
.is_ok()
})
.unwrap_or(false)
}
pub fn set_native_dialog_visible(id: ObjectId, visible: bool) -> bool {
with_jni_env(|env| {
let global = match lookup_view(id) {
Some(g) => g,
None => return false,
};
let dialog = global.as_obj();
let method = if visible { "show" } else { "dismiss" };
env.call_method(dialog, method, "()V", &[]).is_ok()
})
.unwrap_or(false)
}
pub fn destroy_native_dialog(id: ObjectId) -> bool {
if lookup_view(id).is_none() {
return false;
}
let _ = set_native_dialog_visible(id, false);
unregister_view(id);
true
}
fn apply_view_layout(
env: &mut jni::JNIEnv<'_>,
view: &jni::objects::JObject<'_>,
x: jni::sys::jint,
y: jni::sys::jint,
w: jni::sys::jint,
h: jni::sys::jint,
) {
let lp_class = match env.find_class("android/view/ViewGroup$LayoutParams") {
Ok(c) => c,
Err(e) => {
log::error!("[android-jni] apply_view_layout: cannot find LayoutParams class: {e}");
return;
}
};
let lp = match env.new_object(
&lp_class,
"(II)V",
&[jni::objects::JValue::Int(w), jni::objects::JValue::Int(h)],
) {
Ok(lp) => lp,
Err(e) => {
log::error!("[android-jni] apply_view_layout: failed to create LayoutParams: {e}");
return;
}
};
if let Err(e) = env.call_method(view, "setLeft", "(I)V", &[jni::objects::JValue::Int(x)]) {
log::error!("[android-jni] apply_view_layout: setLeft failed: {e}");
}
if let Err(e) = env.call_method(view, "setTop", "(I)V", &[jni::objects::JValue::Int(y)]) {
log::error!("[android-jni] apply_view_layout: setTop failed: {e}");
}
if let Err(e) = env.call_method(
view,
"setLayoutParams",
"(Landroid/view/ViewGroup$LayoutParams;)V",
&[jni::objects::JValue::Object(&lp)],
) {
log::error!("[android-jni] apply_view_layout: setLayoutParams failed: {e}");
}
}
#[no_mangle]
pub extern "system" fn Java_rust_1widgets_RustWidgets_nativeSetViewText<'local>(
mut env: jni::JNIEnv<'local>,
_class: jni::objects::JClass<'local>,
native_ptr: jni::sys::jlong,
text: jni::objects::JString<'local>,
) {
let id = native_ptr as ObjectId;
let text_str: String = match env.get_string(&text) {
Ok(s) => s.into(),
Err(e) => {
log::error!("[android-jni] nativeSetViewText({id}): failed to get text string: {e}");
return;
}
};
let global_ref = match lookup_view(id) {
Some(r) => r,
None => {
log::warn!("[android-jni] nativeSetViewText({id}): view not found in registry");
return;
}
};
let view_obj = global_ref.as_obj();
if let Err(e) = env.call_method(
view_obj,
"setText",
"(Ljava/lang/CharSequence;)V",
&[jni::objects::JValue::Object(&text)],
) {
log::error!("[android-jni] nativeSetViewText({id}): setText failed: {e}");
}
log::info!("[android-jni] nativeSetViewText({id}): text={text_str}");
}
#[no_mangle]
pub extern "system" fn Java_rust_1widgets_RustWidgets_nativeSetViewBounds<'local>(
mut env: jni::JNIEnv<'local>,
_class: jni::objects::JClass<'local>,
native_ptr: jni::sys::jlong,
x: jni::sys::jint,
y: jni::sys::jint,
w: jni::sys::jint,
h: jni::sys::jint,
) {
let id = native_ptr as ObjectId;
log::info!("[android-jni] nativeSetViewBounds({id}): pos=({x},{y}), size=({w},{h})");
let global_ref = match lookup_view(id) {
Some(r) => r,
None => {
log::warn!("[android-jni] nativeSetViewBounds({id}): view not found in registry");
return;
}
};
let view_obj = global_ref.as_obj();
apply_view_layout(&mut env, view_obj, x, y, w, h);
}
#[no_mangle]
pub extern "system" fn Java_rust_1widgets_RustWidgets_nativeSetViewVisibility(
mut env: jni::JNIEnv,
_class: jni::objects::JClass,
native_ptr: jni::sys::jlong,
visible: jni::sys::jboolean,
) {
let id = native_ptr as ObjectId;
let visibility = if visible != 0 { 0 } else { 8 };
let global_ref = match lookup_view(id) {
Some(r) => r,
None => {
log::warn!("[android-jni] nativeSetViewVisibility({id}): view not found in registry");
return;
}
};
let view_obj = global_ref.as_obj();
if let Err(e) =
env.call_method(view_obj, "setVisibility", "(I)V", &[jni::objects::JValue::Int(visibility)])
{
log::error!("[android-jni] nativeSetViewVisibility({id}): setVisibility failed: {e}");
}
}
#[no_mangle]
pub extern "system" fn Java_rust_1widgets_RustWidgets_nativeSetViewEnabled(
mut env: jni::JNIEnv,
_class: jni::objects::JClass,
native_ptr: jni::sys::jlong,
enabled: jni::sys::jboolean,
) {
let id = native_ptr as ObjectId;
let global_ref = match lookup_view(id) {
Some(r) => r,
None => {
log::warn!("[android-jni] nativeSetViewEnabled({id}): view not found in registry");
return;
}
};
let view_obj = global_ref.as_obj();
if let Err(e) = env.call_method(
view_obj,
"setEnabled",
"(Z)V",
&[jni::objects::JValue::Bool(if enabled != 0 {
jni::sys::JNI_TRUE
} else {
jni::sys::JNI_FALSE
})],
) {
log::error!("[android-jni] nativeSetViewEnabled({id}): setEnabled failed: {e}");
}
}
#[no_mangle]
pub extern "system" fn Java_rust_1widgets_RustWidgets_nativeDestroyView(
_env: jni::JNIEnv,
_class: jni::objects::JClass,
native_ptr: jni::sys::jlong,
) {
let id = native_ptr as ObjectId;
log::info!("[android-jni] nativeDestroyView({id})");
unregister_view(id);
}
#[no_mangle]
pub extern "system" fn Java_rust_1widgets_RustWidgets_nativeAttachContext(
mut env: jni::JNIEnv,
_class: jni::objects::JClass,
context: jni::objects::JObject,
) -> jni::sys::jboolean {
init_logging();
if set_activity_context(&mut env, &context) {
jni::sys::JNI_TRUE
} else {
jni::sys::JNI_FALSE
}
}
#[no_mangle]
pub extern "system" fn Java_rust_1widgets_RustWidgets_nativeSelfTestKinds(
_env: jni::JNIEnv,
_class: jni::objects::JClass,
) -> jni::sys::jint {
init_logging();
if !native_view_creation_ready() {
log::error!("[android-jni] nativeSelfTestKinds: bridge not ready");
return -1;
}
#[cfg(not(target_os = "android"))]
{
log::warn!("[android-jni] nativeSelfTestKinds: not an Android target");
-1
}
#[cfg(target_os = "android")]
{
use crate::platform::android::AndroidPlatform;
use crate::platform::Platform;
type KindCreator = (&'static str, fn(&AndroidPlatform, u64) -> u64);
let platform = AndroidPlatform::new();
platform.init();
let window = platform.create_window("selftest", 0, 0, 320, 640);
if window == 0 {
return -2;
}
let creators: [KindCreator; 7] = [
("Button", |p, w| p.create_button(w, "b", 0, 0, 100, 40)),
("Label", |p, w| p.create_label(w, "l", 0, 50, 100, 40)),
("LineEdit", |p, w| p.create_line_edit(w, "e", 0, 100, 100, 40)),
("CheckBox", |p, w| p.create_checkbox(w, "c", 0, 150, 100, 40)),
("RadioButton", |p, w| p.create_radio_button(w, "r", 0, 200, 100, 40)),
("ProgressBar", |p, w| p.create_progress_bar(w, 0, 250, 100, 40)),
("Slider", |p, w| p.create_slider(w, 0, 300, 100, 40)),
];
let mut created = 0i32;
for (name, create) in creators {
let id = create(&platform, window);
if id == 0 {
log::error!("[android-jni] nativeSelfTestKinds: create {name} failed");
return -3;
}
if platform.native_view_of(id).is_none() {
log::error!("[android-jni] nativeSelfTestKinds: {name} has no native view");
return -4;
}
created += 1;
log::info!("[android-jni] nativeSelfTestKinds: {name} -> logical={id} ok");
}
created
}
}
#[no_mangle]
pub extern "system" fn Java_rust_1widgets_RustWidgets_nativeSelfTestDialog(
_env: jni::JNIEnv,
_class: jni::objects::JClass,
) -> jni::sys::jint {
init_logging();
if !native_view_creation_ready() {
log::error!("[android-jni] nativeSelfTestDialog: bridge not ready");
return -1;
}
#[cfg(not(target_os = "android"))]
{
log::warn!("[android-jni] nativeSelfTestDialog: not an Android target");
-1
}
#[cfg(target_os = "android")]
{
use crate::platform::android::AndroidPlatform;
use crate::platform::Platform;
let platform = AndroidPlatform::new();
platform.init();
let window = platform.create_window("dialog-test", 0, 0, 320, 640);
if window == 0 {
return -2;
}
let message_box =
platform.create_message_box(window, "Self Test", "Hello from Rust", 0, 0, 240, 160);
if message_box == 0 {
return -3;
}
if platform.native_view_of(message_box).is_none() {
log::error!("[android-jni] nativeSelfTestDialog: no native dialog");
return -4;
}
platform.set_widget_text(message_box, "Updated by Rust");
platform.hide_widget(message_box);
platform.show_widget(message_box);
log::info!("[android-jni] nativeSelfTestDialog: ok");
1
}
}
#[no_mangle]
pub extern "system" fn Java_rust_1widgets_RustWidgets_nativeSelfTestFileDialog(
_env: jni::JNIEnv,
_class: jni::objects::JClass,
) -> jni::sys::jint {
init_logging();
if !native_view_creation_ready() {
log::error!("[android-jni] nativeSelfTestFileDialog: bridge not ready");
return -1;
}
#[cfg(not(target_os = "android"))]
{
log::warn!("[android-jni] nativeSelfTestFileDialog: not an Android target");
-1
}
#[cfg(target_os = "android")]
{
use crate::platform::android::AndroidPlatform;
use crate::platform::Platform;
let platform = AndroidPlatform::new();
platform.init();
let window = platform.create_window("file-dialog-test", 0, 0, 320, 640);
if window == 0 {
return -2;
}
let file_dialog = platform.create_file_dialog(window, 0, 0, 240, 160);
if file_dialog == 0 {
log::error!("[android-jni] nativeSelfTestFileDialog: logical handle not created");
return -3;
}
if !launch_file_dialog("*/*") {
log::error!("[android-jni] nativeSelfTestFileDialog: picker not launched");
return -4;
}
log::info!("[android-jni] nativeSelfTestFileDialog: ok");
1
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_allocate_id_is_monotonic() {
let id1 = allocate_id();
let id2 = allocate_id();
assert!(id2 > id1);
}
#[test]
fn test_register_and_lookup_view() {
let id = 42;
assert!(lookup_view(id).is_none());
unregister_view(id); assert!(lookup_view(id).is_none());
}
#[test]
fn test_unregister_nonexistent_view() {
unregister_view(999); }
#[test]
fn test_view_class_paths_are_android_widgets() {
let classes = [
AndroidViewClass::Button,
AndroidViewClass::TextView,
AndroidViewClass::EditText,
AndroidViewClass::CheckBox,
AndroidViewClass::RadioButton,
AndroidViewClass::SeekBar,
AndroidViewClass::ProgressBar,
AndroidViewClass::Spinner,
AndroidViewClass::ListView,
AndroidViewClass::ScrollView,
AndroidViewClass::NumberPicker,
AndroidViewClass::FrameLayout,
];
for class in classes {
assert!(
class.jni_class_path().starts_with("android/widget/"),
"unexpected class path {}",
class.jni_class_path()
);
}
}
#[test]
fn test_supports_text_matches_settext_capable_views() {
assert!(AndroidViewClass::Button.supports_text());
assert!(AndroidViewClass::TextView.supports_text());
assert!(AndroidViewClass::EditText.supports_text());
assert!(AndroidViewClass::CheckBox.supports_text());
assert!(AndroidViewClass::RadioButton.supports_text());
assert!(!AndroidViewClass::SeekBar.supports_text());
assert!(!AndroidViewClass::ProgressBar.supports_text());
assert!(!AndroidViewClass::ScrollView.supports_text());
assert!(!AndroidViewClass::FrameLayout.supports_text());
}
#[test]
fn test_native_view_helpers_noop_without_jvm() {
assert!(!is_initialized());
assert!(!has_activity_context());
assert!(!native_view_creation_ready());
assert_eq!(create_native_view(AndroidViewClass::Button, "x", 0, 0, 10, 10), None);
assert!(!destroy_native_view(1234));
assert!(!set_native_view_text(1234, "x"));
assert!(!set_native_view_bounds(1234, 0, 0, 10, 10));
assert!(!set_native_view_visibility(1234, true));
assert!(!set_native_view_enabled(1234, true));
assert!(!append_spinner_item(1234, "item", true));
assert!(!append_list_item(1234, &["item"]));
assert!(!launch_file_dialog("*/*"));
}
#[test]
fn test_native_view_creation_ready_requires_both_vm_and_context() {
assert_eq!(native_view_creation_ready(), is_initialized() && has_activity_context());
assert!(!native_view_creation_ready());
}
#[test]
fn test_android_integration_ready_reports_unready_without_jvm() {
let status = android_integration_ready();
assert!(!status.jni_initialized);
assert_eq!(status.native_methods_count, 13);
assert!(!status.ready);
}
#[test]
fn test_native_kinds_map_to_view_classes() {
use AndroidLogicalKind::*;
assert_eq!(view_class_for(Button), Some(AndroidViewClass::Button));
assert_eq!(view_class_for(Label), Some(AndroidViewClass::TextView));
assert_eq!(view_class_for(StatusBar), Some(AndroidViewClass::TextView));
assert_eq!(view_class_for(LineEdit), Some(AndroidViewClass::EditText));
assert_eq!(view_class_for(CheckBox), Some(AndroidViewClass::CheckBox));
assert_eq!(view_class_for(RadioButton), Some(AndroidViewClass::RadioButton));
assert_eq!(view_class_for(Slider), Some(AndroidViewClass::SeekBar));
assert_eq!(view_class_for(ProgressBar), Some(AndroidViewClass::ProgressBar));
assert_eq!(view_class_for(ComboBox), Some(AndroidViewClass::Spinner));
assert_eq!(view_class_for(ListBox), Some(AndroidViewClass::ListView));
assert_eq!(view_class_for(ListView), Some(AndroidViewClass::ListView));
assert_eq!(view_class_for(ScrollArea), Some(AndroidViewClass::ScrollView));
assert_eq!(view_class_for(SpinBox), Some(AndroidViewClass::NumberPicker));
assert_eq!(view_class_for(Panel), Some(AndroidViewClass::FrameLayout));
assert_eq!(view_class_for(Window), Some(AndroidViewClass::FrameLayout));
}
#[test]
fn test_logical_only_kinds_have_no_native_view() {
use AndroidLogicalKind::*;
for kind in
[MenuBar, Menu, MenuItem, ToolBar, MessageBox, FileDialog, ColorDialog, FontDialog]
{
assert_eq!(view_class_for(kind), None, "{kind:?} must not claim a native view");
}
}
}