diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs
index 21d6336b2c..050bf2b787 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
{
@@ -72,5 +74,14 @@ namespace osu.Android
}
protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager();
+
+ protected override BatteryInfo CreateBatteryInfo() => new AndroidBatteryInfo();
+
+ private class AndroidBatteryInfo : BatteryInfo
+ {
+ public override double ChargeLevel => Battery.ChargeLevel;
+
+ public override bool IsCharging => Battery.PowerSource != BatteryPowerSource.Battery;
+ }
}
}
diff --git a/osu.Android/Properties/AndroidManifest.xml b/osu.Android/Properties/AndroidManifest.xml
index 770eaf2222..e717bab310 100644
--- a/osu.Android/Properties/AndroidManifest.xml
+++ b/osu.Android/Properties/AndroidManifest.xml
@@ -6,5 +6,6 @@
+
\ No newline at end of file
diff --git a/osu.Android/osu.Android.csproj b/osu.Android/osu.Android.csproj
index 54857ac87d..582c856a47 100644
--- a/osu.Android/osu.Android.csproj
+++ b/osu.Android/osu.Android.csproj
@@ -63,5 +63,8 @@
5.0.0
+
+
+
\ No newline at end of file
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
index 280c182259..cfdea31a75 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;
+ [Cached(typeof(BatteryInfo))]
+ private readonly LocalBatteryInfo batteryInfo = new LocalBatteryInfo();
+
private readonly ChangelogOverlay changelogOverlay;
public TestScenePlayerLoader()
@@ -288,6 +292,33 @@ namespace osu.Game.Tests.Visual.Gameplay
}
}
+ [TestCase(false, 1.0, false)] // not charging, above cutoff --> no warning
+ [TestCase(true, 0.1, false)] // charging, below cutoff --> no warning
+ [TestCase(false, 0.25, true)] // not charging, at cutoff --> warning
+ public void TestLowBatteryNotification(bool isCharging, double chargeLevel, bool shouldWarn)
+ {
+ AddStep("reset notification lock", () => sessionStatics.GetBindable(Static.LowBatteryNotificationShownOnce).Value = false);
+
+ // set charge status and level
+ AddStep("load player", () => resetPlayer(false, () =>
+ {
+ batteryInfo.SetCharging(isCharging);
+ batteryInfo.SetChargeLevel(chargeLevel);
+ }));
+ AddUntilStep("wait for player", () => player?.LoadState == LoadState.Ready);
+ AddAssert($"notification {(shouldWarn ? "triggered" : "not triggered")}", () => notificationOverlay.UnreadCount.Value == (shouldWarn ? 1 : 0));
+ 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()
{
@@ -349,5 +380,29 @@ namespace osu.Game.Tests.Visual.Gameplay
throw new TimeoutException();
}
}
+
+ ///
+ /// Mutable dummy BatteryInfo class for
+ ///
+ ///
+ private class LocalBatteryInfo : BatteryInfo
+ {
+ private bool isCharging = true;
+ private double chargeLevel = 1;
+
+ public override bool IsCharging => isCharging;
+
+ public override double ChargeLevel => chargeLevel;
+
+ public void SetCharging(bool value)
+ {
+ isCharging = value;
+ }
+
+ public void SetChargeLevel(double value)
+ {
+ chargeLevel = value;
+ }
+ }
}
}
diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index 0e1f6f6b0c..df6d17f615 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -22,4 +22,4 @@
-
\ No newline at end of file
+
diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs
index 36eb6964dd..71e1a1efcc 100644
--- a/osu.Game/Configuration/SessionStatics.cs
+++ b/osu.Game/Configuration/SessionStatics.cs
@@ -16,6 +16,7 @@ namespace osu.Game.Configuration
{
SetDefault(Static.LoginOverlayDisplayed, false);
SetDefault(Static.MutedAudioNotificationShownOnce, false);
+ SetDefault(Static.LowBatteryNotificationShownOnce, false);
SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null);
SetDefault(Static.SeasonalBackgrounds, null);
}
@@ -25,6 +26,7 @@ namespace osu.Game.Configuration
{
LoginOverlayDisplayed,
MutedAudioNotificationShownOnce,
+ LowBatteryNotificationShownOnce,
///
/// Info about seasonal backgrounds available fetched from API - see .
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index e285faab11..406819cbd2 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;
@@ -156,6 +157,8 @@ namespace osu.Game
protected override UserInputManager CreateUserInputManager() => new OsuUserInputManager();
+ protected virtual BatteryInfo CreateBatteryInfo() => null;
+
///
/// The maximum volume at which audio tracks should playback. This can be set lower than 1 to create some head-room for sound effects.
///
@@ -281,6 +284,11 @@ namespace osu.Game
dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore));
dependencies.Cache(SettingsStore = new SettingsStore(contextFactory));
dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore));
+
+ var powerStatus = CreateBatteryInfo();
+ if (powerStatus != null)
+ dependencies.CacheAs(powerStatus);
+
dependencies.Cache(new SessionStatics());
dependencies.Cache(new OsuColour());
diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
index d95b246c96..669e4cecbe 100644
--- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
+++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
@@ -56,8 +56,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
public virtual IEnumerable GetSamples() => HitObject.Samples;
- private readonly Lazy> nestedHitObjects = new Lazy>();
- public IReadOnlyList NestedHitObjects => nestedHitObjects.IsValueCreated ? nestedHitObjects.Value : (IReadOnlyList)Array.Empty();
+ private readonly List nestedHitObjects = new List();
+ public IReadOnlyList NestedHitObjects => nestedHitObjects;
///
/// Whether this object should handle any user input events.
@@ -249,7 +249,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
// Must be done before the nested DHO is added to occur before the nested Apply()!
drawableNested.ParentHitObject = this;
- nestedHitObjects.Value.Add(drawableNested);
+ nestedHitObjects.Add(drawableNested);
AddNestedHitObject(drawableNested);
}
@@ -305,19 +305,16 @@ namespace osu.Game.Rulesets.Objects.Drawables
if (Samples != null)
Samples.Samples = null;
- if (nestedHitObjects.IsValueCreated)
+ foreach (var obj in nestedHitObjects)
{
- foreach (var obj in nestedHitObjects.Value)
- {
- obj.OnNewResult -= onNewResult;
- obj.OnRevertResult -= onRevertResult;
- obj.ApplyCustomUpdateState -= onApplyCustomUpdateState;
- }
-
- nestedHitObjects.Value.Clear();
- ClearNestedHitObjects();
+ obj.OnNewResult -= onNewResult;
+ obj.OnRevertResult -= onRevertResult;
+ obj.ApplyCustomUpdateState -= onApplyCustomUpdateState;
}
+ nestedHitObjects.Clear();
+ ClearNestedHitObjects();
+
HitObject.DefaultsApplied -= onDefaultsApplied;
OnFree();
diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs
index c40ab4bd94..d55005363c 100644
--- a/osu.Game/Rulesets/UI/Playfield.cs
+++ b/osu.Game/Rulesets/UI/Playfield.cs
@@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.UI
var enumerable = HitObjectContainer.Objects;
- if (nestedPlayfields.IsValueCreated)
+ if (nestedPlayfields.Count != 0)
enumerable = enumerable.Concat(NestedPlayfields.SelectMany(p => p.AllHitObjects));
return enumerable;
@@ -76,9 +76,9 @@ namespace osu.Game.Rulesets.UI
///
/// All s nested inside this .
///
- public IEnumerable NestedPlayfields => nestedPlayfields.IsValueCreated ? nestedPlayfields.Value : Enumerable.Empty();
+ public IEnumerable NestedPlayfields => nestedPlayfields;
- private readonly Lazy> nestedPlayfields = new Lazy>();
+ private readonly List nestedPlayfields = new List();
///
/// Whether judgements should be displayed by this and and all nested s.
@@ -217,7 +217,7 @@ namespace osu.Game.Rulesets.UI
otherPlayfield.HitObjectUsageBegan += h => HitObjectUsageBegan?.Invoke(h);
otherPlayfield.HitObjectUsageFinished += h => HitObjectUsageFinished?.Invoke(h);
- nestedPlayfields.Value.Add(otherPlayfield);
+ nestedPlayfields.Add(otherPlayfield);
}
protected override void LoadComplete()
@@ -279,12 +279,7 @@ namespace osu.Game.Rulesets.UI
return true;
}
- bool removedFromNested = false;
-
- if (nestedPlayfields.IsValueCreated)
- removedFromNested = nestedPlayfields.Value.Any(p => p.Remove(hitObject));
-
- return removedFromNested;
+ return nestedPlayfields.Any(p => p.Remove(hitObject));
}
///
@@ -429,10 +424,7 @@ namespace osu.Game.Rulesets.UI
return;
}
- if (!nestedPlayfields.IsValueCreated)
- return;
-
- foreach (var p in nestedPlayfields.Value)
+ foreach (var p in nestedPlayfields)
p.SetKeepAlive(hitObject, keepAlive);
}
@@ -444,10 +436,7 @@ namespace osu.Game.Rulesets.UI
foreach (var (_, entry) in lifetimeEntryMap)
entry.KeepAlive = true;
- if (!nestedPlayfields.IsValueCreated)
- return;
-
- foreach (var p in nestedPlayfields.Value)
+ foreach (var p in nestedPlayfields)
p.KeepAllAlive();
}
@@ -461,10 +450,7 @@ namespace osu.Game.Rulesets.UI
{
HitObjectContainer.PastLifetimeExtension = value;
- if (!nestedPlayfields.IsValueCreated)
- return;
-
- foreach (var nested in nestedPlayfields.Value)
+ foreach (var nested in nestedPlayfields)
nested.PastLifetimeExtension = value;
}
}
@@ -479,10 +465,7 @@ namespace osu.Game.Rulesets.UI
{
HitObjectContainer.FutureLifetimeExtension = value;
- if (!nestedPlayfields.IsValueCreated)
- return;
-
- foreach (var nested in nestedPlayfields.Value)
+ foreach (var nested in nestedPlayfields)
nested.FutureLifetimeExtension = value;
}
}
diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs
index cf15104809..ce580e2b53 100644
--- a/osu.Game/Screens/Play/PlayerLoader.cs
+++ b/osu.Game/Screens/Play/PlayerLoader.cs
@@ -24,6 +24,7 @@ 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;
@@ -112,6 +113,9 @@ namespace osu.Game.Screens.Play
[Resolved]
private AudioManager audioManager { get; set; }
+ [Resolved(CanBeNull = true)]
+ private BatteryInfo batteryInfo { get; set; }
+
public PlayerLoader(Func createPlayer)
{
this.createPlayer = createPlayer;
@@ -121,6 +125,7 @@ namespace osu.Game.Screens.Play
private void load(SessionStatics sessionStatics)
{
muteWarningShownOnce = sessionStatics.GetBindable(Static.MutedAudioNotificationShownOnce);
+ batteryWarningShownOnce = sessionStatics.GetBindable(Static.LowBatteryNotificationShownOnce);
InternalChild = (content = new LogoTrackingContainer
{
@@ -196,6 +201,7 @@ namespace osu.Game.Screens.Play
Scheduler.Add(new ScheduledDelegate(pushWhenLoaded, Clock.CurrentTime + 1800, 0));
showMuteWarningIfNeeded();
+ showBatteryWarningIfNeeded();
}
public override void OnResuming(IScreen last)
@@ -470,5 +476,48 @@ namespace osu.Game.Screens.Play
}
#endregion
+
+ #region Low battery warning
+
+ private Bindable batteryWarningShownOnce;
+
+ private void showBatteryWarningIfNeeded()
+ {
+ if (batteryInfo == null) return;
+
+ if (!batteryWarningShownOnce.Value)
+ {
+ if (!batteryInfo.IsCharging && batteryInfo.ChargeLevel <= 0.25)
+ {
+ notificationOverlay?.Post(new BatteryWarningNotification());
+ batteryWarningShownOnce.Value = true;
+ }
+ }
+ }
+
+ private class BatteryWarningNotification : SimpleNotification
+ {
+ public override bool IsImportant => true;
+
+ public BatteryWarningNotification()
+ {
+ Text = "Your battery level is low! Charge your device to prevent interruptions during gameplay.";
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours, NotificationOverlay notificationOverlay)
+ {
+ Icon = FontAwesome.Solid.BatteryQuarter;
+ IconBackgound.Colour = colours.RedDark;
+
+ Activated = delegate
+ {
+ notificationOverlay.Hide();
+ return true;
+ };
+ }
+ }
+
+ #endregion
}
}
diff --git a/osu.Game/Utils/BatteryInfo.cs b/osu.Game/Utils/BatteryInfo.cs
new file mode 100644
index 0000000000..dd9b695e1f
--- /dev/null
+++ b/osu.Game/Utils/BatteryInfo.cs
@@ -0,0 +1,18 @@
+// 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
+{
+ ///
+ /// Provides access to the system's power status.
+ ///
+ public abstract class BatteryInfo
+ {
+ ///
+ /// The charge level of the battery, from 0 to 1.
+ ///
+ public abstract double ChargeLevel { get; }
+
+ public abstract bool IsCharging { get; }
+ }
+}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 954cf511b6..b5405f6262 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -1,4 +1,4 @@
-
+
netstandard2.1
Library
diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs
index 5125ad81e0..702aef45f5 100644
--- a/osu.iOS/OsuGameIOS.cs
+++ b/osu.iOS/OsuGameIOS.cs
@@ -5,6 +5,8 @@ using System;
using Foundation;
using osu.Game;
using osu.Game.Updater;
+using osu.Game.Utils;
+using Xamarin.Essentials;
namespace osu.iOS
{
@@ -13,5 +15,14 @@ namespace osu.iOS
public override Version AssemblyVersion => new Version(NSBundle.MainBundle.InfoDictionary["CFBundleVersion"].ToString());
protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager();
+
+ protected override BatteryInfo CreateBatteryInfo() => new IOSBatteryInfo();
+
+ private class IOSBatteryInfo : BatteryInfo
+ {
+ 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..1cbe4422cc 100644
--- a/osu.iOS/osu.iOS.csproj
+++ b/osu.iOS/osu.iOS.csproj
@@ -116,5 +116,8 @@
false
+
+
+