mirror of
https://github.com/flectar/mail.git
synced 2026-11-14 02:53:32 +00:00
160 lines
5.5 KiB
Rust
160 lines
5.5 KiB
Rust
#[cfg(target_os = "android")]
|
|
mod observability {
|
|
use std::ffi::CString;
|
|
use std::io::{self, Write};
|
|
use std::sync::Once;
|
|
|
|
const ANDROID_LOG_INFO: std::ffi::c_int = 4;
|
|
const ANDROID_LOG_ERROR: std::ffi::c_int = 6;
|
|
|
|
#[link(name = "log")]
|
|
unsafe extern "C" {
|
|
fn __android_log_write(
|
|
priority: std::ffi::c_int,
|
|
tag: *const std::ffi::c_char,
|
|
text: *const std::ffi::c_char,
|
|
) -> std::ffi::c_int;
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct AndroidLogWriter(Vec<u8>);
|
|
|
|
impl Write for AndroidLogWriter {
|
|
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
|
|
self.0.extend_from_slice(bytes);
|
|
if self.0.contains(&b'\n') {
|
|
self.flush()?;
|
|
}
|
|
Ok(bytes.len())
|
|
}
|
|
|
|
fn flush(&mut self) -> io::Result<()> {
|
|
if self.0.is_empty() {
|
|
return Ok(());
|
|
}
|
|
let message = String::from_utf8_lossy(&self.0).replace('\0', "�");
|
|
self.0.clear();
|
|
write_log(ANDROID_LOG_INFO, message.trim_end());
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Drop for AndroidLogWriter {
|
|
fn drop(&mut self) {
|
|
let _ = self.flush();
|
|
}
|
|
}
|
|
|
|
fn write_log(priority: std::ffi::c_int, message: &str) {
|
|
static TAG: &[u8] = b"flectar-mail\0";
|
|
let Ok(message) = CString::new(message) else {
|
|
return;
|
|
};
|
|
// SAFETY: both pointers refer to NUL-terminated strings for the
|
|
// duration of this synchronous Android logging call.
|
|
unsafe {
|
|
__android_log_write(priority, TAG.as_ptr().cast(), message.as_ptr());
|
|
}
|
|
}
|
|
|
|
pub(super) fn install() {
|
|
static INSTALL: Once = Once::new();
|
|
INSTALL.call_once(|| {
|
|
let _ = tracing_subscriber::fmt()
|
|
.with_ansi(false)
|
|
.with_max_level(tracing::Level::INFO)
|
|
.with_writer(AndroidLogWriter::default)
|
|
.try_init();
|
|
|
|
let previous = std::panic::take_hook();
|
|
std::panic::set_hook(Box::new(move |info| {
|
|
write_log(ANDROID_LOG_ERROR, &format!("uncaught Rust panic: {info}"));
|
|
previous(info);
|
|
}));
|
|
tracing::info!(
|
|
version = env!("CARGO_PKG_VERSION"),
|
|
abi = std::env::consts::ARCH,
|
|
"Android host initialized"
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "android")]
|
|
fn app_cache_dir(app: &slint::android::AndroidApp) -> Result<std::path::PathBuf, String> {
|
|
use jni::{
|
|
JavaVM,
|
|
objects::{JObject, JString},
|
|
};
|
|
|
|
// NativeActivity supplies an app-private data path. Use Android's
|
|
// designated, purgeable cache directory for reconstructable data.
|
|
let vm = unsafe { JavaVM::from_raw(app.vm_as_ptr().cast()) }.map_err(|e| e.to_string())?;
|
|
let mut env = vm.attach_current_thread().map_err(|e| e.to_string())?;
|
|
let activity = unsafe { JObject::from_raw(app.activity_as_ptr().cast()) };
|
|
let directory = env
|
|
.call_method(&activity, "getCacheDir", "()Ljava/io/File;", &[])
|
|
.and_then(|value| value.l())
|
|
.map_err(|e| e.to_string())?;
|
|
let path = env
|
|
.call_method(&directory, "getAbsolutePath", "()Ljava/lang/String;", &[])
|
|
.and_then(|value| value.l())
|
|
.map_err(|e| e.to_string())?;
|
|
let path: String = env
|
|
.get_string(&JString::from(path))
|
|
.map_err(|e| e.to_string())?
|
|
.into();
|
|
Ok(path.into())
|
|
}
|
|
|
|
#[cfg(target_os = "android")]
|
|
#[unsafe(no_mangle)]
|
|
fn android_main(app: slint::android::AndroidApp) {
|
|
observability::install();
|
|
// SAFETY: AndroidApp owns these live VM and Activity references.
|
|
unsafe { tls::initialize(app.vm_as_ptr().cast(), app.activity_as_ptr().cast()) }
|
|
.expect("failed to initialize Android TLS certificate verification");
|
|
let internal = app
|
|
.internal_data_path()
|
|
.expect("Android did not provide an app-private data directory");
|
|
let cache_dir =
|
|
app_cache_dir(&app).expect("Android did not provide an app-private cache directory");
|
|
tracing::info!(internal_data_path = %internal.display(), "resolved Android app-private root");
|
|
let oauth_redirects = oauth::AndroidOAuthBroker::global(&app, &internal)
|
|
.expect("failed to initialize Android OAuth services");
|
|
if oauth::route_callback_activity(&app, &oauth_redirects) {
|
|
return;
|
|
}
|
|
let credential_store = credentials::AndroidCredentialStore::new(&app, &internal)
|
|
.expect("failed to initialize Android Keystore credential storage");
|
|
pdf::configure(&app).expect("failed to configure bundled PDF preview");
|
|
if let Err(error) = launcher_icon::install(&app) {
|
|
tracing::warn!(%error, "launcher icon choice unavailable");
|
|
}
|
|
let documents =
|
|
documents::AndroidDocuments::new(&app).expect("failed to initialize document access");
|
|
let platform = flectar_mail::PlatformContext::app_private(
|
|
internal.clone(),
|
|
cache_dir,
|
|
documents,
|
|
std::sync::Arc::new(credential_store),
|
|
oauth_redirects,
|
|
);
|
|
slint::android::init(app).expect("failed to initialize Slint's Android backend");
|
|
flectar_mail::run(platform).expect("Flectar Mail terminated with an error");
|
|
}
|
|
#[cfg(target_os = "android")]
|
|
mod credentials;
|
|
#[cfg(target_os = "android")]
|
|
mod oauth;
|
|
#[cfg(target_os = "android")]
|
|
mod tls;
|
|
|
|
#[cfg(target_os = "android")]
|
|
mod documents;
|
|
|
|
#[cfg(target_os = "android")]
|
|
mod launcher_icon;
|
|
#[cfg(target_os = "android")]
|
|
mod pdf;
|