Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion osu.Framework.Tests/Visual/Platform/TestSceneActiveState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osuTK.Graphics;

Expand Down Expand Up @@ -49,7 +50,12 @@ protected override void LoadComplete()
},
};

isActive.BindValueChanged(active => isActiveBox.Colour = active.NewValue ? Color4.Green : Color4.Red, true);
isActive.BindValueChanged(active =>
{
Logger.Log($"Game activity changed from {active.OldValue} to {active.NewValue}");
isActiveBox.Colour = active.NewValue ? Color4.Green : Color4.Red;
}, true);

cursorInWindow?.BindValueChanged(active => cursorInWindowBox.Colour = active.NewValue ? Color4.Green : Color4.Red, true);
}

Expand Down
41 changes: 41 additions & 0 deletions osu.Framework.iOS/IOSCallObserver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Linq;
using CallKit;
using Foundation;

namespace osu.Framework.iOS
{
internal class IOSCallObserver : NSObject, ICXCallObserverDelegate
{
public event Action? OnCall;
public event Action? OnCallEnded;

private readonly CXCallController callController;

public IOSCallObserver()
{
callController = new CXCallController();
callController.CallObserver.SetDelegate(this, null);

if (callController.CallObserver.Calls.Any(c => !c.HasEnded))
OnCall?.Invoke();
}

public void CallChanged(CXCallObserver callObserver, CXCall call)
{
if (!call.HasEnded)
OnCall?.Invoke();
else
OnCallEnded?.Invoke();
}

protected override void Dispose(bool disposing)
{
callController.Dispose();
base.Dispose(disposing);
}
}
}
33 changes: 32 additions & 1 deletion osu.Framework.iOS/IOSWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
using osu.Framework.Graphics;
using osu.Framework.Platform;
using osu.Framework.Platform.SDL3;
using static SDL.SDL3;
using UIKit;
using static SDL.SDL3;

namespace osu.Framework.iOS
{
Expand All @@ -21,6 +21,8 @@ internal class IOSWindow : SDL3MobileWindow, IIOSWindow

public UIViewController ViewController => UIWindow.RootViewController!;

private IOSCallObserver? callObserver;

public override Size Size
{
get => base.Size;
Expand All @@ -47,8 +49,31 @@ public override void Create()

var appDelegate = (GameApplicationDelegate)UIApplication.SharedApplication.Delegate;
appDelegate.DragDrop += TriggerDragDrop;

// osu! cannot operate when a call takes place, as the audio is completely cut from the game, making it behave in unexpected manner.
// while this is o!f code, it's simpler to do this here rather than in osu!.
// we can reconsider this if there are framework consumers which find this behaviour undesirable.
callObserver = new IOSCallObserver();
callObserver.OnCall += onCall;
callObserver.OnCallEnded += onCallEnded;
}

private bool inCall;

private void onCall()
{
inCall = true;
UpdateActiveState();
}

private void onCallEnded()
{
inCall = false;
UpdateActiveState();
}

protected override bool ShouldBeActive => base.ShouldBeActive && !inCall;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While it fixes the issue, I think that forcing the game to be inactive when a call is in progress isn't fully correct.

Image this scenario:

