Two Java classes (MainActivity, NotificationService) hand their lifecycle to a new android-shell crate built on client-core; client-core gains notifications.rs (the /notifications SSE parse and attention_line, ported from Notifications.kt). Packaged as a new app/shellApp Gradle module rather than a rewrite of app/androidApp in place, so that module's working Compose UI is untouched. Both pass conditions held on the emulator: a notification arrived in Android's drawer with the app closed, and a shared text share landed as a real message in a sandbox session's transcript. Found and fixed three real bugs along the way (a silently-wrong JNI signature from a generic JObject parameter, a class-by-name lookup failing on this crate's own background thread for lack of an app ClassLoader, and onStartCommand opening two /notifications connections per enrollment -- the last a latent bug in Notifications.kt itself). Full account, exact commands and what was deliberately cut are in RUST.md's E3 box. Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
557 lines
20 KiB
Rust
557 lines
20 KiB
Rust
//! Where a notification is said, and the foreground service that keeps
|
|
//! the connection open while the app is closed. Ported from
|
|
//! `Notifications.kt`'s `NotificationService`, minus the "session on
|
|
//! screen" / "hand to the app as a banner" branches: those read
|
|
//! process-wide state that only exists because a screen is drawn to
|
|
//! register against, and this experiment draws no screen yet (that is
|
|
//! E4's job, on iris). So every notification here takes the third branch
|
|
//! Kotlin's `show` already had -- the platform's own drawer -- which is
|
|
//! also exactly the case E3's pass condition asks for: **a notification
|
|
//! arrives with the app closed.**
|
|
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
use std::time::Duration;
|
|
|
|
use client_core::api::UreqTransport;
|
|
use client_core::notifications::{SessionNotification, follow_notifications};
|
|
use jni::Env;
|
|
use jni::errors::Result;
|
|
use jni::objects::{JObject, JValue};
|
|
use jni::sys::{JNI_TRUE, jint};
|
|
|
|
use crate::settings::{self, ServerSettings};
|
|
|
|
const ALERT_CHANNEL: &str = "sessions";
|
|
const ONGOING_CHANNEL: &str = "connection";
|
|
const ONGOING_ID: i32 = 1;
|
|
const ALERT_ID: i32 = 2;
|
|
/// Same backoff as `Notifications.kt`'s `RECONNECT_DELAY_MS`.
|
|
const RECONNECT_DELAY: Duration = Duration::from_millis(5_000);
|
|
|
|
/// Whether the follow-loop thread is already running. **A deviation from
|
|
/// `Notifications.kt`, found by testing rather than planned**: the Kotlin
|
|
/// `onStartCommand` spawns a fresh `thread(isDaemon = true) { follow(...) }`
|
|
/// on *every* call, with nothing to notice a previous one is still going --
|
|
/// and `sync()` calling `startForegroundService` when the service is
|
|
/// already running is an ordinary Android start, not a restart, so
|
|
/// `onStartCommand` runs again. Enrolling from `MainActivity` (which calls
|
|
/// `sync` once itself, then again inside `handle_enrollment` after saving
|
|
/// the token) hits exactly this path and was observed opening **two**
|
|
/// concurrent connections to `/notifications` from one process -- caught
|
|
/// on this build via `adb logcat` showing two `jni::vm::java_vm: Attached
|
|
/// thread ai-app-notifications` lines for one enrollment. Guarded here
|
|
/// rather than left to match Kotlin's behaviour exactly, since duplicating
|
|
/// a live connection is a resource leak with no upside; worth carrying the
|
|
/// same guard back to `Notifications.kt` separately.
|
|
static RUNNING: AtomicBool = AtomicBool::new(false);
|
|
|
|
/// Set by `nativeOnDestroy`, checked by the follow loop between
|
|
/// reconnects. **Known gap, recorded rather than hidden**: unlike
|
|
/// `HttpURLConnection.disconnect()` in the Kotlin original, nothing here
|
|
/// can interrupt a `ureq` read already blocked inside one connection --
|
|
/// `Transport::stream` hands back a plain `Read` with no cancellation
|
|
/// handle. So a stop lands at the next reconnect, not mid-read. `/notifications`
|
|
/// is idle between events (a keep-alive, per `server/src/routes.rs`), so in
|
|
/// practice this is a bounded wait rather than a hang; closing that gap
|
|
/// for real means adding a cancellation point to `client_core::Transport`,
|
|
/// which is a decision affecting every caller of that trait, not just this
|
|
/// one -- left for whoever next depends on prompt shutdown.
|
|
static STOPPING: AtomicBool = AtomicBool::new(false);
|
|
|
|
fn static_int(env: &mut Env, class: &str, field: &str) -> Result<i32> {
|
|
crate::jcall::get_static_field(env, class, field, "I")?.i()
|
|
}
|
|
|
|
fn notification_manager<'l>(env: &mut Env<'l>, context: &JObject) -> Result<JObject<'l>> {
|
|
crate::jcall::call_static_method(
|
|
env,
|
|
"androidx/core/app/NotificationManagerCompat",
|
|
"from",
|
|
"(Landroid/content/Context;)Landroidx/core/app/NotificationManagerCompat;",
|
|
&[JValue::Object(context)],
|
|
)?
|
|
.l()
|
|
}
|
|
|
|
fn create_channel(
|
|
env: &mut Env,
|
|
manager: &JObject,
|
|
id: &str,
|
|
name: &str,
|
|
importance: i32,
|
|
) -> Result<()> {
|
|
let id_j = crate::jcall::jstr_obj(env, id)?;
|
|
let builder = crate::jcall::new_object(
|
|
env,
|
|
"androidx/core/app/NotificationChannelCompat$Builder",
|
|
"(Ljava/lang/String;I)V",
|
|
&[JValue::Object(&id_j), JValue::Int(importance)],
|
|
)?;
|
|
let name_j = crate::jcall::jstr_obj(env, name)?;
|
|
crate::jcall::call_method(
|
|
env,
|
|
&builder,
|
|
"setName",
|
|
"(Ljava/lang/CharSequence;)Landroidx/core/app/NotificationChannelCompat$Builder;",
|
|
&[JValue::Object(&name_j)],
|
|
)?;
|
|
let channel = crate::jcall::call_method(
|
|
env,
|
|
&builder,
|
|
"build",
|
|
"()Landroidx/core/app/NotificationChannelCompat;",
|
|
&[],
|
|
)?
|
|
.l()?;
|
|
crate::jcall::call_method(
|
|
env,
|
|
manager,
|
|
"createNotificationChannel",
|
|
"(Landroidx/core/app/NotificationChannelCompat;)V",
|
|
&[JValue::Object(&channel)],
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Two channels, because they are two different things to be told -- see
|
|
/// `Notifications.kt`'s `createChannels` for the reasoning; the names and
|
|
/// importances here are copied from it exactly, since a phone that has
|
|
/// seen both apps should not learn two different vocabularies for the
|
|
/// same fact.
|
|
fn create_channels(env: &mut Env, context: &JObject) -> Result<()> {
|
|
let manager = notification_manager(env, context)?;
|
|
let default = static_int(
|
|
env,
|
|
"androidx/core/app/NotificationManagerCompat",
|
|
"IMPORTANCE_DEFAULT",
|
|
)?;
|
|
let min = static_int(
|
|
env,
|
|
"androidx/core/app/NotificationManagerCompat",
|
|
"IMPORTANCE_MIN",
|
|
)?;
|
|
create_channel(
|
|
env,
|
|
&manager,
|
|
ALERT_CHANNEL,
|
|
"Sessions needing attention",
|
|
default,
|
|
)?;
|
|
create_channel(env, &manager, ONGOING_CHANNEL, "Staying connected", min)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn new_intent_for<'l>(
|
|
env: &mut Env<'l>,
|
|
context: &JObject,
|
|
class_name: &str,
|
|
) -> Result<JObject<'l>> {
|
|
let target_class = crate::jcall::find_class(env, class_name)?;
|
|
crate::jcall::new_object(
|
|
env,
|
|
"android/content/Intent",
|
|
"(Landroid/content/Context;Ljava/lang/Class;)V",
|
|
&[JValue::Object(context), JValue::Object(&target_class)],
|
|
)
|
|
}
|
|
|
|
/// The intent a tap on an alert opens -- mirrors `Notifications.kt`'s
|
|
/// `sessionIntent`, including building the URI through `Uri.Builder`
|
|
/// rather than string concatenation, for the same reason: an id needing
|
|
/// escaping must survive the round trip.
|
|
fn session_intent<'l>(
|
|
env: &mut Env<'l>,
|
|
context: &JObject,
|
|
session_id: &str,
|
|
) -> Result<JObject<'l>> {
|
|
let intent = new_intent_for(env, context, "com/example/aiapp/shell/MainActivity")?;
|
|
let action_view = crate::jcall::jstr_obj(env, "android.intent.action.VIEW")?;
|
|
crate::jcall::call_method(
|
|
env,
|
|
&intent,
|
|
"setAction",
|
|
"(Ljava/lang/String;)Landroid/content/Intent;",
|
|
&[JValue::Object(&action_view)],
|
|
)?;
|
|
let builder = crate::jcall::new_object(env, "android/net/Uri$Builder", "()V", &[])?;
|
|
let scheme = crate::jcall::jstr_obj(env, settings::SCHEME)?;
|
|
crate::jcall::call_method(
|
|
env,
|
|
&builder,
|
|
"scheme",
|
|
"(Ljava/lang/String;)Landroid/net/Uri$Builder;",
|
|
&[JValue::Object(&scheme)],
|
|
)?;
|
|
let authority = crate::jcall::jstr_obj(env, "session")?;
|
|
crate::jcall::call_method(
|
|
env,
|
|
&builder,
|
|
"authority",
|
|
"(Ljava/lang/String;)Landroid/net/Uri$Builder;",
|
|
&[JValue::Object(&authority)],
|
|
)?;
|
|
let path = crate::jcall::jstr_obj(env, session_id)?;
|
|
crate::jcall::call_method(
|
|
env,
|
|
&builder,
|
|
"appendPath",
|
|
"(Ljava/lang/String;)Landroid/net/Uri$Builder;",
|
|
&[JValue::Object(&path)],
|
|
)?;
|
|
let uri = crate::jcall::call_method(env, &builder, "build", "()Landroid/net/Uri;", &[])?.l()?;
|
|
crate::jcall::call_method(
|
|
env,
|
|
&intent,
|
|
"setData",
|
|
"(Landroid/net/Uri;)Landroid/content/Intent;",
|
|
&[JValue::Object(&uri)],
|
|
)?;
|
|
Ok(intent)
|
|
}
|
|
|
|
fn pending_activity<'l>(
|
|
env: &mut Env<'l>,
|
|
context: &JObject,
|
|
intent: &JObject,
|
|
) -> Result<JObject<'l>> {
|
|
let update_current = static_int(env, "android/app/PendingIntent", "FLAG_UPDATE_CURRENT")?;
|
|
let immutable = static_int(env, "android/app/PendingIntent", "FLAG_IMMUTABLE")?;
|
|
crate::jcall::call_static_method(
|
|
env,
|
|
"android/app/PendingIntent",
|
|
"getActivity",
|
|
"(Landroid/content/Context;ILandroid/content/Intent;I)Landroid/app/PendingIntent;",
|
|
&[
|
|
JValue::Object(context),
|
|
JValue::Int(0),
|
|
JValue::Object(intent),
|
|
JValue::Int(update_current | immutable),
|
|
],
|
|
)?
|
|
.l()
|
|
}
|
|
|
|
fn builder_call<'l>(
|
|
env: &mut Env<'l>,
|
|
builder: &JObject<'l>,
|
|
method: &str,
|
|
sig: &str,
|
|
args: &[JValue],
|
|
) -> Result<()> {
|
|
crate::jcall::call_method(env, builder, method, sig, args)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// The type Android 14+ requires a foreground service to declare, and
|
|
/// nothing before it -- mirrors `Notifications.kt`'s `foregroundType`.
|
|
fn foreground_type(env: &mut Env) -> Result<i32> {
|
|
let sdk = static_int(env, "android/os/Build$VERSION", "SDK_INT")?;
|
|
let upside_down_cake = static_int(env, "android/os/Build$VERSION_CODES", "UPSIDE_DOWN_CAKE")?;
|
|
if sdk >= upside_down_cake {
|
|
static_int(
|
|
env,
|
|
"android/content/pm/ServiceInfo",
|
|
"FOREGROUND_SERVICE_TYPE_SPECIAL_USE",
|
|
)
|
|
} else {
|
|
Ok(0)
|
|
}
|
|
}
|
|
|
|
fn ongoing_notification<'l>(env: &mut Env<'l>, context: &JObject) -> Result<JObject<'l>> {
|
|
let channel = crate::jcall::jstr_obj(env, ONGOING_CHANNEL)?;
|
|
let builder = crate::jcall::new_object(
|
|
env,
|
|
"androidx/core/app/NotificationCompat$Builder",
|
|
"(Landroid/content/Context;Ljava/lang/String;)V",
|
|
&[JValue::Object(context), JValue::Object(&channel)],
|
|
)?;
|
|
let title = crate::jcall::jstr_obj(env, "Watching for sessions that need you")?;
|
|
builder_call(
|
|
env,
|
|
&builder,
|
|
"setContentTitle",
|
|
"(Ljava/lang/CharSequence;)Landroidx/core/app/NotificationCompat$Builder;",
|
|
&[JValue::Object(&title)],
|
|
)?;
|
|
let icon = static_int(env, "android/R$drawable", "stat_notify_sync")?;
|
|
builder_call(
|
|
env,
|
|
&builder,
|
|
"setSmallIcon",
|
|
"(I)Landroidx/core/app/NotificationCompat$Builder;",
|
|
&[JValue::Int(icon)],
|
|
)?;
|
|
builder_call(
|
|
env,
|
|
&builder,
|
|
"setOngoing",
|
|
"(Z)Landroidx/core/app/NotificationCompat$Builder;",
|
|
&[JValue::Bool(JNI_TRUE)],
|
|
)?;
|
|
let priority_min = static_int(env, "androidx/core/app/NotificationCompat", "PRIORITY_MIN")?;
|
|
builder_call(
|
|
env,
|
|
&builder,
|
|
"setPriority",
|
|
"(I)Landroidx/core/app/NotificationCompat$Builder;",
|
|
&[JValue::Int(priority_min)],
|
|
)?;
|
|
crate::jcall::call_method(env, &builder, "build", "()Landroid/app/Notification;", &[])?.l()
|
|
}
|
|
|
|
/// Starts the service if there is a server to connect to, and stops it
|
|
/// otherwise -- mirrors `Notifications.kt`'s `NotificationService.sync`.
|
|
pub fn sync(env: &mut Env, context: &JObject) -> Result<()> {
|
|
let service_intent =
|
|
new_intent_for(env, context, "com/example/aiapp/shell/NotificationService")?;
|
|
if settings::load(env, context)?.is_none() {
|
|
crate::jcall::call_method(
|
|
env,
|
|
context,
|
|
"stopService",
|
|
"(Landroid/content/Intent;)Z",
|
|
&[JValue::Object(&service_intent)],
|
|
)?;
|
|
return Ok(());
|
|
}
|
|
create_channels(env, context)?;
|
|
crate::jcall::call_static_method(
|
|
env,
|
|
"androidx/core/content/ContextCompat",
|
|
"startForegroundService",
|
|
"(Landroid/content/Context;Landroid/content/Intent;)V",
|
|
&[JValue::Object(context), JValue::Object(&service_intent)],
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// The `Service.onStartCommand` body -- loads settings, starts the
|
|
/// foreground notification, and spawns the follow-loop thread. Answers the
|
|
/// platform's `START_STICKY`/`START_NOT_STICKY` constant, read from the
|
|
/// framework rather than hardcoded so a wrong guess at their values cannot
|
|
/// silently pick the other behaviour.
|
|
pub fn on_start_command(env: &mut Env, service: JObject) -> jint {
|
|
match try_start(env, &service) {
|
|
Ok(true) => static_int(env, "android/app/Service", "START_STICKY").unwrap_or(1),
|
|
Ok(false) => {
|
|
let _ = crate::jcall::call_method(env, &service, "stopSelf", "()V", &[]);
|
|
static_int(env, "android/app/Service", "START_NOT_STICKY").unwrap_or(2)
|
|
}
|
|
Err(e) => {
|
|
log_error(env, "onStartCommand", &e);
|
|
static_int(env, "android/app/Service", "START_NOT_STICKY").unwrap_or(2)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn try_start(env: &mut Env, service: &JObject) -> Result<bool> {
|
|
let Some(settings) = settings::load(env, service)? else {
|
|
return Ok(false);
|
|
};
|
|
let ca = settings::load_pinned_ca(env)?;
|
|
let notification = ongoing_notification(env, service)?;
|
|
let fg_type = foreground_type(env)?;
|
|
crate::jcall::call_static_method(
|
|
env,
|
|
"androidx/core/app/ServiceCompat",
|
|
"startForeground",
|
|
"(Landroid/app/Service;ILandroid/app/Notification;I)V",
|
|
&[
|
|
JValue::Object(service),
|
|
JValue::Int(ONGOING_ID),
|
|
JValue::Object(¬ification),
|
|
JValue::Int(fg_type),
|
|
],
|
|
)?;
|
|
|
|
// See `RUNNING`'s doc: a second `onStartCommand` while the loop from
|
|
// the first is still going -- the ordinary case for this service,
|
|
// since `sync()` is called from more than one place -- must not open
|
|
// a second connection.
|
|
if RUNNING.swap(true, Ordering::SeqCst) {
|
|
return Ok(true);
|
|
}
|
|
|
|
let vm = env.get_java_vm()?;
|
|
let context = env.new_global_ref(service)?;
|
|
STOPPING.store(false, Ordering::SeqCst);
|
|
std::thread::Builder::new()
|
|
.name("ai-app-notifications".to_string())
|
|
.spawn(move || {
|
|
// Requests a *permanent* attachment (detached only when this thread
|
|
// exits), matching the Kotlin original's `thread(isDaemon = true)`:
|
|
// this is the long-lived follow loop, not a one-shot callback.
|
|
let _: jni::errors::Result<()> = vm.attach_current_thread(|env| {
|
|
follow_loop(env, &context, settings, &ca);
|
|
Ok(())
|
|
});
|
|
})
|
|
.ok();
|
|
Ok(true)
|
|
}
|
|
|
|
/// Follows the backend's notification stream, reconnecting until stopped
|
|
/// -- mirrors `Notifications.kt`'s `follow`. A dropped connection is the
|
|
/// ordinary case, so it retries quietly and forever; nothing is shown when
|
|
/// it cannot connect, for the same reason as the Kotlin original: a
|
|
/// notification saying "I could not tell you whether anything happened" is
|
|
/// noise about a condition nobody can act on.
|
|
fn follow_loop(env: &mut Env, context: &JObject, settings: ServerSettings, ca: &[u8]) {
|
|
while !STOPPING.load(Ordering::SeqCst) {
|
|
if let Ok(transport) = UreqTransport::new(settings.base_url(), settings.token.clone(), ca) {
|
|
let _ = follow_notifications(&transport, |notification| {
|
|
if let Err(e) = show(env, context, ¬ification) {
|
|
log_error(env, "show", &e);
|
|
}
|
|
!STOPPING.load(Ordering::SeqCst)
|
|
});
|
|
}
|
|
if STOPPING.load(Ordering::SeqCst) {
|
|
return;
|
|
}
|
|
std::thread::sleep(RECONNECT_DELAY);
|
|
}
|
|
}
|
|
|
|
/// One notification per session, replacing that session's previous one --
|
|
/// mirrors `Notifications.kt`'s `show`, minus the on-screen/banner
|
|
/// branches this module's doc comment explains.
|
|
fn show(env: &mut Env, context: &JObject, notification: &SessionNotification) -> Result<()> {
|
|
let manager = notification_manager(env, context)?;
|
|
let sdk = static_int(env, "android/os/Build$VERSION", "SDK_INT")?;
|
|
let tiramisu = static_int(env, "android/os/Build$VERSION_CODES", "TIRAMISU")?;
|
|
let allowed = if sdk < tiramisu {
|
|
true
|
|
} else {
|
|
let permission = crate::jcall::jstr_obj(env, "android.permission.POST_NOTIFICATIONS")?;
|
|
let granted = static_int(
|
|
env,
|
|
"android/content/pm/PackageManager",
|
|
"PERMISSION_GRANTED",
|
|
)?;
|
|
let result = crate::jcall::call_static_method(
|
|
env,
|
|
"androidx/core/content/ContextCompat",
|
|
"checkSelfPermission",
|
|
"(Landroid/content/Context;Ljava/lang/String;)I",
|
|
&[JValue::Object(context), JValue::Object(&permission)],
|
|
)?
|
|
.i()?;
|
|
result == granted
|
|
};
|
|
let enabled =
|
|
crate::jcall::call_method(env, &manager, "areNotificationsEnabled", "()Z", &[])?.z()?;
|
|
if !allowed || !enabled {
|
|
return Ok(());
|
|
}
|
|
let intent = session_intent(env, context, ¬ification.session_id)?;
|
|
let pending = pending_activity(env, context, &intent)?;
|
|
let channel = crate::jcall::jstr_obj(env, ALERT_CHANNEL)?;
|
|
let builder = crate::jcall::new_object(
|
|
env,
|
|
"androidx/core/app/NotificationCompat$Builder",
|
|
"(Landroid/content/Context;Ljava/lang/String;)V",
|
|
&[JValue::Object(context), JValue::Object(&channel)],
|
|
)?;
|
|
let title = crate::jcall::jstr_obj(env, ¬ification.title)?;
|
|
builder_call(
|
|
env,
|
|
&builder,
|
|
"setContentTitle",
|
|
"(Ljava/lang/CharSequence;)Landroidx/core/app/NotificationCompat$Builder;",
|
|
&[JValue::Object(&title)],
|
|
)?;
|
|
let text = crate::jcall::jstr_obj(env, notification.kind.attention_line())?;
|
|
builder_call(
|
|
env,
|
|
&builder,
|
|
"setContentText",
|
|
"(Ljava/lang/CharSequence;)Landroidx/core/app/NotificationCompat$Builder;",
|
|
&[JValue::Object(&text)],
|
|
)?;
|
|
let icon = static_int(env, "android/R$drawable", "stat_notify_chat")?;
|
|
builder_call(
|
|
env,
|
|
&builder,
|
|
"setSmallIcon",
|
|
"(I)Landroidx/core/app/NotificationCompat$Builder;",
|
|
&[JValue::Int(icon)],
|
|
)?;
|
|
builder_call(
|
|
env,
|
|
&builder,
|
|
"setContentIntent",
|
|
"(Landroid/app/PendingIntent;)Landroidx/core/app/NotificationCompat$Builder;",
|
|
&[JValue::Object(&pending)],
|
|
)?;
|
|
builder_call(
|
|
env,
|
|
&builder,
|
|
"setAutoCancel",
|
|
"(Z)Landroidx/core/app/NotificationCompat$Builder;",
|
|
&[JValue::Bool(JNI_TRUE)],
|
|
)?;
|
|
let when = (notification.at * 1000.0) as i64;
|
|
builder_call(
|
|
env,
|
|
&builder,
|
|
"setWhen",
|
|
"(J)Landroidx/core/app/NotificationCompat$Builder;",
|
|
&[JValue::Long(when)],
|
|
)?;
|
|
builder_call(
|
|
env,
|
|
&builder,
|
|
"setShowWhen",
|
|
"(Z)Landroidx/core/app/NotificationCompat$Builder;",
|
|
&[JValue::Bool(JNI_TRUE)],
|
|
)?;
|
|
let built =
|
|
crate::jcall::call_method(env, &builder, "build", "()Landroid/app/Notification;", &[])?
|
|
.l()?;
|
|
let tag = crate::jcall::jstr_obj(env, ¬ification.session_id)?;
|
|
crate::jcall::call_method(
|
|
env,
|
|
&manager,
|
|
"notify",
|
|
"(Ljava/lang/String;ILandroid/app/Notification;)V",
|
|
&[
|
|
JValue::Object(&tag),
|
|
JValue::Int(ALERT_ID),
|
|
JValue::Object(&built),
|
|
],
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Ends the follow loop -- mirrors `Notifications.kt`'s `onDestroy`, with
|
|
/// the gap this module's `STOPPING` doc explains.
|
|
pub fn on_destroy() {
|
|
STOPPING.store(true, Ordering::SeqCst);
|
|
// `RUNNING`'s path out. Same race as `STOPPING` itself (this doc's own
|
|
// comment): the old thread may still be inside a blocked read when a
|
|
// new `onStartCommand` follows immediately, which would spawn a
|
|
// second one before the first has actually stopped. Narrower than not
|
|
// resetting at all -- a service destroyed and never restarted would
|
|
// otherwise wedge `RUNNING` true forever -- and no worse than the
|
|
// known gap already accepted above.
|
|
RUNNING.store(false, Ordering::SeqCst);
|
|
}
|
|
|
|
pub fn log_error(env: &mut Env, where_: &str, error: &jni::errors::Error) {
|
|
let message = format!("android-shell: {where_}: {error}");
|
|
let _ = (|| -> Result<()> {
|
|
let tag = crate::jcall::jstr_obj(env, "android-shell")?;
|
|
let msg = crate::jcall::jstr_obj(env, &message)?;
|
|
crate::jcall::call_static_method(
|
|
env,
|
|
"android/util/Log",
|
|
"e",
|
|
"(Ljava/lang/String;Ljava/lang/String;)I",
|
|
&[JValue::Object(&tag), JValue::Object(&msg)],
|
|
)?;
|
|
Ok(())
|
|
})();
|
|
}
|