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 }); + } + }; };