E3: the Kotlin/Java shell over a JNI bridge into Rust (RUST.md)
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>
This commit is contained in:
1 parent
8adda94a7a
commit
c9b273ff16
16 files changed
+2996
-6
No files matched your search
@@ -0,0 +1,50 @@
|
||||
package com.example.aiapp.shell;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.widget.Toast;
|
||||
|
||||
/**
|
||||
* E3's floor, per RUST.md's "How much Java is unavoidable": a class the framework
|
||||
* constructs by name from the manifest, with its lifecycle methods handing straight to Rust
|
||||
* (android-shell's {@code share::handle_intent}). No Compose, no layout -- there is no screen to
|
||||
* draw yet (that is E4's job, on iris); {@link #toast} is this experiment's stand-in for showing
|
||||
* something happened.
|
||||
*/
|
||||
public class MainActivity extends Activity {
|
||||
static {
|
||||
System.loadLibrary("android_shell");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
NotificationService.sync(this);
|
||||
nativeHandleIntent(this, getIntent());
|
||||
}
|
||||
|
||||
// launchMode="singleTop": a notification tap or a share while this activity is already on
|
||||
// top lands here rather than in a second instance -- same reasoning as MainActivity.kt's.
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent);
|
||||
nativeHandleIntent(this, intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from android-shell, sometimes from a background thread (a share's network call is
|
||||
* never made on the calling thread -- see share.rs). {@code Toast} itself is main-thread-only,
|
||||
* so this hops there with a {@link Handler} rather than assuming the caller already has.
|
||||
*/
|
||||
static void toast(Context context, String message) {
|
||||
new Handler(Looper.getMainLooper())
|
||||
.post(() -> Toast.makeText(context, message, Toast.LENGTH_LONG).show());
|
||||
}
|
||||
|
||||
private static native void nativeHandleIntent(Activity activity, Intent intent);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.example.aiapp.shell;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
|
||||
/**
|
||||
* E3's second unavoidable Java class (RUST.md): a foreground service constructed by the framework
|
||||
* from the manifest, existing only to hand its lifecycle to android-shell's {@code notify} module
|
||||
* -- the SSE follow loop, deciding what a notification says, and posting it are all Rust reached
|
||||
* through these three native calls. See {@code Notifications.kt}'s {@code NotificationService} for
|
||||
* the Kotlin original this mirrors.
|
||||
*/
|
||||
public class NotificationService extends Service {
|
||||
static {
|
||||
System.loadLibrary("android_shell");
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
return nativeOnStartCommand(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
nativeOnDestroy();
|
||||
}
|
||||
|
||||
/** Starts this service if there is a server to connect to, and stops it otherwise. */
|
||||
static void sync(Context context) {
|
||||
nativeSync(context);
|
||||
}
|
||||
|
||||
private static native void nativeSync(Context context);
|
||||
|
||||
private static native int nativeOnStartCommand(Service service);
|
||||
|
||||
private static native void nativeOnDestroy();
|
||||
}
|
||||
Reference in new issue
Block a user