From c2b68dcd9000414406dd5ea5bc2b6c810125f322 Mon Sep 17 00:00:00 2001 From: Karim Akra Date: Fri, 26 Sep 2025 00:53:57 +0300 Subject: [PATCH] Android cleanup & some finalization --- source/funkin/InitState.hx | 18 ++- .../funkin/external/android/CallbackUtil.hx | 78 ++++++------- .../funkin/external/android/DataFolderUtil.hx | 1 - source/funkin/external/android/ScreenUtil.hx | 1 - .../java/funkin/extension/CallbackUtil.java | 69 +++++++++++- .../java/funkin/extension/FNFCExtension.java | 105 +++++++++++++----- source/funkin/mobile/util/FNFCUtil.hx | 32 +++++- source/funkin/modding/PolymodHandler.hx | 5 + .../util/plugins/ReloadAssetsDebugPlugin.hx | 4 +- 9 files changed, 229 insertions(+), 84 deletions(-) diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index 19af891bc..390240b67 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -130,7 +130,7 @@ class InitState extends FlxState #end #if mobile - // Setup In-App reviews + // Setup Mobile FNFC launcher. funkin.mobile.util.FNFCUtil.init(); #end @@ -403,7 +403,23 @@ class InitState extends FlxState else { // FlxG.sound.cache(Paths.music('freakyMenu/freakyMenu')); + #if mobile + final fnfcFile = funkin.mobile.util.FNFCUtil.queryFNFC(); + if (fnfcFile != null) + { + trace('launching FNFC from $fnfcFile'); + FlxG.switchState(() -> new ChartPlaytestState( + { + fnfcFilePath: fnfcFile, + })); + } + else + { + FlxG.switchState(() -> new TitleState()); + } + #else FlxG.switchState(() -> new TitleState()); + #end } #else else diff --git a/source/funkin/external/android/CallbackUtil.hx b/source/funkin/external/android/CallbackUtil.hx index ebf8a28c2..0d12ff046 100644 --- a/source/funkin/external/android/CallbackUtil.hx +++ b/source/funkin/external/android/CallbackUtil.hx @@ -7,8 +7,7 @@ import flixel.util.FlxSignal; /** * A Utility class to handle Android API level callbacks and events. */ -@:unreflective -class CallbackUtil +class CallbackUtil #if (lime >= "8.0.0") implements JNISafety #end { /** * The result code for `DATA_FOLDER_CLOSED` activity. @@ -35,7 +34,7 @@ class CallbackUtil * * First argument is the FNFC file path. */ - public static var onOpenFNFC:FlxTypedSignalVoid> = new FlxTypedSignalVoid>(); + public static var onFNCOpen:FlxTypedSignalVoid> = new FlxTypedSignalVoid>(); /** * Initializes the callback utility. @@ -46,46 +45,41 @@ class CallbackUtil if (initCallBackJNI != null) { - initCallBackJNI(new CallbackHandler()); + initCallBackJNI(new CallbackUtil()); + } + } + + /** + * Get an Array that contains every static field from this class + * @return Array + */ + private static function listStaticFields():Array + { + return Type.getClassFields(CallbackUtil); + } + + @:noCompletion + private function new() {} + + @:noCompletion + @:keep + #if (lime >= "8.0.0") + @:runOnMainThread + #end + private function dispatchCallback(callbackName:String, arguments:Array) + { + // TODO: Figure out a way to check if it's a FlxSignal? Don't know if it's possible due to how FlxTypedSignal works + if (listStaticFields().contains(callbackName)) + { + // trace('[CALLBACK] Calling ${callbackName} with args ${arguments == null ? "[]" : '[${arguments.join(',')}]'}'); + final field = Reflect.field(CallbackUtil, callbackName); + final method = Reflect.field(field, "dispatch"); + Reflect.callMethod(field, method, arguments); + } + else + { + trace('[CALLBACK] Callback "${callbackName}" not found.'); } } } - -/** - * Internal class to handle native callback events. - */ -class CallbackHandler #if (lime >= "8.0.0") implements JNISafety #end -{ - @:allow(funkin.external.android.CallbackUtil) - function new():Void {} - - /** - * Handles the activity result callback from native code. - * - * @param requestCode The request code of the acitivty. - * @param resultCode The result code of the acitivty. - */ - @:keep - #if (lime >= "8.0.0") - @:runOnMainThread - #end - public function onActivityResult(requestCode:Int, resultCode:Int):Void - { - 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/DataFolderUtil.hx b/source/funkin/external/android/DataFolderUtil.hx index 4d9a89a95..ca3473272 100644 --- a/source/funkin/external/android/DataFolderUtil.hx +++ b/source/funkin/external/android/DataFolderUtil.hx @@ -4,7 +4,6 @@ package funkin.external.android; /** * A Utility class to manage the Application's Data folder on Android. */ -@:unreflective class DataFolderUtil { /** diff --git a/source/funkin/external/android/ScreenUtil.hx b/source/funkin/external/android/ScreenUtil.hx index fc271a91c..43ad58ca1 100644 --- a/source/funkin/external/android/ScreenUtil.hx +++ b/source/funkin/external/android/ScreenUtil.hx @@ -7,7 +7,6 @@ import lime.system.JNI; /** * A Utility class to get Android screen related informations. */ -@:unreflective class ScreenUtil { /** diff --git a/source/funkin/external/android/java/funkin/extension/CallbackUtil.java b/source/funkin/external/android/java/funkin/extension/CallbackUtil.java index 4f8b474fa..6440c4fc0 100644 --- a/source/funkin/external/android/java/funkin/extension/CallbackUtil.java +++ b/source/funkin/external/android/java/funkin/extension/CallbackUtil.java @@ -25,23 +25,80 @@ public class CallbackUtil extends Extension CallbackUtil.haxeObject = haxeObject; } - public static void callMethod(String methodName, Object[] args) + public static void callMethod(String methodName) { if (haxeObject != null) - haxeObject.call(methodName, args); + { + Object[] args = new Object[2]; + args[0] = methodName; + args[1] = new Object[0]; + haxeObject.call("dispatchCallback", args); + } } - public static void callMethodVoid(String methodName) + public static void callMethod(String methodName, Object arg1) { if (haxeObject != null) - haxeObject.call0(methodName); + { + Object[] _args = {arg1}; + Object[] args = new Object[2]; + args[0] = methodName; + args[1] = _args; + haxeObject.call("dispatchCallback", args); + } + } + + public static void callMethod(String methodName, Object arg1, Object arg2) + { + if (haxeObject != null) + { + Object[] _args = {arg1, arg2}; + Object[] args = new Object[2]; + args[0] = methodName; + args[1] = _args; + haxeObject.call("dispatchCallback", args); + } + } + + public static void callMethod(String methodName, Object arg1, Object arg2, Object arg3) + { + if (haxeObject != null) + { + Object[] _args = {arg1, arg2, arg3}; + Object[] args = new Object[2]; + args[0] = methodName; + args[1] = _args; + haxeObject.call("dispatchCallback", args); + } + } + + public static void callMethod(String methodName, Object arg1, Object arg2, Object arg3, Object arg4) + { + if (haxeObject != null) + { + Object[] _args = {arg1, arg2, arg3, arg4}; + Object[] args = new Object[2]; + args[0] = methodName; + args[1] = _args; + haxeObject.call("dispatchCallback", args); + } + } + + public static void callMethod(String methodName, Object[] args) + { + if (haxeObject != null) + { + Object[] _args = new Object[2]; + args[0] = methodName; + args[1] = args; + haxeObject.call("dispatchCallback", _args); + } } @Override public boolean onActivityResult(int requestCode, int resultCode, Intent data) { - if (haxeObject != null) - haxeObject.call2("onActivityResult", requestCode, resultCode); + callMethod("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 index 950af7a78..af7f67e80 100644 --- a/source/funkin/external/android/java/funkin/extension/FNFCExtension.java +++ b/source/funkin/external/android/java/funkin/extension/FNFCExtension.java @@ -2,16 +2,18 @@ package funkin.extensions; import android.content.Intent; import android.net.Uri; +import android.os.ParcelFileDescriptor; import android.os.Bundle; import android.util.Log; import funkin.extensions.CallbackUtil; import java.io.File; +import java.io.FileInputStream; import java.io.FileOutputStream; +import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.net.URI; import org.haxe.extension.Extension; @@ -19,7 +21,9 @@ import org.haxe.lime.HaxeObject; public class FNFCExtension extends Extension { - public static String launchedFile = null; + public static final String LOG_TAG = "FNFCExtension"; + + public static String lastFNFC = null; @Override public void onCreate(Bundle savedInstanceState) { @@ -40,41 +44,86 @@ public class FNFCExtension extends Extension } } - public static String queryLaunchedFNFC() - { - return launchedFile; - } - - private static void handleIntent(Intent intent, boolean doCallback) { - if (Intent.ACTION_VIEW.equals(intent.getAction())) { + 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()); + if (uri != null) + { + try + { + lastFNFC = copyFNFCToCache(uri); - 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); + if (doCallback) + { + CallbackUtil.callMethod("onFNCOpen", lastFNFC); } } - catch (Exception e) { - Log.e("Exception", e.getMessage()); - } - - launchedFile = output.getAbsolutePath(); - - if (doCallback) + catch (IOException e) { - Object[] args = new Object[1]; - args[0] = launchedFile; - CallbackUtil.callMethod("onOpenFNFC", args); + Log.e(LOG_TAG, e.getMessage()); } } } } + + public static String copyFNFCToCache(Uri uri) throws IOException + { + if (uri != null) + { + String fileName = new File(uri.getPath()).getName(); + + // Clean some Uri leftovers + if (fileName.contains(":")) + fileName = fileName.split(":")[1]; + + File cacheFNFC = new File(mainContext.getCacheDir(), "fnfc"); + File output = new File(cacheFNFC, fileName); + + if (!cacheFNFC.exists()) + cacheFNFC.mkdir(); + + if (output.exists()) + output.delete(); + + ParcelFileDescriptor parcelFileDescriptor = null; + FileInputStream fileInputStream = null; + OutputStream out = null; + + try + { + parcelFileDescriptor = mainContext.getContentResolver().openFileDescriptor(uri, "r"); + fileInputStream = new FileInputStream(parcelFileDescriptor.getFileDescriptor()); + + byte[] fileBytes = new byte[(int) parcelFileDescriptor.getStatSize()]; + fileInputStream.read(fileBytes); + + out = new FileOutputStream(output); + out.write(fileBytes); + + if (output.exists()) + output.deleteOnExit(); // makes the file get deleted when the app closes ig? + } + catch (IOException e) + { + Log.e(LOG_TAG, e.getMessage()); + } + finally + { + if (fileInputStream != null) + fileInputStream.close(); + + if (parcelFileDescriptor != null) + parcelFileDescriptor.close(); + + if (out != null) + out.close(); + } + + return output.getAbsolutePath(); + } + + return null; + } } diff --git a/source/funkin/mobile/util/FNFCUtil.hx b/source/funkin/mobile/util/FNFCUtil.hx index 86effed96..b2be6b20c 100644 --- a/source/funkin/mobile/util/FNFCUtil.hx +++ b/source/funkin/mobile/util/FNFCUtil.hx @@ -3,6 +3,11 @@ package funkin.mobile.util; #if ios import funkin.external.apple.FNFCUtil as NativeFNFCUtil; #end +#if android +import funkin.external.android.JNIUtil; +import funkin.external.android.CallbackUtil; +#end +import lime.system.System; import flixel.util.FlxSignal; /** @@ -18,10 +23,30 @@ class FNFCUtil #if ios FlxG.stage.window.onDropFile.add(function(_) { - final url = lime.system.System.getHint("IOS_UIApplicationLaunchOptionsURLKey"); - onFNCOpen.dispatch(getFNFCFromURL(url)); + final url = queryFNFC(); + if (url != null) onFNCOpen.dispatch(getFNFCFromURL(url)); }); + #elseif android + CallbackUtil.onFNCOpen.add(onFNCOpen.dispatch); #end + + final fnfcFile:String = queryFNFC(); + + if (fnfcFile != null) + { + trace('Got FNFC File from $fnfcFile!'); + } + } + + public static function queryFNFC():Null + { + #if ios + return System.getHint("IOS_UIApplicationLaunchOptionsURLKey"); + #elseif android + final staticField = JNIUtil.createStaticField('funkin/extensions/FNFCExtension', 'lastFNFC', 'Ljava/lang/String;'); + if (staticField != null) return staticField.get(); + #end + return null; } #if ios @@ -30,7 +55,8 @@ class FNFCUtil * @param url A iOS `file://` URL. * @return The new path that the file the URL points at inside of the cache directory. */ - public static function getFNFCFromURL(url:String):String + @:noCompletion + private static function getFNFCFromURL(url:String):String { var cURL:cpp.ConstCharStar = cast url; diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx index 0b37812ca..4f5d5432b 100644 --- a/source/funkin/modding/PolymodHandler.hx +++ b/source/funkin/modding/PolymodHandler.hx @@ -445,6 +445,11 @@ class PolymodHandler var className:String = Type.getClassName(cls); Polymod.blacklistImport(className); } + + // External classes for android that bridge to private JNI methods & callbacks + Polymod.blacklistImport('funkin.external.android.CallbackUtil'); + Polymod.blacklistImport('funkin.external.android.DataFolderUtil'); + Polymod.blacklistImport('funkin.external.android.JNIUtil'); } /** diff --git a/source/funkin/util/plugins/ReloadAssetsDebugPlugin.hx b/source/funkin/util/plugins/ReloadAssetsDebugPlugin.hx index 8e11a1087..57b6b255c 100644 --- a/source/funkin/util/plugins/ReloadAssetsDebugPlugin.hx +++ b/source/funkin/util/plugins/ReloadAssetsDebugPlugin.hx @@ -91,9 +91,9 @@ class ReloadAssetsDebugPlugin extends FlxBasic #if android @:noCompletion - function onActivityResult(resultCode:Int, requestCode:Int):Void + function onActivityResult(requestCode:Int, resultCode:Int):Void { - if (resultCode == CallbackUtil.DATA_FOLDER_CLOSED) + if (requestCode == CallbackUtil.DATA_FOLDER_CLOSED) { reload(); }