1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
//! What an Android process starts with, whichever part of the application
//! the system started it for.
//!
//! The system starts the process for the activity, and also for a service
//! alone: Google Play services starts it to hand over what a paired watch
//! sends. A process started for a service has no activity, so only what needs
//! no window starts here: the logger, the application's folders, the services
//! that work from the application context, and the application's own
//! [`AppLauncher::with_process_start`](crate::AppLauncher::with_process_start).
#![expect(unsafe_code)]
use std::sync::{Once, OnceLock};
use jni::{
EnvUnowned, JavaVM, Outcome,
objects::{JClass, JObject},
};
use crate::{AppSettings, android_jni::AndroidContext};
static CONTEXT: OnceLock<AndroidContext> = OnceLock::new();
/// Starts the process once, with the context `context` gives, and then the
/// application's process start.
fn start(settings: &AppSettings, context: impl FnOnce() -> Result<AndroidContext, String>) {
static STARTED: Once = Once::new();
STARTED.call_once(|| {
crate::android::init_logging(
settings
.log_tag
.as_deref()
.unwrap_or(crate::android::DEFAULT_LOG_TAG),
);
install_panic_log();
match context() {
Ok(context) => register(CONTEXT.get_or_init(|| context)),
Err(error) => log::error!("the process started without its services: {error}"),
}
});
settings.start_process();
}
/// Logs a panic with its place and backtrace before the process ends, which
/// Android's stderr would drop.
fn install_panic_log() {
let previous_hook = std::panic::take_hook();
std::panic::set_hook(crate::android_panic_hook::chained_panic_hook(
|panic_info| {
let location = panic_info.location().map_or_else(
|| "unknown location".to_string(),
|l| format!("{}:{}:{}", l.file(), l.line(), l.column()),
);
let message = panic_info
.payload()
.downcast_ref::<&str>()
.copied()
.or_else(|| {
panic_info
.payload()
.downcast_ref::<String>()
.map(String::as_str)
})
.unwrap_or("Box<dyn Any>");
let backtrace = std::backtrace::Backtrace::force_capture();
log::error!("PANIC at {location}: {message}\n{backtrace}");
},
previous_hook,
));
}
fn register(context: &'static AndroidContext) {
crate::android_host::install_process(context);
// Present only when the application declares the `wearable` service in
// its Gradle build; without it the link reports itself unavailable.
crate::android_wearable::register(context);
// Present only when the application declares `microphone-standby`.
crate::android_microphone_standby::register(context);
}
/// Starts the process for the activity `app` runs, if no service started it.
pub(crate) fn start_for_activity(app: &android_activity::AndroidApp, settings: &AppSettings) {
start(settings, || AndroidContext::of_activity(app));
}
/// Starts the process for a service, from `CranposeProcess.nativeStart`.
///
/// With `share` the process has no activity, and the context is also set
/// where crates such as cpal read it, which the activity's start sets
/// otherwise. `CranposeProcess.beforeActivity` hands it back with
/// `nativeYieldContext` before an activity sets it.
pub(crate) fn start_for_service(
mut env: EnvUnowned<'_>,
_class: JClass<'_>,
context: JObject<'_>,
share: bool,
settings: &AppSettings,
) {
let started = env.with_env(|env| -> jni::errors::Result<()> {
start(settings, || {
let context = AndroidContext::new(env, &context)?;
if share {
share_with_ndk(&context)?;
}
Ok(context)
});
Ok(())
});
if let Outcome::Err(_) | Outcome::Panic(_) = started.into_outcome() {
log::error!("the process start for a service failed");
}
}
fn share_with_ndk(context: &AndroidContext) -> Result<(), String> {
let vm = JavaVM::singleton().map_err(|error| error.to_string())?;
// SAFETY: the VM and the global reference to the application context
// live as long as the process; Java calls this only where no activity
// set the context, and hands it back before one does.
unsafe {
ndk_context::initialize_android_context(vm.get_raw().cast(), context.raw().cast());
}
Ok(())
}
/// Hands back the context a process start set, before an activity sets it.
#[doc(hidden)]
#[unsafe(no_mangle)]
pub extern "system" fn Java_dev_cranpose_android_CranposeProcess_nativeYieldContext(
_env: EnvUnowned<'_>,
_class: JClass<'_>,
) {
// SAFETY: Java calls this once, on the main thread, only after a start
// that set the context.
unsafe { ndk_context::release_android_context() };
}