diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index 391cc6eef7b9..272559bb31e3 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -51,7 +51,7 @@ public class OsuGameActivity : AndroidGameActivity public new bool IsTablet { get; private set; } - private readonly OsuGameAndroid game; + private OsuGameAndroid? game; private bool gameCreated; @@ -60,6 +60,9 @@ protected override Framework.Game CreateGame() if (gameCreated) throw new InvalidOperationException("Framework tried to create a game twice."); + if (game == null) + throw new InvalidOperationException("Game was not initialised."); + gameCreated = true; return game; } @@ -80,6 +83,8 @@ protected override void OnCreate(Bundle? savedInstanceState) // first use because the internal Platform.CurrentActivity is null. Microsoft.Maui.ApplicationModel.Platform.Init(this, savedInstanceState); + + // OnNewIntent() only fires for an activity if it's *re-launched* while it's on top of the activity stack. // on first launch we still have to fire manually. // reference: https://developer.android.com/reference/android/app/Activity#onNewIntent(android.content.Intent) @@ -145,7 +150,7 @@ private void handleIntent(Intent? intent) else if (osu_url_schemes.Contains(intent.Scheme)) { if (intent.DataString != null) - game.HandleLink(intent.DataString); + game?.HandleLink(intent.DataString); } break; @@ -188,7 +193,7 @@ await Task.WhenAll(uris.Select(async uri => } })).ConfigureAwait(false); - await game.Import(tasks.ToArray()).ConfigureAwait(false); + if (game != null) await game.Import(tasks.ToArray()).ConfigureAwait(false); }, TaskCreationOptions.LongRunning); } } diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 7571cfc05330..35971ca6b6e6 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -27,7 +27,35 @@ public partial class OsuGameAndroid : OsuGame [Cached] private readonly OsuGameActivity gameActivity; - private readonly PackageInfo? packageInfo; + private readonly object packageInfoLock = new object(); + private PackageInfo? packageInfo; + private bool packageInfoChecked; + + private PackageInfo? getPackageInfo() + { + lock (packageInfoLock) + { + if (packageInfoChecked) + return packageInfo; + + try + { + // Use the activity instance directly instead of Application.Context to ensure + // the PackageManager is accessible even on newer/stricter Android versions. + packageInfo = gameActivity.PackageManager?.GetPackageInfo(gameActivity.PackageName!, 0); + } + catch (Exception e) + { + Debug.WriteLine($"[osu!] Failed to retrieve package info: {e.Message}"); + } + finally + { + packageInfoChecked = true; + } + + return packageInfo; + } + } public override Vector2 ScalingContainerTargetDrawSize => DrawWidth > 0 && DrawHeight > 0 ? new Vector2(1024, 1024 * DrawHeight / DrawWidth) @@ -53,16 +81,6 @@ public OsuGameAndroid(OsuGameActivity activity) : base(null) { gameActivity = activity; - - try - { - packageInfo = Application.Context.ApplicationContext!.PackageManager!.GetPackageInfo(Application.Context.ApplicationContext.PackageName!, 0); - } - catch (Exception e) - { - Debug.WriteLine($"[osu!] Failed to retrieve package info: {e.Message}"); - packageInfo = null; - } } public override string Version @@ -72,7 +90,7 @@ public override string Version if (!IsDeployedBuild) return @"local " + (DebugUtils.IsDebugBuild ? @"debug" : @"release"); - return packageInfo?.VersionName ?? @"unknown"; + return getPackageInfo()?.VersionName ?? @"unknown"; } } @@ -82,7 +100,7 @@ public override Version AssemblyVersion { try { - string? versionName = packageInfo?.VersionName; + string? versionName = getPackageInfo()?.VersionName; if (!string.IsNullOrEmpty(versionName)) return new Version(versionName.Split('-').First()); @@ -97,12 +115,12 @@ public override Version AssemblyVersion } [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load() { - config.BindWith(OsuSetting.AndroidPerformanceMode, performanceMode); - config.BindWith(OsuSetting.AndroidLowLatencyAudio, lowLatencyAudio); - config.BindWith(OsuSetting.AndroidVulkanProbe, vulkanProbeEnabled); - config.BindWith(OsuSetting.AudioOffset, audioOffset); + LocalConfig.BindWith(OsuSetting.AndroidPerformanceMode, performanceMode); + LocalConfig.BindWith(OsuSetting.AndroidLowLatencyAudio, lowLatencyAudio); + LocalConfig.BindWith(OsuSetting.AndroidVulkanProbe, vulkanProbeEnabled); + LocalConfig.BindWith(OsuSetting.AudioOffset, audioOffset); } protected override void LoadComplete() @@ -209,9 +227,17 @@ private void selectHighestRefreshRate() { try { - var display = gameActivity.WindowManager?.DefaultDisplay; + if (gameActivity.IsFinishing || gameActivity.IsDestroyed) + return; + + var window = gameActivity.Window; + var windowManager = gameActivity.WindowManager; + + if (window == null || windowManager == null) + return; - if (display == null || gameActivity.Window == null) + var display = windowManager.DefaultDisplay; + if (display == null) return; #pragma warning disable CA1422 @@ -222,17 +248,30 @@ private void selectHighestRefreshRate() return; var preferred = modes.OrderByDescending(m => m.RefreshRate).First(); - var layoutParams = gameActivity.Window.Attributes; - if (layoutParams != null) + gameActivity.RunOnUiThread(() => { - layoutParams.PreferredDisplayModeId = preferred.ModeId; - gameActivity.Window.Attributes = layoutParams; - } + try + { + if (window.Attributes is WindowManagerLayoutParams layoutParams) + { + layoutParams.PreferredDisplayModeId = preferred.ModeId; + window.Attributes = layoutParams; + Debug.WriteLine($"[osu!] Highest refresh rate selected: {preferred.RefreshRate}Hz (mode {preferred.ModeId})"); + } + } + catch (Exception e) + { + // On some devices (e.g. Samsung S23 on Android 16), accessing display properties + // via the vendor property 'vendor.display.enable_optimal_refresh_rate' can trigger + // SELinux denials or crashes if the window is not yet fully trusted. + Debug.WriteLine($"[osu!] Failed to apply preferred display mode: {e.Message}"); + } + }); } catch (Exception e) { - Debug.WriteLine($"[osu!] Failed to select highest refresh rate: {e.Message}"); + Debug.WriteLine($"[osu!] Failed to query supported display modes: {e.Message}"); } }