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 {
|
||||
fn new(mut ui_state: AndroidUiState, rsc: &mut StdRsc<Self>) -> Self {
|
||||
impl BenchClient {
|
||||
pub(super) fn new(mut ui_state: AndroidUiState, rsc: &mut StdRsc<Self>) -> Self {
|
||||
let content = WidgetPtr::new().add(rsc);
|
||||
let loading = placeholder(rsc, "Loading fixture...");
|
||||
content(rsc).set(loading);
|
||||
@@ -186,16 +186,24 @@ impl AndroidAppState for BenchClient {
|
||||
}
|
||||
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)));
|
||||
}
|
||||
|
||||
fn back_pressed(&mut self, _rsc: &mut StdRsc<Self>) -> bool {
|
||||
fn back_pressed(&mut self, _rsc: &mut Self::Resources) -> bool {
|
||||
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 {
|
||||
self.last_top_pad = insets.top;
|
||||
let controls = bench_controls(rsc, insets.top);
|
||||
|
||||
@@ -36,7 +36,7 @@ extern "system" fn new_view_peer<'local>(
|
||||
view: View<'local>,
|
||||
context: Context<'local>,
|
||||
) -> jlong {
|
||||
iris::android::new_peer::<ActiveClient>(env, view, context)
|
||||
iris::android::new_peer::<ActiveClient>(env, view, context, ActiveClient::new)
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
|
||||
@@ -89,8 +89,8 @@ fn frame_report_controls(rsc: &mut StdRsc<TranscriptClient>) -> WeakWidget {
|
||||
(report, reset).span(Dir::RIGHT).height(56).add(rsc)
|
||||
}
|
||||
|
||||
impl AndroidAppState for TranscriptClient {
|
||||
fn new(mut ui_state: AndroidUiState, rsc: &mut StdRsc<Self>) -> Self {
|
||||
impl TranscriptClient {
|
||||
pub(super) fn new(mut ui_state: AndroidUiState, rsc: &mut StdRsc<Self>) -> Self {
|
||||
let content = WidgetPtr::new().add(rsc);
|
||||
let loading = placeholder(rsc, "Loading sessions...");
|
||||
content(rsc).set(loading);
|
||||
@@ -112,8 +112,12 @@ impl AndroidAppState for TranscriptClient {
|
||||
client.spawn_fetch_sessions(rsc);
|
||||
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"
|
||||
// answer `iris-android-app`'s tabs `Client` already gives.
|
||||
false
|
||||
|
||||
@@ -790,6 +790,33 @@ ssh case are one implementation.
|
||||
`--bind <ip>` is an *explicit, logged* override for development, a
|
||||
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/`)
|
||||
|
||||
One Rust crate owns platform-free client logic and Iris widget trees. Android
|
||||
|
||||
Generated
+75
-6
@@ -681,6 +681,46 @@ dependencies = [
|
||||
"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]]
|
||||
name = "cc"
|
||||
version = "1.4.5"
|
||||
@@ -965,7 +1005,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1587,6 +1627,12 @@ dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.21.1"
|
||||
@@ -2406,7 +2452,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3030,7 +3076,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.12.1",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3078,6 +3124,10 @@ name = "semver"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "send_wrapper"
|
||||
@@ -3115,6 +3165,19 @@ dependencies = [
|
||||
"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]]
|
||||
name = "serde_repr"
|
||||
version = "0.1.21"
|
||||
@@ -3325,7 +3388,7 @@ dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"once_cell",
|
||||
"rustix 1.1.4",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3522,7 +3585,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"tempfile",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3975,7 +4038,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4713,6 +4776,12 @@ version = "0.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
version = "0.5.3"
|
||||
|
||||
@@ -44,6 +44,7 @@ harness = false
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"cargo-iris",
|
||||
"core",
|
||||
"macro",
|
||||
"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 quote::quote;
|
||||
use syn::{
|
||||
Attribute, Block, Error, GenericParam, Generics, Ident, ItemStruct, ItemTrait, Signature,
|
||||
Token, Type, Visibility,
|
||||
Attribute, Block, Error, FnArg, GenericParam, Generics, Ident, ItemFn, ItemStruct, ItemTrait,
|
||||
ReturnType, Signature, Token, Type, Visibility,
|
||||
parse::{Parse, ParseStream, Result},
|
||||
parse_macro_input, parse_quote,
|
||||
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 {
|
||||
attrs: Vec<Attribute>,
|
||||
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`.
|
||||
|
||||
## 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:
|
||||
1. does what I want it to (text, images, video, animations)
|
||||
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> {
|
||||
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)
|
||||
.flatten()
|
||||
}
|
||||
|
||||
pub(super) fn update_ime_selection(&mut self, ctx: &mut CallbackCtx) {
|
||||
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 {
|
||||
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,
|
||||
);
|
||||
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 start = byte_to_utf16(text.text(), sel.start) 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;
|
||||
}
|
||||
let focus = self.focus()?;
|
||||
let text = &self.rsc[focus];
|
||||
let text = focus.get(&self.rsc);
|
||||
let sel = text.selection_range()?;
|
||||
let end_16 = byte_to_utf16(text.text(), sel.start);
|
||||
let start_16 = end_16.saturating_sub(n as usize);
|
||||
@@ -129,7 +129,7 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
|
||||
return None;
|
||||
}
|
||||
let focus = self.focus()?;
|
||||
let text = &self.rsc[focus];
|
||||
let text = focus.get(&self.rsc);
|
||||
let sel = text.selection_range()?;
|
||||
let len_16 = byte_to_utf16(text.text(), text.text().len());
|
||||
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>> {
|
||||
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 {
|
||||
let Some(focus) = self.focus() else {
|
||||
return 0;
|
||||
};
|
||||
let text = &self.rsc[focus];
|
||||
let text = focus.get(&self.rsc);
|
||||
let Some(caret) = text.caret() else {
|
||||
return 0;
|
||||
};
|
||||
@@ -164,7 +164,7 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
|
||||
let Some(focus) = self.focus() else {
|
||||
return false;
|
||||
};
|
||||
let text = &self.rsc[focus];
|
||||
let text = focus.get(&self.rsc);
|
||||
let Some(sel) = text.selection_range() else {
|
||||
return false;
|
||||
};
|
||||
@@ -223,7 +223,7 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
|
||||
let Some(focus) = self.focus() else {
|
||||
return false;
|
||||
};
|
||||
let text = &self.rsc[focus];
|
||||
let text = focus.get(&self.rsc);
|
||||
let content = text.text();
|
||||
let byte = utf16_to_byte(content, end.max(0) as usize);
|
||||
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`
|
||||
/// reports for a plain key press. Returns whether anything used the event.
|
||||
pub(super) fn on_key<'local, State: AndroidAppState>(
|
||||
rsc: &mut StdRsc<State>,
|
||||
rsc: &mut State::Resources,
|
||||
state: &mut State,
|
||||
env: &mut JNIEnv<'local>,
|
||||
key_code: Keycode,
|
||||
|
||||
+33
-1
@@ -10,9 +10,41 @@ mod view;
|
||||
pub use insets::Insets;
|
||||
pub use render::AndroidRenderer;
|
||||
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
|
||||
/// `android_view::register_view_class` covers (window insets -- see
|
||||
/// `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 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!(
|
||||
"iris renderer: surface={:?}, view={:?}, color_space=Srgb",
|
||||
formats.surface,
|
||||
|
||||
+60
-34
@@ -89,8 +89,8 @@ impl AndroidUiState {
|
||||
}
|
||||
}
|
||||
|
||||
impl<State: 'static> HasRoot<StdRsc<State>> for AndroidUiState {
|
||||
fn set_root(&mut self, rsc: &mut StdRsc<State>, root: StrongWidget) {
|
||||
impl<Rsc: HasEvents> HasRoot<Rsc> for AndroidUiState {
|
||||
fn set_root(&mut self, rsc: &mut Rsc, root: StrongWidget) {
|
||||
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;
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
fn new(ui_state: AndroidUiState, rsc: &mut StdRsc<Self>) -> Self;
|
||||
type Resources: AndroidResources<Self>;
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn back_pressed(&mut self, rsc: &mut StdRsc<Self>) -> bool {
|
||||
fn back_pressed(&mut self, rsc: &mut Self::Resources) -> bool {
|
||||
false
|
||||
}
|
||||
#[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)]
|
||||
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.
|
||||
@@ -144,9 +169,9 @@ impl WindowInsets {
|
||||
/// `RustView` instance; `new_peer` (below) builds it and hands the id to
|
||||
/// Java the same way android-view's own demo does.
|
||||
pub struct IrisViewPeer<State: AndroidAppState> {
|
||||
pub(super) rsc: StdRsc<State>,
|
||||
pub(super) rsc: State::Resources,
|
||||
pub(super) state: State,
|
||||
task_recv: TaskMsgReceiver<StdRsc<State>>,
|
||||
task_recv: TaskMsgReceiver<State::Resources>,
|
||||
/// Converts input and Choreographer timestamps onto one monotonic clock.
|
||||
device_clock: Option<DeviceClock>,
|
||||
}
|
||||
@@ -169,7 +194,7 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
|
||||
let ui_state = self.state.android_state_mut();
|
||||
let cursor = ui_state.cursor.clone();
|
||||
let old_focus = ui_state.focus;
|
||||
let render_state = self.rsc.ui.render_state();
|
||||
let render_state = self.rsc.ui().render_state();
|
||||
render_state
|
||||
.get()
|
||||
.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();
|
||||
ui_state.cursor.end_frame();
|
||||
let render_state = self.rsc.ui.render_state();
|
||||
let render_state = self.rsc.ui().render_state();
|
||||
if render_state
|
||||
.get()
|
||||
.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={:?}",
|
||||
ui_state.root.is_some(),
|
||||
self.rsc.widgets().len(),
|
||||
self.rsc.ui.render_state().get().active_widgets(),
|
||||
self.rsc.ui().render_state().get().active_widgets(),
|
||||
ui_state
|
||||
.root
|
||||
.as_ref()
|
||||
.and_then(|r| {
|
||||
self.rsc
|
||||
.ui
|
||||
.ui()
|
||||
.render_state()
|
||||
.get()
|
||||
.window_region(r, &self.rsc)
|
||||
@@ -313,7 +338,7 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
|
||||
);
|
||||
}
|
||||
let frame_start = Instant::now();
|
||||
let animating = self.rsc.ui.tick_animations(now);
|
||||
let animating = self.rsc.ui_mut().tick_animations(now);
|
||||
if animating {
|
||||
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 {
|
||||
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 {
|
||||
log::info!(
|
||||
"iris frame diagnostics: frame={} masks_resized={} moves_resized={} \
|
||||
@@ -344,20 +369,20 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
|
||||
.android_state_mut()
|
||||
.frame_report
|
||||
.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);
|
||||
if crate::diagnostics::trace_enabled() {
|
||||
let ui_state = self.state.android_state();
|
||||
log::debug!(
|
||||
target: "iris::frame",
|
||||
"render(): after update active={} root_px={:?}",
|
||||
self.rsc.ui.render_state().get().active_widgets(),
|
||||
self.rsc.ui().render_state().get().active_widgets(),
|
||||
ui_state
|
||||
.root
|
||||
.as_ref()
|
||||
.and_then(|r| {
|
||||
self.rsc
|
||||
.ui
|
||||
.ui()
|
||||
.render_state()
|
||||
.get()
|
||||
.window_region(r, &self.rsc)
|
||||
@@ -368,7 +393,7 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
|
||||
let ui_state = self.state.android_state_mut();
|
||||
if let Some(tree_update) = ui_state.access.update(
|
||||
self.rsc.widgets(),
|
||||
&self.rsc.ui.render_state().get(),
|
||||
&self.rsc.ui().render_state().get(),
|
||||
&self.rsc,
|
||||
) {
|
||||
let ui_state = self.state.android_state_mut();
|
||||
@@ -424,7 +449,7 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
|
||||
}
|
||||
return handled;
|
||||
}
|
||||
if self.rsc.events.controllers.command_target_blocks_input() {
|
||||
if self.rsc.events().controllers.command_target_blocks_input() {
|
||||
return true;
|
||||
}
|
||||
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 y = event.y(&mut ctx.env);
|
||||
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 mut source = AndroidAccessSource {
|
||||
widgets: self.rsc.widgets(),
|
||||
@@ -596,15 +621,15 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
|
||||
) {
|
||||
self.drain_tasks();
|
||||
// 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.
|
||||
let already_live = self.state.android_state().renderer.is_some();
|
||||
log::info!(
|
||||
"iris surface: surface_changed {width}x{height} already_live={already_live} \
|
||||
glyphs_cached={} atlas_pages={}",
|
||||
self.rsc.ui.text.atlas.glyph_count(),
|
||||
self.rsc.ui.text.atlas.page_count(),
|
||||
self.rsc.ui().text.atlas.glyph_count(),
|
||||
self.rsc.ui().text.atlas.page_count(),
|
||||
);
|
||||
if already_live {
|
||||
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: \
|
||||
glyphs={} pages={}",
|
||||
renderer.adapter_backend,
|
||||
self.rsc.ui.text.atlas.glyph_count(),
|
||||
self.rsc.ui.text.atlas.page_count(),
|
||||
self.rsc.ui().text.atlas.glyph_count(),
|
||||
self.rsc.ui().text.atlas.page_count(),
|
||||
);
|
||||
self.rsc.ui.textures.reupload();
|
||||
self.rsc.ui.paints.reupload();
|
||||
self.rsc.ui_mut().textures.reupload();
|
||||
self.rsc.ui_mut().paints.reupload();
|
||||
self.state.android_state_mut().renderer = Some(renderer);
|
||||
self.render(ctx, Instant::now());
|
||||
}
|
||||
@@ -673,8 +698,8 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
|
||||
log::info!(
|
||||
"iris surface: surface_destroyed, tearing the renderer down \
|
||||
(glyphs_cached={} atlas_pages={})",
|
||||
self.rsc.ui.text.atlas.glyph_count(),
|
||||
self.rsc.ui.text.atlas.page_count(),
|
||||
self.rsc.ui().text.atlas.glyph_count(),
|
||||
self.rsc.ui().text.atlas.page_count(),
|
||||
);
|
||||
self.state.android_state_mut().renderer = None;
|
||||
}
|
||||
@@ -724,7 +749,7 @@ impl<State: AndroidAppState> AccessibilityNodeProvider for IrisViewPeer<State> {
|
||||
ctx: &mut CallbackCtx<'local>,
|
||||
virtual_view_id: jint,
|
||||
) -> 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 mut source = AndroidAccessSource {
|
||||
widgets: self.rsc.widgets(),
|
||||
@@ -745,7 +770,7 @@ impl<State: AndroidAppState> AccessibilityNodeProvider for IrisViewPeer<State> {
|
||||
ctx: &mut CallbackCtx<'local>,
|
||||
focus_type: jint,
|
||||
) -> 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 mut source = AndroidAccessSource {
|
||||
widgets: self.rsc.widgets(),
|
||||
@@ -797,6 +822,7 @@ pub fn new_peer<'local, State: AndroidAppState>(
|
||||
mut env: JNIEnv<'local>,
|
||||
view: View<'local>,
|
||||
context: Context<'local>,
|
||||
init: fn(AndroidUiState, &mut State::Resources) -> State,
|
||||
) -> android_view::jni::sys::jlong {
|
||||
// `DisplayMetrics.density` -- physical pixels per dp on this device.
|
||||
// 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 global_view = env.new_global_ref(&view.0).unwrap();
|
||||
let redraw: Arc<dyn RequestRedraw> = Arc::new(AndroidRedrawHandle::new(vm, global_view));
|
||||
let (mut rsc, task_recv) = StdRsc::new(redraw);
|
||||
rsc.ui.set_density(content_scale);
|
||||
let (mut rsc, task_recv) = State::Resources::new(redraw);
|
||||
rsc.ui_mut().set_density(content_scale);
|
||||
let shared = Rc::new(RefCell::new(Shared::default()));
|
||||
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_view = env.new_global_ref(&view.0).unwrap();
|
||||
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_macro as macros;
|
||||
pub use iris_macro::app_init;
|
||||
|
||||
pub mod prelude {
|
||||
use super::*;
|
||||
|
||||
Reference in new issue
Block a user