use crate::window::WindowId;
use crossbeam_channel::Sender;
pub use jni::{
self,
errors::Result as JniResult,
objects::{GlobalRef, JClass, JMap, JObject, JString},
sys::jobject,
JNIEnv,
};
use log::Level;
pub use ndk;
use ndk::{
input_queue::InputQueue,
looper::{FdEvent, ForeignLooper, ThreadLooper},
};
use once_cell::sync::{Lazy, OnceCell};
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
use std::{
collections::{BTreeMap, HashSet},
ffi::{c_void, CStr, CString},
fs::File,
io::{BufRead, BufReader},
os::unix::prelude::*,
sync::{
atomic::{AtomicBool, Ordering},
Arc, Condvar, Mutex, RwLock, RwLockReadGuard,
},
thread,
time::Duration,
};
pub static PACKAGE: OnceCell<&str> = OnceCell::new();
const DATA_URL_ENCODING_SET: &AsciiSet = &CONTROLS
.add(b' ')
.add(b'"')
.add(b'#')
.add(b'%')
.add(b'&')
.add(b'<')
.add(b'>')
.add(b'?')
.add(b'[')
.add(b'\\')
.add(b']')
.add(b'^')
.add(b'`')
.add(b'{')
.add(b'|')
.add(b'}');
#[rustfmt::skip]
#[macro_export]
macro_rules! android_binding {
($domain:ident, $package:ident, $activity:ident, $on_activity_create:path, $main:ident) => {
::tao::android_binding!($domain, $package, $activity, $setup, $main, ::tao)
};
($domain:ident, $package:ident, $activity:ident, $on_activity_create:path, $main:ident, $tao:path) => {{
use $tao::{platform::android::prelude::android_fn, platform::android::prelude::*};
fn _____tao_store_package_name__() {
PACKAGE.get_or_init(move || generate_package_name!($domain, $package));
}
android_fn!(
$domain,
$package,
$activity,
create,
[JObject],
__VOID__,
[$main],
);
android_fn!(
$domain,
$package,
$activity,
onActivityCreate,
[JObject],
__VOID__,
[$on_activity_create],
_____tao_store_package_name__,
);
android_fn!($domain, $package, $activity, start, [JObject]);
android_fn!($domain, $package, $activity, stop, [JObject]);
android_fn!($domain, $package, $activity, resume, [JObject]);
android_fn!($domain, $package, $activity, pause, [JObject]);
android_fn!($domain, $package, $activity, onActivitySaveInstanceState, [JObject]);
android_fn!($domain, $package, $activity, onActivityDestroy, [JObject]);
android_fn!($domain, $package, $activity, onActivityLowMemory, [JObject]);
android_fn!($domain, $package, $activity, onWindowFocusChanged, [JObject,i32]);
android_fn!($domain, $package, $activity, onNewIntent, [JObject]);
}};
}
pub const NDK_GLUE_LOOPER_EVENT_PIPE_IDENT: i32 = 0;
pub const NDK_GLUE_LOOPER_INPUT_QUEUE_IDENT: i32 = 1;
pub fn android_log(level: Level, tag: &CStr, msg: &CStr) {
let prio = match level {
Level::Error => ndk_sys::android_LogPriority::ANDROID_LOG_ERROR,
Level::Warn => ndk_sys::android_LogPriority::ANDROID_LOG_WARN,
Level::Info => ndk_sys::android_LogPriority::ANDROID_LOG_INFO,
Level::Debug => ndk_sys::android_LogPriority::ANDROID_LOG_DEBUG,
Level::Trace => ndk_sys::android_LogPriority::ANDROID_LOG_VERBOSE,
};
unsafe {
ndk_sys::__android_log_write(prio.0 as _, tag.as_ptr(), msg.as_ptr());
}
}
fn find_class<'a>(
env: &mut JNIEnv<'a>,
activity: &JObject<'_>,
name: String,
) -> JniResult<JClass<'a>> {
let class_name = env.new_string(name.replace('/', "."))?;
let my_class = env
.call_method(
activity,
"getAppClass",
"(Ljava/lang/String;)Ljava/lang/Class;",
&[(&class_name).into()],
)?
.l()?;
Ok(my_class.into())
}
#[derive(Clone, Debug)]
pub struct AndroidContext {
pub java_vm: *mut c_void,
pub context_jobject: *mut c_void,
pub activity_name: String,
pub window_created: bool,
}
impl AndroidContext {
pub fn create_activity(&self, activity_name: &str) -> JniResult<ActivityId> {
let vm = unsafe { jni::JavaVM::from_raw(self.java_vm.cast()) }?;
let mut env = vm.attach_current_thread_as_daemon()?;
let main_activity = unsafe { JObject::from_raw(self.context_jobject.cast()) };
let activity_class = find_class(
&mut env,
&main_activity,
format!("{}/{activity_name}", PACKAGE.get().unwrap()),
)?;
let activity_id = env
.call_method(
&main_activity,
"startActivity",
"(Ljava/lang/Class;)I",
&[(&activity_class).into()],
)?
.i()?;
let (tx, rx) = crossbeam_channel::bounded(1);
ACTIVITY_CREATED_SENDERS
.lock()
.unwrap()
.insert(activity_id, tx);
rx.recv_timeout(Duration::from_secs(5)).map_err(|e| {
log::error!("failed to create activity {activity_name}: {e}");
jni::errors::Error::JniCall(jni::errors::JniError::Unknown)
})?;
Ok(activity_id)
}
}
unsafe impl Send for AndroidContext {}
unsafe impl Sync for AndroidContext {}
pub type ActivityId = i32;
pub(crate) static CONTEXTS: Lazy<Mutex<BTreeMap<ActivityId, AndroidContext>>> =
Lazy::new(Default::default);
static WINDOW_MANAGER: Lazy<Mutex<BTreeMap<ActivityId, GlobalRef>>> = Lazy::new(Default::default);
pub(crate) static ACTIVITY_CREATED_SENDERS: Lazy<Mutex<BTreeMap<ActivityId, Sender<()>>>> =
Lazy::new(Default::default);
static INTENT_URLS: Lazy<Mutex<Vec<url::Url>>> = Lazy::new(Default::default);
static INPUT_QUEUE: Lazy<RwLock<Option<InputQueue>>> = Lazy::new(Default::default);
static CONTENT_RECT: Lazy<RwLock<Rect>> = Lazy::new(Default::default);
static LOOPER: Lazy<Mutex<Option<ForeignLooper>>> = Lazy::new(Default::default);
static DID_RESUME: AtomicBool = AtomicBool::new(false);
pub fn main_window_manager() -> Option<GlobalRef> {
WINDOW_MANAGER.lock().unwrap().values().next().cloned()
}
pub fn activity_window_manager(activity_id: ActivityId) -> Option<GlobalRef> {
WINDOW_MANAGER.lock().unwrap().get(&activity_id).cloned()
}
pub fn window_manager(activity_id: ActivityId) -> Option<GlobalRef> {
WINDOW_MANAGER.lock().unwrap().get(&activity_id).cloned()
}
pub fn input_queue() -> RwLockReadGuard<'static, Option<InputQueue>> {
INPUT_QUEUE.read().unwrap()
}
pub fn content_rect() -> Rect {
CONTENT_RECT.read().unwrap().clone()
}
pub fn main_android_context() -> Option<AndroidContext> {
CONTEXTS.lock().unwrap().values().next().cloned()
}
pub fn next_available_activity() -> Option<(ActivityId, AndroidContext)> {
CONTEXTS
.lock()
.unwrap()
.iter()
.filter(|(_, ctx)| !ctx.window_created)
.next()
.map(|(id, ctx)| (*id, ctx.clone()))
}
pub static PIPE: Lazy<[OwnedFd; 2]> = Lazy::new(|| {
let mut pipe: [RawFd; 2] = Default::default();
unsafe { libc::pipe(pipe.as_mut_ptr()) };
pipe.map(|fd| unsafe { OwnedFd::from_raw_fd(fd) })
});
pub fn poll_events() -> Option<Event> {
unsafe {
let size = std::mem::size_of::<Event>();
let mut event = Event::Start;
if libc::read(PIPE[0].as_raw_fd(), &mut event as *mut _ as *mut _, size)
== size as libc::ssize_t
{
Some(event)
} else {
None
}
}
}
pub fn take_intent_urls() -> Vec<url::Url> {
INTENT_URLS.lock().unwrap().drain(..).collect()
}
unsafe fn wake(event: Event) {
log::trace!("{:?}", event);
let size = std::mem::size_of::<Event>();
let res = libc::write(PIPE[1].as_raw_fd(), &event as *const _ as *const _, size);
assert_eq!(res, size as libc::ssize_t);
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Rect {
pub left: u32,
pub top: u32,
pub right: u32,
pub bottom: u32,
}
#[derive(Clone, Debug, Eq, PartialEq, Copy)]
#[repr(u8)]
pub enum Event {
Start,
Resume,
Pause,
Stop,
LowMemory,
WindowEvent { id: WindowId, event: WindowEvent },
ContentRectChanged,
Opened,
}
#[derive(Clone, Debug, Eq, PartialEq, Copy)]
#[repr(u8)]
pub enum WindowEvent {
Focused(bool),
Created,
Resized,
RedrawNeeded,
Destroyed,
}
pub unsafe fn create(_env: JNIEnv, _: JClass, _: JObject, main: fn()) {
let logpipe = {
let mut logpipe: [RawFd; 2] = Default::default();
libc::pipe(logpipe.as_mut_ptr());
libc::dup2(logpipe[1], libc::STDOUT_FILENO);
libc::dup2(logpipe[1], libc::STDERR_FILENO);
logpipe.map(|fd| unsafe { OwnedFd::from_raw_fd(fd) })
};
thread::spawn(move || {
let tag = CStr::from_bytes_with_nul(b"RustStdoutStderr\0").unwrap();
let file = File::from_raw_fd(logpipe[0].as_raw_fd());
let mut reader = BufReader::new(file);
let mut buffer = String::new();
loop {
buffer.clear();
if let Ok(len) = reader.read_line(&mut buffer) {
if len == 0 {
break;
} else if let Ok(msg) = CString::new(buffer.clone()) {
android_log(Level::Info, tag, &msg);
}
}
}
});
let looper_ready = Arc::new(Condvar::new());
let signal_looper_ready = looper_ready.clone();
thread::spawn(move || {
let looper = ThreadLooper::prepare();
let foreign = looper.into_foreign();
foreign
.add_fd(
PIPE[0].as_fd(),
NDK_GLUE_LOOPER_EVENT_PIPE_IDENT,
FdEvent::INPUT,
std::ptr::null_mut(),
)
.unwrap();
{
let mut locked_looper = LOOPER.lock().unwrap();
*locked_looper = Some(foreign);
signal_looper_ready.notify_one();
}
main();
});
let locked_looper = LOOPER.lock().unwrap();
let _mutex_guard = looper_ready
.wait_while(locked_looper, |looper| looper.is_none())
.unwrap();
}
#[allow(non_snake_case)]
pub unsafe fn onActivityCreate(
mut env: JNIEnv,
_jclass: JClass,
activity: JObject,
setup: unsafe fn(&str, JNIEnv, &ThreadLooper, GlobalRef),
) {
let intent = env
.call_method(&activity, "getIntent", "()Landroid/content/Intent;", &[])
.unwrap()
.l()
.unwrap();
let activity_id = env
.call_method(&activity, "getId", "()I", &[])
.unwrap()
.i()
.unwrap();
let activity_name: JString = env
.call_method(&activity, "getLocalClassName", "()Ljava/lang/String;", &[])
.unwrap()
.l()
.unwrap()
.into();
let activity_name = env
.get_string(&activity_name)
.unwrap()
.to_string_lossy()
.to_string();
let window_manager = env
.call_method(
&activity,
"getWindowManager",
"()Landroid/view/WindowManager;",
&[],
)
.unwrap()
.l()
.unwrap();
let window_manager = env.new_global_ref(window_manager).unwrap();
WINDOW_MANAGER
.lock()
.unwrap()
.insert(activity_id, window_manager);
let activity = env.new_global_ref(activity).unwrap();
let vm = env.get_java_vm().unwrap();
let thread_env = vm.attach_current_thread_as_daemon().unwrap();
CONTEXTS.lock().unwrap().insert(
activity_id,
AndroidContext {
java_vm: vm.get_java_vm_pointer() as *mut _,
context_jobject: activity.as_obj().as_raw() as *mut _,
activity_name,
window_created: false,
},
);
let looper = ThreadLooper::for_thread().unwrap();
setup(PACKAGE.get().unwrap(), thread_env, &looper, activity);
if let Some(tx) = ACTIVITY_CREATED_SENDERS
.lock()
.unwrap()
.remove(&activity_id)
{
let _ = tx.send(());
}
handle_intent(env, intent);
}
pub unsafe fn resume(_: JNIEnv, _: JClass, _: JObject) {
let did_resume = DID_RESUME.swap(true, Ordering::Relaxed);
if did_resume {
wake(Event::Resume);
}
}
pub unsafe fn pause(_: JNIEnv, _: JClass, _: JObject) {
wake(Event::Pause);
}
#[allow(non_snake_case)]
pub unsafe fn onWindowFocusChanged(
mut env: JNIEnv,
_: JClass,
activity: JObject,
has_focus: libc::c_int,
) {
let activity_id = env
.call_method(&activity, "getId", "()I", &[])
.unwrap()
.i()
.unwrap();
let event = Event::WindowEvent {
id: WindowId(super::WindowId(activity_id)),
event: WindowEvent::Focused(has_focus != 0),
};
wake(event);
}
#[allow(non_snake_case)]
pub unsafe fn onNewIntent(env: JNIEnv, _: JClass, intent: JObject) {
handle_intent(env, intent);
}
pub unsafe fn handle_intent(mut env: JNIEnv, intent: JObject) {
let action = env
.call_method(&intent, "getAction", "()Ljava/lang/String;", &[])
.unwrap()
.l()
.unwrap()
.into();
let action = env
.get_string(&action)
.map(|action| action.to_string_lossy().to_string());
let Ok(action) = action else {
return;
};
if action != "android.intent.action.SEND"
&& action != "android.intent.action.VIEW"
&& action != "android.intent.action.SEND_MULTIPLE"
{
return;
}
let mut urls = HashSet::new();
let intent_type = env
.call_method(&intent, "getType", "()Ljava/lang/String;", &[])
.ok()
.and_then(|result| result.l().ok())
.map(|jstr| jstr.into())
.map(|intent_type| {
env
.get_string(&intent_type)
.unwrap()
.to_string_lossy()
.to_string()
});
if intent_type.as_deref() == Some("text/plain") {
let extra_text = env
.call_method(
&intent,
"getStringExtra",
"(Ljava/lang/String;)Ljava/lang/String;",
&[(&env.new_string("android.intent.extra.TEXT").unwrap()).into()],
)
.ok()
.and_then(|result| result.l().ok())
.and_then(|jstr| {
let jstr: JString = jstr.into();
env
.get_string(&jstr)
.ok()
.map(|s| s.to_string_lossy().to_string())
});
if let Some(text) = extra_text {
if !text.is_empty() {
if let Ok(url) = url::Url::parse(&text) {
urls.insert(url);
} else {
let encoded = utf8_percent_encode(&text, DATA_URL_ENCODING_SET).to_string();
if let Ok(url) = url::Url::parse(&format!("data:text/plain,{}", encoded)) {
urls.insert(url);
}
}
}
}
}
let clip_data = env
.call_method(&intent, "getClipData", "()Landroid/content/ClipData;", &[])
.ok()
.and_then(|result| result.l().ok());
if let Some(clip_data) = clip_data {
if let Ok(item_count) = env
.call_method(&clip_data, "getItemCount", "()I", &[])
.map(|count| count.i().unwrap())
{
for i in 0..item_count {
let clip_item = env
.call_method(
&clip_data,
"getItemAt",
"(I)Landroid/content/ClipData$Item;",
&[i.into()],
)
.unwrap()
.l()
.unwrap();
let uri = env
.call_method(&clip_item, "getUri", "()Landroid/net/Uri;", &[])
.ok()
.and_then(|result| result.l().ok());
if let Some(uri) = uri {
let uri_string: JString = env
.call_method(&uri, "toString", "()Ljava/lang/String;", &[])
.unwrap()
.l()
.unwrap()
.into();
let uri_str = env
.get_string(&uri_string)
.unwrap()
.to_string_lossy()
.to_string();
if let Ok(url) = url::Url::parse(&uri_str) {
urls.insert(url);
} else {
log::error!("failed to parse URI: {}", uri_str);
}
}
}
}
}
let extras = env
.call_method(&intent, "getExtras", "()Landroid/os/Bundle;", &[])
.ok()
.and_then(|result| result.l().ok());
if let Some(extras) = extras {
let extra_stream = env
.call_method(
&extras,
"get",
"(Ljava/lang/String;)Ljava/lang/Object;",
&[(&env.new_string("android.intent.extra.STREAM").unwrap()).into()],
)
.ok()
.and_then(|result| result.l().ok());
if let Some(stream_uri) = extra_stream {
let uri_string: JString = env
.call_method(&stream_uri, "toString", "()Ljava/lang/String;", &[])
.unwrap()
.l()
.unwrap()
.into();
let uri_str = env
.get_string(&uri_string)
.unwrap()
.to_string_lossy()
.to_string();
if let Ok(url) = url::Url::parse(&uri_str) {
urls.insert(url);
} else {
log::error!("failed to parse URI: {}", uri_str);
}
}
}
if action == "android.intent.action.VIEW" {
let data_string = env
.call_method(&intent, "getDataString", "()Ljava/lang/String;", &[])
.ok()
.and_then(|result| result.l().ok())
.and_then(|jstr| {
let jstr: JString = jstr.into();
env
.get_string(&jstr)
.ok()
.map(|s| s.to_string_lossy().to_string())
});
if let Some(data_str) = data_string {
if let Ok(url) = url::Url::parse(&data_str) {
urls.insert(url);
} else {
log::error!("failed to parse data string: {}", data_str);
}
} else {
log::error!("Intent data string is null");
}
}
if !urls.is_empty() {
INTENT_URLS.lock().unwrap().extend(urls);
wake(Event::Opened);
}
}
pub unsafe fn start(_: JNIEnv, _: JClass, _: JObject) {
wake(Event::Start);
}
pub unsafe fn stop(_: JNIEnv, _: JClass, _: JObject) {
wake(Event::Stop);
}
#[allow(non_snake_case)]
pub unsafe fn onActivityDestroy(mut env: JNIEnv, _: JClass, activity: JObject) {
let activity_id = env
.call_method(&activity, "getId", "()I", &[])
.unwrap()
.i()
.unwrap();
let is_changing_configurations = env
.call_method(&activity, "isChangingConfigurations", "()Z", &[])
.unwrap()
.z()
.unwrap();
if !is_changing_configurations {
wake(Event::WindowEvent {
id: WindowId(super::WindowId(activity_id)),
event: WindowEvent::Destroyed,
});
CONTEXTS.lock().unwrap().remove(&activity_id);
WINDOW_MANAGER.lock().unwrap().remove(&activity_id);
}
}
#[allow(non_snake_case)]
pub unsafe fn onActivitySaveInstanceState(_: JNIEnv, _: JClass, _: JObject) {}
#[allow(non_snake_case)]
pub unsafe fn onActivityLowMemory(_: JNIEnv, _: JClass, _: JObject) {
wake(Event::LowMemory);
}