Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions android/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# android/: the Android hello

The host shell of an APK (`AndroidManifest.xml`, `src/FizzyActivity.java`) and a hello
(`src/main.zig`: dvui on SDL3, a tap changes what is drawn). The build is `build/android.zig`
(`zig build apk -Dandroid-sdk=<dir>`); `README.md` has the steps and the emulator run. The hello
is not an app entry: fizzy on Android is `build/exe.zig` with an Android target.

- **SDL needs its Java `SDLActivity`.** SDL3 has no NativeActivity mode, so "no Java at all" would
mean a second platform layer beside SDL (input, surface, IME over JNI). Java stays until the
Zig windowing library (plans#51) grows an Android backend.
- **Zig cannot link the library:** it ships no Android libc, so the NDK's clang does, with the
hello and SDL archives whole (SDL finds its JNI entry points and `SDL_main` by name).
- **minSdk is 24:** `std.Io` calls `preadv64`/`pwritev64`, which bionic has from API 24.
- **Use a Zig global cache of its own** (`ZIG_GLOBAL_CACHE_DIR`) when the shared one holds dvui's
other lazy dependencies: Zig unpacks them into `zig-pkg`, and cairo/expat then fail to compile.
- **Nothing in CI builds this yet.** `zig build apk` needs the NDK and SDK; a compile-only step
is for when CI has them.
20 changes: 20 additions & 0 deletions android/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="dev.fizzyedit.hello">

<uses-feature android:glEsVersion="0x00020000" />
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />

<application android:label="Fizzy" android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
<activity
android:name=".FizzyActivity"
android:exported="true"
android:configChanges="layoutDirection|locale|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
1 change: 1 addition & 0 deletions android/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
56 changes: 56 additions & 0 deletions android/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Android hello

An SDL3 window with dvui drawing, packaged as an APK: the first step of fizzyedit/plans#60. A tap
on the button changes what is drawn. Plugins are compiled in (there are no plugin libraries on
mobile); this hello draws no plugin yet, and it is dvui's own SDL3 backend, not fizzy's.

## Build

`zig build apk` (`build/android.zig`, in the root build graph) cross-compiles `android/src/main.zig`, dvui and SDL3, links them with the NDK's clang into
`libfizzy_hello.so`, and packages `zig-out/fizzy-hello.apk` with the Android SDK's tools (`javac`,
`d8`, `aapt2`, `zipalign`, `apksigner`). There is no Gradle project: SDL's Java side is compiled
from the same SDL source the native side is built from, with the 9-line `src/FizzyActivity.java`.

You need a JDK 17 or newer on `PATH` and the Android SDK (platform 35, build-tools 35.0.0, NDK
27.2.12479018) on a Linux x86_64 host. A debug keystore is made once with
`keytool -genkey -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android
-keypass android -keyalg RSA -validity 10000 -dname CN=debug`.

```sh
export ANDROID_HOME=$HOME/android/sdk
zig build apk -Dandroid-sdk=$ANDROID_HOME -Dandroid-arch=x86_64 # the emulator on an x86_64 host
zig build apk -Dandroid-sdk=$ANDROID_HOME # a phone (aarch64, the default)
```

dvui's `sdl3` dependency is lazy: the first `zig build apk` configures, fetches it and configures
again. Other options: `-Dandroid-ndk`, `-Dandroid-build-tools`, `-Dandroid-platform`, `-Dandroid-keystore`. The app id is `dev.fizzy.hello` (`build/bundle_id.zig`).

## Run in the emulator (pop-os)

The x86_64 emulator needs hardware acceleration, so `/dev/kvm` must be writable by you:
`sudo usermod -aG kvm $USER`, then log in again.

```sh
SDK=$ANDROID_HOME
$SDK/cmdline-tools/latest/bin/sdkmanager "system-images;android-35;google_apis;x86_64" "emulator"
$SDK/cmdline-tools/latest/bin/avdmanager create avd -n fizzy -k "system-images;android-35;google_apis;x86_64" -d pixel_6
$SDK/emulator/emulator -avd fizzy -no-snapshot & # add -no-window -gpu swiftshader_indirect headless
$SDK/platform-tools/adb install -r zig-out/fizzy-hello.apk
$SDK/platform-tools/adb shell am start -n dev.fizzy.hello/dev.fizzyedit.hello.FizzyActivity
$SDK/platform-tools/adb shell input tap 540 1200 # the tap button
$SDK/platform-tools/adb exec-out screencap -p > hello.png
```

## Pitfalls

See `AGENTS.md`: the Zig cache, minSdk 24, linking with the NDK's clang, and why Java stays
(SDL3 runs only under its `SDLActivity`; a NativeActivity would be a second platform layer).

