diff --git a/android/Sdk.zig b/android/Sdk.zig
index 998db6b1..a826f789 100644
--- a/android/Sdk.zig
+++ b/android/Sdk.zig
@@ -406,6 +406,7 @@ pub fn createApp(
sdk: *Sdk,
apk_file: []const u8,
src_file: []const u8,
+ dex_file_opt: ?[]const u8,
app_config: AppConfig,
mode: std.builtin.Mode,
wanted_targets: AppTargetConfig,
@@ -460,35 +461,28 @@ pub fn createApp(
, .{perm}) catch unreachable;
}
- if (app_config.fullscreen) {
- writer.writeAll(
- \\
- \\
- \\
- \\
- \\
- \\
- \\
- \\
- \\
- \\
- \\
- ) catch unreachable;
- } else {
- writer.writeAll(
- \\
- \\
- \\
- \\
- \\
- \\
- \\
- \\
- \\
- \\
- \\
- ) catch unreachable;
- }
+ const theme = if (app_config.fullscreen)
+ \\android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
+ else
+ \\
+ ;
+
+ writer.print(
+ \\
+ \\
+ \\
+ \\
+ \\
+ \\
+ \\
+ \\
+ \\
+ \\
+ \\
+ , .{
+ .hasCode = dex_file_opt != null,
+ .theme = theme,
+ }) catch unreachable;
break :blk buf.toOwnedSlice() catch unreachable;
});
@@ -546,6 +540,8 @@ pub fn createApp(
sdk.b.pathFromRoot(unaligned_apk_file),
"-I", // add an existing package to base include set
root_jar,
+ "-I",
+ "classes.dex",
});
make_unsigned_apk.addArg("-M"); // specify full path to AndroidManifest.xml to include in zip
@@ -576,6 +572,12 @@ pub fn createApp(
const align_step = sdk.alignApk(unaligned_apk_file, apk_file);
+ if (dex_file_opt) |dex_file| {
+ const copy_dex_to_zip = CopyToZipStep.create(sdk, unaligned_apk_file, null, std.build.FileSource.relative(dex_file));
+ copy_dex_to_zip.step.dependOn(&make_unsigned_apk.step); // enforces creation of APK before the execution
+ align_step.dependOn(©_dex_to_zip.step);
+ }
+
const sign_step = sdk.signApk(apk_file, key_store);
sign_step.dependOn(align_step);
@@ -686,16 +688,18 @@ const CreateResourceDirectory = struct {
const CopyToZipStep = struct {
step: Step,
sdk: *Sdk,
- target_dir: []const u8,
+ target_dir: ?[]const u8,
input_file: std.build.FileSource,
apk_file: []const u8,
- fn create(sdk: *Sdk, apk_file: []const u8, target_dir: []const u8, input_file: std.build.FileSource) *CopyToZipStep {
- std.debug.assert(target_dir[target_dir.len - 1] == '/');
+ fn create(sdk: *Sdk, apk_file: []const u8, target_dir_opt: ?[]const u8, input_file: std.build.FileSource) *CopyToZipStep {
+ if (target_dir_opt) |target_dir| {
+ std.debug.assert(target_dir[target_dir.len - 1] == '/');
+ }
const self = sdk.b.allocator.create(CopyToZipStep) catch unreachable;
self.* = CopyToZipStep{
.step = Step.init(.custom, "copy to zip", sdk.b.allocator, make),
- .target_dir = target_dir,
+ .target_dir = target_dir_opt,
.input_file = input_file,
.sdk = sdk,
.apk_file = sdk.b.pathFromRoot(apk_file),
@@ -712,10 +716,10 @@ const CopyToZipStep = struct {
const output_path = self.input_file.getPath(self.sdk.b);
- var zip_name = std.mem.concat(self.sdk.b.allocator, u8, &[_][]const u8{
- self.target_dir,
+ var zip_name = if (self.target_dir) |target_dir| std.mem.concat(self.sdk.b.allocator, u8, &[_][]const u8{
+ target_dir,
std.fs.path.basename(output_path),
- }) catch unreachable;
+ }) catch unreachable else std.fs.path.basename(output_path);
const args = [_][]const u8{
self.sdk.host_tools.zip_add.getOutputSource().getPath(self.sdk.b),
diff --git a/android/build.zig b/android/build.zig
index 39724df6..0d782bee 100644
--- a/android/build.zig
+++ b/android/build.zig
@@ -68,16 +68,22 @@ pub fn build(b: *std.build.Builder) !void {
// Replace by your app's main file.
// Here this is some code to choose the example to run
- const ExampleType = enum { egl, textview };
+ const ExampleType = enum { egl, textview, invocationhandler };
const example = b.option(ExampleType, "example", "Which example to run") orelse .egl;
const src = switch (example) {
.egl => "examples/egl/main.zig",
.textview => "examples/textview/main.zig",
+ .invocationhandler => "examples/invocationhandler/main.zig",
+ };
+ const dex: ?[:0]const u8 = switch (example) {
+ .invocationhandler => "classes.dex",
+ else => null,
};
const app = sdk.createApp(
"app-template.apk",
src,
+ dex,
config,
mode,
.{
diff --git a/android/examples/egl/main.zig b/android/examples/egl/main.zig
index c2f2ded9..b983c077 100644
--- a/android/examples/egl/main.zig
+++ b/android/examples/egl/main.zig
@@ -8,6 +8,7 @@ pub const log = android.log;
const EGLContext = android.egl.EGLContext;
const JNI = android.JNI;
+const NativeActivity = android.NativeActivity;
const c = android.egl.c;
const app_log = std.log.scoped(.app);
@@ -200,10 +201,10 @@ pub const AndroidApp = struct {
});
if (event_type == .AKEY_EVENT_ACTION_DOWN) {
- var jni = JNI.init(self.activity);
- defer jni.deinit();
+ var native_activity = NativeActivity.init(self.activity);
+ defer native_activity.deinit();
- var codepoint = jni.AndroidGetUnicodeChar(
+ var codepoint = try native_activity.AndroidGetUnicodeChar(
android.AKeyEvent_getKeyCode(event),
android.AKeyEvent_getMetaState(event),
);
@@ -252,21 +253,21 @@ pub const AndroidApp = struct {
const event_type = @intToEnum(android.AMotionEventActionType, android.AMotionEvent_getAction(event));
{
- var jni = JNI.init(self.activity);
- defer jni.deinit();
+ var native_activity = NativeActivity.init(self.activity);
+ defer native_activity.deinit();
// Show/Hide keyboard
- // _ = jni.AndroidDisplayKeyboard(true);
+ // _ = native_activity.AndroidDisplayKeyboard(true);
// this allows you to send the app in the background
- // const success = jni.AndroidSendToBack(true);
+ // const success = native_activity.AndroidSendToBack(true);
// _ = success;
// std.log.scoped(.input).debug("SendToBack() = {}\n", .{success});
// This is a demo on how to request permissions:
if (event_type == .AMOTION_EVENT_ACTION_UP) {
- if (!JNI.AndroidHasPermissions(&jni, "android.permission.RECORD_AUDIO")) {
- JNI.AndroidRequestAppPermissions(&jni, "android.permission.RECORD_AUDIO");
+ if (!try NativeActivity.AndroidHasPermissions(&native_activity, "android.permission.RECORD_AUDIO")) {
+ try NativeActivity.AndroidRequestAppPermissions(&native_activity, "android.permission.RECORD_AUDIO");
}
}
}
@@ -350,11 +351,11 @@ pub const AndroidApp = struct {
fn mainLoop(self: *Self) !void {
// This code somehow crashes yet. Needs more investigations
- var jni = JNI.init(self.activity);
- defer jni.deinit();
+ var native_activity = NativeActivity.init(self.activity);
+ defer native_activity.deinit();
// Must be called from main thread…
- _ = jni.AndroidMakeFullscreen();
+ _ = try native_activity.AndroidMakeFullscreen();
var loop: usize = 0;
app_log.info("mainLoop() started\n", .{});
diff --git a/android/examples/invocationhandler/main.zig b/android/examples/invocationhandler/main.zig
new file mode 100644
index 00000000..57d5d5ce
--- /dev/null
+++ b/android/examples/invocationhandler/main.zig
@@ -0,0 +1,243 @@
+const std = @import("std");
+
+const android = @import("android");
+
+pub const panic = android.panic;
+pub const log = android.log;
+
+const EGLContext = android.egl.EGLContext;
+const JNI = android.JNI;
+const NativeActivity = android.NativeActivity;
+const c = android.egl.c;
+const NativeInvocationHandler = android.NativeInvocationHandler;
+
+const app_log = std.log.scoped(.app);
+comptime {
+ _ = android.ANativeActivity_createFunc;
+ _ = @import("root").log;
+}
+
+const ButtonData = struct {
+ count: usize = 0,
+};
+
+pub fn timerInvoke(data: ?*anyopaque, jni: *android.JNI, method: android.jobject, args: android.jobjectArray) !android.jobject {
+ var btn_data = @ptrCast(*ButtonData, @alignCast(@alignOf(*ButtonData), data));
+ btn_data.count += 1;
+ std.log.info("Running invoke!", .{});
+ const method_name = try android.JNI.String.init(jni, try jni.callObjectMethod(method, "getName", "()Ljava/lang/String;", .{}));
+ defer method_name.deinit(jni);
+ std.log.info("Method {}", .{std.unicode.fmtUtf16le(method_name.slice)});
+
+ const length = try jni.invokeJni(.GetArrayLength, .{args});
+ var i: i32 = 0;
+ while (i < length) : (i += 1) {
+ const object = try jni.invokeJni(.GetObjectArrayElement, .{ args, i });
+ const string = try android.JNI.String.init(jni, try jni.callObjectMethod(object, "toString", "()Ljava/lang/String;", .{}));
+ defer string.deinit(jni);
+ std.log.info("Arg {}: {}", .{ i, std.unicode.fmtUtf16le(string.slice) });
+
+ if (i == 0) {
+ const Button = try jni.findClass("android/widget/Button");
+ var buf: [256:0]u8 = undefined;
+ const str = std.fmt.bufPrintZ(&buf, "Pressed {} times!", .{btn_data.count}) catch "formatting bug";
+ try Button.callVoidMethod(object, "setText", "(Ljava/lang/CharSequence;)V", .{try jni.newString(str)});
+ }
+ }
+
+ return null;
+}
+
+pub const AndroidApp = struct {
+ allocator: std.mem.Allocator,
+ activity: *android.ANativeActivity,
+ thread: ?std.Thread = null,
+ running: bool = true,
+
+ // The JNIEnv of the UI thread
+ uiJni: android.NativeActivity = undefined,
+ // The JNIEnv of the app thread
+ mainJni: android.NativeActivity = undefined,
+
+ invocation_handler: NativeInvocationHandler = undefined,
+
+ // This is needed because to run a callback on the UI thread Looper you must
+ // react to a fd change, so we use a pipe to force it
+ pipe: [2]std.os.fd_t = undefined,
+ // This is used with futexes so that runOnUiThread waits until the callback is completed
+ // before returning.
+ uiThreadCondition: std.atomic.Atomic(u32) = std.atomic.Atomic(u32).init(0),
+ uiThreadLooper: *android.ALooper = undefined,
+ uiThreadId: std.Thread.Id = undefined,
+
+ btn_data: ButtonData = .{},
+
+ pub fn init(allocator: std.mem.Allocator, activity: *android.ANativeActivity, stored_state: ?[]const u8) !AndroidApp {
+ _ = stored_state;
+
+ return AndroidApp{
+ .allocator = allocator,
+ .activity = activity,
+ };
+ }
+
+ pub fn start(self: *AndroidApp) !void {
+ // Initialize the variables we need to execute functions on the UI thread
+ self.uiThreadLooper = android.ALooper_forThread().?;
+ self.uiThreadId = std.Thread.getCurrentId();
+ self.pipe = try std.os.pipe();
+ android.ALooper_acquire(self.uiThreadLooper);
+
+ var native_activity = android.NativeActivity.init(self.activity);
+ var jni = native_activity.jni;
+ self.uiJni = native_activity;
+
+ // Get the window object attached to our activity
+ const ActivityClass = try jni.findClass("android/app/NativeActivity");
+ const activityWindow = try ActivityClass.callObjectMethod(self.activity.clazz, "getWindow", "()Landroid/view/Window;", .{});
+
+ // This disables the surface handler set by default by android.view.NativeActivity
+ // This way we let the content view do the drawing instead of us.
+ const WindowClass = try jni.findClass("android/view/Window");
+ try WindowClass.callVoidMethod(activityWindow, "takeSurface", "(Landroid/view/SurfaceHolder$Callback2;)V", .{@as(android.jobject, null)});
+
+ // Do the same but with the input queue. This allows the content view to handle input.
+ try WindowClass.callVoidMethod(activityWindow, "takeInputQueue", "(Landroid/view/InputQueue$Callback;)V", .{@as(android.jobject, null)});
+
+ self.thread = try std.Thread.spawn(.{}, mainLoop, .{self});
+ }
+
+ /// Run the given function on the Android UI thread. This is necessary for manipulating the view hierarchy.
+ /// Note: this function is not thread-safe, but could be made so simply using a mutex
+ pub fn runOnUiThread(self: *AndroidApp, comptime func: anytype, args: anytype) !void {
+ if (std.Thread.getCurrentId() == self.uiThreadId) {
+ // runOnUiThread has been called from the UI thread.
+ @call(.auto, func, args);
+ return;
+ }
+
+ const Args = @TypeOf(args);
+ const allocator = self.allocator;
+
+ const Data = struct { args: Args, self: *AndroidApp };
+
+ const data_ptr = try allocator.create(Data);
+ data_ptr.* = .{ .args = args, .self = self };
+ errdefer allocator.destroy(data_ptr);
+
+ const Instance = struct {
+ fn callback(_: c_int, _: c_int, data: ?*anyopaque) callconv(.C) c_int {
+ const data_struct = @ptrCast(*Data, @alignCast(@alignOf(Data), data.?));
+ const self_ptr = data_struct.self;
+ defer self_ptr.allocator.destroy(data_struct);
+
+ @call(.auto, func, data_struct.args);
+ std.Thread.Futex.wake(&self_ptr.uiThreadCondition, 1);
+ return 0;
+ }
+ };
+
+ const result = android.ALooper_addFd(
+ self.uiThreadLooper,
+ self.pipe[0],
+ 0,
+ android.ALOOPER_EVENT_INPUT,
+ Instance.callback,
+ data_ptr,
+ );
+ std.debug.assert(try std.os.write(self.pipe[1], "hello") == 5);
+ if (result == -1) {
+ return error.LooperError;
+ }
+
+ std.Thread.Futex.wait(&self.uiThreadCondition, 0);
+ }
+
+ pub fn getJni(self: *AndroidApp) JNI {
+ return JNI.get(self.activity);
+ }
+
+ pub fn deinit(self: *AndroidApp) void {
+ @atomicStore(bool, &self.running, false, .SeqCst);
+ if (self.thread) |thread| {
+ thread.join();
+ self.thread = null;
+ }
+ android.ALooper_release(self.uiThreadLooper);
+ self.uiJni.deinit();
+ }
+
+ fn setAppContentView(self: *AndroidApp) void {
+ setAppContentViewImpl(self) catch |e| {
+ app_log.err("Encountered error while setting app content view: {s}", .{@errorName(e)});
+ };
+ }
+
+ fn setAppContentViewImpl(self: *AndroidApp) !void {
+ const native_activity = android.NativeActivity.get(self.activity);
+ const jni = native_activity.jni;
+
+ std.log.warn("Creating android.widget.Button", .{});
+ const Button = try jni.findClass("android/widget/Button");
+
+ // We create a new Button..
+ const button = try Button.newObject("(Landroid/content/Context;)V", .{self.activity.clazz});
+
+ // .. set its text to "Hello from Zig!" ..
+ try Button.callVoidMethod(button, "setText", "(Ljava/lang/CharSequence;)V", .{try jni.newString("Hello from Zig!")});
+
+ // .. and set its callback
+ const listener = try self.getOnClickListener(jni);
+ try Button.callVoidMethod(button, "setOnClickListener", "(Landroid/view/View$OnClickListener;)V", .{listener});
+
+ // And then we use it as our content view!
+ std.log.err("Attempt to call NativeActivity.setContentView()", .{});
+ const NativeActivityClass = try jni.findClass("android/app/NativeActivity");
+ try NativeActivityClass.callVoidMethod(self.activity.clazz, "setContentView", "(Landroid/view/View;)V", .{button});
+ }
+
+ fn mainLoop(self: *AndroidApp) !void {
+ self.mainJni = android.NativeActivity.init(self.activity);
+ defer self.mainJni.deinit();
+
+ try self.runOnUiThread(setAppContentView, .{self});
+ while (self.running) {
+ std.time.sleep(1 * std.time.ns_per_s);
+ }
+ }
+
+ fn getOnClickListener(self: *AndroidApp, jni: *JNI) !android.jobject {
+ // Get class loader instance
+ const ActivityClass = try jni.findClass("android/app/NativeActivity");
+ const cls = try ActivityClass.callObjectMethod(self.activity.clazz, "getClassLoader", "()Ljava/lang/ClassLoader;", .{});
+
+ // Class loader class object
+ const ClassLoader = try jni.findClass("java/lang/ClassLoader");
+ const strClassName = try jni.newString("NativeInvocationHandler");
+ defer jni.invokeJniNoException(.DeleteLocalRef, .{strClassName});
+ const NativeInvocationHandlerClass = try ClassLoader.callObjectMethod(cls, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;", .{strClassName});
+
+ // Get invocation handler factory
+ self.invocation_handler = try NativeInvocationHandler.init(jni, NativeInvocationHandlerClass);
+
+ // Create a NativeInvocationHandler
+ const invocation_handler = try self.invocation_handler.createAlloc(jni, self.allocator, &self.btn_data, &timerInvoke);
+
+ // Make an object array with 1 item, the android.view.View$OnClickListener interface class
+ const interface_array = try jni.invokeJni(.NewObjectArray, .{
+ 1,
+ try jni.invokeJni(.FindClass, .{"java/lang/Class"}),
+ try jni.invokeJni(.FindClass, .{"android/view/View$OnClickListener"}),
+ });
+
+ // Create a Proxy class implementing the OnClickListener interface
+ const Proxy = try jni.findClass("java/lang/reflect/Proxy");
+ const proxy = Proxy.callStaticObjectMethod(
+ "newProxyInstance",
+ "(Ljava/lang/ClassLoader;[Ljava/lang/Class;Ljava/lang/reflect/InvocationHandler;)Ljava/lang/Object;",
+ .{ cls, interface_array, invocation_handler },
+ );
+
+ return proxy;
+ }
+};
diff --git a/android/examples/textview/main.zig b/android/examples/textview/main.zig
index adb32eac..7dbf143f 100644
--- a/android/examples/textview/main.zig
+++ b/android/examples/textview/main.zig
@@ -8,6 +8,7 @@ pub const log = android.log;
const EGLContext = android.egl.EGLContext;
const JNI = android.JNI;
+const NativeActivity = android.NativeActivity;
const c = android.egl.c;
const app_log = std.log.scoped(.app);
@@ -23,9 +24,9 @@ pub const AndroidApp = struct {
running: bool = true,
// The JNIEnv of the UI thread
- uiJni: JNI = undefined,
+ uiJni: NativeActivity = undefined,
// The JNIEnv of the app thread
- mainJni: JNI = undefined,
+ mainJni: NativeActivity = undefined,
// This is needed because to run a callback on the UI thread Looper you must
// react to a fd change, so we use a pipe to force it
@@ -52,31 +53,22 @@ pub const AndroidApp = struct {
self.pipe = try std.os.pipe();
android.ALooper_acquire(self.uiThreadLooper);
- var jni = JNI.init(self.activity);
- self.uiJni = jni;
+ var native_activity = NativeActivity.init(self.activity);
+ self.uiJni = native_activity;
+ const jni = native_activity.jni;
// Get the window object attached to our activity
- const activityClass = jni.findClass("android/app/NativeActivity");
- const getWindow = jni.invokeJni(.GetMethodID, .{ activityClass, "getWindow", "()Landroid/view/Window;" });
- const activityWindow = jni.invokeJni(.CallObjectMethod, .{ self.activity.clazz, getWindow });
- const WindowClass = jni.findClass("android/view/Window");
+ const activityWindow = try native_activity.activity_class.callObjectMethod(self.activity.clazz, "getWindow", "()Landroid/view/Window;", .{});
+
+ const WindowClass = try jni.findClass("android/view/Window");
// This disables the surface handler set by default by android.view.NativeActivity
// This way we let the content view do the drawing instead of us.
- const takeSurface = jni.invokeJni(.GetMethodID, .{ WindowClass, "takeSurface", "(Landroid/view/SurfaceHolder$Callback2;)V" });
- jni.invokeJni(.CallVoidMethod, .{
- activityWindow,
- takeSurface,
- @as(android.jobject, null),
- });
+ try WindowClass.callVoidMethod(activityWindow, "takeSurface", "(Landroid/view/SurfaceHolder$Callback2;)V", .{@as(android.jobject, null)});
// Do the same but with the input queue. This allows the content view to handle input.
- const takeInputQueue = jni.invokeJni(.GetMethodID, .{ WindowClass, "takeInputQueue", "(Landroid/view/InputQueue$Callback;)V" });
- jni.invokeJni(.CallVoidMethod, .{
- activityWindow,
- takeInputQueue,
- @as(android.jobject, null),
- });
+ try WindowClass.callVoidMethod(activityWindow, "takeInputQueue", "(Landroid/view/InputQueue$Callback;)V", .{@as(android.jobject, null)});
+
self.thread = try std.Thread.spawn(.{}, mainLoop, .{self});
}
@@ -92,10 +84,7 @@ pub const AndroidApp = struct {
const Args = @TypeOf(args);
const allocator = self.allocator;
- const Data = struct {
- args: Args,
- self: *AndroidApp
- };
+ const Data = struct { args: Args, self: *AndroidApp };
const data_ptr = try allocator.create(Data);
data_ptr.* = .{ .args = args, .self = self };
@@ -113,7 +102,8 @@ pub const AndroidApp = struct {
}
};
- const result = android.ALooper_addFd(self.uiThreadLooper,
+ const result = android.ALooper_addFd(
+ self.uiThreadLooper,
self.pipe[0],
0,
android.ALOOPER_EVENT_INPUT,
@@ -128,8 +118,8 @@ pub const AndroidApp = struct {
std.Thread.Futex.wait(&self.uiThreadCondition, 0);
}
- pub fn getJni(self: *AndroidApp) JNI {
- return JNI.get(self.activity);
+ pub fn getActivity(self: *AndroidApp) NativeActivity {
+ return NativeActivity.get(self.activity);
}
pub fn deinit(self: *AndroidApp) void {
@@ -143,34 +133,33 @@ pub const AndroidApp = struct {
}
fn setAppContentView(self: *AndroidApp) void {
- const jni = self.getJni();
+ self.setAppContentViewImpl() catch |e| {
+ app_log.err("Error occured while running setAppContentView: {s}", .{ @errorName(e) });
+ };
+ }
+
+ fn setAppContentViewImpl(self: *AndroidApp) !void {
+ const native_activity = self.getActivity();
+ const jni = native_activity.jni;
// We create a new TextView..
std.log.warn("Creating android.widget.TextView", .{});
- const TextView = jni.findClass("android/widget/TextView");
- const textViewInit = jni.invokeJni(.GetMethodID, .{ TextView, "", "(Landroid/content/Context;)V" });
- const textView = jni.invokeJni(.NewObject, .{ TextView, textViewInit, self.activity.clazz });
+ const TextView = try jni.findClass("android/widget/TextView");
+ const textView = try TextView.newObject("(Landroid/content/Context;)V", .{self.activity.clazz});
// .. and set its text to "Hello from Zig!"
- const setText = jni.invokeJni(.GetMethodID, .{ TextView, "setText", "(Ljava/lang/CharSequence;)V" });
- jni.invokeJni(.CallVoidMethod, .{ textView, setText, jni.newString("Hello from Zig!") });
+ try TextView.callVoidMethod(textView, "setText", "(Ljava/lang/CharSequence;)V", .{try jni.newString("Hello from Zig!")});
// And then we use it as our content view!
std.log.err("Attempt to call NativeActivity.setContentView()", .{});
- const activityClass = jni.findClass("android/app/NativeActivity");
- const setContentView = jni.invokeJni(.GetMethodID, .{ activityClass, "setContentView", "(Landroid/view/View;)V" });
- jni.invokeJni(.CallVoidMethod, .{
- self.activity.clazz,
- setContentView,
- textView,
- });
+ try native_activity.activity_class.callVoidMethod(self.activity.clazz, "setContentView", "(Landroid/view/View;)V", .{textView});
}
fn mainLoop(self: *AndroidApp) !void {
- self.mainJni = JNI.init(self.activity);
+ self.mainJni = NativeActivity.init(self.activity);
defer self.mainJni.deinit();
- try self.runOnUiThread(setAppContentView, .{ self });
+ try self.runOnUiThread(setAppContentView, .{self});
while (self.running) {
std.time.sleep(1 * std.time.ns_per_s);
}
diff --git a/android/src/NativeActivity.zig b/android/src/NativeActivity.zig
new file mode 100644
index 00000000..c2ec7049
--- /dev/null
+++ b/android/src/NativeActivity.zig
@@ -0,0 +1,204 @@
+const std = @import("std");
+const log = std.log.scoped(.jni);
+const android = @import("android-support.zig");
+
+const Self = @This();
+
+activity: *android.ANativeActivity,
+jni: *android.JNI,
+activity_class: android.JNI.Class,
+
+pub fn init(activity: *android.ANativeActivity) Self {
+ var env: *android.JNIEnv = undefined;
+ _ = activity.vm.*.AttachCurrentThread(activity.vm, &env, null);
+ return fromJniEnv(activity, env);
+}
+
+/// Get the JNIEnv associated with the current thread.
+pub fn get(activity: *android.ANativeActivity) Self {
+ var env: *android.JNIEnv = undefined;
+ _ = activity.vm.*.GetEnv(activity.vm, @ptrCast(*?*anyopaque, &env), android.JNI_VERSION_1_6);
+ return fromJniEnv(activity, env);
+}
+
+fn fromJniEnv(activity: *android.ANativeActivity, env: *android.JNIEnv) Self {
+ var jni = @ptrCast(*android.JNI, env);
+ var activityClass = jni.findClass("android/app/NativeActivity") catch @panic("Could not get NativeActivity class");
+
+ return Self{
+ .activity = activity,
+ .jni = jni,
+ .activity_class = activityClass,
+ };
+}
+
+pub fn deinit(self: *Self) void {
+ _ = self.activity.vm.*.DetachCurrentThread(self.activity.vm);
+ self.* = undefined;
+}
+
+pub fn AndroidGetUnicodeChar(self: *Self, keyCode: c_int, metaState: c_int) !u21 {
+ // https://stackoverflow.com/questions/21124051/receive-complete-android-unicode-input-in-c-c/43871301
+ const eventType = android.AKEY_EVENT_ACTION_DOWN;
+
+ const KeyEvent = try self.jni.findClass("android/view/KeyEvent");
+
+ const event_obj = try KeyEvent.newObject("(II)V", .{ eventType, keyCode });
+ const unicode_key = try KeyEvent.callIntMethod(event_obj, "getUnicodeChar", "(I)I", .{metaState});
+
+ return @intCast(u21, unicode_key);
+}
+
+pub fn AndroidMakeFullscreen(self: *Self) !void {
+ // Partially based on
+ // https://stackoverflow.com/questions/47507714/how-do-i-enable-full-screen-immersive-mode-for-a-native-activity-ndk-app
+
+ // Get android.app.NativeActivity, then get getWindow method handle, returns
+ // view.Window type
+ const ActivityClass = try self.jni.findClass("android/app/NativeActivity");
+ const window = try ActivityClass.callObjectMethod(self.activity.clazz, "getWindow", "()Landroid/view/Window;", .{});
+
+ // Get android.view.Window class, then get getDecorView method handle, returns
+ // view.View type
+ const WindowClass = try self.jni.findClass("android/view/Window");
+ const decorView = try WindowClass.callObjectMethod(window, "getDecorView", "()Landroid/view/View;", .{});
+
+ // Get the flag values associated with systemuivisibility
+ const ViewClass = try self.jni.findClass("android/view/View");
+ const flagLayoutHideNavigation = try ViewClass.getStaticIntField("SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION");
+ const flagLayoutFullscreen = try ViewClass.getStaticIntField("SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN");
+ const flagLowProfile = try ViewClass.getStaticIntField("SYSTEM_UI_FLAG_LOW_PROFILE");
+ const flagHideNavigation = try ViewClass.getStaticIntField("SYSTEM_UI_FLAG_HIDE_NAVIGATION");
+ const flagFullscreen = try ViewClass.getStaticIntField("SYSTEM_UI_FLAG_FULLSCREEN");
+ const flagImmersiveSticky = try ViewClass.getStaticIntField("SYSTEM_UI_FLAG_IMMERSIVE_STICKY");
+
+ // Call the decorView.setSystemUiVisibility(FLAGS)
+ try ViewClass.callVoidMethod(decorView, "setSystemUiVisibility", "(I)V", .{
+ (flagLayoutHideNavigation | flagLayoutFullscreen | flagLowProfile | flagHideNavigation | flagFullscreen | flagImmersiveSticky),
+ });
+
+ // now set some more flags associated with layoutmanager -- note the $ in the
+ // class path search for api-versions.xml
+ // https://android.googlesource.com/platform/development/+/refs/tags/android-9.0.0_r48/sdk/api-versions.xml
+ const LayoutManagerClass = try self.jni.findClass("android/view/WindowManager$LayoutParams");
+ const flag_WinMan_Fullscreen = try LayoutManagerClass.getStaticIntField("FLAG_FULLSCREEN");
+ const flag_WinMan_KeepScreenOn = try LayoutManagerClass.getStaticIntField("FLAG_KEEP_SCREEN_ON");
+ const flag_WinMan_hw_acc = try LayoutManagerClass.getStaticIntField("FLAG_HARDWARE_ACCELERATED");
+
+ // const int flag_WinMan_flag_not_fullscreen =
+ // env.GetStaticIntField(layoutManagerClass,
+ // (env.GetStaticFieldID(layoutManagerClass, "FLAG_FORCE_NOT_FULLSCREEN",
+ // "I") ));
+ // call window.addFlags(FLAGS)
+
+ try WindowClass.callVoidMethod(window, "addFlags", "(I)V", .{(flag_WinMan_Fullscreen | flag_WinMan_KeepScreenOn | flag_WinMan_hw_acc)});
+}
+
+pub fn AndroidDisplayKeyboard(self: *Self, show: bool) !bool {
+ // Based on
+ // https://stackoverflow.com/questions/5864790/how-to-show-the-soft-keyboard-on-native-activity
+ var lFlags: android.jint = 0;
+
+ // Retrieves Context.INPUT_METHOD_SERVICE.
+ const ClassContext = try self.jni.findClass("android/content/Context");
+ const INPUT_METHOD_SERVICE = try ClassContext.getStaticObjectField("INPUT_METHOD_SERVICE", "Ljava/lang/String;");
+
+ // Runs getSystemService(Context.INPUT_METHOD_SERVICE).
+ const ClassInputMethodManager = try self.jni.findClass("android/view/inputmethod/InputMethodManager");
+ const lInputMethodManager = try ClassInputMethodManager.callObjectMethod(self.activity.clazz, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;", .{INPUT_METHOD_SERVICE});
+
+ // Runs getWindow().getDecorView().
+ const lWindow = try ClassContext.callObjectMethod(self.activity.clazz, "getWindow", "()Landroid/view/Window;", .{});
+ const ClassWindow = try self.jni.findClass("android/view/Window");
+ const lDecorView = try ClassWindow.callObjectMethod(lWindow, "getDecorView", "()Landroid/view/View;", .{});
+
+ if (show) {
+ // Runs lInputMethodManager.showSoftInput(...).
+ return ClassInputMethodManager.callBooleanMethod(lInputMethodManager, "showSoftInput", "(Landroid/view/View;I)Z", .{ lDecorView, lFlags });
+ } else {
+ // Runs lWindow.getViewToken()
+ const ClassView = try self.jni.findClass("android/view/View");
+ const lBinder = try ClassView.callObjectMethod(lDecorView, "getWindowToken", "()Landroid/os/IBinder;", .{});
+
+ // lInputMethodManager.hideSoftInput(...).
+ return ClassInputMethodManager.callBooleanMethod(lInputMethodManager, "hideSoftInputFromWindow", "(Landroid/os/IBinder;I)Z", .{ lBinder, lFlags });
+ }
+}
+
+/// Move the task containing this activity to the back of the activity stack.
+/// The activity's order within the task is unchanged.
+/// nonRoot: If false then this only works if the activity is the root of a task; if true it will work for any activity in a task.
+/// returns: If the task was moved (or it was already at the back) true is returned, else false.
+pub fn AndroidSendToBack(self: *Self, nonRoot: bool) !bool {
+ const ClassActivity = try self.jni.findClass("android/app/Activity");
+ return ClassActivity.callBooleanMethod(self.activity.clazz, "moveTaskToBack", "(Z)Z", .{if (nonRoot) @as(c_int, 1) else 0});
+}
+
+pub fn AndroidHasPermissions(self: *Self, perm_name: [:0]const u8) !bool {
+ if (android.sdk_version < 23) {
+ log.err(
+ "Android SDK version {} does not support AndroidRequestAppPermissions\n",
+ .{android.sdk_version},
+ );
+ return false;
+ }
+
+ const ls_PERM = try self.jni.newString(perm_name);
+
+ const PERMISSION_GRANTED = blk: {
+ var ClassPackageManager = try self.jni.findClass("android/content/pm/PackageManager");
+ break :blk try ClassPackageManager.getStaticIntField("PERMISSION_GRANTED");
+ };
+
+ const ClassContext = try self.jni.findClass("android/content/Context");
+ const int_result = try ClassContext.callIntMethod(self.activity.clazz, "checkSelfPermission", "(Ljava/lang/String;)I", .{ls_PERM});
+ return (int_result == PERMISSION_GRANTED);
+}
+
+pub fn AndroidRequestAppPermissions(self: *Self, perm_name: [:0]const u8) !void {
+ if (android.sdk_version < 23) {
+ log.err(
+ "Android SDK version {} does not support AndroidRequestAppPermissions\n",
+ .{android.sdk_version},
+ );
+ return;
+ }
+
+ const perm_array = try self.jni.invokeJni(.NewObjectArray, .{
+ 1,
+ try self.jni.invokeJni(.FindClass, .{"java/lang/String"}),
+ try self.jni.newString(perm_name),
+ });
+
+ // Last arg (0) is just for the callback (that I do not use)
+ try self.activity_class.callVoidMethod(self.activity.clazz, "requestPermissions", "([Ljava/lang/String;I)V", .{ perm_array, @as(c_int, 0) });
+}
+
+pub fn getFilesDir(self: *Self, allocator: std.mem.Allocator) ![:0]const u8 {
+ const files_dir = try self.activity_class.callVoidMethod(self.activity.clazz, "getFilesDir", "()Ljava/io/File;", .{});
+
+ const FileClass = try self.jni.findClass("java/io/File");
+
+ const path_string = try FileClass.callObjectMethod(files_dir, "getPath", "()Ljava/lang/String;", .{});
+
+ const utf8_or_null = try self.jni.invokeJni(.GetStringUTFChars, .{ path_string, null });
+
+ if (utf8_or_null) |utf8_ptr| {
+ defer self.jni.invokeJniNoException(.ReleaseStringUTFChars, .{ path_string, utf8_ptr });
+
+ const utf8 = std.mem.sliceTo(utf8_ptr, 0);
+
+ return try allocator.dupeZ(u8, utf8);
+ } else {
+ return error.OutOfMemory;
+ }
+}
+
+comptime {
+ _ = AndroidGetUnicodeChar;
+ _ = AndroidMakeFullscreen;
+ _ = AndroidDisplayKeyboard;
+ _ = AndroidSendToBack;
+ _ = AndroidHasPermissions;
+ _ = AndroidRequestAppPermissions;
+}
diff --git a/android/src/NativeInvocationHandler.java b/android/src/NativeInvocationHandler.java
new file mode 100644
index 00000000..caf2277a
--- /dev/null
+++ b/android/src/NativeInvocationHandler.java
@@ -0,0 +1,15 @@
+import java.lang.Object;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+
+public class NativeInvocationHandler implements InvocationHandler {
+ public NativeInvocationHandler(long ptr) { this.ptr = ptr; }
+
+ public Object invoke(Object proxy, Method method, Object[] args) {
+ return invoke0(proxy, method, args);
+ }
+
+ native private Object invoke0(Object proxy, Method method, Object[] args);
+
+ private long ptr;
+}
diff --git a/android/src/NativeInvocationHandler.zig b/android/src/NativeInvocationHandler.zig
new file mode 100644
index 00000000..43252405
--- /dev/null
+++ b/android/src/NativeInvocationHandler.zig
@@ -0,0 +1,65 @@
+const std = @import("std");
+const android = @import("android-support.zig");
+const Self = @This();
+
+class: android.jobject,
+initFn: android.jmethodID,
+
+pub fn init(jni: *android.JNI, class: android.jobject) !Self {
+ const methods = [_]android.JNINativeMethod{
+ .{
+ .name = "invoke0",
+ .signature = "(Ljava/lang/Object;Ljava/lang/reflect/Method;[Ljava/lang/Object;)Ljava/lang/Object;",
+ .fnPtr = InvocationHandler.invoke0,
+ },
+ };
+ _ = try jni.invokeJni(.RegisterNatives, .{ class, &methods, methods.len });
+ return Self{
+ .class = class,
+ .initFn = try jni.invokeJni(.GetMethodID, .{ class, "", "(J)V" }),
+ };
+}
+
+pub fn createAlloc(self: Self, jni: *android.JNI, alloc: std.mem.Allocator, pointer: ?*anyopaque, function: InvokeFn) !android.jobject {
+ // Create a InvocationHandler struct
+ var handler = try alloc.create(InvocationHandler);
+ errdefer alloc.destroy(handler);
+ handler.* = .{
+ .pointer = pointer,
+ .function = function,
+ };
+
+ const handler_value = @ptrToInt(handler);
+ std.debug.assert(handler_value <= 0x7fffffffffffffff);
+
+ // Call handler constructor
+ const result = try jni.invokeJni(.NewObject, .{ self.class, self.initFn, handler_value }) orelse return error.InvocationHandlerInitError;
+ return result;
+
+ // return handler;
+}
+
+/// Function signature for invoke functions
+pub const InvokeFn = *const fn (?*anyopaque, *android.JNI, android.jobject, android.jobjectArray) anyerror!android.jobject;
+
+/// InvocationHandler Technique found here https://groups.google.com/g/android-ndk/c/SRgy93Un8vM
+const InvocationHandler = struct {
+ pointer: ?*anyopaque,
+ function: InvokeFn,
+
+ /// Called by java class NativeInvocationHandler
+ pub fn invoke0(jni: *android.JNI, this: android.jobject, proxy: android.jobject, method: android.jobject, args: android.jobjectArray) android.jobject {
+ return invoke_impl(jni, this, proxy, method, args) catch |e| switch (e) {
+ else => @panic(@errorName(e)),
+ };
+ }
+
+ fn invoke_impl(jni: *android.JNI, this: android.jobject, proxy: android.jobject, method: android.jobject, args: android.jobjectArray) anyerror!android.jobject {
+ _ = proxy; // This is the proxy object. Calling anything on it will cause invoke to be called. If this isn't explicitly handled, it will recurse infinitely
+ const Class = try jni.invokeJni(.GetObjectClass, .{this});
+ const ptrField = try jni.invokeJni(.GetFieldID, .{ Class, "ptr", "J" });
+ const jptr = try jni.getLongField(this, ptrField);
+ const h = @intToPtr(*InvocationHandler, @intCast(usize, jptr));
+ return h.function(h.pointer, jni, method, args);
+ }
+};
diff --git a/android/src/android-bind.zig b/android/src/android-bind.zig
index 3e051a67..af1b38e4 100644
--- a/android/src/android-bind.zig
+++ b/android/src/android-bind.zig
@@ -512,7 +512,7 @@ pub const jobjectRefType = enum_jobjectRefType;
const struct_unnamed_15 = extern struct {
name: [*c]const u8,
signature: [*c]const u8,
- fnPtr: ?*anyopaque,
+ fnPtr: ?*const anyopaque,
};
pub const JNINativeMethod = struct_unnamed_15;
pub const JNINativeInterface = extern struct {
diff --git a/android/src/android-support.zig b/android/src/android-support.zig
index 252a70a8..48e01b06 100644
--- a/android/src/android-support.zig
+++ b/android/src/android-support.zig
@@ -9,6 +9,8 @@ const build_options = @import("build_options");
pub const egl = @import("egl.zig");
pub const JNI = @import("jni.zig").JNI;
pub const audio = @import("audio.zig");
+pub const NativeActivity = @import("NativeActivity.zig");
+pub const NativeInvocationHandler = @import("NativeInvocationHandler.zig");
const app_log = std.log.scoped(.app_glue);
diff --git a/android/src/jni.zig b/android/src/jni.zig
index 161386a0..60c31b7b 100644
--- a/android/src/jni.zig
+++ b/android/src/jni.zig
@@ -2,254 +2,148 @@ const std = @import("std");
const log = std.log.scoped(.jni);
const android = @import("android-support.zig");
-pub const JNI = struct {
- const Self = @This();
-
- activity: *android.ANativeActivity,
- env: *android.JNIEnv,
- activity_class: android.jclass,
-
- pub fn init(activity: *android.ANativeActivity) Self {
- var env: *android.JNIEnv = undefined;
- _ = activity.vm.*.AttachCurrentThread(activity.vm, &env, null);
- return fromJniEnv(activity, env);
- }
-
- /// Get the JNIEnv associated with the current thread.
- pub fn get(activity: *android.ANativeActivity) Self {
- var env: *android.JNIEnv = undefined;
- _ = activity.vm.*.GetEnv(activity.vm, @ptrCast(*?*anyopaque, &env), android.JNI_VERSION_1_6);
- return fromJniEnv(activity, env);
- }
-
- fn fromJniEnv(activity: *android.ANativeActivity, env: *android.JNIEnv) Self {
- var activityClass = env.*.FindClass(env, "android/app/NativeActivity");
-
- return Self{
- .activity = activity,
- .env = env,
- .activity_class = activityClass,
- };
- }
-
- pub fn deinit(self: *Self) void {
- _ = self.activity.vm.*.DetachCurrentThread(self.activity.vm);
- self.* = undefined;
- }
-
+/// Wraps JNIEnv to provide a better Zig API.
+/// *android.JNIEnv can be directly cast to `*JNI`. For example:
+/// ```
+/// const jni = @ptrCast(*JNI, jni_env);
+/// ```
+pub const JNI = opaque {
+ // Underlying implementation
fn JniReturnType(comptime function: @TypeOf(.literal)) type {
@setEvalBranchQuota(10_000);
return @typeInfo(@typeInfo(std.meta.fieldInfo(android.JNINativeInterface, function).type).Pointer.child).Fn.return_type.?;
}
- pub inline fn invokeJni(self: Self, comptime function: @TypeOf(.literal), args: anytype) JniReturnType(function) {
+ pub inline fn invokeJniNoException(jni: *JNI, comptime function: @TypeOf(.literal), args: anytype) JniReturnType(function) {
+ const env = @ptrCast(*android.JNIEnv, @alignCast(@alignOf(*android.JNIEnv), jni));
return @call(
.auto,
- @field(self.env.*, @tagName(function)),
- .{self.env} ++ args,
+ @field(env.*, @tagName(function)),
+ .{env} ++ args,
);
}
- pub fn findClass(self: Self, class: [:0]const u8) android.jclass {
- return self.invokeJni(.FindClass, .{class.ptr});
- }
+ /// Possible JNI Errors
+ const Error = error{
+ ExceptionThrown,
+ ClassNotDefined,
+ };
- pub fn newString(self: Self, string: [*:0]const u8) android.jstring {
- return self.invokeJni(.NewStringUTF, .{ string });
+ pub inline fn invokeJni(jni: *JNI, comptime function: @TypeOf(.literal), args: anytype) Error!JniReturnType(function) {
+ const value = jni.invokeJniNoException(function, args);
+ if (jni.invokeJniNoException(.ExceptionCheck, .{}) == android.JNI_TRUE) {
+ log.err("Encountered exception while calling: {s} {any}", .{ @tagName(function), args });
+ return Error.ExceptionThrown;
+ }
+ return value;
}
- pub fn AndroidGetUnicodeChar(self: *Self, keyCode: c_int, metaState: c_int) u21 {
- // https://stackoverflow.com/questions/21124051/receive-complete-android-unicode-input-in-c-c/43871301
- const eventType = android.AKEY_EVENT_ACTION_DOWN;
-
- const class_key_event = self.findClass("android/view/KeyEvent");
-
- const method_get_unicode_char = self.invokeJni(.GetMethodID, .{ class_key_event, "getUnicodeChar", "(I)I" });
- const eventConstructor = self.invokeJni(.GetMethodID, .{ class_key_event, "", "(II)V" });
- const eventObj = self.invokeJni(.NewObject, .{ class_key_event, eventConstructor, eventType, keyCode });
+ // Convenience functions
- const unicodeKey = self.invokeJni(.CallIntMethod, .{ eventObj, method_get_unicode_char, metaState });
-
- return @intCast(u21, unicodeKey);
+ pub fn findClass(jni: *JNI, class: [:0]const u8) Error!Class {
+ return Class.init(jni, class);
}
- pub fn AndroidMakeFullscreen(self: *Self) void {
- // Partially based on
- // https://stackoverflow.com/questions/47507714/how-do-i-enable-full-screen-immersive-mode-for-a-native-activity-ndk-app
-
- // Get android.app.NativeActivity, then get getWindow method handle, returns
- // view.Window type
- const activityClass = self.findClass("android/app/NativeActivity");
- const getWindow = self.invokeJni(.GetMethodID, .{ activityClass, "getWindow", "()Landroid/view/Window;" });
- const window = self.invokeJni(.CallObjectMethod, .{ self.activity.clazz, getWindow });
-
- // Get android.view.Window class, then get getDecorView method handle, returns
- // view.View type
- const windowClass = self.findClass("android/view/Window");
- const getDecorView = self.invokeJni(.GetMethodID, .{ windowClass, "getDecorView", "()Landroid/view/View;" });
- const decorView = self.invokeJni(.CallObjectMethod, .{ window, getDecorView });
-
- // Get the flag values associated with systemuivisibility
- const viewClass = self.findClass("android/view/View");
- const flagLayoutHideNavigation = self.invokeJni(.GetStaticIntField, .{ viewClass, self.invokeJni(.GetStaticFieldID, .{ viewClass, "SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION", "I" }) });
- const flagLayoutFullscreen = self.invokeJni(.GetStaticIntField, .{ viewClass, self.invokeJni(.GetStaticFieldID, .{ viewClass, "SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN", "I" }) });
- const flagLowProfile = self.invokeJni(.GetStaticIntField, .{ viewClass, self.invokeJni(.GetStaticFieldID, .{ viewClass, "SYSTEM_UI_FLAG_LOW_PROFILE", "I" }) });
- const flagHideNavigation = self.invokeJni(.GetStaticIntField, .{ viewClass, self.invokeJni(.GetStaticFieldID, .{ viewClass, "SYSTEM_UI_FLAG_HIDE_NAVIGATION", "I" }) });
- const flagFullscreen = self.invokeJni(.GetStaticIntField, .{ viewClass, self.invokeJni(.GetStaticFieldID, .{ viewClass, "SYSTEM_UI_FLAG_FULLSCREEN", "I" }) });
- const flagImmersiveSticky = self.invokeJni(.GetStaticIntField, .{ viewClass, self.invokeJni(.GetStaticFieldID, .{ viewClass, "SYSTEM_UI_FLAG_IMMERSIVE_STICKY", "I" }) });
-
- const setSystemUiVisibility = self.invokeJni(.GetMethodID, .{ viewClass, "setSystemUiVisibility", "(I)V" });
-
- // Call the decorView.setSystemUiVisibility(FLAGS)
- self.invokeJni(.CallVoidMethod, .{
- decorView,
- setSystemUiVisibility,
- (flagLayoutHideNavigation | flagLayoutFullscreen | flagLowProfile | flagHideNavigation | flagFullscreen | flagImmersiveSticky),
- });
-
- // now set some more flags associated with layoutmanager -- note the $ in the
- // class path search for api-versions.xml
- // https://android.googlesource.com/platform/development/+/refs/tags/android-9.0.0_r48/sdk/api-versions.xml
-
- const layoutManagerClass = self.findClass("android/view/WindowManager$LayoutParams");
- const flag_WinMan_Fullscreen = self.invokeJni(.GetStaticIntField, .{ layoutManagerClass, self.invokeJni(.GetStaticFieldID, .{ layoutManagerClass, "FLAG_FULLSCREEN", "I" }) });
- const flag_WinMan_KeepScreenOn = self.invokeJni(.GetStaticIntField, .{ layoutManagerClass, self.invokeJni(.GetStaticFieldID, .{ layoutManagerClass, "FLAG_KEEP_SCREEN_ON", "I" }) });
- const flag_WinMan_hw_acc = self.invokeJni(.GetStaticIntField, .{ layoutManagerClass, self.invokeJni(.GetStaticFieldID, .{ layoutManagerClass, "FLAG_HARDWARE_ACCELERATED", "I" }) });
- // const int flag_WinMan_flag_not_fullscreen =
- // env.GetStaticIntField(layoutManagerClass,
- // (env.GetStaticFieldID(layoutManagerClass, "FLAG_FORCE_NOT_FULLSCREEN",
- // "I") ));
- // call window.addFlags(FLAGS)
- self.invokeJni(.CallVoidMethod, .{
- window,
- self.invokeJni(.GetMethodID, .{ windowClass, "addFlags", "(I)V" }),
- (flag_WinMan_Fullscreen | flag_WinMan_KeepScreenOn | flag_WinMan_hw_acc),
- });
+ pub fn getClassNameString(jni: *JNI, object: android.jobject) Error!String {
+ const object_class = try jni.invokeJni(.GetObjectClass, .{object});
+ const ClassClass = try jni.findClass("java/lang/Class");
+ const getName = try jni.invokeJni(.GetMethodID, .{ ClassClass, "getName", "()Ljava/lang/String;" });
+ const name = try jni.invokeJni(.CallObjectMethod, .{ object_class, getName });
+ return String.init(jni, name);
}
- pub fn AndroidDisplayKeyboard(self: *Self, show: bool) bool {
- // Based on
- // https://stackoverflow.com/questions/5864790/how-to-show-the-soft-keyboard-on-native-activity
- var lFlags: android.jint = 0;
-
- // Retrieves Context.INPUT_METHOD_SERVICE.
- const ClassContext = self.findClass("android/content/Context");
- const FieldINPUT_METHOD_SERVICE = self.invokeJni(.GetStaticFieldID, .{ ClassContext, "INPUT_METHOD_SERVICE", "Ljava/lang/String;" });
- const INPUT_METHOD_SERVICE = self.invokeJni(.GetStaticObjectField, .{ ClassContext, FieldINPUT_METHOD_SERVICE });
-
- // Runs getSystemService(Context.INPUT_METHOD_SERVICE).
- const ClassInputMethodManager = self.findClass("android/view/inputmethod/InputMethodManager");
- const MethodGetSystemService = self.invokeJni(.GetMethodID, .{ self.activity_class, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;" });
- const lInputMethodManager = self.invokeJni(.CallObjectMethod, .{ self.activity.clazz, MethodGetSystemService, INPUT_METHOD_SERVICE });
-
- // Runs getWindow().getDecorView().
- const MethodGetWindow = self.invokeJni(.GetMethodID, .{ self.activity_class, "getWindow", "()Landroid/view/Window;" });
- const lWindow = self.invokeJni(.CallObjectMethod, .{ self.activity.clazz, MethodGetWindow });
- const ClassWindow = self.findClass("android/view/Window");
- const MethodGetDecorView = self.invokeJni(.GetMethodID, .{ ClassWindow, "getDecorView", "()Landroid/view/View;" });
- const lDecorView = self.invokeJni(.CallObjectMethod, .{ lWindow, MethodGetDecorView });
-
- if (show) {
- // Runs lInputMethodManager.showSoftInput(...).
- const MethodShowSoftInput = self.invokeJni(.GetMethodID, .{ ClassInputMethodManager, "showSoftInput", "(Landroid/view/View;I)Z" });
- return 0 != self.invokeJni(.CallBooleanMethod, .{ lInputMethodManager, MethodShowSoftInput, lDecorView, lFlags });
- } else {
- // Runs lWindow.getViewToken()
- const ClassView = self.findClass("android/view/View");
- const MethodGetWindowToken = self.invokeJni(.GetMethodID, .{ ClassView, "getWindowToken", "()Landroid/os/IBinder;" });
- const lBinder = self.invokeJni(.CallObjectMethod, .{ lDecorView, MethodGetWindowToken });
-
- // lInputMethodManager.hideSoftInput(...).
- const MethodHideSoftInput = self.invokeJni(.GetMethodID, .{ ClassInputMethodManager, "hideSoftInputFromWindow", "(Landroid/os/IBinder;I)Z" });
- return 0 != self.invokeJni(.CallBooleanMethod, .{ lInputMethodManager, MethodHideSoftInput, lBinder, lFlags });
- }
+ pub fn printToString(jni: *JNI, object: android.jobject) void {
+ const string = try String.init(jni, try jni.callObjectMethod(object, "toString", "()Ljava/lang/String;", .{}));
+ defer string.deinit(jni);
+ log.info("{any}: {}", .{ object, std.unicode.fmtUtf16le(string.slice) });
}
- /// Move the task containing this activity to the back of the activity stack.
- /// The activity's order within the task is unchanged.
- /// nonRoot: If false then this only works if the activity is the root of a task; if true it will work for any activity in a task.
- /// returns: If the task was moved (or it was already at the back) true is returned, else false.
- pub fn AndroidSendToBack(self: *Self, nonRoot: bool) bool {
- const ClassActivity = self.findClass("android/app/Activity");
- const MethodmoveTaskToBack = self.invokeJni(.GetMethodID, .{ ClassActivity, "moveTaskToBack", "(Z)Z" });
-
- return 0 != self.invokeJni(.CallBooleanMethod, .{ self.activity.clazz, MethodmoveTaskToBack, if (nonRoot) @as(c_int, 1) else 0 });
+ pub fn newString(jni: *JNI, string: [*:0]const u8) Error!android.jstring {
+ return jni.invokeJni(.NewStringUTF, .{string});
}
- pub fn AndroidHasPermissions(self: *Self, perm_name: [:0]const u8) bool {
- if (android.sdk_version < 23) {
- log.err(
- "Android SDK version {} does not support AndroidRequestAppPermissions\n",
- .{android.sdk_version},
- );
- return false;
- }
-
- const ls_PERM = self.invokeJni(.NewStringUTF, .{perm_name});
-
- const PERMISSION_GRANTED = blk: {
- var ClassPackageManager = self.findClass("android/content/pm/PackageManager");
- var lid_PERMISSION_GRANTED = self.invokeJni(.GetStaticFieldID, .{ ClassPackageManager, "PERMISSION_GRANTED", "I" });
- break :blk self.invokeJni(.GetStaticIntField, .{ ClassPackageManager, lid_PERMISSION_GRANTED });
- };
-
- const ClassContext = self.findClass("android/content/Context");
- const MethodcheckSelfPermission = self.invokeJni(.GetMethodID, .{ ClassContext, "checkSelfPermission", "(Ljava/lang/String;)I" });
- const int_result = self.invokeJni(.CallIntMethod, .{ self.activity.clazz, MethodcheckSelfPermission, ls_PERM });
- return (int_result == PERMISSION_GRANTED);
+ pub fn getLongField(jni: *JNI, object: android.jobject, field_id: android.jfieldID) !android.jlong {
+ return jni.invokeJni(.GetLongField, .{ object, field_id });
}
- pub fn AndroidRequestAppPermissions(self: *Self, perm_name: [:0]const u8) void {
- if (android.sdk_version < 23) {
- log.err(
- "Android SDK version {} does not support AndroidRequestAppPermissions\n",
- .{android.sdk_version},
- );
- return;
- }
-
- const perm_array = self.invokeJni(.NewObjectArray, .{
- 1,
- self.findClass("java/lang/String"),
- self.invokeJni(.NewStringUTF, .{perm_name}),
- });
-
- const MethodrequestPermissions = self.invokeJni(.GetMethodID, .{ self.activity_class, "requestPermissions", "([Ljava/lang/String;I)V" });
-
- // Last arg (0) is just for the callback (that I do not use)
- self.invokeJni(.CallVoidMethod, .{ self.activity.clazz, MethodrequestPermissions, perm_array, @as(c_int, 0) });
+ pub inline fn callObjectMethod(jni: *JNI, object: android.jobject, name: [:0]const u8, signature: [:0]const u8, args: anytype) Error!JniReturnType(.CallObjectMethod) {
+ const object_class = try jni.invokeJni(.GetObjectClass, .{object});
+ const method_id = try jni.invokeJni(.GetMethodID, .{ object_class, name, signature });
+ return jni.invokeJni(.CallObjectMethod, .{ object, method_id } ++ args);
}
- pub fn getFilesDir(self: *Self, allocator: std.mem.Allocator) ![:0]const u8 {
- const getFilesDirMethod = self.invokeJni(.GetMethodID, .{ self.activity_class, "getFilesDir", "()Ljava/io/File;" });
+ pub const Class = struct {
+ jni: *JNI,
+ class: android.jclass,
- const files_dir = self.env.*.CallObjectMethod(self.env, self.activity.clazz, getFilesDirMethod);
+ pub fn init(jni: *JNI, class_name: [:0]const u8) !Class {
+ const class = jni.invokeJni(.FindClass, .{class_name.ptr}) catch {
+ log.err("Class Not Found: {s}", .{class_name});
+ return Error.ClassNotDefined;
+ };
+ return Class{
+ .jni = jni,
+ .class = class,
+ };
+ }
- const fileClass = self.findClass("java/io/File");
+ pub inline fn newObject(class: Class, signature: [:0]const u8, args: anytype) Error!JniReturnType(.NewObject) {
+ const method_id = try class.jni.invokeJni(.GetMethodID, .{ class.class, "", signature });
+ return try class.jni.invokeJni(.NewObject, .{ class.class, method_id } ++ args);
+ }
- const getPathMethod = self.invokeJni(.GetMethodID, .{ fileClass, "getPath", "()Ljava/lang/String;" });
+ pub inline fn getStaticIntField(class: Class, name: [:0]const u8) !android.jint {
+ const field_id = try class.jni.invokeJni(.GetStaticFieldID, .{ class.class, name, "I" });
+ return try class.jni.invokeJni(.GetStaticIntField, .{ class.class, field_id });
+ }
- const path_string = self.env.*.CallObjectMethod(self.env, files_dir, getPathMethod);
+ pub inline fn getStaticObjectField(class: Class, name: [:0]const u8, signature: [:0]const u8) !android.jobject {
+ const field_id = try class.jni.invokeJni(.GetStaticFieldID, .{ class.class, name, signature });
+ return try class.jni.invokeJni(.GetStaticObjectField, .{ class.class, field_id });
+ }
- const utf8_or_null = self.invokeJni(.GetStringUTFChars, .{ path_string, null });
+ pub inline fn callVoidMethod(class: Class, object: android.jobject, name: [:0]const u8, signature: [:0]const u8, args: anytype) Error!void {
+ const method_id = try class.jni.invokeJni(.GetMethodID, .{ class.class, name, signature });
+ try class.jni.invokeJni(.CallVoidMethod, .{ object, method_id } ++ args);
+ }
- if (utf8_or_null) |utf8_ptr| {
- defer self.invokeJni(.ReleaseStringUTFChars, .{ path_string, utf8_ptr });
+ pub inline fn callIntMethod(class: Class, object: android.jobject, name: [:0]const u8, signature: [:0]const u8, args: anytype) Error!android.jint {
+ const method_id = try class.jni.invokeJni(.GetMethodID, .{ class.class, name, signature });
+ return try class.jni.invokeJni(.CallIntMethod, .{ object, method_id } ++ args);
+ }
- const utf8 = std.mem.sliceTo(utf8_ptr, 0);
+ pub inline fn callBooleanMethod(class: Class, object: android.jobject, name: [:0]const u8, signature: [:0]const u8, args: anytype) Error!bool {
+ const method_id = try class.jni.invokeJni(.GetMethodID, .{ class.class, name, signature });
+ return try class.jni.invokeJni(.CallBooleanMethod, .{ object, method_id } ++ args) == android.JNI_TRUE;
+ }
- return try allocator.dupeZ(u8, utf8);
- } else {
- return error.OutOfMemory;
+ pub inline fn callObjectMethod(class: Class, object: android.jobject, name: [:0]const u8, signature: [:0]const u8, args: anytype) Error!android.jobject {
+ const method_id = try class.jni.invokeJni(.GetMethodID, .{ class.class, name, signature });
+ return class.jni.invokeJni(.CallObjectMethod, .{ object, method_id } ++ args);
}
- }
- comptime {
- _ = AndroidGetUnicodeChar;
- _ = AndroidMakeFullscreen;
- _ = AndroidDisplayKeyboard;
- _ = AndroidSendToBack;
- _ = AndroidHasPermissions;
- _ = AndroidRequestAppPermissions;
- }
+ pub inline fn callStaticObjectMethod(class: Class, name: [:0]const u8, signature: [:0]const u8, args: anytype) Error!android.jobject {
+ const method_id = try class.jni.invokeJni(.GetStaticMethodID, .{ class.class, name, signature });
+ return try class.jni.invokeJni(.CallStaticObjectMethod, .{ class.class, method_id } ++ args);
+ }
+ };
+
+ pub const String = struct {
+ jstring: android.jstring,
+ slice: []const u16,
+
+ pub fn init(jni: *JNI, string: android.jstring) Error!String {
+ const len = try jni.invokeJni(.GetStringLength, .{string});
+ const ptr = try jni.invokeJni(.GetStringChars, .{ string, null });
+ const slice = ptr[0..@intCast(usize, len)];
+ return String{
+ .jstring = string,
+ .slice = slice,
+ };
+ }
+
+ pub fn deinit(string: String, jni: *JNI) void {
+ jni.invokeJniNoException(.ReleaseStringChars, .{ string.jstring, string.slice.ptr });
+ }
+ };
};