diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 21d6336b2c..4c82a175fc 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -7,6 +7,8 @@ using Android.OS; using osu.Framework.Allocation; using osu.Game; using osu.Game.Updater; +using osu.Game.Utils; +using Xamarin.Essentials; namespace osu.Android { @@ -19,6 +21,7 @@ namespace osu.Android : base(null) { gameActivity = activity; + powerStatus = new AndroidPowerStatus(); } public override Version AssemblyVersion @@ -72,5 +75,11 @@ namespace osu.Android } protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager(); + public class AndroidPowerStatus : PowerStatus + { + public override double ChargeLevel => Battery.ChargeLevel; + + public override bool IsCharging => Battery.PowerSource != BatteryPowerSource.Battery; + } } } diff --git a/osu.Android/osu.Android.csproj b/osu.Android/osu.Android.csproj index 54857ac87d..64d5e5b1c8 100644 --- a/osu.Android/osu.Android.csproj +++ b/osu.Android/osu.Android.csproj @@ -63,5 +63,9 @@ 5.0.0 + + + + \ No newline at end of file diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 0c21c75290..b6c57b219d 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -21,6 +21,8 @@ using osu.Game.Updater; using osu.Desktop.Windows; using osu.Framework.Threading; using osu.Game.IO; +using osu.Game.Utils; +using osu.Framework.Allocation; namespace osu.Desktop { @@ -33,6 +35,7 @@ namespace osu.Desktop : base(args) { noVersionOverlay = args?.Any(a => a == "--no-version-overlay") ?? false; + powerStatus = new DefaultPowerStatus(); } public override StableStorage GetStorageForStableInstall() diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index a31d53e3b6..c5cc10993c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -25,6 +25,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Play.PlayerSettings; +using osu.Game.Utils; using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay @@ -48,6 +49,9 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached] private readonly VolumeOverlay volumeOverlay; + [Resolved] + private PowerStatus powerStatus { get; set; } + private readonly ChangelogOverlay changelogOverlay; public TestScenePlayerLoader() @@ -93,26 +97,6 @@ namespace osu.Game.Tests.Visual.Gameplay LoadScreen(loader = new TestPlayerLoader(() => player = new TestPlayer(interactive, interactive))); } - /// - /// Sets the input manager child to a new test player loader container instance with a custom battery level - /// - /// If the test player should behave like the production one. - /// If the player's device is plugged in. - /// A custom battery level for the test player. - private void resetPlayerWithBattery(bool interactive, bool pluggedIn, double batteryLevel) - { - Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); - Beatmap.Value.BeatmapInfo.EpilepsyWarning = epilepsyWarning; - - foreach (var mod in SelectedMods.Value.OfType()) - mod.ApplyToTrack(Beatmap.Value.Track); - - loader = new TestPlayerLoader(() => player = new TestPlayer(interactive, interactive)); - loader.batteryManager.ChargeLevel = batteryLevel; - loader.batteryManager.PluggedIn = pluggedIn; - LoadScreen(loader); - } - [Test] public void TestEarlyExitBeforePlayerConstruction() { @@ -290,32 +274,6 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for player load", () => player.IsLoaded); } - [TestCase(false, 1.0)] // not plugged in, full battery, no notification - [TestCase(false, 0.2)] // not plugged in, at warning level, no notification - [TestCase(true, 0.1)] // plugged in, charging, below warning level, no notification - [TestCase(false, 0.1)] // not plugged in, below warning level, notification - public void TestLowBatteryNotification(bool pluggedIn, double batteryLevel) - { - AddStep("reset notification lock", () => sessionStatics.GetBindable(Static.BatteryLowNotificationShownOnce).Value = false); - - // mock phone on battery - AddStep("load player", () => resetPlayerWithBattery(false, pluggedIn, batteryLevel)); - AddUntilStep("wait for player", () => player?.LoadState == LoadState.Ready); - int notificationCount = !pluggedIn && batteryLevel < PlayerLoader.battery_tolerance ? 1 : 0; - AddAssert("check for notification", () => notificationOverlay.UnreadCount.Value == notificationCount); - AddStep("click notification", () => - { - var scrollContainer = (OsuScrollContainer)notificationOverlay.Children.Last(); - var flowContainer = scrollContainer.Children.OfType>().First(); - var notification = flowContainer.First(); - - InputManager.MoveMouseTo(notification); - InputManager.Click(MouseButton.Left); - }); - - AddUntilStep("wait for player load", () => player.IsLoaded); - } - [TestCase(true)] [TestCase(false)] public void TestEpilepsyWarning(bool warning) @@ -334,6 +292,30 @@ namespace osu.Game.Tests.Visual.Gameplay } } + [TestCase(false, 1.0)] // not charging, full battery --> no warning + [TestCase(false, 0.2)] // not charging, at cutoff --> warning + [TestCase(false, 0.1)] // charging, below cutoff --> warning + public void TestLowBatteryNotification(bool isCharging, double chargeLevel) + { + AddStep("reset notification lock", () => sessionStatics.GetBindable(Static.BatteryLowNotificationShownOnce).Value = false); + + // set charge status and level + AddStep("load player", () => resetPlayer(false, () => { powerStatus.IsCharging = isCharging; powerStatus.ChargeLevel = chargeLevel; })); + AddUntilStep("wait for player", () => player?.LoadState == LoadState.Ready); + int notificationCount = !isCharging && chargeLevel <= powerStatus.BatteryCutoff ? 1 : 0; + AddAssert("check for notification", () => notificationOverlay.UnreadCount.Value == notificationCount); + AddStep("click notification", () => + { + var scrollContainer = (OsuScrollContainer)notificationOverlay.Children.Last(); + var flowContainer = scrollContainer.Children.OfType>().First(); + var notification = flowContainer.First(); + + InputManager.MoveMouseTo(notification); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("wait for player load", () => player.IsLoaded); + } + [Test] public void TestEpilepsyWarningEarlyExit() { @@ -356,8 +338,6 @@ namespace osu.Game.Tests.Visual.Gameplay public IReadOnlyList DisplayedMods => MetadataInfo.Mods.Value; - public new BatteryManager batteryManager => base.batteryManager; - public TestPlayerLoader(Func createPlayer) : base(createPlayer) { diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 21313d0596..53873b0127 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -40,6 +40,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Skinning; +using osu.Game.Utils; using osuTK.Input; using RuntimeInfo = osu.Framework.RuntimeInfo; @@ -95,6 +96,8 @@ namespace osu.Game protected Storage Storage { get; set; } + protected PowerStatus powerStatus; + [Cached] [Cached(typeof(IBindable))] protected readonly Bindable Ruleset = new Bindable(); @@ -329,6 +332,8 @@ namespace osu.Game dependencies.CacheAs(MusicController); Ruleset.BindValueChanged(onRulesetChanged); + + dependencies.CacheAs(powerStatus); } private void onRulesetChanged(ValueChangedEvent r) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 01a51e6e7a..9710fa7e46 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -24,9 +24,9 @@ using osu.Game.Overlays.Notifications; using osu.Game.Screens.Menu; using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Users; +using osu.Game.Utils; using osuTK; using osuTK.Graphics; -using Xamarin.Essentials; namespace osu.Game.Screens.Play { @@ -113,6 +113,9 @@ namespace osu.Game.Screens.Play [Resolved] private AudioManager audioManager { get; set; } + [Resolved] + private PowerStatus powerStatus { get; set; } + public PlayerLoader(Func createPlayer) { this.createPlayer = createPlayer; @@ -265,7 +268,6 @@ namespace osu.Game.Screens.Play } #endregion - protected override void Update() { base.Update(); @@ -477,45 +479,18 @@ namespace osu.Game.Screens.Play #region Low battery warning private Bindable batteryWarningShownOnce; - // Send a warning if battery is less than 20% - public const double battery_tolerance = 0.2; - private void showBatteryWarningIfNeeded() { if (!batteryWarningShownOnce.Value) { - // Checks if the notification has not been shown yet, device is unplugged and if device battery is low. - if (!batteryManager.PluggedIn && batteryManager.ChargeLevel < battery_tolerance) + // Checks if the notification has not been shown yet, device is unplugged and if device battery is at or below the cutoff + if (!powerStatus.IsCharging && powerStatus.ChargeLevel <= powerStatus.BatteryCutoff) { - Console.WriteLine("Battery level: {0}", batteryManager.ChargeLevel); notificationOverlay?.Post(new BatteryWarningNotification()); batteryWarningShownOnce.Value = true; } } } - public BatteryManager batteryManager = new BatteryManager(); - public class BatteryManager - { - public double ChargeLevel; - public bool PluggedIn; - public BatteryManager() - { - // Attempt to get battery information using Xamarin.Essentials - // Xamarin.Essentials throws a NotSupportedOrImplementedException on .NET standard so this only works on iOS/Android - // https://github.com/xamarin/Essentials/blob/7218ab88f7fbe00ec3379bd54e6c0ce35ffc0c22/Xamarin.Essentials/Battery/Battery.netstandard.tvos.cs - try - { - ChargeLevel = Battery.ChargeLevel; - PluggedIn = Battery.PowerSource == BatteryPowerSource.Battery; - } - catch (NotImplementedException e) - { - Console.WriteLine("Could not access battery info: {0}", e); - ChargeLevel = 1.0; - PluggedIn = false; - } - } - } private class BatteryWarningNotification : SimpleNotification { diff --git a/osu.Game/Tests/OsuTestBrowser.cs b/osu.Game/Tests/OsuTestBrowser.cs index 71b0b02fa6..afce7dd38b 100644 --- a/osu.Game/Tests/OsuTestBrowser.cs +++ b/osu.Game/Tests/OsuTestBrowser.cs @@ -7,11 +7,16 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Graphics; using osu.Game.Screens.Backgrounds; +using osu.Game.Utils; namespace osu.Game.Tests { public class OsuTestBrowser : OsuGameBase { + public OsuTestBrowser() + { + powerStatus = new DefaultPowerStatus(); + } protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game/Utils/PowerStatus.cs b/osu.Game/Utils/PowerStatus.cs new file mode 100644 index 0000000000..0657c98af1 --- /dev/null +++ b/osu.Game/Utils/PowerStatus.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Utils +{ + public abstract class PowerStatus + { + /// + /// The maximum battery level before a warning notification + /// is sent. + /// + public virtual double BatteryCutoff { get; } = 0.2; + public virtual double ChargeLevel { get; set; } + public virtual bool IsCharging { get; set; } + } + + public class DefaultPowerStatus : PowerStatus + { + public override double ChargeLevel { get; set; } = 1; + public override bool IsCharging { get; set; } = true; + } +} diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index 5125ad81e0..21dce338dc 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -5,13 +5,30 @@ using System; using Foundation; using osu.Game; using osu.Game.Updater; +using osu.Game.Utils; +using Xamarin.Essentials; namespace osu.iOS { public class OsuGameIOS : OsuGame { + public OsuGameIOS() + : base(null) + { + powerStatus = new IOSPowerStatus(); + } public override Version AssemblyVersion => new Version(NSBundle.MainBundle.InfoDictionary["CFBundleVersion"].ToString()); protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager(); + + public class IOSPowerStatus : PowerStatus + { + // The low battery alert appears at 20% on iOS + // https://github.com/ppy/osu/issues/12239 + public override double BatteryCutoff => 0.25; + public override double ChargeLevel => Battery.ChargeLevel; + + public override bool IsCharging => Battery.PowerSource != BatteryPowerSource.Battery; + } } } diff --git a/osu.iOS/osu.iOS.csproj b/osu.iOS/osu.iOS.csproj index 1e9a21865d..ed6f52c60e 100644 --- a/osu.iOS/osu.iOS.csproj +++ b/osu.iOS/osu.iOS.csproj @@ -116,5 +116,9 @@ false + + + +