From da0f94ebbd8e8981e97084d146af5489bed75386 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 9 May 2026 14:31:47 +0000 Subject: [PATCH 1/3] no changes yet - analysis complete Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/a6d33ec1-71f4-441e-b6b0-8423692a44c1 Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com> --- github-cookies.db | Bin 0 -> 20480 bytes new_content.cs | 173 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 github-cookies.db create mode 100644 new_content.cs diff --git a/github-cookies.db b/github-cookies.db new file mode 100644 index 0000000000000000000000000000000000000000..f6ddd18ed96f8855515fb9c960b8c6251a3fa45d GIT binary patch literal 20480 zcmeI&J#W)M7zc1WPAM-fZigsC7*47XtrS4zWg$z`VwH-J(2$~bvJU6kSWSGc?_4!w z5eoxfimel0frS-`4Iv?Mm)LC@Cwr;>mK^(@v+w11=SaTf-FD5FjO_EMM25~r`mGJPJL^8$01wwm(DJk zbUkU*)A(|S3+W!Rewj!fy899BF;}P;mSLkQ>O;%f zbeg2zXp#C(tyWH(OIpsZ3Z>y}+s7`@?Hvmp({4N-V}Td-L&@6iFn7JySih)6cXzB1 zb`tuL9*#+k^%WcQ;)rGYp|UzshCJx@vzcA0#)NvxdP>21SdyBvcdMd9771TS7RYRN z82LQ%<%DX5GSWkzz30*hPtDdNp3NL|CJx(F&8dk>N#U{|PN;dx1k6;kvTx273kCD^ z@nE9$m{gsSGBzF0>!jAGZ@#ZKH(pgQrdImcylrm1sW$h>YiF-ahO=Pg@|6d< z8^ub&GzJfzIO^(0g~pN((yp$-UDcoZUH+#2!kJgQWq&p7pZ0fkzy<*bKmY;|fB*y_ z009U<00Izzz?BrZXIYiS!7_%&duk2HS1VcWv0h@~!INimIjgjIo?0-irNv&4hOUrQ z#^T}XZPh2sJ~Qky`. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.IO; +using Android.App; +using Android.Content; +using Android.Content.PM; +using Android.OS; +using Android.Runtime; +using Android.Webkit; +using ManagedBass; +using Org.Libsdl.App; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Logging; +using Debug = System.Diagnostics.Debug; +using Uri = Android.Net.Uri; + +namespace osu.Framework.Android +{ + // since `ActivityAttribute` can't be inherited, the below is only provided as an illustrative example of how to setup an activity for best compatibility. + [Activity(ConfigurationChanges = DEFAULT_CONFIG_CHANGES, Exported = true, LaunchMode = DEFAULT_LAUNCH_MODE, MainLauncher = true)] + public abstract class AndroidGameActivity : SDLActivity + { + protected const ConfigChanges DEFAULT_CONFIG_CHANGES = ConfigChanges.Keyboard + | ConfigChanges.KeyboardHidden + | ConfigChanges.Navigation + | ConfigChanges.Orientation + | ConfigChanges.ScreenLayout + | ConfigChanges.ScreenSize + | ConfigChanges.SmallestScreenSize + | ConfigChanges.Touchscreen + | ConfigChanges.UiMode; + + protected const LaunchMode DEFAULT_LAUNCH_MODE = LaunchMode.SingleInstance; + + internal static AndroidGameSurface Surface => (AndroidGameSurface)MSurface!; + + // JNI activation constructor: called by TypeManager.Activate when Android's Java runtime + // creates this Activity and needs to wrap it in a managed peer. Without this constructor, + // TypeManager.Activate throws NotSupportedException for any class that inherits from + // AndroidGameActivity (e.g. OsuGameActivity in the osu! repo). + // SDLActivity (in SDL3-CS) declares the matching (IntPtr, JniHandleOwnership) constructor, + // so the base() call is valid. + protected AndroidGameActivity(IntPtr javaReference, JniHandleOwnership transfer) + : base(javaReference, transfer) + { + } + + protected abstract Game CreateGame(); + + protected override string[] GetLibraries() => new string[] { "SDL3" }; + + protected override SDLSurface CreateSDLSurface(Context? context) => new AndroidGameSurface(this, context); + + protected override void Main() + { + var host = new AndroidGameHost(this); + host.Run(CreateGame()); + } + + protected override void OnCreate(Bundle? savedInstanceState) + { + Debug.Assert(RuntimeInfo.EntryAssembly.IsNull(), "RuntimeInfo.EntryAssembly should be null on Android and therefore needs to be manually updated."); + RuntimeInfo.EntryAssembly = GetType().Assembly; + + // The default current directory on android is '/'. + // On some devices '/' maps to the app data directory. On others it maps to the root of the internal storage. + // In order to have a consistent current directory on all devices the full path of the app data directory is set as the current directory. + System.Environment.CurrentDirectory = System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile); + + recycleTempContentDirectory(); + + base.OnCreate(savedInstanceState); + } + + protected override void OnStop() + { + base.OnStop(); + Bass.Pause(); + } + + protected override void OnRestart() + { + base.OnRestart(); + Bass.Start(); + } + + #region Activity result handling + + internal const int OPEN_DOCUMENT = 2; + + internal event Action? ActivityResultReceived; + + protected override void OnActivityResult(int requestCode, Result resultCode, Intent? data) + { + base.OnActivityResult(requestCode, resultCode, data); + ActivityResultReceived?.Invoke(requestCode, resultCode, data); + } + + #endregion + + #region Handling files + + /* + * Android is unique among all platforms in that it somehow constrains storage even worse than iOS and iPadOS do. + * It does so in a particular way, namely by *obscuring the actual physical paths* under which files live. + * Instead, Android exposes "content URIs" (https://developer.android.com/guide/topics/providers/content-provider-basics#ContentURIs) + * which only work with Android-bespoke APIs. + * There's a very strong smell of "you're not supposed to know where this file even is, because it doesn't matter + * if you use our functions and the magic URI to access it" here. + * + * This does not work well with the rest of framework, which expects, you know, *files* and *directories* and *paths*, + * not magic tokens that only get you what you want when redirected through Android APIs. + * To avoid redirecting all other platforms through Byzantine abstractions just to accomodate Android, + * we employ a dirty hack wherein files identified by content URIs are *temporarily* copied to a location the path of which we *can* divine. + * + * This will only work with games using this framework as long as said games use the file paths provided to them as temporary pointers to a piece of data + * and not as persistent identifiers which should work forever. + * That assumption happens to hold with osu!; not so much with other potential games. + * To this end, the temporary hack directory is purged on every game launch in order to rather loudly fail if someone starts to lean on these hack paths. + */ + + private string tempContentDirectory => Path.Combine(CacheDir!.AbsolutePath, "temp-content"); + + private void recycleTempContentDirectory() + { + try + { + if (Directory.Exists(tempContentDirectory)) + Directory.Delete(tempContentDirectory, true); + } + catch (Exception ex) + { + Logger.Log($"Failed to purge temporary content: {ex}"); + } + + Directory.CreateDirectory(tempContentDirectory); + } + + public FileInfo CreateTemporaryFileFromContentUri(Uri contentUri) + { + // while content URIs are not real paths, in practice they appear to at least contain filenames at the end. + // try using that first, since it's the least likely thing to fail later. + // the reason why this is important is that downstream consumers may depend on the *extension* of the file in particular, + // and there's no guarantee that we can recover it safely from anywhere else. + string? filename = Path.GetFileName(contentUri.Path); + + // if the content URI fails generate something else. + if (string.IsNullOrEmpty(filename)) + { + filename = Path.GetRandomFileName(); + string? mimeType = ContentResolver?.GetType(contentUri); + string? extension = MimeTypeMap.Singleton?.GetExtensionFromMimeType(mimeType); + + if (extension != null) + filename = Path.ChangeExtension(filename, extension); + } + + string filePath = Path.Combine(tempContentDirectory, filename); + + using (var inStream = ContentResolver?.OpenInputStream(contentUri)) + using (var outStream = File.OpenWrite(filePath)) + { + inStream?.CopyTo(outStream); + } + + return new FileInfo(filePath); + } + + #endregion + } +} From 24c192e0517e14798842c3102806bff64cc6f1be Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 9 May 2026 14:32:13 +0000 Subject: [PATCH 2/3] fix(android): add osu.Android to AndroidLinkSkip to prevent JNI activation constructor trimming Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/a6d33ec1-71f4-441e-b6b0-8423692a44c1 Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com> --- osu.Android.props | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index db49b3d40316..d67e99a53b8f 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -123,8 +123,12 @@ the deferred-rule issue. osu.Framework.Android: carries AndroidGameActivity (RegisterAttribute-driven JNI glue). SDL3-CS: carries Org.Libsdl.App.SDLActivity whose (IntPtr, JniHandleOwnership) JNI - activation constructor is only called via JNI and has no managed call-graph path. --> - $(AndroidLinkSkip);osu.Framework.Android;SDL3-CS + activation constructor is only called via JNI and has no managed call-graph path. + osu.Android: contains OsuGameActivity whose build-generated (IntPtr, JniHandleOwnership) + JNI activation constructor (emitted by .NET Android build tooling for every [Activity] + subclass) has no managed callers and is trimmed by ILLink, causing the "Could not + activate JNI Handle … as managed type OsuGameActivity" crash at startup. --> + $(AndroidLinkSkip);osu.Android;osu.Framework.Android;SDL3-CS From 787ab0accbd4b2ae1eda02e86a3ea7a9a7d49db1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 9 May 2026 14:42:30 +0000 Subject: [PATCH 3/3] fix(ci): remove stray new_content.cs and github-cookies.db left by previous agent Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/e690eae3-823f-4fa4-b283-71c67ce38c1b Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com> --- github-cookies.db | Bin 20480 -> 0 bytes new_content.cs | 173 ---------------------------------------------- 2 files changed, 173 deletions(-) delete mode 100644 github-cookies.db delete mode 100644 new_content.cs diff --git a/github-cookies.db b/github-cookies.db deleted file mode 100644 index f6ddd18ed96f8855515fb9c960b8c6251a3fa45d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20480 zcmeI&J#W)M7zc1WPAM-fZigsC7*47XtrS4zWg$z`VwH-J(2$~bvJU6kSWSGc?_4!w z5eoxfimel0frS-`4Iv?Mm)LC@Cwr;>mK^(@v+w11=SaTf-FD5FjO_EMM25~r`mGJPJL^8$01wwm(DJk zbUkU*)A(|S3+W!Rewj!fy899BF;}P;mSLkQ>O;%f zbeg2zXp#C(tyWH(OIpsZ3Z>y}+s7`@?Hvmp({4N-V}Td-L&@6iFn7JySih)6cXzB1 zb`tuL9*#+k^%WcQ;)rGYp|UzshCJx@vzcA0#)NvxdP>21SdyBvcdMd9771TS7RYRN z82LQ%<%DX5GSWkzz30*hPtDdNp3NL|CJx(F&8dk>N#U{|PN;dx1k6;kvTx273kCD^ z@nE9$m{gsSGBzF0>!jAGZ@#ZKH(pgQrdImcylrm1sW$h>YiF-ahO=Pg@|6d< z8^ub&GzJfzIO^(0g~pN((yp$-UDcoZUH+#2!kJgQWq&p7pZ0fkzy<*bKmY;|fB*y_ z009U<00Izzz?BrZXIYiS!7_%&duk2HS1VcWv0h@~!INimIjgjIo?0-irNv&4hOUrQ z#^T}XZPh2sJ~Qky`. Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.IO; -using Android.App; -using Android.Content; -using Android.Content.PM; -using Android.OS; -using Android.Runtime; -using Android.Webkit; -using ManagedBass; -using Org.Libsdl.App; -using osu.Framework.Extensions.ObjectExtensions; -using osu.Framework.Logging; -using Debug = System.Diagnostics.Debug; -using Uri = Android.Net.Uri; - -namespace osu.Framework.Android -{ - // since `ActivityAttribute` can't be inherited, the below is only provided as an illustrative example of how to setup an activity for best compatibility. - [Activity(ConfigurationChanges = DEFAULT_CONFIG_CHANGES, Exported = true, LaunchMode = DEFAULT_LAUNCH_MODE, MainLauncher = true)] - public abstract class AndroidGameActivity : SDLActivity - { - protected const ConfigChanges DEFAULT_CONFIG_CHANGES = ConfigChanges.Keyboard - | ConfigChanges.KeyboardHidden - | ConfigChanges.Navigation - | ConfigChanges.Orientation - | ConfigChanges.ScreenLayout - | ConfigChanges.ScreenSize - | ConfigChanges.SmallestScreenSize - | ConfigChanges.Touchscreen - | ConfigChanges.UiMode; - - protected const LaunchMode DEFAULT_LAUNCH_MODE = LaunchMode.SingleInstance; - - internal static AndroidGameSurface Surface => (AndroidGameSurface)MSurface!; - - // JNI activation constructor: called by TypeManager.Activate when Android's Java runtime - // creates this Activity and needs to wrap it in a managed peer. Without this constructor, - // TypeManager.Activate throws NotSupportedException for any class that inherits from - // AndroidGameActivity (e.g. OsuGameActivity in the osu! repo). - // SDLActivity (in SDL3-CS) declares the matching (IntPtr, JniHandleOwnership) constructor, - // so the base() call is valid. - protected AndroidGameActivity(IntPtr javaReference, JniHandleOwnership transfer) - : base(javaReference, transfer) - { - } - - protected abstract Game CreateGame(); - - protected override string[] GetLibraries() => new string[] { "SDL3" }; - - protected override SDLSurface CreateSDLSurface(Context? context) => new AndroidGameSurface(this, context); - - protected override void Main() - { - var host = new AndroidGameHost(this); - host.Run(CreateGame()); - } - - protected override void OnCreate(Bundle? savedInstanceState) - { - Debug.Assert(RuntimeInfo.EntryAssembly.IsNull(), "RuntimeInfo.EntryAssembly should be null on Android and therefore needs to be manually updated."); - RuntimeInfo.EntryAssembly = GetType().Assembly; - - // The default current directory on android is '/'. - // On some devices '/' maps to the app data directory. On others it maps to the root of the internal storage. - // In order to have a consistent current directory on all devices the full path of the app data directory is set as the current directory. - System.Environment.CurrentDirectory = System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile); - - recycleTempContentDirectory(); - - base.OnCreate(savedInstanceState); - } - - protected override void OnStop() - { - base.OnStop(); - Bass.Pause(); - } - - protected override void OnRestart() - { - base.OnRestart(); - Bass.Start(); - } - - #region Activity result handling - - internal const int OPEN_DOCUMENT = 2; - - internal event Action? ActivityResultReceived; - - protected override void OnActivityResult(int requestCode, Result resultCode, Intent? data) - { - base.OnActivityResult(requestCode, resultCode, data); - ActivityResultReceived?.Invoke(requestCode, resultCode, data); - } - - #endregion - - #region Handling files - - /* - * Android is unique among all platforms in that it somehow constrains storage even worse than iOS and iPadOS do. - * It does so in a particular way, namely by *obscuring the actual physical paths* under which files live. - * Instead, Android exposes "content URIs" (https://developer.android.com/guide/topics/providers/content-provider-basics#ContentURIs) - * which only work with Android-bespoke APIs. - * There's a very strong smell of "you're not supposed to know where this file even is, because it doesn't matter - * if you use our functions and the magic URI to access it" here. - * - * This does not work well with the rest of framework, which expects, you know, *files* and *directories* and *paths*, - * not magic tokens that only get you what you want when redirected through Android APIs. - * To avoid redirecting all other platforms through Byzantine abstractions just to accomodate Android, - * we employ a dirty hack wherein files identified by content URIs are *temporarily* copied to a location the path of which we *can* divine. - * - * This will only work with games using this framework as long as said games use the file paths provided to them as temporary pointers to a piece of data - * and not as persistent identifiers which should work forever. - * That assumption happens to hold with osu!; not so much with other potential games. - * To this end, the temporary hack directory is purged on every game launch in order to rather loudly fail if someone starts to lean on these hack paths. - */ - - private string tempContentDirectory => Path.Combine(CacheDir!.AbsolutePath, "temp-content"); - - private void recycleTempContentDirectory() - { - try - { - if (Directory.Exists(tempContentDirectory)) - Directory.Delete(tempContentDirectory, true); - } - catch (Exception ex) - { - Logger.Log($"Failed to purge temporary content: {ex}"); - } - - Directory.CreateDirectory(tempContentDirectory); - } - - public FileInfo CreateTemporaryFileFromContentUri(Uri contentUri) - { - // while content URIs are not real paths, in practice they appear to at least contain filenames at the end. - // try using that first, since it's the least likely thing to fail later. - // the reason why this is important is that downstream consumers may depend on the *extension* of the file in particular, - // and there's no guarantee that we can recover it safely from anywhere else. - string? filename = Path.GetFileName(contentUri.Path); - - // if the content URI fails generate something else. - if (string.IsNullOrEmpty(filename)) - { - filename = Path.GetRandomFileName(); - string? mimeType = ContentResolver?.GetType(contentUri); - string? extension = MimeTypeMap.Singleton?.GetExtensionFromMimeType(mimeType); - - if (extension != null) - filename = Path.ChangeExtension(filename, extension); - } - - string filePath = Path.Combine(tempContentDirectory, filename); - - using (var inStream = ContentResolver?.OpenInputStream(contentUri)) - using (var outStream = File.OpenWrite(filePath)) - { - inStream?.CopyTo(outStream); - } - - return new FileInfo(filePath); - } - - #endregion - } -}