diff --git a/Knossos.NET.Android/Properties/AndroidManifest.xml b/Knossos.NET.Android/Properties/AndroidManifest.xml index a4306270..62f6b296 100644 --- a/Knossos.NET.Android/Properties/AndroidManifest.xml +++ b/Knossos.NET.Android/Properties/AndroidManifest.xml @@ -46,5 +46,12 @@ android:process=":game" android:launchMode="singleTask"> + diff --git a/Knossos.NET.Android/java/com/knossosnet/knossosnet/FlagsActivity.java b/Knossos.NET.Android/java/com/knossosnet/knossosnet/FlagsActivity.java new file mode 100644 index 00000000..48ed094a --- /dev/null +++ b/Knossos.NET.Android/java/com/knossosnet/knossosnet/FlagsActivity.java @@ -0,0 +1,149 @@ +package com.knossosnet.knossosnet; +import android.content.Intent; +import android.os.Bundle; +import android.view.WindowManager; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.lang.ref.WeakReference; +import android.os.ResultReceiver; +import android.app.Activity; + +public class FlagsActivity extends org.libsdl.app.SDLActivity { + + private static WeakReference _self = null; + private static String _workingFolder = ""; + private static volatile boolean _delivered = false; + private ResultReceiver _receiver = null; + + /* FSO API */ + public static String getWorkingFolder() { return _workingFolder; } + + public static void setFlagsJson(String json) { + FlagsActivity act = (_self != null) ? _self.get() : null; + if (act != null) act.deliver(json); + } + + public static void enableOverlay() { + } + + public static void disableOverlay() { + } + + /* ******* */ + + @Override + protected String[] getArguments() { + Intent i = getIntent(); + ArrayList args = (i != null) ? i.getStringArrayListExtra("fsoArgs") : null; + if (args == null || args.isEmpty()) { + return new String[] { "-get_flags", "json_v2" }; + } + return args.toArray(new String[0]); + } + + @Override + protected String[] getLibraries() { return new String[] { }; } + + @Override + protected String getMainSharedObject() { + String path = getIntent().getStringExtra("engineLibName"); + return (path == null || path.isEmpty()) ? null : path; + } + + @Override + protected String getMainFunction() { return "android_main"; } + + @Override + protected void onCreate(Bundle savedInstanceState) { + _self = new WeakReference<>(this); + _delivered = false; + + Intent i = getIntent(); + if (i != null) { + _workingFolder = i.getStringExtra("workingFolder"); + if (_workingFolder == null) _workingFolder = ""; + _receiver = i.getParcelableExtra("flagsReceiver"); + } + + loadNatives(); + + setResult(RESULT_CANCELED); + + super.onCreate(savedInstanceState); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + _self = null; + try { + String proc = android.app.Application.getProcessName(); + if (proc != null && proc.endsWith(":flags")) { + android.os.Process.killProcess(android.os.Process.myPid()); + } + } catch (Throwable ignored) {} + } + + private static final String[] PREFERRED_ORDER = new String[] { + "libSDL2.so", "libshaderc.so", "libopenal.so", "libavutil.so", + "libswresample.so", "libswscale.so", "libavcodec.so", + "libavformat.so", "libavfilter.so" + }; + + private void loadNatives() { + File dir = new File(getFilesDir(), "natives/"); + List loadList = orderForLoad(dir); + List failed = new ArrayList<>(); + for (File so : loadList) if (!tryLoad(so)) failed.add(so); + for (File so : failed) tryLoad(so); + } + + private static List orderForLoad(File dir) { + ArrayList ordered = new ArrayList<>(); + if (dir == null || !dir.isDirectory()) return ordered; + for (String name : PREFERRED_ORDER) { + File f = new File(dir, name); + if (f.isFile()) ordered.add(f); + } + File[] arr = dir.listFiles((d, name) -> name != null && name.endsWith(".so")); + if (arr != null) { + for (File f : arr) { + if (!containsName(ordered, f.getName()) && !isEngineName(f.getName())) { + ordered.add(f); + } + } + } + return ordered; + } + + private boolean tryLoad(File so) { + try { + if (so != null && so.isFile()) { System.load(so.getAbsolutePath()); return true; } + } catch (UnsatisfiedLinkError e) { e.printStackTrace(); } + return false; + } + + private static boolean containsName(List list, String name) { + for (File f : list) if (f.getName().equals(name)) return true; + return false; + } + + private static boolean isEngineName(String name) { + if (name == null) return false; + return name.startsWith("libfso") || name.contains("libfs2"); + } + + private void deliver(final String json) { + if (_delivered) return; + _delivered = true; + runOnUiThread(() -> { + if (_receiver != null) { + Bundle b = new Bundle(); + b.putString("flagsJson", json); + _receiver.send(Activity.RESULT_OK, b); + } + finish(); + }); + } +} \ No newline at end of file diff --git a/Knossos.NET/Classes/AndroidHelper.cs b/Knossos.NET/Classes/AndroidHelper.cs index 6413df55..f17038eb 100644 --- a/Knossos.NET/Classes/AndroidHelper.cs +++ b/Knossos.NET/Classes/AndroidHelper.cs @@ -2,8 +2,10 @@ using Android.App; using System.Linq; using Android.Content; -#endif using Avalonia.Threading; +using Android.OS; +#endif + using System; using System.Collections.Generic; using System.Diagnostics; @@ -183,6 +185,73 @@ public static void LaunchFSO(string engineLibPath, string? workingFolder, string } } + /// + /// Gets FSO flags + /// + /// + /// + /// json string or empty string + public static async Task GetFlagsStringFSOAsync(string engineLibPath, int timeoutMs = 30000) + { + try + { + var ctx = Application.Context; + string dstAbiDir = System.IO.Path.Combine(ctx.FilesDir!.AbsolutePath, "natives"); + var fi = new FileInfo(engineLibPath); + var folderPath = fi.Directory!.FullName; + if (!folderPath.EndsWith("/")) + folderPath += "/"; + StageAllToInternal(folderPath, dstAbiDir); + var libName = fi.Name; + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + using var receiver = new FlagsResultReceiver(tcs); + + var intent = new Intent(); + intent.SetClassName(ctx, "com.knossosnet.knossosnet.FlagsActivity"); + intent.AddFlags(ActivityFlags.NewTask); + intent.PutExtra("engineLibName", System.IO.Path.Combine(dstAbiDir, libName)); + + intent.PutStringArrayListExtra("fsoArgs", new List { "-get_flags", "json_v2" }); + intent.PutExtra("flagsReceiver", receiver); + + ctx.StartActivity(intent); + + var completed = await Task.WhenAny(tcs.Task, Task.Delay(timeoutMs)).ConfigureAwait(false); + if (completed != tcs.Task) + { + Log.Add(Log.LogSeverity.Error, "AndroidHelper.GetFlagsStringFSO", "Timeout waiting for fso flags (" + timeoutMs + "ms)."); + return ""; + } + + return await tcs.Task.ConfigureAwait(false); + } + catch (Exception ex) + { + Log.Add(Log.LogSeverity.Error, "AndroidHelper.GetFlagsStringFSO", ex); + } + return ""; + } + + private sealed class FlagsResultReceiver : ResultReceiver + { + private readonly TaskCompletionSource _tcs; + + public FlagsResultReceiver(TaskCompletionSource tcs) + : base(new Handler(Looper.MainLooper!)) + { + _tcs = tcs; + } + + protected override void OnReceiveResult(int resultCode, Bundle? resultData) + { + if (resultCode == (int)Result.Ok) + _tcs.TrySetResult(resultData?.GetString("flagsJson") ?? ""); + else + _tcs.TrySetResult(""); + } + } + #else //Stubs public static string? GetExternalAppFilesDir() => ""; @@ -193,6 +262,7 @@ public static void LaunchFSO(string engineLibPath, string? workingFolder, string public static string GetDefaultKnetDataDir() => ""; public static string GetDefaultFSODataDir() => ""; public static void LaunchFSO(string engineLibPath, string? workingFolder, string cmdline) { } + public static async Task GetFlagsStringFSOAsync(string engineLibPath, int timeoutMs = 30000) { await Task.Delay(1); return ""; } #endif } diff --git a/Knossos.NET/Classes/FsoBuild.cs b/Knossos.NET/Classes/FsoBuild.cs index 1605a065..48087341 100644 --- a/Knossos.NET/Classes/FsoBuild.cs +++ b/Knossos.NET/Classes/FsoBuild.cs @@ -330,10 +330,6 @@ public async Task RunFSO(FsoExecType executableType, string cmdline, /// A FlagsJsonV1 structure or null if failed public async Task GetFlagsV1Async() { -#if ANDROID - // no flags on android - return null; -#else var executable = GetExecutable(FsoExecType.Flags); var fullpath = GetExecutablePath(executable); if (fullpath == null) @@ -353,16 +349,21 @@ public async Task RunFSO(FsoExecType executableType, string cmdline, } Log.Add(Log.LogSeverity.Information, "FsoBuild.GetFlags()", "Getting FSO Flags from file: " + fullpath); - - if(KnUtils.IsLinux || KnUtils.IsMacOS) - { - KnUtils.Chmod(fullpath!,"+x"); - } - string output = string.Empty; string stderr = string.Empty; try { +#if ANDROID + output = await AndroidHelper.GetFlagsStringFSOAsync(fullpath!); + if (output == "") return null; + return JsonSerializer.Deserialize(output); +#else + + if (KnUtils.IsLinux || KnUtils.IsMacOS) + { + KnUtils.Chmod(fullpath!,"+x"); + } + using (var cmd = new Process()) { cmd.StartInfo.FileName = fullpath; @@ -433,6 +434,7 @@ public async Task RunFSO(FsoExecType executableType, string cmdline, } return JsonSerializer.Deserialize(result); } +#endif } catch (JsonException exJson) { @@ -455,7 +457,6 @@ public async Task RunFSO(FsoExecType executableType, string cmdline, Log.Add(Log.LogSeverity.Error, "FSO EXE OUTPUT ", output); return null; } -#endif } ///