From 1b0a0a476d1809620b509883a3ffa5c45fae1e4e Mon Sep 17 00:00:00 2001 From: Karim Akra Date: Sun, 31 Aug 2025 21:30:22 +0300 Subject: [PATCH] Base Android implementation --- project.hxp | 65 ++++++++++++++- .../funkin/external/android/CallbackUtil.hx | 21 +++++ .../java/funkin/extension/CallbackUtil.java | 14 +++- .../java/funkin/extension/FNFCExtension.java | 80 +++++++++++++++++++ 4 files changed, 176 insertions(+), 4 deletions(-) create mode 100644 source/funkin/external/android/java/funkin/extension/FNFCExtension.java diff --git a/project.hxp b/project.hxp index 3180b2bdd..d753cb93b 100644 --- a/project.hxp +++ b/project.hxp @@ -113,7 +113,10 @@ class Project extends HXProject */ static final ANDROID_TARGET_SDK_VERSION:Int = 35; - static final ANDROID_EXTENSIONS:Array = ["funkin.extensions.CallbackUtil"]; + static final ANDROID_EXTENSIONS:Array = [ + "funkin.extensions.CallbackUtil", + "funkin.extensions.FNFCExtension" + ]; /** * The team ID to use for the iOS app. Configured in XCode. @@ -1088,6 +1091,7 @@ class Project extends HXProject } } + // Custom android.application XML Children for the AndroidManifest final modsFolderProvider:Xml = Xml.createElement('provider'); modsFolderProvider.set('android:authorities', '$PACKAGE_NAME.docprovider'); modsFolderProvider.set('android:name', 'funkin.provider.DataFolderProvider'); @@ -1107,14 +1111,69 @@ class Project extends HXProject { if (config.get('android.application').xmlChildren != null) { - config.get('android.application').xmlChildren.push(Printer.print(modsFolderProvider)); + config.get('android.application').xmlChildren.push(Printer.print(modsFolderProvider, true)); } else { - config.get('android.application').xmlChildren = [Printer.print(modsFolderProvider)]; + config.get('android.application').xmlChildren = [Printer.print(modsFolderProvider, true)]; } } + // Custom android.activity XML Children for the AndroidManifest + final intentFilter:Xml = Xml.createElement('intent-filter'); + + final action:Xml = Xml.createElement('action'); + action.set('android:name', 'android.intent.action.VIEW'); + intentFilter.addChild(action); + + final intentFilterCategories:Array = [ + "android.intent.category.DEFAULT", + "android.intent.category.BROWSABLE" + ]; + + for (categoryName in intentFilterCategories) + { + final category:Xml = Xml.createElement('category'); + category.set('android:name', categoryName); + intentFilter.addChild(category); + } + + final intentFilterDatas:Map> = [ + "android:mimeType" => ["application/zip"], + // When this is added, it makes the game show up only when it's a FNFC file. + // But it also for some reason make it not show up at all in ZArchiver no matter what the file is... + // "android:host" => ["*"], + "android:scheme" => ["file", "content"], + // I think having all these makes it so the extension check would work even when the files has dots like "epic-song-v2.0.fnfc" + "android:pathPattern" => [ + ".*\\.fnfc", ".*\\..*\\.fnfc", + ".*\\..*\\..*\\.fnfc", + ".*\\..*\\..*\\..*\\.fnfc", + ".*\\..*\\..*\\..*\\..*\\.fnfc" + ] + ]; + + for (key => values in intentFilterDatas) { + for (value in values) { + final data:Xml = Xml.createElement("data"); + data.set(key, value); + intentFilter.addChild(data); + } + } + + @:nullSafety(Off) + { + if (config.get('android.activity').xmlChildren != null) + { + config.get('android.activity').xmlChildren.push(Printer.print(intentFilter, true)); + } + else + { + config.get('android.activity').xmlChildren = [Printer.print(intentFilter, true)]; + } + } + + final extensions:Null> = config.getArrayString('android.extension'); for (extension in ANDROID_EXTENSIONS) diff --git a/source/funkin/external/android/CallbackUtil.hx b/source/funkin/external/android/CallbackUtil.hx index 4715dad3f..ebf8a28c2 100644 --- a/source/funkin/external/android/CallbackUtil.hx +++ b/source/funkin/external/android/CallbackUtil.hx @@ -30,6 +30,13 @@ class CallbackUtil */ public static var onActivityResult:FlxTypedSignalInt->Void> = new FlxTypedSignalInt->Void>(); + /** + * Signal triggered when the user opens a FNFC file with the game in runtime. + * + * First argument is the FNFC file path. + */ + public static var onOpenFNFC:FlxTypedSignalVoid> = new FlxTypedSignalVoid>(); + /** * Initializes the callback utility. */ @@ -66,5 +73,19 @@ class CallbackHandler #if (lime >= "8.0.0") implements JNISafety #end { if (CallbackUtil.onActivityResult != null) CallbackUtil.onActivityResult.dispatch(requestCode, resultCode); } + + /** + * Handles the onOpenFNFC callback from native code. + * + * @param file Path to the FNFC file that was opened. + */ + @:keep + #if (lime >= "8.0.0") + @:runOnMainThread + #end + public function onOpenFNFC(file:String):Void + { + if (CallbackUtil.onOpenFNFC != null) CallbackUtil.onOpenFNFC.dispatch(file); + } } #end diff --git a/source/funkin/external/android/java/funkin/extension/CallbackUtil.java b/source/funkin/external/android/java/funkin/extension/CallbackUtil.java index 617a6f3b7..4f8b474fa 100644 --- a/source/funkin/external/android/java/funkin/extension/CallbackUtil.java +++ b/source/funkin/external/android/java/funkin/extension/CallbackUtil.java @@ -25,10 +25,22 @@ public class CallbackUtil extends Extension CallbackUtil.haxeObject = haxeObject; } + public static void callMethod(String methodName, Object[] args) + { + if (haxeObject != null) + haxeObject.call(methodName, args); + } + + public static void callMethodVoid(String methodName) + { + if (haxeObject != null) + haxeObject.call0(methodName); + } + @Override public boolean onActivityResult(int requestCode, int resultCode, Intent data) { - if (haxeObject != null) + if (haxeObject != null) haxeObject.call2("onActivityResult", requestCode, resultCode); return true; diff --git a/source/funkin/external/android/java/funkin/extension/FNFCExtension.java b/source/funkin/external/android/java/funkin/extension/FNFCExtension.java new file mode 100644 index 000000000..950af7a78 --- /dev/null +++ b/source/funkin/external/android/java/funkin/extension/FNFCExtension.java @@ -0,0 +1,80 @@ +package funkin.extensions; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; + +import funkin.extensions.CallbackUtil; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; + +import org.haxe.extension.Extension; + +import org.haxe.lime.HaxeObject; + +public class FNFCExtension extends Extension +{ + public static String launchedFile = null; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Intent intent = mainActivity.getIntent(); + if (intent != null && intent.getData() != null) { + handleIntent(intent, false); + } + } + + @Override + public void onNewIntent(Intent intent) { + super.onNewIntent(intent); + + if (intent != null && intent.getData() != null) { + handleIntent(mainActivity.getIntent(), true); + } + } + + public static String queryLaunchedFNFC() + { + return launchedFile; + } + + + private static void handleIntent(Intent intent, boolean doCallback) + { + if (Intent.ACTION_VIEW.equals(intent.getAction())) { + Uri uri = intent.getData(); + if (uri != null) { + File output = new File(mainContext.getCacheDir(), new File("fnfc", uri.getPath()).getName()); + + try { + InputStream in = mainContext.getContentResolver().openInputStream(uri); + OutputStream out = new FileOutputStream(output); + byte[] buffer = new byte[8192]; + int len; + while ((len = in.read(buffer)) > 0) { + out.write(buffer, 0, len); + } + } + catch (Exception e) { + Log.e("Exception", e.getMessage()); + } + + launchedFile = output.getAbsolutePath(); + + if (doCallback) + { + Object[] args = new Object[1]; + args[0] = launchedFile; + CallbackUtil.callMethod("onOpenFNFC", args); + } + } + } + } +}