## What is not here yet

This hello is dvui on dvui's own SDL3 backend and its lazy `sdl3` package: a second SDL pin beside
`backend/build.zig.zon`'s, kept as a stated, temporary exception. Fizzy's backend (`backend/`: its
GPU renderer, glass, OS windows) has no Android target in its build yet, and linking the hello
against it, with SDL's Java sources from the same pinned `sdl_source`, was not attempted here. The
editor is not built for Android either, and `app/store/registry/compat.zig`'s `hostKey()` must map
Android to `android-aarch64` before any Android build downloads a plugin.
9 changes: 9 additions & 0 deletions android/src/FizzyActivity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package dev.fizzyedit.hello;

import org.libsdl.app.SDLActivity;

public class FizzyActivity extends SDLActivity {
protected String[] getLibraries() {
return new String[] { "fizzy_hello" }; // SDL3 is statically linked into it
}
}
77 changes: 77 additions & 0 deletions android/src/main.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//! The Android hello: a dvui App on the SDL3 backend. A tap on the button changes what is drawn.
const std = @import("std");
const builtin = @import("builtin");
const dvui = @import("dvui");

pub const dvui_app: dvui.App = .{
.config = .{
.options = .{
.size = .{ .w = 800.0, .h = 600.0 },
.min_size = .{ .w = 250.0, .h = 350.0 },
.title = "Fizzy",
},
},
.frameFn = appFrame,
};
pub const main = dvui.App.main;

// SDL's activity looks this symbol up in the library and calls it on its own thread. This is a
// static lib linked into a JNI library, so zig's start.zig never runs and never builds the
// std.process.Init dvui.App.main needs; hand-build the same minimal Init here.
export fn SDL_main(argc: c_int, argv: [*][*:0]u8) callconv(.c) c_int {
return runDvuiMain(argc, argv) catch |err| {
std.log.err("SDL_main failed: {t}", .{err});
return 1;
};
}

extern "c" var environ: ?[*:null]?[*:0]u8;

fn runDvuiMain(argc: c_int, argv: [*][*:0]u8) !u8 {
const gpa = std.heap.c_allocator;

var arena_allocator: std.heap.ArenaAllocator = .init(std.heap.page_allocator);
defer arena_allocator.deinit();

const args_vector: std.process.Args.Vector = argv[0..@intCast(argc)];
const no_environ: [0:null]?[*:0]u8 = .{};
const environ_block: std.process.Environ.Block = .{ .slice = std.mem.span(environ orelse &no_environ) };

var threaded: std.Io.Threaded = .init(gpa, .{
.argv0 = .init(.{ .vector = args_vector }),
.environ = .{ .block = environ_block },
});
defer threaded.deinit();

var environ_map = try std.process.Environ.createMap(.{ .block = environ_block }, gpa);
defer environ_map.deinit();

const preopens = try std.process.Preopens.init(arena_allocator.allocator());

return dvui.App.main(.{
.minimal = .{ .args = .{ .vector = args_vector }, .environ = .{ .block = environ_block } },
.arena = &arena_allocator,
.gpa = gpa,
.io = threaded.io(),
.environ_map = &environ_map,
.preopens = preopens,
});
}
pub const panic = dvui.App.panic;
pub const std_options: std.Options = .{
.logFn = dvui.App.logFn,
};

var taps: u32 = 0;

fn appFrame() !dvui.App.Result {
var root = dvui.box(@src(), .{}, .{ .expand = .both, .style = .window, .background = true });
defer root.deinit();

dvui.label(@src(), "Fizzy on Android", .{}, .{ .font = .theme(.title), .gravity_x = 0.5 });
dvui.label(@src(), "{s} on {s}", .{ @tagName(dvui.backend.kind), @tagName(builtin.cpu.arch) }, .{ .gravity_x = 0.5 });
dvui.label(@src(), "taps: {d}", .{taps}, .{ .gravity_x = 0.5, .tag = "tap-count" });

if (dvui.button(@src(), "Tap me", .{}, .{ .gravity_x = 0.5, .min_size_content = .{ .w = 240, .h = 120 }, .tag = "tap-button" })) taps += 1;
return .ok;
}
2 changes: 1 addition & 1 deletion app/ActivityFeed.zig
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const Action = struct {
pub const Act = enum {
/// Redo step `tag` ("Undid Claude's Draw").
redo,
/// Go back to before the last run of author `tag` ("Claude · Draw").
/// Go back to before the last run of author `tag` ("Claude . Draw").
go_back,
/// The same, told that it also takes back some of the person's own steps.
go_back_anyway,
Expand Down
Loading
Loading