Add Iris Android APK tooling
This commit is contained in:
1 parent
e137f38a5d
commit
df1290904b
24 files changed
+1814
-63
No files matched your search
@@ -112,8 +112,8 @@ fn battery_line(samples: &[i32]) -> String {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AndroidAppState for BenchClient {
|
impl BenchClient {
|
||||||
fn new(mut ui_state: AndroidUiState, rsc: &mut StdRsc<Self>) -> Self {
|
pub(super) fn new(mut ui_state: AndroidUiState, rsc: &mut StdRsc<Self>) -> Self {
|
||||||
let content = WidgetPtr::new().add(rsc);
|
let content = WidgetPtr::new().add(rsc);
|
||||||
let loading = placeholder(rsc, "Loading fixture...");
|
let loading = placeholder(rsc, "Loading fixture...");
|
||||||
content(rsc).set(loading);
|
content(rsc).set(loading);
|
||||||
@@ -186,16 +186,24 @@ impl AndroidAppState for BenchClient {
|
|||||||
}
|
}
|
||||||
client
|
client
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn platform_ready(&mut self, _rsc: &mut StdRsc<Self>, vm: JavaVM, view: GlobalRef) {
|
impl AndroidAppState for BenchClient {
|
||||||
|
type Resources = StdRsc<Self>;
|
||||||
|
|
||||||
|
fn platform_ready(&mut self, _rsc: &mut Self::Resources, vm: JavaVM, view: GlobalRef) {
|
||||||
self.platform = Some(Arc::new(PlatformHandle::new(vm, view)));
|
self.platform = Some(Arc::new(PlatformHandle::new(vm, view)));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn back_pressed(&mut self, _rsc: &mut StdRsc<Self>) -> bool {
|
fn back_pressed(&mut self, _rsc: &mut Self::Resources) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_insets_changed(&mut self, rsc: &mut StdRsc<Self>, insets: iris::android::WindowInsets) {
|
fn on_insets_changed(
|
||||||
|
&mut self,
|
||||||
|
rsc: &mut Self::Resources,
|
||||||
|
insets: iris::android::WindowInsets,
|
||||||
|
) {
|
||||||
if insets.top != self.last_top_pad {
|
if insets.top != self.last_top_pad {
|
||||||
self.last_top_pad = insets.top;
|
self.last_top_pad = insets.top;
|
||||||
let controls = bench_controls(rsc, insets.top);
|
let controls = bench_controls(rsc, insets.top);
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ extern "system" fn new_view_peer<'local>(
|
|||||||
view: View<'local>,
|
view: View<'local>,
|
||||||
context: Context<'local>,
|
context: Context<'local>,
|
||||||
) -> jlong {
|
) -> jlong {
|
||||||
iris::android::new_peer::<ActiveClient>(env, view, context)
|
iris::android::new_peer::<ActiveClient>(env, view, context, ActiveClient::new)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// # Safety
|
/// # Safety
|
||||||
|
|||||||
@@ -89,8 +89,8 @@ fn frame_report_controls(rsc: &mut StdRsc<TranscriptClient>) -> WeakWidget {
|
|||||||
(report, reset).span(Dir::RIGHT).height(56).add(rsc)
|
(report, reset).span(Dir::RIGHT).height(56).add(rsc)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AndroidAppState for TranscriptClient {
|
impl TranscriptClient {
|
||||||
fn new(mut ui_state: AndroidUiState, rsc: &mut StdRsc<Self>) -> Self {
|
pub(super) fn new(mut ui_state: AndroidUiState, rsc: &mut StdRsc<Self>) -> Self {
|
||||||
let content = WidgetPtr::new().add(rsc);
|
let content = WidgetPtr::new().add(rsc);
|
||||||
let loading = placeholder(rsc, "Loading sessions...");
|
let loading = placeholder(rsc, "Loading sessions...");
|
||||||
content(rsc).set(loading);
|
content(rsc).set(loading);
|
||||||
@@ -112,8 +112,12 @@ impl AndroidAppState for TranscriptClient {
|
|||||||
client.spawn_fetch_sessions(rsc);
|
client.spawn_fetch_sessions(rsc);
|
||||||
client
|
client
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn back_pressed(&mut self, _rsc: &mut StdRsc<Self>) -> bool {
|
impl AndroidAppState for TranscriptClient {
|
||||||
|
type Resources = StdRsc<Self>;
|
||||||
|
|
||||||
|
fn back_pressed(&mut self, _rsc: &mut Self::Resources) -> bool {
|
||||||
// No screen stack of its own -- same "let the activity finish"
|
// No screen stack of its own -- same "let the activity finish"
|
||||||
// answer `iris-android-app`'s tabs `Client` already gives.
|
// answer `iris-android-app`'s tabs `Client` already gives.
|
||||||
false
|
false
|
||||||
|
|||||||
@@ -790,6 +790,33 @@ ssh case are one implementation.
|
|||||||
`--bind <ip>` is an *explicit, logged* override for development, a
|
`--bind <ip>` is an *explicit, logged* override for development, a
|
||||||
deliberate flag and never a fallback.
|
deliberate flag and never a fallback.
|
||||||
|
|
||||||
|
## Iris Android application contract (2026-09-11)
|
||||||
|
|
||||||
|
**An Iris Android application supplies an initialization function and its own
|
||||||
|
state/resources; Iris supplies the JNI host and APK packager.**
|
||||||
|
`#[iris::app_init]` marks the factory Android calls when it creates the Iris
|
||||||
|
view. The macro target-gates the factory and generates the single exported
|
||||||
|
`JNI_OnLoad` plus the concrete `android-view` registration callback. The
|
||||||
|
function returns the application state, whose `AndroidAppState::Resources`
|
||||||
|
associated type selects the resource bundle. `StdRsc` is the default, never a
|
||||||
|
host requirement; a custom bundle works by implementing the narrow
|
||||||
|
`AndroidResources` capabilities.
|
||||||
|
|
||||||
|
`cargo-iris` is an installable Cargo subcommand, rather than a script callers
|
||||||
|
must find inside an Iris checkout. `cargo iris apk` builds the Rust `cdylib`
|
||||||
|
with cargo-ndk and packages it directly with the installed Android SDK tools:
|
||||||
|
javac, d8, aapt2, jar, zipalign and apksigner. Gradle is not in the ordinary
|
||||||
|
path because Iris's fixed Java view host has no Maven/AAR dependency or
|
||||||
|
variant graph for it to manage. An application that later embeds Iris in a
|
||||||
|
larger Gradle project can use that project as the packaging authority instead.
|
||||||
|
|
||||||
|
The tool discovers and validates prerequisites but never installs an SDK,
|
||||||
|
NDK, JDK, system image or emulator. `cargo iris run` requires an explicit
|
||||||
|
device serial; it never chooses, creates, starts or stops a device. Debug APKs
|
||||||
|
use Android's conventional debug key. Release signing requires the caller's
|
||||||
|
explicit keystore, alias and environment-supplied passwords, since Iris must
|
||||||
|
not create or own an application's permanent update identity.
|
||||||
|
|
||||||
## App (`app/`)
|
## App (`app/`)
|
||||||
|
|
||||||
One Rust crate owns platform-free client logic and Iris widget trees. Android
|
One Rust crate owns platform-free client logic and Iris widget trees. Android
|
||||||
|
|||||||
Generated
+75
-6
@@ -681,6 +681,46 @@ dependencies = [
|
|||||||
"wayland-client",
|
"wayland-client",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "camino"
|
||||||
|
version = "1.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cargo-iris"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"cargo_metadata",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cargo-platform"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dd0061da739915fae12ea00e16397555ed4371a6bb285431aab930f61b0aa4ba"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cargo_metadata"
|
||||||
|
version = "0.23.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9"
|
||||||
|
dependencies = [
|
||||||
|
"camino",
|
||||||
|
"cargo-platform",
|
||||||
|
"semver",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror 2.0.20",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.4.5"
|
version = "1.4.5"
|
||||||
@@ -965,7 +1005,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1587,6 +1627,12 @@ dependencies = [
|
|||||||
"either",
|
"either",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jni"
|
name = "jni"
|
||||||
version = "0.21.1"
|
version = "0.21.1"
|
||||||
@@ -2406,7 +2452,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3030,7 +3076,7 @@ dependencies = [
|
|||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.12.1",
|
"linux-raw-sys 0.12.1",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3078,6 +3124,10 @@ name = "semver"
|
|||||||
version = "1.0.28"
|
version = "1.0.28"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "send_wrapper"
|
name = "send_wrapper"
|
||||||
@@ -3115,6 +3165,19 @@ dependencies = [
|
|||||||
"syn 3.0.5",
|
"syn 3.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.151"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_repr"
|
name = "serde_repr"
|
||||||
version = "0.1.21"
|
version = "0.1.21"
|
||||||
@@ -3325,7 +3388,7 @@ dependencies = [
|
|||||||
"getrandom 0.4.3",
|
"getrandom 0.4.3",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix 1.1.4",
|
"rustix 1.1.4",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3522,7 +3585,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"memoffset",
|
"memoffset",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3975,7 +4038,7 @@ version = "0.1.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4713,6 +4776,12 @@ version = "0.6.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12"
|
checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zune-core"
|
name = "zune-core"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ harness = false
|
|||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
|
"cargo-iris",
|
||||||
"core",
|
"core",
|
||||||
"macro",
|
"macro",
|
||||||
"rig-input",
|
"rig-input",
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[package]
|
||||||
|
name = "cargo-iris"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cargo_metadata = "0.23.1"
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package dev.iris.android;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.widget.ScrollView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import org.linebender.android.rustview.RustView;
|
||||||
|
|
||||||
|
public final class IrisView extends RustView {
|
||||||
|
@Override
|
||||||
|
protected native long newViewPeer(Context context);
|
||||||
|
|
||||||
|
native void applyWindowInsetsNative(
|
||||||
|
long peer, int left, int top, int right, int bottom, int imeBottom, int imeVisible);
|
||||||
|
|
||||||
|
native void unregisterInsetsNative(long peer);
|
||||||
|
|
||||||
|
public IrisView(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
void applyWindowInsets(
|
||||||
|
int left, int top, int right, int bottom, int imeBottom, int imeVisible) {
|
||||||
|
applyWindowInsetsNative(mViewPeer, left, top, right, bottom, imeBottom, imeVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDetachedFromWindow() {
|
||||||
|
unregisterInsetsNative(mViewPeer);
|
||||||
|
super.onDetachedFromWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
void showRendererError(String report) {
|
||||||
|
Context context = getContext();
|
||||||
|
if (!(context instanceof Activity)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Activity activity = (Activity) context;
|
||||||
|
TextView text = new TextView(activity);
|
||||||
|
text.setText(report);
|
||||||
|
text.setTextIsSelectable(true);
|
||||||
|
text.setGravity(Gravity.TOP | Gravity.START);
|
||||||
|
int pad = (int) (16 * activity.getResources().getDisplayMetrics().density);
|
||||||
|
text.setPadding(pad, pad, pad, pad);
|
||||||
|
ScrollView scroll = new ScrollView(activity);
|
||||||
|
scroll.addView(text);
|
||||||
|
activity.setContentView(scroll);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package dev.iris.android;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.view.WindowInsetsAnimation;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class MainActivity extends Activity {
|
||||||
|
static {
|
||||||
|
System.loadLibrary("IRIS_NATIVE_LIBRARY");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle state) {
|
||||||
|
super.onCreate(state);
|
||||||
|
IrisView view = new IrisView(this);
|
||||||
|
view.setLayoutParams(new FrameLayout.LayoutParams(
|
||||||
|
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
|
||||||
|
view.setFocusable(true);
|
||||||
|
view.setFocusableInTouchMode(true);
|
||||||
|
FrameLayout layout = new FrameLayout(this);
|
||||||
|
layout.addView(view);
|
||||||
|
setContentView(layout);
|
||||||
|
view.requestFocus();
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
drawBehindSystemBars();
|
||||||
|
view.setWindowInsetsAnimationCallback(new WindowInsetsAnimation.Callback(
|
||||||
|
WindowInsetsAnimation.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
|
||||||
|
@Override
|
||||||
|
public WindowInsets onProgress(
|
||||||
|
WindowInsets insets, List<WindowInsetsAnimation> running) {
|
||||||
|
sendInsets(view, insets);
|
||||||
|
return insets;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnd(WindowInsetsAnimation animation) {
|
||||||
|
WindowInsets settled = view.getRootWindowInsets();
|
||||||
|
if (settled != null) {
|
||||||
|
sendInsets(view, settled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
view.setOnApplyWindowInsetsListener((v, insets) -> {
|
||||||
|
sendInsets((IrisView) v, insets);
|
||||||
|
return insets;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Android 15 deprecated this API in favor of edge-to-edge enforcement,
|
||||||
|
// but API 30 through 34 still need it and Iris supports that whole range.
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
private void drawBehindSystemBars() {
|
||||||
|
getWindow().setDecorFitsSystemWindows(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// API 29 has no replacement for these four system-window inset getters;
|
||||||
|
// the API 30 methods cannot run on Iris's supported minimum.
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
private static void sendInsets(IrisView view, WindowInsets insets) {
|
||||||
|
int imeBottom = 0;
|
||||||
|
int imeVisible = 0;
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
imeBottom = insets.getInsets(WindowInsets.Type.ime()).bottom;
|
||||||
|
imeVisible = insets.isVisible(WindowInsets.Type.ime()) ? 1 : 0;
|
||||||
|
}
|
||||||
|
view.applyWindowInsets(
|
||||||
|
insets.getSystemWindowInsetLeft(),
|
||||||
|
insets.getSystemWindowInsetTop(),
|
||||||
|
insets.getSystemWindowInsetRight(),
|
||||||
|
insets.getSystemWindowInsetBottom(),
|
||||||
|
imeBottom,
|
||||||
|
imeVisible);
|
||||||
|
}
|
||||||
|
}
|
||||||
+153
@@ -0,0 +1,153 @@
|
|||||||
|
package org.linebender.android.rustview;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.view.inputmethod.CompletionInfo;
|
||||||
|
import android.view.inputmethod.CorrectionInfo;
|
||||||
|
import android.view.inputmethod.ExtractedText;
|
||||||
|
import android.view.inputmethod.ExtractedTextRequest;
|
||||||
|
import android.view.inputmethod.InputConnection;
|
||||||
|
import android.view.inputmethod.InputContentInfo;
|
||||||
|
|
||||||
|
class RustInputConnection implements InputConnection {
|
||||||
|
private final RustView mView;
|
||||||
|
|
||||||
|
RustInputConnection(RustView view) {
|
||||||
|
mView = view;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getViewPeer() {
|
||||||
|
return mView.mViewPeer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CharSequence getTextBeforeCursor(int n, int flags) {
|
||||||
|
return mView.getTextBeforeCursorNative(getViewPeer(), n);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CharSequence getTextAfterCursor(int n, int flags) {
|
||||||
|
return mView.getTextAfterCursorNative(getViewPeer(), n);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CharSequence getSelectedText(int flags) {
|
||||||
|
return mView.getSelectedTextNative(getViewPeer());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCursorCapsMode(int reqModes) {
|
||||||
|
return mView.getCursorCapsModeNative(getViewPeer(), reqModes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
|
||||||
|
return mView.deleteSurroundingTextNative(getViewPeer(), beforeLength, afterLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) {
|
||||||
|
return mView.deleteSurroundingTextInCodePointsNative(getViewPeer(), beforeLength, afterLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean setComposingText(CharSequence text, int newCursorPosition) {
|
||||||
|
return mView.setComposingTextNative(getViewPeer(), text.toString(), newCursorPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean setComposingRegion(int start, int end) {
|
||||||
|
return mView.setComposingRegionNative(getViewPeer(), start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean finishComposingText() {
|
||||||
|
return mView.finishComposingTextNative(getViewPeer());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean commitText(CharSequence text, int newCursorPosition) {
|
||||||
|
return mView.commitTextNative(getViewPeer(), text.toString(), newCursorPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean commitCompletion(CompletionInfo text) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean commitCorrection(CorrectionInfo correctionInfo) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean setSelection(int start, int end) {
|
||||||
|
return mView.setSelectionNative(getViewPeer(), start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean performEditorAction(int editorAction) {
|
||||||
|
return mView.performEditorActionNative(getViewPeer(), editorAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean performContextMenuAction(int id) {
|
||||||
|
return mView.performContextMenuActionNative(getViewPeer(), id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean beginBatchEdit() {
|
||||||
|
return mView.beginBatchEditNative(getViewPeer());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean endBatchEdit() {
|
||||||
|
return mView.endBatchEditNative(getViewPeer());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean sendKeyEvent(KeyEvent event) {
|
||||||
|
return mView.inputConnectionSendKeyEventNative(getViewPeer(), event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean clearMetaKeyStates(int states) {
|
||||||
|
return mView.inputConnectionClearMetaKeyStatesNative(getViewPeer(), states);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean reportFullscreenMode(boolean enabled) {
|
||||||
|
return mView.inputConnectionReportFullscreenModeNative(getViewPeer(), enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean performPrivateCommand(String action, Bundle data) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean requestCursorUpdates(int cursorUpdateMode) {
|
||||||
|
return mView.requestCursorUpdatesNative(getViewPeer(), cursorUpdateMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Handler getHandler() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void closeConnection() {
|
||||||
|
mView.closeInputConnectionNative(getViewPeer());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,287 @@
|
|||||||
|
package org.linebender.android.rustview;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.Choreographer;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.SurfaceHolder;
|
||||||
|
import android.view.SurfaceView;
|
||||||
|
import android.view.accessibility.AccessibilityNodeInfo;
|
||||||
|
import android.view.accessibility.AccessibilityNodeProvider;
|
||||||
|
import android.view.inputmethod.EditorInfo;
|
||||||
|
import android.view.inputmethod.InputConnection;
|
||||||
|
import android.view.inputmethod.InputMethodManager;
|
||||||
|
|
||||||
|
public abstract class RustView extends SurfaceView
|
||||||
|
implements SurfaceHolder.Callback, Choreographer.FrameCallback {
|
||||||
|
// Vendored from android-view bec6c62. The only local change is `protected`,
|
||||||
|
// allowing IrisView to forward insets through this native peer.
|
||||||
|
protected final long mViewPeer;
|
||||||
|
final InputMethodManager mInputMethodManager;
|
||||||
|
|
||||||
|
protected abstract long newViewPeer(Context context);
|
||||||
|
|
||||||
|
public RustView(Context context) {
|
||||||
|
super(context);
|
||||||
|
mViewPeer = newViewPeer(context);
|
||||||
|
getHolder().addCallback(this);
|
||||||
|
mInputMethodManager =
|
||||||
|
(InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private native int[] onMeasureNative(long peer, int widthSpec, int heightSpec);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onMeasure(int widthSpec, int heightSpec) {
|
||||||
|
int[] result = onMeasureNative(mViewPeer, widthSpec, heightSpec);
|
||||||
|
if (result != null) {
|
||||||
|
setMeasuredDimension(result[0], result[1]);
|
||||||
|
} else {
|
||||||
|
super.onMeasure(widthSpec, heightSpec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private native void onLayoutNative(
|
||||||
|
long peer, boolean changed, int left, int top, int right, int bottom);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||||
|
onLayoutNative(mViewPeer, changed, left, top, right, bottom);
|
||||||
|
super.onLayout(changed, left, top, right, bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
private native void onSizeChangedNative(long peer, int w, int h, int oldw, int oldh);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||||
|
onSizeChangedNative(mViewPeer, w, h, oldw, oldh);
|
||||||
|
super.onSizeChanged(w, h, oldw, oldh);
|
||||||
|
}
|
||||||
|
|
||||||
|
private native boolean onKeyDownNative(long peer, int keyCode, KeyEvent event);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||||
|
return onKeyDownNative(mViewPeer, keyCode, event) || super.onKeyDown(keyCode, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private native boolean onKeyUpNative(long peer, int keyCode, KeyEvent event);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||||
|
return onKeyUpNative(mViewPeer, keyCode, event) || super.onKeyUp(keyCode, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private native boolean onTrackballEventNative(long peer, MotionEvent event);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onTrackballEvent(MotionEvent event) {
|
||||||
|
return onTrackballEventNative(mViewPeer, event) || super.onTrackballEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private native boolean onTouchEventNative(long peer, MotionEvent event);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onTouchEvent(MotionEvent event) {
|
||||||
|
return onTouchEventNative(mViewPeer, event) || super.onTouchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private native boolean onGenericMotionEventNative(long peer, MotionEvent event);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onGenericMotionEvent(MotionEvent event) {
|
||||||
|
return onGenericMotionEventNative(mViewPeer, event) || super.onGenericMotionEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private native boolean onHoverEventNative(long peer, MotionEvent event);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onHoverEvent(MotionEvent event) {
|
||||||
|
return onHoverEventNative(mViewPeer, event) || super.onHoverEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private native void onFocusChangedNative(
|
||||||
|
long peer, boolean gainFocus, int direction, Rect previouslyFocusedRect);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
|
||||||
|
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
|
||||||
|
onFocusChangedNative(mViewPeer, gainFocus, direction, previouslyFocusedRect);
|
||||||
|
}
|
||||||
|
|
||||||
|
private native void onWindowFocusChangedNative(long peer, boolean hasWindowFocus);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWindowFocusChanged(boolean hasWindowFocus) {
|
||||||
|
super.onWindowFocusChanged(hasWindowFocus);
|
||||||
|
onWindowFocusChangedNative(mViewPeer, hasWindowFocus);
|
||||||
|
}
|
||||||
|
|
||||||
|
private native void onAttachedToWindowNative(long peer);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onAttachedToWindow() {
|
||||||
|
super.onAttachedToWindow();
|
||||||
|
onAttachedToWindowNative(mViewPeer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private native void onDetachedFromWindowNative(long peer);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDetachedFromWindow() {
|
||||||
|
super.onDetachedFromWindow();
|
||||||
|
onDetachedFromWindowNative(mViewPeer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private native void onWindowVisibilityChangedNative(long peer, int visibility);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onWindowVisibilityChanged(int visibility) {
|
||||||
|
super.onWindowVisibilityChanged(visibility);
|
||||||
|
onWindowVisibilityChangedNative(mViewPeer, visibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
private native void surfaceCreatedNative(long peer, SurfaceHolder holder);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void surfaceCreated(SurfaceHolder holder) {
|
||||||
|
surfaceCreatedNative(mViewPeer, holder);
|
||||||
|
}
|
||||||
|
|
||||||
|
private native void surfaceChangedNative(
|
||||||
|
long peer, SurfaceHolder holder, int format, int width, int height);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
|
||||||
|
surfaceChangedNative(mViewPeer, holder, format, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
private native void surfaceDestroyedNative(long peer, SurfaceHolder holder);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||||
|
surfaceDestroyedNative(mViewPeer, holder);
|
||||||
|
}
|
||||||
|
|
||||||
|
void postFrameCallback() {
|
||||||
|
Choreographer c = Choreographer.getInstance();
|
||||||
|
c.removeFrameCallback(this);
|
||||||
|
c.postFrameCallback(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeFrameCallback() {
|
||||||
|
Choreographer.getInstance().removeFrameCallback(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private native void doFrameNative(long peer, long frameTimeNanos);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doFrame(long frameTimeNanos) {
|
||||||
|
doFrameNative(mViewPeer, frameTimeNanos);
|
||||||
|
}
|
||||||
|
|
||||||
|
private native void delayedCallbackNative(long peer);
|
||||||
|
|
||||||
|
private final Runnable mDelayedCallback =
|
||||||
|
new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
delayedCallbackNative(mViewPeer);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
boolean postDelayed(long delayMillis) {
|
||||||
|
return postDelayed(mDelayedCallback, delayMillis);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean removeDelayedCallbacks() {
|
||||||
|
return removeCallbacks(mDelayedCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
private native boolean hasAccessibilityNodeProviderNative(long peer);
|
||||||
|
|
||||||
|
private native AccessibilityNodeInfo createAccessibilityNodeInfoNative(
|
||||||
|
long peer, int virtualViewId);
|
||||||
|
|
||||||
|
private native AccessibilityNodeInfo accessibilityFindFocusNative(long peer, int virtualViewId);
|
||||||
|
|
||||||
|
private native boolean performAccessibilityActionNative(
|
||||||
|
long peer, int virtualViewId, int action, Bundle arguments);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AccessibilityNodeProvider getAccessibilityNodeProvider() {
|
||||||
|
if (!hasAccessibilityNodeProviderNative(mViewPeer)) {
|
||||||
|
return super.getAccessibilityNodeProvider();
|
||||||
|
}
|
||||||
|
return new AccessibilityNodeProvider() {
|
||||||
|
@Override
|
||||||
|
public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
|
||||||
|
return createAccessibilityNodeInfoNative(mViewPeer, virtualViewId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AccessibilityNodeInfo findFocus(int focusType) {
|
||||||
|
return accessibilityFindFocusNative(mViewPeer, focusType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean performAction(int virtualViewId, int action, Bundle arguments) {
|
||||||
|
return performAccessibilityActionNative(
|
||||||
|
mViewPeer, virtualViewId, action, arguments);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private native boolean onCreateInputConnectionNative(long peer, EditorInfo outAttrs);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
|
||||||
|
if (!onCreateInputConnectionNative(mViewPeer, outAttrs)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new RustInputConnection(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
native String getTextBeforeCursorNative(long peer, int n);
|
||||||
|
|
||||||
|
native String getTextAfterCursorNative(long peer, int n);
|
||||||
|
|
||||||
|
native String getSelectedTextNative(long peer);
|
||||||
|
|
||||||
|
native int getCursorCapsModeNative(long peer, int reqModes);
|
||||||
|
|
||||||
|
native boolean deleteSurroundingTextNative(long peer, int beforeLength, int afterLength);
|
||||||
|
|
||||||
|
native boolean deleteSurroundingTextInCodePointsNative(
|
||||||
|
long peer, int beforeLength, int afterLength);
|
||||||
|
|
||||||
|
native boolean setComposingTextNative(long peer, String text, int newCursorPosition);
|
||||||
|
|
||||||
|
native boolean setComposingRegionNative(long peer, int start, int end);
|
||||||
|
|
||||||
|
native boolean finishComposingTextNative(long peer);
|
||||||
|
|
||||||
|
native boolean commitTextNative(long peer, String text, int newCursorPosition);
|
||||||
|
|
||||||
|
native boolean setSelectionNative(long peer, int start, int end);
|
||||||
|
|
||||||
|
native boolean performEditorActionNative(long peer, int editorAction);
|
||||||
|
|
||||||
|
native boolean performContextMenuActionNative(long peer, int id);
|
||||||
|
|
||||||
|
native boolean beginBatchEditNative(long peer);
|
||||||
|
|
||||||
|
native boolean endBatchEditNative(long peer);
|
||||||
|
|
||||||
|
native boolean inputConnectionSendKeyEventNative(long peer, KeyEvent event);
|
||||||
|
|
||||||
|
native boolean inputConnectionClearMetaKeyStatesNative(long peer, int states);
|
||||||
|
|
||||||
|
native boolean inputConnectionReportFullscreenModeNative(long peer, boolean enabled);
|
||||||
|
|
||||||
|
native boolean requestCursorUpdatesNative(long peer, int cursorUpdateMode);
|
||||||
|
|
||||||
|
native void closeInputConnectionNative(long peer);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
mod package;
|
||||||
|
|
||||||
|
use std::{env, process::ExitCode};
|
||||||
|
|
||||||
|
fn main() -> ExitCode {
|
||||||
|
match package::run(env::args().skip(1).collect()) {
|
||||||
|
Ok(()) => ExitCode::SUCCESS,
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("cargo iris: {error}");
|
||||||
|
ExitCode::FAILURE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,710 @@
|
|||||||
|
use cargo_metadata::{CrateType, MetadataCommand, Package};
|
||||||
|
use std::{
|
||||||
|
env,
|
||||||
|
ffi::OsStr,
|
||||||
|
fs,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
process::{Command, Stdio},
|
||||||
|
};
|
||||||
|
|
||||||
|
const MIN_SDK: u32 = 29;
|
||||||
|
const ACTIVITY: &str = "dev.iris.android.MainActivity";
|
||||||
|
|
||||||
|
pub fn run(mut args: Vec<String>) -> Result<(), String> {
|
||||||
|
if args.first().is_some_and(|arg| arg == "iris") {
|
||||||
|
args.remove(0);
|
||||||
|
}
|
||||||
|
let command = args.first().map(String::as_str).unwrap_or("help");
|
||||||
|
if matches!(command, "help" | "--help" | "-h") {
|
||||||
|
print_help();
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if !matches!(command, "apk" | "run") {
|
||||||
|
return Err(format!(
|
||||||
|
"unknown command {command:?}; run `cargo iris --help`"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let options = Options::parse(&args[1..], command == "run")?;
|
||||||
|
let built = build(&options)?;
|
||||||
|
println!("{}", built.apk.display());
|
||||||
|
if command == "run" {
|
||||||
|
install_and_run(&built, options.device.as_deref().unwrap())?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_help() {
|
||||||
|
println!(
|
||||||
|
"Build an Iris library as an installable Android APK.\n\n\
|
||||||
|
Usage:\n cargo iris apk [OPTIONS]\n cargo iris run --device SERIAL [OPTIONS]\n\n\
|
||||||
|
Options:\n --manifest-path PATH\n --package NAME\n --abi arm64-v8a|x86_64\n --release\n\
|
||||||
|
\x20 --application-id ID\n --label TEXT\n --keystore PATH --key-alias ALIAS\n\n\
|
||||||
|
Release signing passwords come from IRIS_KEYSTORE_PASSWORD and, when different,\n\
|
||||||
|
IRIS_KEY_PASSWORD. Iris uses the Android SDK and emulator/device supplied by you."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Options {
|
||||||
|
manifest_path: Option<PathBuf>,
|
||||||
|
package: Option<String>,
|
||||||
|
abi: String,
|
||||||
|
release: bool,
|
||||||
|
application_id: Option<String>,
|
||||||
|
label: Option<String>,
|
||||||
|
keystore: Option<PathBuf>,
|
||||||
|
key_alias: Option<String>,
|
||||||
|
device: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Options {
|
||||||
|
fn parse(args: &[String], run: bool) -> Result<Self, String> {
|
||||||
|
let mut options = Self {
|
||||||
|
abi: "arm64-v8a".into(),
|
||||||
|
..Self::default()
|
||||||
|
};
|
||||||
|
let mut i = 0;
|
||||||
|
while i < args.len() {
|
||||||
|
let value = |name: &str, i: &mut usize| -> Result<String, String> {
|
||||||
|
*i += 1;
|
||||||
|
args.get(*i)
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| format!("{name} needs a value"))
|
||||||
|
};
|
||||||
|
match args[i].as_str() {
|
||||||
|
"--manifest-path" => {
|
||||||
|
options.manifest_path = Some(value("--manifest-path", &mut i)?.into())
|
||||||
|
}
|
||||||
|
"--package" => options.package = Some(value("--package", &mut i)?),
|
||||||
|
"--abi" => options.abi = value("--abi", &mut i)?,
|
||||||
|
"--application-id" => {
|
||||||
|
options.application_id = Some(value("--application-id", &mut i)?)
|
||||||
|
}
|
||||||
|
"--label" => options.label = Some(value("--label", &mut i)?),
|
||||||
|
"--keystore" => options.keystore = Some(value("--keystore", &mut i)?.into()),
|
||||||
|
"--key-alias" => options.key_alias = Some(value("--key-alias", &mut i)?),
|
||||||
|
"--device" => options.device = Some(value("--device", &mut i)?),
|
||||||
|
"--release" => options.release = true,
|
||||||
|
other => return Err(format!("unknown option {other:?}")),
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
if !matches!(options.abi.as_str(), "arm64-v8a" | "x86_64") {
|
||||||
|
return Err(format!(
|
||||||
|
"unsupported ABI {:?}; use arm64-v8a or x86_64",
|
||||||
|
options.abi
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if run && options.device.is_none() {
|
||||||
|
return Err(
|
||||||
|
"`cargo iris run` needs --device SERIAL; Iris never chooses or starts an emulator"
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if options.release && (options.keystore.is_none() || options.key_alias.is_none()) {
|
||||||
|
return Err("a release APK needs --keystore PATH and --key-alias ALIAS".into());
|
||||||
|
}
|
||||||
|
Ok(options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Built {
|
||||||
|
apk: PathBuf,
|
||||||
|
application_id: String,
|
||||||
|
sdk: Sdk,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(options: &Options) -> Result<Built, String> {
|
||||||
|
let mut metadata = MetadataCommand::new();
|
||||||
|
if let Some(path) = &options.manifest_path {
|
||||||
|
metadata.manifest_path(path);
|
||||||
|
}
|
||||||
|
let metadata = metadata
|
||||||
|
.exec()
|
||||||
|
.map_err(|error| format!("could not read Cargo metadata: {error}"))?;
|
||||||
|
let package = select_package(
|
||||||
|
&metadata.packages,
|
||||||
|
metadata.root_package(),
|
||||||
|
options.package.as_deref(),
|
||||||
|
)?;
|
||||||
|
let target = package
|
||||||
|
.targets
|
||||||
|
.iter()
|
||||||
|
.find(|target| {
|
||||||
|
target
|
||||||
|
.crate_types
|
||||||
|
.iter()
|
||||||
|
.any(|kind| kind == &CrateType::CDyLib)
|
||||||
|
})
|
||||||
|
.ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"package {} has no cdylib target; add `[lib] crate-type = [\"cdylib\", \"rlib\"]` to {}",
|
||||||
|
package.name, package.manifest_path
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let sdk = Sdk::find()?;
|
||||||
|
let application_id = options
|
||||||
|
.application_id
|
||||||
|
.clone()
|
||||||
|
.or_else(|| metadata_string(package, "application-id"))
|
||||||
|
.unwrap_or_else(|| default_application_id(&package.name));
|
||||||
|
validate_application_id(&application_id)?;
|
||||||
|
let label = options
|
||||||
|
.label
|
||||||
|
.clone()
|
||||||
|
.or_else(|| metadata_string(package, "label"))
|
||||||
|
.unwrap_or_else(|| package.name.to_string());
|
||||||
|
let variant = if options.release { "release" } else { "debug" };
|
||||||
|
let output = metadata
|
||||||
|
.target_directory
|
||||||
|
.as_std_path()
|
||||||
|
.join("iris-android")
|
||||||
|
.join(package.name.as_str())
|
||||||
|
.join(variant)
|
||||||
|
.join(&options.abi);
|
||||||
|
recreate(&output)?;
|
||||||
|
let native = output.join("native");
|
||||||
|
|
||||||
|
let mut cargo = Command::new("cargo");
|
||||||
|
cargo
|
||||||
|
.args(["ndk", "-t", &options.abi, "-P", &MIN_SDK.to_string(), "-o"])
|
||||||
|
.arg(&native)
|
||||||
|
.arg("build")
|
||||||
|
.arg("--lib")
|
||||||
|
.arg("--manifest-path")
|
||||||
|
.arg(package.manifest_path.as_std_path());
|
||||||
|
if options.release {
|
||||||
|
cargo.arg("--release");
|
||||||
|
}
|
||||||
|
run_command(
|
||||||
|
&mut cargo,
|
||||||
|
"Rust Android library",
|
||||||
|
"install cargo-ndk with `cargo install cargo-ndk`",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let library = native
|
||||||
|
.join(&options.abi)
|
||||||
|
.join(format!("lib{}.so", target.name));
|
||||||
|
if !library.is_file() {
|
||||||
|
return Err(format!("cargo-ndk did not produce {}", library.display()));
|
||||||
|
}
|
||||||
|
let classes = output.join("classes");
|
||||||
|
fs::create_dir_all(&classes).map_err(io_error("create Java output", &classes))?;
|
||||||
|
let sources = materialize_host(&output, &target.name)?;
|
||||||
|
let java_files = files_with_extension(&sources, "java")?;
|
||||||
|
let mut javac = Command::new("javac");
|
||||||
|
javac
|
||||||
|
.args(["--release", "17", "-classpath"])
|
||||||
|
.arg(&sdk.android_jar)
|
||||||
|
.arg("-d")
|
||||||
|
.arg(&classes)
|
||||||
|
.args(&java_files);
|
||||||
|
run_command(
|
||||||
|
&mut javac,
|
||||||
|
"Iris Android Java host",
|
||||||
|
"install a JDK containing javac",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let dex = output.join("dex");
|
||||||
|
fs::create_dir_all(&dex).map_err(io_error("create DEX output", &dex))?;
|
||||||
|
let class_files = files_with_extension(&classes, "class")?;
|
||||||
|
let mut d8 = Command::new(&sdk.d8);
|
||||||
|
d8.args(["--min-api", &MIN_SDK.to_string(), "--lib"])
|
||||||
|
.arg(&sdk.android_jar)
|
||||||
|
.arg("--output")
|
||||||
|
.arg(&dex)
|
||||||
|
.args(&class_files);
|
||||||
|
if options.release {
|
||||||
|
d8.arg("--release");
|
||||||
|
} else {
|
||||||
|
d8.arg("--debug");
|
||||||
|
}
|
||||||
|
run_command(
|
||||||
|
&mut d8,
|
||||||
|
"Iris Android DEX",
|
||||||
|
"install Android SDK Build Tools",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let manifest = output.join("AndroidManifest.xml");
|
||||||
|
fs::write(
|
||||||
|
&manifest,
|
||||||
|
manifest_xml(&application_id, &label, &target.name, sdk.api),
|
||||||
|
)
|
||||||
|
.map_err(io_error("write Android manifest", &manifest))?;
|
||||||
|
let unsigned = output.join("unsigned.apk");
|
||||||
|
let mut aapt = Command::new(&sdk.aapt2);
|
||||||
|
aapt.arg("link")
|
||||||
|
.arg("-o")
|
||||||
|
.arg(&unsigned)
|
||||||
|
.arg("-I")
|
||||||
|
.arg(&sdk.android_jar)
|
||||||
|
.arg("--manifest")
|
||||||
|
.arg(&manifest)
|
||||||
|
.args([
|
||||||
|
"--min-sdk-version",
|
||||||
|
&MIN_SDK.to_string(),
|
||||||
|
"--target-sdk-version",
|
||||||
|
&sdk.api.to_string(),
|
||||||
|
]);
|
||||||
|
run_command(
|
||||||
|
&mut aapt,
|
||||||
|
"Android resources",
|
||||||
|
"install Android SDK Build Tools",
|
||||||
|
)?;
|
||||||
|
append_payload(
|
||||||
|
&unsigned,
|
||||||
|
&output,
|
||||||
|
&dex.join("classes.dex"),
|
||||||
|
&library,
|
||||||
|
&options.abi,
|
||||||
|
&target.name,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let aligned = output.join("aligned.apk");
|
||||||
|
let mut zipalign = Command::new(&sdk.zipalign);
|
||||||
|
zipalign
|
||||||
|
.args(["-P", "16", "-f", "4"])
|
||||||
|
.arg(&unsigned)
|
||||||
|
.arg(&aligned);
|
||||||
|
run_command(
|
||||||
|
&mut zipalign,
|
||||||
|
"APK alignment",
|
||||||
|
"install Android SDK Build Tools",
|
||||||
|
)?;
|
||||||
|
let apk = output.join(format!("{}-{variant}.apk", package.name));
|
||||||
|
sign(&sdk, options, &aligned, &apk)?;
|
||||||
|
verify(&sdk, &apk)?;
|
||||||
|
Ok(Built {
|
||||||
|
apk,
|
||||||
|
application_id,
|
||||||
|
sdk,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_package<'a>(
|
||||||
|
packages: &'a [Package],
|
||||||
|
root: Option<&'a Package>,
|
||||||
|
wanted: Option<&str>,
|
||||||
|
) -> Result<&'a Package, String> {
|
||||||
|
if let Some(wanted) = wanted {
|
||||||
|
return packages
|
||||||
|
.iter()
|
||||||
|
.find(|package| package.name == wanted)
|
||||||
|
.ok_or_else(|| format!("Cargo workspace has no package named {wanted:?}"));
|
||||||
|
}
|
||||||
|
root.ok_or_else(|| {
|
||||||
|
"this is a virtual workspace; select an application with --package NAME".into()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn metadata_string(package: &Package, key: &str) -> Option<String> {
|
||||||
|
package
|
||||||
|
.metadata
|
||||||
|
.get("iris")?
|
||||||
|
.get("android")?
|
||||||
|
.get(key)?
|
||||||
|
.as_str()
|
||||||
|
.map(str::to_owned)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_application_id(package: &str) -> String {
|
||||||
|
let segment: String = package
|
||||||
|
.chars()
|
||||||
|
.map(|c| {
|
||||||
|
if c.is_ascii_alphanumeric() {
|
||||||
|
c.to_ascii_lowercase()
|
||||||
|
} else {
|
||||||
|
'_'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
format!("dev.iris.app.{segment}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_application_id(id: &str) -> Result<(), String> {
|
||||||
|
let valid = id.split('.').count() >= 2
|
||||||
|
&& id.split('.').all(|segment| {
|
||||||
|
!segment.is_empty()
|
||||||
|
&& segment.as_bytes()[0].is_ascii_alphabetic()
|
||||||
|
&& segment
|
||||||
|
.bytes()
|
||||||
|
.all(|c| c.is_ascii_alphanumeric() || c == b'_')
|
||||||
|
});
|
||||||
|
if valid {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(format!(
|
||||||
|
"application ID {id:?} is invalid; use dot-separated Java identifiers"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Sdk {
|
||||||
|
api: u32,
|
||||||
|
android_jar: PathBuf,
|
||||||
|
aapt2: PathBuf,
|
||||||
|
d8: PathBuf,
|
||||||
|
zipalign: PathBuf,
|
||||||
|
apksigner: PathBuf,
|
||||||
|
adb: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sdk {
|
||||||
|
fn find() -> Result<Self, String> {
|
||||||
|
let root = env::var_os("ANDROID_HOME")
|
||||||
|
.or_else(|| env::var_os("ANDROID_SDK_ROOT"))
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.ok_or_else(|| "ANDROID_HOME is unset; point it at your Android SDK".to_string())?;
|
||||||
|
let (api, platform) = newest_numbered(&root.join("platforms"), "android-")?;
|
||||||
|
let (_, tools) = newest_numbered(&root.join("build-tools"), "")?;
|
||||||
|
let executable = |name: &str, windows_extension: &str| {
|
||||||
|
tools.join(if cfg!(windows) {
|
||||||
|
format!("{name}.{windows_extension}")
|
||||||
|
} else {
|
||||||
|
name.to_string()
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let sdk = Self {
|
||||||
|
android_jar: platform.join("android.jar"),
|
||||||
|
aapt2: executable("aapt2", "exe"),
|
||||||
|
d8: executable("d8", "bat"),
|
||||||
|
zipalign: executable("zipalign", "exe"),
|
||||||
|
apksigner: executable("apksigner", "bat"),
|
||||||
|
adb: root
|
||||||
|
.join("platform-tools")
|
||||||
|
.join(if cfg!(windows) { "adb.exe" } else { "adb" }),
|
||||||
|
api,
|
||||||
|
};
|
||||||
|
for (name, path) in [
|
||||||
|
("android.jar", &sdk.android_jar),
|
||||||
|
("aapt2", &sdk.aapt2),
|
||||||
|
("d8", &sdk.d8),
|
||||||
|
("zipalign", &sdk.zipalign),
|
||||||
|
("apksigner", &sdk.apksigner),
|
||||||
|
] {
|
||||||
|
if !path.is_file() {
|
||||||
|
return Err(format!(
|
||||||
|
"Android SDK is missing {name} at {}; install a platform and Build Tools",
|
||||||
|
path.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(sdk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn newest_numbered(parent: &Path, prefix: &str) -> Result<(u32, PathBuf), String> {
|
||||||
|
let entries = fs::read_dir(parent).map_err(|error| {
|
||||||
|
format!(
|
||||||
|
"cannot read {}: {error}; install the required Android SDK component",
|
||||||
|
parent.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
entries
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.filter_map(|entry| {
|
||||||
|
let name = entry.file_name();
|
||||||
|
let version = name
|
||||||
|
.to_string_lossy()
|
||||||
|
.strip_prefix(prefix)?
|
||||||
|
.split('.')
|
||||||
|
.map(str::parse::<u32>)
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.ok()?;
|
||||||
|
Some((version, entry.path()))
|
||||||
|
})
|
||||||
|
.max_by(|(left, _), (right, _)| left.cmp(right))
|
||||||
|
.map(|(version, path)| (version[0], path))
|
||||||
|
.ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"no installed Android SDK component found under {}",
|
||||||
|
parent.display()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn materialize_host(output: &Path, library: &str) -> Result<PathBuf, String> {
|
||||||
|
let root = output.join("java");
|
||||||
|
for (relative, contents) in HOST_FILES {
|
||||||
|
let path = root.join(relative);
|
||||||
|
fs::create_dir_all(path.parent().unwrap())
|
||||||
|
.map_err(io_error("create Java source directory", &path))?;
|
||||||
|
let contents = if relative.ends_with("MainActivity.java") {
|
||||||
|
contents.replace("IRIS_NATIVE_LIBRARY", library)
|
||||||
|
} else {
|
||||||
|
contents.to_string()
|
||||||
|
};
|
||||||
|
fs::write(&path, contents).map_err(io_error("write Java host source", &path))?;
|
||||||
|
}
|
||||||
|
Ok(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
const HOST_FILES: &[(&str, &str)] = &[
|
||||||
|
(
|
||||||
|
"dev/iris/android/MainActivity.java",
|
||||||
|
include_str!("../android-host/dev/iris/android/MainActivity.java"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"dev/iris/android/IrisView.java",
|
||||||
|
include_str!("../android-host/dev/iris/android/IrisView.java"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"org/linebender/android/rustview/RustView.java",
|
||||||
|
include_str!("../android-host/org/linebender/android/rustview/RustView.java"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"org/linebender/android/rustview/RustInputConnection.java",
|
||||||
|
include_str!("../android-host/org/linebender/android/rustview/RustInputConnection.java"),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
fn manifest_xml(application_id: &str, label: &str, library: &str, target_sdk: u32) -> String {
|
||||||
|
format!(
|
||||||
|
r#"<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="{}" android:versionCode="1" android:versionName="0.1.0">
|
||||||
|
<uses-sdk android:minSdkVersion="{MIN_SDK}" android:targetSdkVersion="{target_sdk}" />
|
||||||
|
<application android:allowBackup="true" android:extractNativeLibs="false" android:label="{}" android:theme="@android:style/Theme.Material.Light.NoActionBar">
|
||||||
|
<activity android:name="{ACTIVITY}" android:configChanges="orientation|screenSize|screenLayout|keyboardHidden" android:exported="true" android:windowSoftInputMode="adjustResize">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data android:name="android.app.lib_name" android:value="{}" />
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
|
"#,
|
||||||
|
xml_escape(application_id),
|
||||||
|
xml_escape(label),
|
||||||
|
xml_escape(library)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn xml_escape(value: &str) -> String {
|
||||||
|
value
|
||||||
|
.replace('&', "&")
|
||||||
|
.replace('<', "<")
|
||||||
|
.replace('>', ">")
|
||||||
|
.replace('"', """)
|
||||||
|
.replace('\'', "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn files_with_extension(root: &Path, extension: &str) -> Result<Vec<PathBuf>, String> {
|
||||||
|
fn visit(dir: &Path, extension: &str, output: &mut Vec<PathBuf>) -> Result<(), String> {
|
||||||
|
for entry in fs::read_dir(dir).map_err(io_error("read directory", dir))? {
|
||||||
|
let path = entry
|
||||||
|
.map_err(|error| format!("cannot read entry under {}: {error}", dir.display()))?
|
||||||
|
.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
visit(&path, extension, output)?;
|
||||||
|
} else if path.extension() == Some(OsStr::new(extension)) {
|
||||||
|
output.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
let mut files = Vec::new();
|
||||||
|
visit(root, extension, &mut files)?;
|
||||||
|
files.sort();
|
||||||
|
Ok(files)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_payload(
|
||||||
|
apk: &Path,
|
||||||
|
output: &Path,
|
||||||
|
dex: &Path,
|
||||||
|
library: &Path,
|
||||||
|
abi: &str,
|
||||||
|
library_name: &str,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let payload = output.join("payload");
|
||||||
|
let native_dir = payload.join("lib").join(abi);
|
||||||
|
fs::create_dir_all(&native_dir).map_err(io_error("create APK payload", &native_dir))?;
|
||||||
|
fs::copy(dex, payload.join("classes.dex")).map_err(io_error("stage classes.dex", dex))?;
|
||||||
|
let native_name = format!("lib{library_name}.so");
|
||||||
|
fs::copy(library, native_dir.join(&native_name))
|
||||||
|
.map_err(io_error("stage native library", library))?;
|
||||||
|
|
||||||
|
// `jar` is part of the JDK already needed for javac. Storing the native
|
||||||
|
// library uncompressed lets zipalign give it the 16 KiB page alignment
|
||||||
|
// required by current Android devices.
|
||||||
|
let mut jar = Command::new("jar");
|
||||||
|
jar.args(["--update", "--file"])
|
||||||
|
.arg(apk)
|
||||||
|
.args(["--no-manifest", "--no-compress", "-C"])
|
||||||
|
.arg(&payload)
|
||||||
|
.arg("classes.dex")
|
||||||
|
.arg("-C")
|
||||||
|
.arg(&payload)
|
||||||
|
.arg("lib");
|
||||||
|
run_command(
|
||||||
|
&mut jar,
|
||||||
|
"APK native payload",
|
||||||
|
"install a JDK containing jar",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sign(sdk: &Sdk, options: &Options, input: &Path, output: &Path) -> Result<(), String> {
|
||||||
|
let (keystore, alias, store_password, key_password) = if options.release {
|
||||||
|
let store = env::var("IRIS_KEYSTORE_PASSWORD")
|
||||||
|
.map_err(|_| "IRIS_KEYSTORE_PASSWORD is unset for release signing".to_string())?;
|
||||||
|
let key = env::var("IRIS_KEY_PASSWORD").unwrap_or_else(|_| store.clone());
|
||||||
|
(
|
||||||
|
options.keystore.clone().unwrap(),
|
||||||
|
options.key_alias.clone().unwrap(),
|
||||||
|
store,
|
||||||
|
key,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let home = env::var_os("HOME")
|
||||||
|
.ok_or_else(|| "HOME is unset; cannot locate the Android debug keystore".to_string())?;
|
||||||
|
let keystore = PathBuf::from(home).join(".android/debug.keystore");
|
||||||
|
ensure_debug_keystore(&keystore)?;
|
||||||
|
(
|
||||||
|
keystore,
|
||||||
|
"androiddebugkey".into(),
|
||||||
|
"android".into(),
|
||||||
|
"android".into(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut command = Command::new(&sdk.apksigner);
|
||||||
|
command
|
||||||
|
.arg("sign")
|
||||||
|
.args([
|
||||||
|
"--ks-pass",
|
||||||
|
"env:IRIS_APK_STORE_PASSWORD",
|
||||||
|
"--key-pass",
|
||||||
|
"env:IRIS_APK_KEY_PASSWORD",
|
||||||
|
"--ks-key-alias",
|
||||||
|
])
|
||||||
|
.arg(alias)
|
||||||
|
.arg("--ks")
|
||||||
|
.arg(keystore)
|
||||||
|
.arg("--out")
|
||||||
|
.arg(output)
|
||||||
|
.arg(input)
|
||||||
|
.env("IRIS_APK_STORE_PASSWORD", store_password)
|
||||||
|
.env("IRIS_APK_KEY_PASSWORD", key_password);
|
||||||
|
run_command(
|
||||||
|
&mut command,
|
||||||
|
"APK signing",
|
||||||
|
"check the keystore, alias, and signing passwords",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_debug_keystore(path: &Path) -> Result<(), String> {
|
||||||
|
if path.is_file() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
fs::create_dir_all(path.parent().unwrap())
|
||||||
|
.map_err(io_error("create Android configuration directory", path))?;
|
||||||
|
let mut keytool = Command::new("keytool");
|
||||||
|
keytool.args(["-genkeypair", "-keystore"]).arg(path).args([
|
||||||
|
"-storepass",
|
||||||
|
"android",
|
||||||
|
"-alias",
|
||||||
|
"androiddebugkey",
|
||||||
|
"-keypass",
|
||||||
|
"android",
|
||||||
|
"-dname",
|
||||||
|
"CN=Android Debug,O=Android,C=US",
|
||||||
|
"-keyalg",
|
||||||
|
"RSA",
|
||||||
|
"-keysize",
|
||||||
|
"2048",
|
||||||
|
"-validity",
|
||||||
|
"10000",
|
||||||
|
]);
|
||||||
|
run_command(
|
||||||
|
&mut keytool,
|
||||||
|
"Android debug key",
|
||||||
|
"install a JDK containing keytool",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify(sdk: &Sdk, apk: &Path) -> Result<(), String> {
|
||||||
|
let mut align = Command::new(&sdk.zipalign);
|
||||||
|
align.args(["-c", "-P", "16", "4"]).arg(apk);
|
||||||
|
run_command(
|
||||||
|
&mut align,
|
||||||
|
"APK alignment verification",
|
||||||
|
"this indicates a cargo-iris packaging defect",
|
||||||
|
)?;
|
||||||
|
let mut sign = Command::new(&sdk.apksigner);
|
||||||
|
sign.args(["verify", "--verbose"]).arg(apk);
|
||||||
|
run_command(
|
||||||
|
&mut sign,
|
||||||
|
"APK signature verification",
|
||||||
|
"this indicates a cargo-iris signing defect",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_and_run(built: &Built, device: &str) -> Result<(), String> {
|
||||||
|
if !built.sdk.adb.is_file() {
|
||||||
|
return Err(format!(
|
||||||
|
"Android SDK is missing adb at {}; install Platform Tools",
|
||||||
|
built.sdk.adb.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut install = Command::new(&built.sdk.adb);
|
||||||
|
install
|
||||||
|
.args(["-s", device, "install", "--no-streaming", "-r"])
|
||||||
|
.arg(&built.apk);
|
||||||
|
run_command(
|
||||||
|
&mut install,
|
||||||
|
"APK install",
|
||||||
|
"check that the selected device is connected and authorized",
|
||||||
|
)?;
|
||||||
|
let component = format!("{}/{}", built.application_id, ACTIVITY);
|
||||||
|
let mut launch = Command::new(&built.sdk.adb);
|
||||||
|
launch.args(["-s", device, "shell", "am", "start", "-n", &component]);
|
||||||
|
run_command(
|
||||||
|
&mut launch,
|
||||||
|
"APK launch",
|
||||||
|
"check the package activity in the generated APK",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recreate(path: &Path) -> Result<(), String> {
|
||||||
|
if path.exists() {
|
||||||
|
fs::remove_dir_all(path).map_err(io_error("clear prior APK staging directory", path))?;
|
||||||
|
}
|
||||||
|
fs::create_dir_all(path).map_err(io_error("create APK staging directory", path))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_command(command: &mut Command, thing: &str, fix: &str) -> Result<(), String> {
|
||||||
|
command.stdin(Stdio::null());
|
||||||
|
let status = command
|
||||||
|
.status()
|
||||||
|
.map_err(|error| format!("could not start {thing}: {error}; {fix}"))?;
|
||||||
|
if status.success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(format!("{thing} failed with {status}; {fix}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn io_error<'a>(action: &'a str, path: &'a Path) -> impl FnOnce(std::io::Error) -> String + 'a {
|
||||||
|
move |error| format!("cannot {action} {}: {error}", path.display())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn application_ids_are_validated() {
|
||||||
|
assert!(validate_application_id("dev.iris.app.demo_2").is_ok());
|
||||||
|
assert!(validate_application_id("one").is_err());
|
||||||
|
assert!(validate_application_id("dev.2demo").is_err());
|
||||||
|
assert!(validate_application_id("dev.iris.bad-name").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn manifest_values_are_escaped() {
|
||||||
|
let manifest = manifest_xml("dev.iris.demo", "A & <demo>", "demo", 37);
|
||||||
|
assert!(manifest.contains("A & <demo>"));
|
||||||
|
assert!(manifest.contains("android:minSdkVersion=\"29\""));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/Cargo.lock
|
||||||
|
/target/
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "iris-apk-fixture"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
iris = { path = "../../.." }
|
||||||
|
|
||||||
|
[workspace]
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
use iris::prelude::*;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[derive(AndroidUiState)]
|
||||||
|
pub struct State {
|
||||||
|
ui_state: AndroidUiState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AndroidAppState for State {
|
||||||
|
type Resources = Resources;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[iris::app_init]
|
||||||
|
fn create(mut ui_state: AndroidUiState, rsc: &mut Resources) -> State {
|
||||||
|
rsc.launches += 1;
|
||||||
|
rect(PaintId::RED)
|
||||||
|
.label("Iris APK fixture")
|
||||||
|
.set_root(rsc, &mut ui_state);
|
||||||
|
State { ui_state }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Resources {
|
||||||
|
ui: Ui,
|
||||||
|
events: EventManager<Self>,
|
||||||
|
tasks: Tasks<Self>,
|
||||||
|
widget_state: WidgetState,
|
||||||
|
launches: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AndroidResources<State> for Resources {
|
||||||
|
fn new(redraw: Arc<dyn RequestRedraw>) -> (Self, TaskMsgReceiver<Self>) {
|
||||||
|
let (tasks, receiver) = Tasks::init(redraw);
|
||||||
|
(
|
||||||
|
Self {
|
||||||
|
ui: Ui::default(),
|
||||||
|
events: EventManager::default(),
|
||||||
|
tasks,
|
||||||
|
widget_state: WidgetState::default(),
|
||||||
|
launches: 0,
|
||||||
|
},
|
||||||
|
receiver,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UiRsc for Resources {
|
||||||
|
fn ui(&self) -> &Ui {
|
||||||
|
&self.ui
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ui_mut(&mut self) -> &mut Ui {
|
||||||
|
&mut self.ui
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_draw(&mut self, active: &ActiveData) {
|
||||||
|
self.events.draw(active);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_undraw(&mut self, active: &ActiveData) {
|
||||||
|
self.events.undraw(active);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_remove(&mut self, id: WidgetId) {
|
||||||
|
self.events.remove(id);
|
||||||
|
self.widget_state.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasState for Resources {
|
||||||
|
type State = State;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasEvents for Resources {
|
||||||
|
fn events(&self) -> &EventManager<Self> {
|
||||||
|
&self.events
|
||||||
|
}
|
||||||
|
|
||||||
|
fn events_mut(&mut self) -> &mut EventManager<Self> {
|
||||||
|
&mut self.events
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasTasks for Resources {
|
||||||
|
fn tasks_mut(&mut self) -> &mut Tasks<Self> {
|
||||||
|
&mut self.tasks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasWidgetState for Resources {
|
||||||
|
fn widget_state(&self) -> &WidgetState {
|
||||||
|
&self.widget_state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn widget_state_mut(&mut self) -> &mut WidgetState {
|
||||||
|
&mut self.widget_state
|
||||||
|
}
|
||||||
|
}
|
||||||
+85
-2
@@ -2,13 +2,96 @@ extern crate proc_macro;
|
|||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use syn::{
|
use syn::{
|
||||||
Attribute, Block, Error, GenericParam, Generics, Ident, ItemStruct, ItemTrait, Signature,
|
Attribute, Block, Error, FnArg, GenericParam, Generics, Ident, ItemFn, ItemStruct, ItemTrait,
|
||||||
Token, Type, Visibility,
|
ReturnType, Signature, Token, Type, Visibility,
|
||||||
parse::{Parse, ParseStream, Result},
|
parse::{Parse, ParseStream, Result},
|
||||||
parse_macro_input, parse_quote,
|
parse_macro_input, parse_quote,
|
||||||
spanned::Spanned,
|
spanned::Spanned,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Marks the factory called when Android creates an Iris view.
|
||||||
|
///
|
||||||
|
/// An attribute is necessary here because the Android loader requires one
|
||||||
|
/// exported `JNI_OnLoad` symbol and `android-view` requires a plain function
|
||||||
|
/// pointer monomorphized for the returned application state. The generated
|
||||||
|
/// linker and JNI glue is Android-gated; the annotated function therefore
|
||||||
|
/// does not need its own `cfg` attribute.
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn app_init(args: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
|
if !args.is_empty() {
|
||||||
|
return Error::new(
|
||||||
|
proc_macro2::Span::call_site(),
|
||||||
|
"app_init takes no arguments",
|
||||||
|
)
|
||||||
|
.into_compile_error()
|
||||||
|
.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
let function = parse_macro_input!(item as ItemFn);
|
||||||
|
let name = &function.sig.ident;
|
||||||
|
let ReturnType::Type(_, state) = &function.sig.output else {
|
||||||
|
return Error::new(
|
||||||
|
function.sig.output.span(),
|
||||||
|
"an app_init function must return its application state",
|
||||||
|
)
|
||||||
|
.into_compile_error()
|
||||||
|
.into();
|
||||||
|
};
|
||||||
|
if function.sig.inputs.len() != 2
|
||||||
|
|| function
|
||||||
|
.sig
|
||||||
|
.inputs
|
||||||
|
.iter()
|
||||||
|
.any(|argument| !matches!(argument, FnArg::Typed(_)))
|
||||||
|
{
|
||||||
|
return Error::new(
|
||||||
|
function.sig.inputs.span(),
|
||||||
|
"an app_init function takes AndroidUiState and &mut State::Resources",
|
||||||
|
)
|
||||||
|
.into_compile_error()
|
||||||
|
.into();
|
||||||
|
}
|
||||||
|
if function.sig.asyncness.is_some()
|
||||||
|
|| function.sig.constness.is_some()
|
||||||
|
|| matches!(function.sig.safety, syn::Safety::Unsafe(_))
|
||||||
|
|| !function.sig.generics.params.is_empty()
|
||||||
|
{
|
||||||
|
return Error::new(
|
||||||
|
function.sig.span(),
|
||||||
|
"an app_init function must be a plain, non-generic synchronous function",
|
||||||
|
)
|
||||||
|
.into_compile_error()
|
||||||
|
.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
#function
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
mod __iris_android_app {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
extern "system" fn new_view_peer<'local>(
|
||||||
|
env: ::iris::android::__private::JNIEnv<'local>,
|
||||||
|
view: ::iris::android::__private::View<'local>,
|
||||||
|
context: ::iris::android::__private::Context<'local>,
|
||||||
|
) -> ::iris::android::__private::JLong {
|
||||||
|
::iris::android::new_peer::<#state>(env, view, context, super::#name)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "system" fn JNI_OnLoad(
|
||||||
|
vm: *mut ::iris::android::__private::RawJavaVM,
|
||||||
|
_: *mut ::core::ffi::c_void,
|
||||||
|
) -> ::iris::android::__private::JInt {
|
||||||
|
unsafe { ::iris::android::__private::on_load(vm, new_view_peer) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
struct Input {
|
struct Input {
|
||||||
attrs: Vec<Attribute>,
|
attrs: Vec<Attribute>,
|
||||||
vis: Visibility,
|
vis: Visibility,
|
||||||
|
|||||||
@@ -6,6 +6,76 @@ It's currently designed around using retained data structures (widgets), rather
|
|||||||
|
|
||||||
Examples are in `examples`, eg. `cargo run --example tabs`.
|
Examples are in `examples`, eg. `cargo run --example tabs`.
|
||||||
|
|
||||||
|
## Android applications
|
||||||
|
|
||||||
|
An Android application is a library because Android loads its Rust code as a
|
||||||
|
native shared library:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
iris = { path = "../iris" }
|
||||||
|
|
||||||
|
[package.metadata.iris.android]
|
||||||
|
application-id = "com.example.myapp"
|
||||||
|
label = "My app"
|
||||||
|
```
|
||||||
|
|
||||||
|
`#[iris::app_init]` marks the factory called when Android creates the Iris
|
||||||
|
view. The attribute supplies its own Android target gate and generates the JNI
|
||||||
|
loader glue. The returned state chooses its resources through
|
||||||
|
`AndroidAppState::Resources`; `StdRsc` is the standard bundle, not a
|
||||||
|
requirement.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use iris::prelude::*;
|
||||||
|
|
||||||
|
#[derive(AndroidUiState)]
|
||||||
|
struct State {
|
||||||
|
ui_state: AndroidUiState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AndroidAppState for State {
|
||||||
|
type Resources = StdRsc<Self>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[iris::app_init]
|
||||||
|
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<State>) -> State {
|
||||||
|
rect(PaintId::RED).set_root(rsc, &mut ui_state);
|
||||||
|
State { ui_state }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Install the Cargo subcommand from a checkout, then invoke it from the
|
||||||
|
application's directory:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo install --path /path/to/iris/cargo-iris
|
||||||
|
cargo iris apk --abi arm64-v8a
|
||||||
|
cargo iris run --abi x86_64 --device emulator-5554
|
||||||
|
```
|
||||||
|
|
||||||
|
`cargo iris` packages directly with `cargo-ndk`, `javac`, `d8`, `aapt2`,
|
||||||
|
`jar`, `zipalign`, and `apksigner`; it does not require Gradle. The caller
|
||||||
|
provides a JDK, Android SDK and NDK, and any emulator or physical device. Set
|
||||||
|
`ANDROID_HOME` to the SDK. `run` always requires an explicit device and never
|
||||||
|
creates or starts one.
|
||||||
|
|
||||||
|
Debug APKs use the standard key at `~/.android/debug.keystore`, creating it
|
||||||
|
with `keytool` when absent. A release build requires the long-lived signing
|
||||||
|
identity explicitly:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
IRIS_KEYSTORE_PASSWORD=... IRIS_KEY_PASSWORD=... \
|
||||||
|
cargo iris apk --release --keystore /secure/upload.jks --key-alias upload
|
||||||
|
```
|
||||||
|
|
||||||
|
APK staging and output live under
|
||||||
|
`target/iris-android/<package>/<debug|release>/<abi>/`; the command's final
|
||||||
|
line is the verified APK's absolute path.
|
||||||
|
|
||||||
Goals, in general order:
|
Goals, in general order:
|
||||||
1. does what I want it to (text, images, video, animations)
|
1. does what I want it to (text, images, video, animations)
|
||||||
2. very easy to use ignoring ergonomic ref counting
|
2. very easy to use ignoring ergonomic ref counting
|
||||||
|
|||||||
@@ -31,14 +31,14 @@ fn utf16_to_byte(text: &str, utf16_idx: usize) -> usize {
|
|||||||
|
|
||||||
impl<State: AndroidAppState> IrisViewPeer<State> {
|
impl<State: AndroidAppState> IrisViewPeer<State> {
|
||||||
fn focus(&self) -> Option<WeakWidget<TextEdit>> {
|
fn focus(&self) -> Option<WeakWidget<TextEdit>> {
|
||||||
(!self.rsc.events.controllers.command_target_blocks_input())
|
(!self.rsc.events().controllers.command_target_blocks_input())
|
||||||
.then_some(self.state.android_state().focus)
|
.then_some(self.state.android_state().focus)
|
||||||
.flatten()
|
.flatten()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn update_ime_selection(&mut self, ctx: &mut CallbackCtx) {
|
pub(super) fn update_ime_selection(&mut self, ctx: &mut CallbackCtx) {
|
||||||
let Some(focus) = self.focus() else { return };
|
let Some(focus) = self.focus() else { return };
|
||||||
let text = &self.rsc[focus];
|
let text = focus.get(&self.rsc);
|
||||||
let Some(sel) = text.selection_range() else {
|
let Some(sel) = text.selection_range() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -87,7 +87,7 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
|
|||||||
IME_FLAG_NO_FULLSCREEN | IME_FLAG_NO_EXTRACT_UI | IME_FLAG_NO_ENTER_ACTION,
|
IME_FLAG_NO_FULLSCREEN | IME_FLAG_NO_EXTRACT_UI | IME_FLAG_NO_ENTER_ACTION,
|
||||||
);
|
);
|
||||||
if let Some(focus) = self.focus() {
|
if let Some(focus) = self.focus() {
|
||||||
let text = &self.rsc[focus];
|
let text = focus.get(&self.rsc);
|
||||||
let sel = text.selection_range().unwrap_or(0..0);
|
let sel = text.selection_range().unwrap_or(0..0);
|
||||||
let start = byte_to_utf16(text.text(), sel.start) as i32;
|
let start = byte_to_utf16(text.text(), sel.start) as i32;
|
||||||
let end = byte_to_utf16(text.text(), sel.end) as i32;
|
let end = byte_to_utf16(text.text(), sel.end) as i32;
|
||||||
@@ -112,7 +112,7 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let focus = self.focus()?;
|
let focus = self.focus()?;
|
||||||
let text = &self.rsc[focus];
|
let text = focus.get(&self.rsc);
|
||||||
let sel = text.selection_range()?;
|
let sel = text.selection_range()?;
|
||||||
let end_16 = byte_to_utf16(text.text(), sel.start);
|
let end_16 = byte_to_utf16(text.text(), sel.start);
|
||||||
let start_16 = end_16.saturating_sub(n as usize);
|
let start_16 = end_16.saturating_sub(n as usize);
|
||||||
@@ -129,7 +129,7 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let focus = self.focus()?;
|
let focus = self.focus()?;
|
||||||
let text = &self.rsc[focus];
|
let text = focus.get(&self.rsc);
|
||||||
let sel = text.selection_range()?;
|
let sel = text.selection_range()?;
|
||||||
let len_16 = byte_to_utf16(text.text(), text.text().len());
|
let len_16 = byte_to_utf16(text.text(), text.text().len());
|
||||||
let start_16 = byte_to_utf16(text.text(), sel.end);
|
let start_16 = byte_to_utf16(text.text(), sel.end);
|
||||||
@@ -140,14 +140,14 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
|
|||||||
|
|
||||||
fn selected_text<'slf>(&'slf mut self, _ctx: &mut CallbackCtx) -> Option<Cow<'slf, str>> {
|
fn selected_text<'slf>(&'slf mut self, _ctx: &mut CallbackCtx) -> Option<Cow<'slf, str>> {
|
||||||
let focus = self.focus()?;
|
let focus = self.focus()?;
|
||||||
Some(Cow::Owned(self.rsc[focus].selected_text()?))
|
Some(Cow::Owned(focus.get(&self.rsc).selected_text()?))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cursor_caps_mode(&mut self, ctx: &mut CallbackCtx, req_modes: u32) -> u32 {
|
fn cursor_caps_mode(&mut self, ctx: &mut CallbackCtx, req_modes: u32) -> u32 {
|
||||||
let Some(focus) = self.focus() else {
|
let Some(focus) = self.focus() else {
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
let text = &self.rsc[focus];
|
let text = focus.get(&self.rsc);
|
||||||
let Some(caret) = text.caret() else {
|
let Some(caret) = text.caret() else {
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
@@ -164,7 +164,7 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
|
|||||||
let Some(focus) = self.focus() else {
|
let Some(focus) = self.focus() else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let text = &self.rsc[focus];
|
let text = focus.get(&self.rsc);
|
||||||
let Some(sel) = text.selection_range() else {
|
let Some(sel) = text.selection_range() else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
@@ -223,7 +223,7 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
|
|||||||
let Some(focus) = self.focus() else {
|
let Some(focus) = self.focus() else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let text = &self.rsc[focus];
|
let text = focus.get(&self.rsc);
|
||||||
let content = text.text();
|
let content = text.text();
|
||||||
let byte = utf16_to_byte(content, end.max(0) as usize);
|
let byte = utf16_to_byte(content, end.max(0) as usize);
|
||||||
focus.edit(&mut self.rsc).set_cursor_byte(byte);
|
focus.edit(&mut self.rsc).set_cursor_byte(byte);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use super::view::AndroidAppState;
|
|||||||
/// the arrow keys on a physical keyboard) plus whatever `unicode_char`
|
/// the arrow keys on a physical keyboard) plus whatever `unicode_char`
|
||||||
/// reports for a plain key press. Returns whether anything used the event.
|
/// reports for a plain key press. Returns whether anything used the event.
|
||||||
pub(super) fn on_key<'local, State: AndroidAppState>(
|
pub(super) fn on_key<'local, State: AndroidAppState>(
|
||||||
rsc: &mut StdRsc<State>,
|
rsc: &mut State::Resources,
|
||||||
state: &mut State,
|
state: &mut State,
|
||||||
env: &mut JNIEnv<'local>,
|
env: &mut JNIEnv<'local>,
|
||||||
key_code: Keycode,
|
key_code: Keycode,
|
||||||
|
|||||||
+33
-1
@@ -10,9 +10,41 @@ mod view;
|
|||||||
pub use insets::Insets;
|
pub use insets::Insets;
|
||||||
pub use render::AndroidRenderer;
|
pub use render::AndroidRenderer;
|
||||||
pub use view::{
|
pub use view::{
|
||||||
AndroidAppState, AndroidUiState, HasAndroidUiState, IrisViewPeer, WindowInsets, new_peer,
|
AndroidAppState, AndroidResources, AndroidUiState, HasAndroidUiState, IrisViewPeer,
|
||||||
|
WindowInsets, new_peer,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Types used by `#[iris::app_init]`'s generated JNI boundary.
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub mod __private {
|
||||||
|
pub use android_view::{Context, View, jni::JNIEnv};
|
||||||
|
pub type JInt = android_view::jni::sys::jint;
|
||||||
|
pub type JLong = android_view::jni::sys::jlong;
|
||||||
|
pub type RawJavaVM = android_view::jni::sys::JavaVM;
|
||||||
|
|
||||||
|
/// Registers the fixed view class shipped by `cargo-iris`.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `vm` must be the live VM pointer Android passed to `JNI_OnLoad`.
|
||||||
|
pub unsafe fn on_load(
|
||||||
|
vm: *mut RawJavaVM,
|
||||||
|
new_peer: for<'local> extern "system" fn(
|
||||||
|
JNIEnv<'local>,
|
||||||
|
View<'local>,
|
||||||
|
Context<'local>,
|
||||||
|
) -> JLong,
|
||||||
|
) -> JInt {
|
||||||
|
use android_view::{jni::JavaVM, register_view_class};
|
||||||
|
|
||||||
|
let vm = unsafe { JavaVM::from_raw(vm) }.unwrap();
|
||||||
|
let mut env = vm.get_env().unwrap();
|
||||||
|
const VIEW_CLASS: &str = "dev/iris/android/IrisView";
|
||||||
|
register_view_class(&mut env, VIEW_CLASS, new_peer);
|
||||||
|
super::register_native_methods(&mut env, VIEW_CLASS);
|
||||||
|
android_view::jni::sys::JNI_VERSION_1_6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Registers the extra native methods this backend needs beyond what
|
/// Registers the extra native methods this backend needs beyond what
|
||||||
/// `android_view::register_view_class` covers (window insets -- see
|
/// `android_view::register_view_class` covers (window insets -- see
|
||||||
/// `insets.rs`'s doc comment for why that one could not ride along on an
|
/// `insets.rs`'s doc comment for why that one could not ride along on an
|
||||||
|
|||||||
@@ -191,7 +191,25 @@ impl AndroidRenderer {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let surface_caps = surface.get_capabilities(&adapter);
|
let surface_caps = surface.get_capabilities(&adapter);
|
||||||
let formats = iris_core::srgb_surface_format(&surface_caps)?;
|
let mut formats = iris_core::srgb_surface_format(&surface_caps)?;
|
||||||
|
let downlevel = adapter.get_downlevel_capabilities();
|
||||||
|
if formats.view != formats.surface
|
||||||
|
&& !downlevel
|
||||||
|
.flags
|
||||||
|
.contains(DownlevelFlags::SURFACE_VIEW_FORMATS)
|
||||||
|
{
|
||||||
|
// GLES implementations are allowed to lack texture-view format
|
||||||
|
// reinterpretation. Configuring the advertised non-sRGB format
|
||||||
|
// with an sRGB view then fails validation and leaves the surface
|
||||||
|
// unusable. Rendering through the advertised format is the only
|
||||||
|
// viable fallback on those devices.
|
||||||
|
log::warn!(
|
||||||
|
"iris renderer: {:?} cannot use an sRGB surface view; rendering through {:?}",
|
||||||
|
formats.surface,
|
||||||
|
formats.surface,
|
||||||
|
);
|
||||||
|
formats.view = formats.surface;
|
||||||
|
}
|
||||||
log::info!(
|
log::info!(
|
||||||
"iris renderer: surface={:?}, view={:?}, color_space=Srgb",
|
"iris renderer: surface={:?}, view={:?}, color_space=Srgb",
|
||||||
formats.surface,
|
formats.surface,
|
||||||
|
|||||||
+60
-34
@@ -89,8 +89,8 @@ impl AndroidUiState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<State: 'static> HasRoot<StdRsc<State>> for AndroidUiState {
|
impl<Rsc: HasEvents> HasRoot<Rsc> for AndroidUiState {
|
||||||
fn set_root(&mut self, rsc: &mut StdRsc<State>, root: StrongWidget) {
|
fn set_root(&mut self, rsc: &mut Rsc, root: StrongWidget) {
|
||||||
self.root = Some(crate::overlay::default_overlay_root(rsc, root));
|
self.root = Some(crate::overlay::default_overlay_root(rsc, root));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,16 +100,41 @@ pub trait HasAndroidUiState: Sized + 'static {
|
|||||||
fn android_state_mut(&mut self) -> &mut AndroidUiState;
|
fn android_state_mut(&mut self) -> &mut AndroidUiState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Application state retained for the lifetime of one Android `View`.
|
||||||
|
///
|
||||||
|
/// [`StdRsc`] is the usual [`AndroidResources`] implementation, but the host only
|
||||||
|
/// requires the capabilities in [`AndroidResources`]. An application may add
|
||||||
|
/// its own resources by supplying another implementation.
|
||||||
pub trait AndroidAppState: HasAndroidUiState {
|
pub trait AndroidAppState: HasAndroidUiState {
|
||||||
fn new(ui_state: AndroidUiState, rsc: &mut StdRsc<Self>) -> Self;
|
type Resources: AndroidResources<Self>;
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn back_pressed(&mut self, rsc: &mut StdRsc<Self>) -> bool {
|
fn back_pressed(&mut self, rsc: &mut Self::Resources) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn platform_ready(&mut self, rsc: &mut StdRsc<Self>, vm: JavaVM, view: GlobalRef) {}
|
fn platform_ready(&mut self, rsc: &mut Self::Resources, vm: JavaVM, view: GlobalRef) {}
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn on_insets_changed(&mut self, rsc: &mut StdRsc<Self>, insets: WindowInsets) {}
|
fn on_insets_changed(&mut self, rsc: &mut Self::Resources, insets: WindowInsets) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resources the Android host needs to draw and dispatch application events.
|
||||||
|
///
|
||||||
|
/// This deliberately names capabilities rather than storage. Custom bundles
|
||||||
|
/// can embed or replace [`StdRsc`] as long as they implement these traits and
|
||||||
|
/// create the task receiver paired with their [`Tasks`] value.
|
||||||
|
pub trait AndroidResources<State>:
|
||||||
|
HasTasks<State = State> + HasWidgetState + Sized + 'static
|
||||||
|
where
|
||||||
|
State: 'static,
|
||||||
|
{
|
||||||
|
fn new(redraw: Arc<dyn RequestRedraw>) -> (Self, TaskMsgReceiver<Self>);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<State: 'static> AndroidResources<State> for StdRsc<State> {
|
||||||
|
fn new(redraw: Arc<dyn RequestRedraw>) -> (Self, TaskMsgReceiver<Self>) {
|
||||||
|
StdRsc::new(redraw)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Widget-facing insets in physical pixels, decoupled from JNI's integer shape.
|
/// Widget-facing insets in physical pixels, decoupled from JNI's integer shape.
|
||||||
@@ -144,9 +169,9 @@ impl WindowInsets {
|
|||||||
/// `RustView` instance; `new_peer` (below) builds it and hands the id to
|
/// `RustView` instance; `new_peer` (below) builds it and hands the id to
|
||||||
/// Java the same way android-view's own demo does.
|
/// Java the same way android-view's own demo does.
|
||||||
pub struct IrisViewPeer<State: AndroidAppState> {
|
pub struct IrisViewPeer<State: AndroidAppState> {
|
||||||
pub(super) rsc: StdRsc<State>,
|
pub(super) rsc: State::Resources,
|
||||||
pub(super) state: State,
|
pub(super) state: State,
|
||||||
task_recv: TaskMsgReceiver<StdRsc<State>>,
|
task_recv: TaskMsgReceiver<State::Resources>,
|
||||||
/// Converts input and Choreographer timestamps onto one monotonic clock.
|
/// Converts input and Choreographer timestamps onto one monotonic clock.
|
||||||
device_clock: Option<DeviceClock>,
|
device_clock: Option<DeviceClock>,
|
||||||
}
|
}
|
||||||
@@ -169,7 +194,7 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
|
|||||||
let ui_state = self.state.android_state_mut();
|
let ui_state = self.state.android_state_mut();
|
||||||
let cursor = ui_state.cursor.clone();
|
let cursor = ui_state.cursor.clone();
|
||||||
let old_focus = ui_state.focus;
|
let old_focus = ui_state.focus;
|
||||||
let render_state = self.rsc.ui.render_state();
|
let render_state = self.rsc.ui().render_state();
|
||||||
render_state
|
render_state
|
||||||
.get()
|
.get()
|
||||||
.run_sensors(&mut self.rsc, &mut self.state, cursor, window_size);
|
.run_sensors(&mut self.rsc, &mut self.state, cursor, window_size);
|
||||||
@@ -200,7 +225,7 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
|
|||||||
|
|
||||||
let ui_state = self.state.android_state_mut();
|
let ui_state = self.state.android_state_mut();
|
||||||
ui_state.cursor.end_frame();
|
ui_state.cursor.end_frame();
|
||||||
let render_state = self.rsc.ui.render_state();
|
let render_state = self.rsc.ui().render_state();
|
||||||
if render_state
|
if render_state
|
||||||
.get()
|
.get()
|
||||||
.needs_redraw(&ui_state.root, self.rsc.widgets())
|
.needs_redraw(&ui_state.root, self.rsc.widgets())
|
||||||
@@ -298,13 +323,13 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
|
|||||||
"render(): root={:?} widgets={} active={} root_px={:?} out_size={:?}",
|
"render(): root={:?} widgets={} active={} root_px={:?} out_size={:?}",
|
||||||
ui_state.root.is_some(),
|
ui_state.root.is_some(),
|
||||||
self.rsc.widgets().len(),
|
self.rsc.widgets().len(),
|
||||||
self.rsc.ui.render_state().get().active_widgets(),
|
self.rsc.ui().render_state().get().active_widgets(),
|
||||||
ui_state
|
ui_state
|
||||||
.root
|
.root
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|r| {
|
.and_then(|r| {
|
||||||
self.rsc
|
self.rsc
|
||||||
.ui
|
.ui()
|
||||||
.render_state()
|
.render_state()
|
||||||
.get()
|
.get()
|
||||||
.window_region(r, &self.rsc)
|
.window_region(r, &self.rsc)
|
||||||
@@ -313,7 +338,7 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
let frame_start = Instant::now();
|
let frame_start = Instant::now();
|
||||||
let animating = self.rsc.ui.tick_animations(now);
|
let animating = self.rsc.ui_mut().tick_animations(now);
|
||||||
if animating {
|
if animating {
|
||||||
ctx.view.post_frame_callback(&mut ctx.env);
|
ctx.view.post_frame_callback(&mut ctx.env);
|
||||||
}
|
}
|
||||||
@@ -323,7 +348,7 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
|
|||||||
let Some(renderer) = &mut ui_state.renderer else {
|
let Some(renderer) = &mut ui_state.renderer else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let frame_diagnostics = renderer.update(&mut self.rsc.ui);
|
let frame_diagnostics = renderer.update(self.rsc.ui_mut());
|
||||||
if renderer.frame_count() <= DIAGNOSTIC_FRAMES {
|
if renderer.frame_count() <= DIAGNOSTIC_FRAMES {
|
||||||
log::info!(
|
log::info!(
|
||||||
"iris frame diagnostics: frame={} masks_resized={} moves_resized={} \
|
"iris frame diagnostics: frame={} masks_resized={} moves_resized={} \
|
||||||
@@ -344,20 +369,20 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
|
|||||||
.android_state_mut()
|
.android_state_mut()
|
||||||
.frame_report
|
.frame_report
|
||||||
.record(now, parts, animating);
|
.record(now, parts, animating);
|
||||||
let render_state = self.rsc.ui.render_state();
|
let render_state = self.rsc.ui().render_state();
|
||||||
crate::diagnostics::log_frame(&render_state.get(), now, parts, animating);
|
crate::diagnostics::log_frame(&render_state.get(), now, parts, animating);
|
||||||
if crate::diagnostics::trace_enabled() {
|
if crate::diagnostics::trace_enabled() {
|
||||||
let ui_state = self.state.android_state();
|
let ui_state = self.state.android_state();
|
||||||
log::debug!(
|
log::debug!(
|
||||||
target: "iris::frame",
|
target: "iris::frame",
|
||||||
"render(): after update active={} root_px={:?}",
|
"render(): after update active={} root_px={:?}",
|
||||||
self.rsc.ui.render_state().get().active_widgets(),
|
self.rsc.ui().render_state().get().active_widgets(),
|
||||||
ui_state
|
ui_state
|
||||||
.root
|
.root
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|r| {
|
.and_then(|r| {
|
||||||
self.rsc
|
self.rsc
|
||||||
.ui
|
.ui()
|
||||||
.render_state()
|
.render_state()
|
||||||
.get()
|
.get()
|
||||||
.window_region(r, &self.rsc)
|
.window_region(r, &self.rsc)
|
||||||
@@ -368,7 +393,7 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
|
|||||||
let ui_state = self.state.android_state_mut();
|
let ui_state = self.state.android_state_mut();
|
||||||
if let Some(tree_update) = ui_state.access.update(
|
if let Some(tree_update) = ui_state.access.update(
|
||||||
self.rsc.widgets(),
|
self.rsc.widgets(),
|
||||||
&self.rsc.ui.render_state().get(),
|
&self.rsc.ui().render_state().get(),
|
||||||
&self.rsc,
|
&self.rsc,
|
||||||
) {
|
) {
|
||||||
let ui_state = self.state.android_state_mut();
|
let ui_state = self.state.android_state_mut();
|
||||||
@@ -424,7 +449,7 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
|
|||||||
}
|
}
|
||||||
return handled;
|
return handled;
|
||||||
}
|
}
|
||||||
if self.rsc.events.controllers.command_target_blocks_input() {
|
if self.rsc.events().controllers.command_target_blocks_input() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
let handled = super::input::on_key(
|
let handled = super::input::on_key(
|
||||||
@@ -542,7 +567,7 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
|
|||||||
let x = event.x(&mut ctx.env);
|
let x = event.x(&mut ctx.env);
|
||||||
let y = event.y(&mut ctx.env);
|
let y = event.y(&mut ctx.env);
|
||||||
let access_events = {
|
let access_events = {
|
||||||
let render_handle = self.rsc.ui.render_state();
|
let render_handle = self.rsc.ui().render_state();
|
||||||
let render_state = render_handle.get();
|
let render_state = render_handle.get();
|
||||||
let mut source = AndroidAccessSource {
|
let mut source = AndroidAccessSource {
|
||||||
widgets: self.rsc.widgets(),
|
widgets: self.rsc.widgets(),
|
||||||
@@ -596,15 +621,15 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
|
|||||||
) {
|
) {
|
||||||
self.drain_tasks();
|
self.drain_tasks();
|
||||||
// The layout canvas and wgpu surface are separate and both use physical pixels.
|
// The layout canvas and wgpu surface are separate and both use physical pixels.
|
||||||
self.rsc.ui.resize((width as f32, height as f32));
|
self.rsc.ui_mut().resize((width as f32, height as f32));
|
||||||
|
|
||||||
// Resizing preserves GPU resources; recreating a destroyed surface does not.
|
// Resizing preserves GPU resources; recreating a destroyed surface does not.
|
||||||
let already_live = self.state.android_state().renderer.is_some();
|
let already_live = self.state.android_state().renderer.is_some();
|
||||||
log::info!(
|
log::info!(
|
||||||
"iris surface: surface_changed {width}x{height} already_live={already_live} \
|
"iris surface: surface_changed {width}x{height} already_live={already_live} \
|
||||||
glyphs_cached={} atlas_pages={}",
|
glyphs_cached={} atlas_pages={}",
|
||||||
self.rsc.ui.text.atlas.glyph_count(),
|
self.rsc.ui().text.atlas.glyph_count(),
|
||||||
self.rsc.ui.text.atlas.page_count(),
|
self.rsc.ui().text.atlas.page_count(),
|
||||||
);
|
);
|
||||||
if already_live {
|
if already_live {
|
||||||
let ui_state = self.state.android_state_mut();
|
let ui_state = self.state.android_state_mut();
|
||||||
@@ -648,11 +673,11 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
|
|||||||
"iris surface: new renderer built ({:?}), re-uploading textures: \
|
"iris surface: new renderer built ({:?}), re-uploading textures: \
|
||||||
glyphs={} pages={}",
|
glyphs={} pages={}",
|
||||||
renderer.adapter_backend,
|
renderer.adapter_backend,
|
||||||
self.rsc.ui.text.atlas.glyph_count(),
|
self.rsc.ui().text.atlas.glyph_count(),
|
||||||
self.rsc.ui.text.atlas.page_count(),
|
self.rsc.ui().text.atlas.page_count(),
|
||||||
);
|
);
|
||||||
self.rsc.ui.textures.reupload();
|
self.rsc.ui_mut().textures.reupload();
|
||||||
self.rsc.ui.paints.reupload();
|
self.rsc.ui_mut().paints.reupload();
|
||||||
self.state.android_state_mut().renderer = Some(renderer);
|
self.state.android_state_mut().renderer = Some(renderer);
|
||||||
self.render(ctx, Instant::now());
|
self.render(ctx, Instant::now());
|
||||||
}
|
}
|
||||||
@@ -673,8 +698,8 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
|
|||||||
log::info!(
|
log::info!(
|
||||||
"iris surface: surface_destroyed, tearing the renderer down \
|
"iris surface: surface_destroyed, tearing the renderer down \
|
||||||
(glyphs_cached={} atlas_pages={})",
|
(glyphs_cached={} atlas_pages={})",
|
||||||
self.rsc.ui.text.atlas.glyph_count(),
|
self.rsc.ui().text.atlas.glyph_count(),
|
||||||
self.rsc.ui.text.atlas.page_count(),
|
self.rsc.ui().text.atlas.page_count(),
|
||||||
);
|
);
|
||||||
self.state.android_state_mut().renderer = None;
|
self.state.android_state_mut().renderer = None;
|
||||||
}
|
}
|
||||||
@@ -724,7 +749,7 @@ impl<State: AndroidAppState> AccessibilityNodeProvider for IrisViewPeer<State> {
|
|||||||
ctx: &mut CallbackCtx<'local>,
|
ctx: &mut CallbackCtx<'local>,
|
||||||
virtual_view_id: jint,
|
virtual_view_id: jint,
|
||||||
) -> AccessibilityNodeInfo<'local> {
|
) -> AccessibilityNodeInfo<'local> {
|
||||||
let render_handle = self.rsc.ui.render_state();
|
let render_handle = self.rsc.ui().render_state();
|
||||||
let render_state = render_handle.get();
|
let render_state = render_handle.get();
|
||||||
let mut source = AndroidAccessSource {
|
let mut source = AndroidAccessSource {
|
||||||
widgets: self.rsc.widgets(),
|
widgets: self.rsc.widgets(),
|
||||||
@@ -745,7 +770,7 @@ impl<State: AndroidAppState> AccessibilityNodeProvider for IrisViewPeer<State> {
|
|||||||
ctx: &mut CallbackCtx<'local>,
|
ctx: &mut CallbackCtx<'local>,
|
||||||
focus_type: jint,
|
focus_type: jint,
|
||||||
) -> AccessibilityNodeInfo<'local> {
|
) -> AccessibilityNodeInfo<'local> {
|
||||||
let render_handle = self.rsc.ui.render_state();
|
let render_handle = self.rsc.ui().render_state();
|
||||||
let render_state = render_handle.get();
|
let render_state = render_handle.get();
|
||||||
let mut source = AndroidAccessSource {
|
let mut source = AndroidAccessSource {
|
||||||
widgets: self.rsc.widgets(),
|
widgets: self.rsc.widgets(),
|
||||||
@@ -797,6 +822,7 @@ pub fn new_peer<'local, State: AndroidAppState>(
|
|||||||
mut env: JNIEnv<'local>,
|
mut env: JNIEnv<'local>,
|
||||||
view: View<'local>,
|
view: View<'local>,
|
||||||
context: Context<'local>,
|
context: Context<'local>,
|
||||||
|
init: fn(AndroidUiState, &mut State::Resources) -> State,
|
||||||
) -> android_view::jni::sys::jlong {
|
) -> android_view::jni::sys::jlong {
|
||||||
// `DisplayMetrics.density` -- physical pixels per dp on this device.
|
// `DisplayMetrics.density` -- physical pixels per dp on this device.
|
||||||
// Read once here, at the one point in this file already handed a
|
// Read once here, at the one point in this file already handed a
|
||||||
@@ -810,11 +836,11 @@ pub fn new_peer<'local, State: AndroidAppState>(
|
|||||||
let vm = env.get_java_vm().unwrap();
|
let vm = env.get_java_vm().unwrap();
|
||||||
let global_view = env.new_global_ref(&view.0).unwrap();
|
let global_view = env.new_global_ref(&view.0).unwrap();
|
||||||
let redraw: Arc<dyn RequestRedraw> = Arc::new(AndroidRedrawHandle::new(vm, global_view));
|
let redraw: Arc<dyn RequestRedraw> = Arc::new(AndroidRedrawHandle::new(vm, global_view));
|
||||||
let (mut rsc, task_recv) = StdRsc::new(redraw);
|
let (mut rsc, task_recv) = State::Resources::new(redraw);
|
||||||
rsc.ui.set_density(content_scale);
|
rsc.ui_mut().set_density(content_scale);
|
||||||
let shared = Rc::new(RefCell::new(Shared::default()));
|
let shared = Rc::new(RefCell::new(Shared::default()));
|
||||||
let ui_state = AndroidUiState::new(shared.clone(), content_scale);
|
let ui_state = AndroidUiState::new(shared.clone(), content_scale);
|
||||||
let mut state = State::new(ui_state, &mut rsc);
|
let mut state = init(ui_state, &mut rsc);
|
||||||
let platform_vm = env.get_java_vm().unwrap();
|
let platform_vm = env.get_java_vm().unwrap();
|
||||||
let platform_view = env.new_global_ref(&view.0).unwrap();
|
let platform_view = env.new_global_ref(&view.0).unwrap();
|
||||||
state.platform_ready(&mut rsc, platform_vm, platform_view);
|
state.platform_ready(&mut rsc, platform_vm, platform_view);
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ mod layout_tests;
|
|||||||
|
|
||||||
pub use iris_core as core;
|
pub use iris_core as core;
|
||||||
pub use iris_macro as macros;
|
pub use iris_macro as macros;
|
||||||
|
pub use iris_macro::app_init;
|
||||||
|
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
Reference in new issue
Block a user