  1. Play osu!
  2. Get a call
    • game is paused as expected
  3. Answer call (so that osu! goes to the background)
  4. Go back to osu!
    • game is focused, but IsActive == false, leading to weird behaviour (likely: input doesn't work, the game cannot be unpaused)
  5. End call
  6. Go back to osu!
    • everything is as expected

Instead of the game checking if a call is in progress, I think it should check if a call is currently ringing. Looking at the apple docs, I would consider a call to be ringing iff:

  • !isOutgoing &&
  • !hasConnected &&
  • !hasEnded &&
  • !isOnHold.

I think it's fine to force the game to be inactive when a call is ringing, as the user is focused on answering or declining the call.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @frenzibyte, looks better than what you have.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be more optimal, but the audio system always remains frozen while a call is active, from the point of ringing until the call is ended. Therefore it doesn't help to make the game focused while a call is active (the user would be faced with a frozen beatmap and they will receive the "audio is not working" message etc.).

If anything, we should prevent the user from trying to unpause the game during a call, but that's pretty much follow-up effort for this PR.

Copy link
Member

@Susko3 Susko3 Feb 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the audio system always remains frozen while a call is active

😳 Is this how other games behave? If not, maybe something is wrong with BASS.

Is this documented somewhere?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initial testing with Arcaea showed much proper handling. When an incoming call is triggered, the level still continues progressing, the audio stops for a short time but returns back, and after accepting a call the audio remains working.

I'll report this to BASS.

Copy link
Member

@peppy peppy Feb 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly let's just go with what we have for now. The above scenario is an edge case of an edge case. We can iterate.


protected override unsafe void RunMainLoop()
{
// Delegate running the main loop to CADisplayLink.
Expand All @@ -62,6 +87,12 @@ protected override unsafe void RunMainLoop()
SDL_SetiOSAnimationCallback(SDLWindowHandle, 1, &runFrame, ObjectHandle.Handle);
}

public override void Dispose()
{
base.Dispose();
callObserver?.Dispose();
}

[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
private static void runFrame(IntPtr userdata)
{
Expand Down
2 changes: 1 addition & 1 deletion osu.Framework/Platform/SDL3/SDL3Window.cs
Original file line number Diff line number Diff line change
Expand Up @@ -676,7 +676,7 @@ internal virtual void SetIconFromGroup(IconGroup iconGroup)

#endregion

public void Dispose()
public virtual void Dispose()
{
Close();
SDL_Quit();
Expand Down
26 changes: 12 additions & 14 deletions osu.Framework/Platform/SDL3/SDL3Window_Windowing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,22 +122,10 @@ private void initialiseWindowingAfterCreation()
WindowMode.TriggerChange();
}

private bool focused;

/// <summary>
/// Whether the window currently has focus.
/// </summary>
public bool Focused
{
get => focused;
protected set
{
if (value == focused)
return;

isActive.Value = focused = value;
}
}
public bool Focused { get; private set; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public bool Focused { get; private set; }
public bool Focused { get; protected set; }

To fix Android build.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather fix this by updating android code if we can.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Susko3 can you test the android change i committed?


public WindowMode DefaultWindowMode => RuntimeInfo.IsMobile ? Configuration.WindowMode.Fullscreen : Configuration.WindowMode.Windowed;

Expand Down Expand Up @@ -222,9 +210,17 @@ public Size MaxSize
/// </summary>
public Bindable<WindowMode> WindowMode { get; } = new Bindable<WindowMode>();

public IBindable<bool> IsActive => isActive;

private readonly BindableBool isActive = new BindableBool();

public IBindable<bool> IsActive => isActive;
/// <summary>
/// Whether <see cref="IsActive"/> should be <c>true</c>.
/// Takes effect on next call to <see cref="UpdateActiveState"/>.
/// </summary>
protected virtual bool ShouldBeActive => Focused;

protected void UpdateActiveState() => isActive.Value = ShouldBeActive;

private readonly BindableBool cursorInWindow = new BindableBool();

Expand Down Expand Up @@ -505,11 +501,13 @@ private unsafe void handleWindowEvent(SDL_WindowEvent evtWindow)
case SDL_EventType.SDL_EVENT_WINDOW_RESTORED:
case SDL_EventType.SDL_EVENT_WINDOW_FOCUS_GAINED:
Focused = true;
UpdateActiveState();
break;

case SDL_EventType.SDL_EVENT_WINDOW_MINIMIZED:
case SDL_EventType.SDL_EVENT_WINDOW_FOCUS_LOST:
Focused = false;
UpdateActiveState();
break;

case SDL_EventType.SDL_EVENT_WINDOW_CLOSE_REQUESTED:
Expand Down
Loading