mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 00:45:22 +00:00
feat: add product flavors and TorProvider abstraction (#508)
* feat: add product flavors and TorProvider abstraction Introduces build flavors to separate Tor functionality from the standard build, reducing APK size for users who don't need Tor. - Creates `standard` and `tor` product flavors. - The `standard` flavor is the default, lightweight build. - The `tor` flavor includes the Arti (Tor) dependency and is identified by the `.tor` application ID suffix. - Adds a `TorProvider` interface and a `TorProviderFactory` to abstract Tor implementation details between flavors. Prepares architecture for optional Tor support to reduce APK size from 142MB to ~4-5MB for standard builds Related to #454 * Refactor: implement StandardTorProvider and RealTorProvider This commit refactors the Tor integration by introducing a `TorProvider` interface and creating separate implementations for 'tor' and 'standard' product flavors. - The original `TorManager` singleton has been moved into `RealTorProvider` for the 'tor' flavor. - A no-op `StandardTorProvider` is introduced for the 'standard' flavor, which reports Tor as unavailable. - A `TorProviderFactory` is used to create the appropriate provider at runtime based on the build variant. * refactor: migrate to TorProvider abstraction Replaced direct calls to the static `TorManager` with an instance obtained from `TorProviderFactory`. This change allows for different Tor implementations based on build flavors, improving modularity and abstracting the Tor provider logic. Updated `BitchatApplication`, `ChatHeader`, `OkHttpProvider`, and `AboutSheet` to use the new factory pattern for accessing Tor functionalities. * build: add flavor-specific ProGuard rules and CI/CD - Split ProGuard rules: base, standard-specific, tor-specific - Move Arti/Guardian Project rules to proguard-tor.pro - Update CI/CD workflow to build both flavors - Add separate artifact uploads for each flavor - Add descriptive release notes template CI now builds both standard (~4-5MB) and tor (~140MB) APKs." resolves #454 * refactor: centralize network reset logic Extracts the repeated network connection reset logic into a new private function `resetNetworkConnections()`. This change also replaces direct `_statusFlow.value = ...` assignments with the safer `_statusFlow.update { ... }` function to prevent race conditions. * ci: run separate build steps for flavors * feat: disable Tor toggle if not available in build * ci: parallelize builds and add conditional tor lint Optimizes CI workflow: - Parallel matrix builds (4 runners instead of sequential) - Conditional tor lint only when app/src/tor/ changes in PRs - Merged test+lint jobs to reduce setup overhead Reduces CI time by ~50% (8-11 min vs 18-26 min) * refactor: Unify Tor implementation and remove build flavors This commit refactors the Tor integration by removing the `standard` and `tor` product flavors in favor of a single, unified build that always includes a custom-built Arti (Tor) library. Key changes include: * **Removed Build Flavors:** Deleted the `standard` and `tor` product flavors from `build.gradle.kts`, simplifying the build process and CI configuration. * **Unified Tor Manager:** Replaced the `TorProvider` interface and flavor-specific implementations (`StandardTorProvider`, `RealTorProvider`) with a new singleton, `ArtiTorManager`. This class now manages the Arti lifecycle for all builds. * **Custom Arti Wrapper:** Introduced a new `ArtiProxy` class to provide a compatible API wrapper around the custom-built native Arti library (`libarti_android.so`). This replaces the dependency on the external `arti-mobile-ex` library. * **Updated Proguard:** Consolidated and updated Proguard rules into the main `proguard-rules.pro` file to keep the necessary `ArtiTorManager` and native library classes. * **CI/CD Simplification:** Updated GitHub Actions workflows (`release.yml`, `android-build.yml`) to build and release a single APK instead of separate ones for each flavor. * build: Configure ABI filters for debug and release builds For debug builds, include `x86_64` to support emulators. For release builds, only include `arm64-v8a` to minimize the final APK size. * feat: Ignore jniLibs directory This change adds the `app/src/main/jniLibs/` directory to the `.gitignore` file to prevent native libraries from being committed to the repository. * feat: Refine .gitignore for Arti build artifacts Improves the `.gitignore` file by: - Ignoring all `build/` directories except for `tools/arti-build/`. - Adding specific ignores for Arti build artifacts, including the cloned source repository and the Rust build cache directory. * feat: Update arti android native library * feat: add build script and JNI wrapper for Arti Adds a comprehensive build system for creating custom Arti (Tor in Rust) shared libraries for Android. This replaces the dependency on external, outdated AARs with a fully transparent and reproducible build process. Key changes: - Introduces `build-arti.sh`, a script to clone the official Arti repository, apply a JNI wrapper, and build `.so` files for Android. - Adds `ARTI_VERSION` to pin the build to a specific Arti release (v1.7.0). - Implements a new Rust JNI wrapper (`src/lib.rs`) that exposes core functions like `initialize`, `startSocksProxy`, and `stop` to the Android app. - Includes a `Cargo.toml` with release profile optimizations for size (`lto`, `strip`, `opt-level = "z"`). - Provides detailed documentation in `README.md` explaining the build process, prerequisites, and architecture. * build script for mac * consolidate both scripts * improve script --------- Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,493 @@
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::{JClass, JString, JObject, GlobalRef};
|
||||
use jni::sys::{jint, jstring};
|
||||
use jni::JavaVM;
|
||||
|
||||
use arti_client::TorClient;
|
||||
use arti_client::config::TorClientConfigBuilder;
|
||||
use tor_rtcompat::PreferredRuntime;
|
||||
|
||||
use std::sync::{Arc, Mutex, Once};
|
||||
use std::path::PathBuf;
|
||||
use anyhow::Result;
|
||||
|
||||
// ============================================================================
|
||||
// Global State
|
||||
// ============================================================================
|
||||
|
||||
/// Global Arti client instance
|
||||
static ARTI_CLIENT: Mutex<Option<Arc<TorClient<PreferredRuntime>>>> = Mutex::new(None);
|
||||
|
||||
/// Global Tokio runtime (must persist for Arti to work)
|
||||
static TOKIO_RUNTIME: Mutex<Option<tokio::runtime::Runtime>> = Mutex::new(None);
|
||||
|
||||
/// Global JavaVM reference (cached on first JNI call)
|
||||
static JAVA_VM: Mutex<Option<JavaVM>> = Mutex::new(None);
|
||||
|
||||
/// Global log callback reference
|
||||
static LOG_CALLBACK: Mutex<Option<GlobalRef>> = Mutex::new(None);
|
||||
|
||||
/// Handle to SOCKS server task (for graceful shutdown)
|
||||
static SOCKS_TASK: Mutex<Option<tokio::task::JoinHandle<()>>> = Mutex::new(None);
|
||||
|
||||
/// Initialization flag
|
||||
static INIT_ONCE: Once = Once::new();
|
||||
|
||||
// ============================================================================
|
||||
// Logging Integration
|
||||
// ============================================================================
|
||||
|
||||
/// Send log message to Java callback
|
||||
fn send_log_to_java(message: String) {
|
||||
let vm_opt = JAVA_VM.lock().unwrap();
|
||||
let callback_opt = LOG_CALLBACK.lock().unwrap();
|
||||
|
||||
if let (Some(vm), Some(callback)) = (vm_opt.as_ref(), callback_opt.as_ref()) {
|
||||
if let Ok(mut env) = vm.attach_current_thread() {
|
||||
if let Ok(jmessage) = env.new_string(&message) {
|
||||
let _ = env.call_method(
|
||||
callback.as_obj(),
|
||||
"onLogLine",
|
||||
"(Ljava/lang/String;)V",
|
||||
&[(&jmessage).into()]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Macro for logging to both Android logcat and Java callback
|
||||
macro_rules! log_info {
|
||||
($($arg:tt)*) => {{
|
||||
let msg = format!($($arg)*);
|
||||
android_logger::log(&format!("Arti: {}", msg));
|
||||
send_log_to_java(msg);
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! log_error {
|
||||
($($arg:tt)*) => {{
|
||||
let msg = format!("ERROR: {}", format!($($arg)*));
|
||||
android_logger::log(&format!("Arti: {}", msg));
|
||||
send_log_to_java(msg);
|
||||
}};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// JNI Functions
|
||||
// ============================================================================
|
||||
|
||||
/// Get Arti version string
|
||||
#[no_mangle]
|
||||
pub extern "C" fn Java_org_torproject_arti_ArtiNative_getVersion(
|
||||
env: JNIEnv,
|
||||
_class: JClass,
|
||||
) -> jstring {
|
||||
// Cache JavaVM on first call
|
||||
if JAVA_VM.lock().unwrap().is_none() {
|
||||
if let Ok(vm) = env.get_java_vm() {
|
||||
*JAVA_VM.lock().unwrap() = Some(vm);
|
||||
}
|
||||
}
|
||||
|
||||
let version = format!("Arti {} (custom build with rustls)", env!("CARGO_PKG_VERSION"));
|
||||
let output = env.new_string(version).expect("Couldn't create java string!");
|
||||
output.into_raw()
|
||||
}
|
||||
|
||||
/// Set log callback for Arti logs
|
||||
#[no_mangle]
|
||||
pub extern "C" fn Java_org_torproject_arti_ArtiNative_setLogCallback(
|
||||
env: JNIEnv,
|
||||
_class: JClass,
|
||||
callback: JObject,
|
||||
) {
|
||||
// Cache JavaVM if not already cached
|
||||
if JAVA_VM.lock().unwrap().is_none() {
|
||||
if let Ok(vm) = env.get_java_vm() {
|
||||
*JAVA_VM.lock().unwrap() = Some(vm);
|
||||
}
|
||||
}
|
||||
|
||||
// Store global reference to callback
|
||||
if let Ok(global_ref) = env.new_global_ref(callback) {
|
||||
*LOG_CALLBACK.lock().unwrap() = Some(global_ref);
|
||||
log_info!("Log callback registered");
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize Arti runtime
|
||||
#[no_mangle]
|
||||
pub extern "C" fn Java_org_torproject_arti_ArtiNative_initialize(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
data_dir: JString,
|
||||
) -> jint {
|
||||
// Cache JavaVM if not already cached
|
||||
if JAVA_VM.lock().unwrap().is_none() {
|
||||
if let Ok(vm) = env.get_java_vm() {
|
||||
*JAVA_VM.lock().unwrap() = Some(vm);
|
||||
}
|
||||
}
|
||||
|
||||
let data_dir_str: String = match env.get_string(&data_dir) {
|
||||
Ok(s) => s.into(),
|
||||
Err(e) => {
|
||||
log_error!("Failed to convert data_dir: {:?}", e);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
log_info!("AMEx: state changed to Initialized");
|
||||
log_info!("Initializing Arti with data directory: {}", data_dir_str);
|
||||
|
||||
// Initialize Tokio runtime (once)
|
||||
INIT_ONCE.call_once(|| {
|
||||
match tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
{
|
||||
Ok(rt) => {
|
||||
log_info!("Tokio runtime created successfully");
|
||||
*TOKIO_RUNTIME.lock().unwrap() = Some(rt);
|
||||
}
|
||||
Err(e) => {
|
||||
log_error!("Failed to create Tokio runtime: {:?}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Check if runtime exists
|
||||
let runtime_guard = TOKIO_RUNTIME.lock().unwrap();
|
||||
let runtime = match runtime_guard.as_ref() {
|
||||
Some(rt) => rt,
|
||||
None => {
|
||||
log_error!("Tokio runtime not initialized");
|
||||
return -2;
|
||||
}
|
||||
};
|
||||
|
||||
// Create config with explicit Android paths
|
||||
let data_path = PathBuf::from(data_dir_str);
|
||||
let cache_dir = data_path.join("cache");
|
||||
let state_dir = data_path.join("state");
|
||||
|
||||
// Create directories if they don't exist
|
||||
std::fs::create_dir_all(&cache_dir).ok();
|
||||
std::fs::create_dir_all(&state_dir).ok();
|
||||
|
||||
let result: Result<()> = runtime.block_on(async {
|
||||
log_info!("Creating Arti client...");
|
||||
log_info!("Cache dir: {:?}", cache_dir);
|
||||
log_info!("State dir: {:?}", state_dir);
|
||||
|
||||
// Create config with Android-specific directories
|
||||
let config = TorClientConfigBuilder::from_directories(state_dir, cache_dir)
|
||||
.build()?;
|
||||
|
||||
// Create client with Android-specific config
|
||||
let client = TorClient::create_bootstrapped(config).await?;
|
||||
|
||||
log_info!("Arti client created successfully");
|
||||
|
||||
// Store client globally
|
||||
*ARTI_CLIENT.lock().unwrap() = Some(Arc::new(client));
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
log_info!("Arti initialized successfully");
|
||||
0
|
||||
}
|
||||
Err(e) => {
|
||||
log_error!("Failed to initialize Arti: {:?}", e);
|
||||
-3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Start SOCKS proxy on specified port
|
||||
#[no_mangle]
|
||||
pub extern "C" fn Java_org_torproject_arti_ArtiNative_startSocksProxy(
|
||||
_env: JNIEnv,
|
||||
_class: JClass,
|
||||
port: jint,
|
||||
) -> jint {
|
||||
log_info!("AMEx: state changed to Starting");
|
||||
log_info!("Starting SOCKS proxy on port {}", port);
|
||||
|
||||
// Stop any existing SOCKS server first
|
||||
if let Some(handle) = SOCKS_TASK.lock().unwrap().take() {
|
||||
log_info!("Aborting previous SOCKS server task");
|
||||
handle.abort();
|
||||
}
|
||||
|
||||
let client_guard = ARTI_CLIENT.lock().unwrap();
|
||||
let client = match client_guard.as_ref() {
|
||||
Some(c) => Arc::clone(c),
|
||||
None => {
|
||||
log_error!("Arti client not initialized - call initialize() first");
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
drop(client_guard);
|
||||
|
||||
let runtime_guard = TOKIO_RUNTIME.lock().unwrap();
|
||||
let runtime = match runtime_guard.as_ref() {
|
||||
Some(rt) => rt,
|
||||
None => {
|
||||
log_error!("Tokio runtime not initialized");
|
||||
return -2;
|
||||
}
|
||||
};
|
||||
|
||||
// Try to bind IMMEDIATELY to detect port conflicts before returning
|
||||
let addr = format!("127.0.0.1:{}", port);
|
||||
|
||||
// Use block_on to synchronously attempt binding
|
||||
let bind_result = runtime.block_on(async {
|
||||
tokio::net::TcpListener::bind(&addr).await
|
||||
});
|
||||
|
||||
let listener = match bind_result {
|
||||
Ok(l) => {
|
||||
log_info!("SOCKS proxy bound to {}", addr);
|
||||
l
|
||||
}
|
||||
Err(e) => {
|
||||
log_error!("Failed to bind SOCKS proxy to {}: {:?}", addr, e);
|
||||
return -3;
|
||||
}
|
||||
};
|
||||
|
||||
// Now spawn the background task with the already-bound listener
|
||||
let handle = runtime.spawn(async move {
|
||||
log_info!("SOCKS proxy listening on {}", addr);
|
||||
log_info!("Sufficiently bootstrapped; system SOCKS now functional");
|
||||
|
||||
// Signal bootstrap completion to bitchat-android (expected by ArtiTorManager)
|
||||
// This sets bootstrapPercent to 100% and stops inactivity restarts
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
log_info!("We have found that guard [scrubbed] is usable.");
|
||||
|
||||
// Accept connections
|
||||
loop {
|
||||
match listener.accept().await {
|
||||
Ok((stream, peer_addr)) => {
|
||||
log_info!("SOCKS connection from: {}", peer_addr);
|
||||
let client_clone = Arc::clone(&client);
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_socks_connection(stream, client_clone).await {
|
||||
log_error!("SOCKS connection error: {:?}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
log_error!("Failed to accept SOCKS connection: {:?}", e);
|
||||
break; // Exit loop on error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log_info!("SOCKS proxy task exiting");
|
||||
});
|
||||
|
||||
// Store handle for cleanup
|
||||
*SOCKS_TASK.lock().unwrap() = Some(handle);
|
||||
|
||||
log_info!("SOCKS proxy started on port {}", port);
|
||||
0
|
||||
}
|
||||
|
||||
/// Handle a single SOCKS connection
|
||||
async fn handle_socks_connection(
|
||||
mut stream: tokio::net::TcpStream,
|
||||
client: Arc<TorClient<PreferredRuntime>>,
|
||||
) -> Result<()> {
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
// Simple SOCKS5 handshake
|
||||
let mut buf = [0u8; 512];
|
||||
|
||||
// Read version + methods
|
||||
let n = stream.read(&mut buf).await?;
|
||||
if n < 2 {
|
||||
return Err(anyhow::anyhow!("Invalid SOCKS handshake"));
|
||||
}
|
||||
|
||||
// Send "no auth required" response
|
||||
stream.write_all(&[0x05, 0x00]).await?;
|
||||
|
||||
// Read request
|
||||
let n = stream.read(&mut buf).await?;
|
||||
if n < 10 {
|
||||
return Err(anyhow::anyhow!("Invalid SOCKS request"));
|
||||
}
|
||||
|
||||
// Parse SOCKS5 request: VER(1) CMD(1) RSV(1) ATYP(1) DST.ADDR DST.PORT(2)
|
||||
let version = buf[0];
|
||||
let cmd = buf[1];
|
||||
let atyp = buf[3];
|
||||
|
||||
if version != 0x05 {
|
||||
return Err(anyhow::anyhow!("Unsupported SOCKS version: {}", version));
|
||||
}
|
||||
|
||||
if cmd != 0x01 {
|
||||
// Only support CONNECT command
|
||||
stream.write_all(&[0x05, 0x07, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?;
|
||||
return Err(anyhow::anyhow!("Unsupported SOCKS command: {}", cmd));
|
||||
}
|
||||
|
||||
// Parse target address and port
|
||||
let (target_host, target_port) = match atyp {
|
||||
0x01 => {
|
||||
// IPv4: 4 bytes
|
||||
let ip = format!("{}.{}.{}.{}", buf[4], buf[5], buf[6], buf[7]);
|
||||
let port = u16::from_be_bytes([buf[8], buf[9]]);
|
||||
(ip, port)
|
||||
}
|
||||
0x03 => {
|
||||
// Domain name: length byte + domain
|
||||
let len = buf[4] as usize;
|
||||
if n < 5 + len + 2 {
|
||||
return Err(anyhow::anyhow!("Invalid domain name length"));
|
||||
}
|
||||
let domain = String::from_utf8_lossy(&buf[5..5 + len]).to_string();
|
||||
let port = u16::from_be_bytes([buf[5 + len], buf[5 + len + 1]]);
|
||||
(domain, port)
|
||||
}
|
||||
0x04 => {
|
||||
// IPv6: 16 bytes + 2 bytes port = 22 bytes total
|
||||
if n < 22 {
|
||||
stream.write_all(&[0x05, 0x01, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?;
|
||||
return Err(anyhow::anyhow!("Truncated IPv6 request"));
|
||||
}
|
||||
let ip = format!(
|
||||
"{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}",
|
||||
buf[4], buf[5], buf[6], buf[7], buf[8], buf[9], buf[10], buf[11],
|
||||
buf[12], buf[13], buf[14], buf[15], buf[16], buf[17], buf[18], buf[19]
|
||||
);
|
||||
let port = u16::from_be_bytes([buf[20], buf[21]]);
|
||||
(ip, port)
|
||||
}
|
||||
_ => {
|
||||
stream.write_all(&[0x05, 0x08, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?;
|
||||
return Err(anyhow::anyhow!("Unsupported address type: {}", atyp));
|
||||
}
|
||||
};
|
||||
|
||||
log_info!("SOCKS5 CONNECT to {}:{}", target_host, target_port);
|
||||
|
||||
// Establish Tor connection
|
||||
let tor_stream = match client.connect((target_host.as_str(), target_port)).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
log_error!("Failed to connect through Tor: {:?}", e);
|
||||
// Send SOCKS5 error: general failure
|
||||
stream.write_all(&[0x05, 0x05, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?;
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
|
||||
log_info!("Tor connection established to {}:{}", target_host, target_port);
|
||||
|
||||
// Send SOCKS5 success response
|
||||
stream.write_all(&[0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?;
|
||||
|
||||
// Bidirectional data forwarding
|
||||
let (mut client_read, mut client_write) = stream.split();
|
||||
let (mut tor_read, mut tor_write) = tor_stream.split();
|
||||
|
||||
let client_to_tor = async {
|
||||
tokio::io::copy(&mut client_read, &mut tor_write).await
|
||||
};
|
||||
|
||||
let tor_to_client = async {
|
||||
tokio::io::copy(&mut tor_read, &mut client_write).await
|
||||
};
|
||||
|
||||
// Run both directions concurrently, exit when either completes
|
||||
tokio::select! {
|
||||
result = client_to_tor => {
|
||||
if let Err(ref e) = result {
|
||||
log_error!("Client->Tor copy error: {:?}", e);
|
||||
}
|
||||
}
|
||||
result = tor_to_client => {
|
||||
if let Err(ref e) = result {
|
||||
log_error!("Tor->Client copy error: {:?}", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
log_info!("SOCKS connection closed for {}:{}", target_host, target_port);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop Arti and cleanup
|
||||
#[no_mangle]
|
||||
pub extern "C" fn Java_org_torproject_arti_ArtiNative_stop(
|
||||
_env: JNIEnv,
|
||||
_class: JClass,
|
||||
) -> jint {
|
||||
log_info!("AMEx: state changed to Stopping");
|
||||
log_info!("Stopping Arti...");
|
||||
|
||||
// Abort SOCKS proxy task (releases the port)
|
||||
if let Some(handle) = SOCKS_TASK.lock().unwrap().take() {
|
||||
log_info!("Aborting SOCKS server task");
|
||||
handle.abort();
|
||||
}
|
||||
|
||||
// Give the abort a moment to complete and release the port
|
||||
if let Some(rt) = TOKIO_RUNTIME.lock().unwrap().as_ref() {
|
||||
rt.block_on(async {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
});
|
||||
}
|
||||
|
||||
// NOTE: We do NOT clear ARTI_CLIENT here!
|
||||
// The TorClient can be reused for multiple SOCKS proxy start/stop cycles.
|
||||
// Only clear it if you want to force full reinitialization.
|
||||
|
||||
// Uncomment this line only if you want to force reinitialization on every start:
|
||||
// *ARTI_CLIENT.lock().unwrap() = None;
|
||||
|
||||
log_info!("AMEx: state changed to Stopped");
|
||||
log_info!("Arti stopped successfully");
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Android Logger (simple implementation)
|
||||
// ============================================================================
|
||||
|
||||
mod android_logger {
|
||||
use std::ffi::CString;
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
type c_int = i32;
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
type c_char = i8;
|
||||
|
||||
extern "C" {
|
||||
fn __android_log_write(prio: c_int, tag: *const c_char, text: *const c_char) -> c_int;
|
||||
}
|
||||
|
||||
const ANDROID_LOG_INFO: c_int = 4;
|
||||
|
||||
pub fn log(message: &str) {
|
||||
unsafe {
|
||||
let tag = CString::new("ArtiNative").unwrap();
|
||||
let text = CString::new(message).unwrap();
|
||||
__android_log_write(ANDROID_LOG_INFO, tag.as_ptr() as *const c_char, text.as_ptr() as *const c_char);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user