Repository: MonoGame/MonoGame
Pull request #8688: Fix GamePad button mappings for Android and iOS devices.
Author: login: CartBlanche
Created: 2025-03-31T16:29:26Z | Merged: 2025-04-04T15:07:55Z

Fixes #8522

**Known Issue** 
iOS *ThumbStickButton is always null. Issue raised here - https://github.com/dotnet/macios/issues/22524





Linked issue #8522: The GamePad.Back may remain set true on Android after the application calls the Exit()
### Prerequisites

- [X] I have verified this issue is present in the `develop` branch
- [X] I have searched [open](https://github.com/MonoGame/MonoGame/issues) and [closed](https://github.com/MonoGame/MonoGame/issues?q=is%3Aissue+is%3Aclosed) issues to ensure it has not already been reported.

### MonoGame Version

MonoGame 3.8.x

### Which MonoGame platform are you using?

MonoGame Android Application (mgandroid)

### Operating System

Windows

### Description

The **GamePad.Back** is set true in the **OnKeyDown**, and it should be reset to false in the **OnKeyUp**.
But it seems there's a race condition and sometimes the **OnKeyUp** is not received.
That makes the **GamePad.Back = true** permanently.

### Steps to Reproduce

That is reproduced on the default android template. You just have to click back a few times.

### Minimal Example Repo

_No response_

### Expected Behavior

The  **GamePad.Back** should be set to false after the application comes to foreground again.

### Resulting Behavior

The  **GamePad.Back** remains set to true after the application comes to foreground again.

### Files

_No response_
Linked issue comments: user: login: CartBlanche
created_at: 2024-10-08T04:14:23Z
body: @dellis1972 Is this something you can look into?
author_association: CONTRIBUTOR
user: login: lanosgnatiuc
created_at: 2024-10-08T06:20:03Z
body: I've set logs at the Back = true and Back = false, and here it is - the first time it was called, the second wasn't. It's like 50/50 chance.

[Keycode.Back] set true
[Keycode.Back] set false
[OpenGLRenderer] Unable to match the desired swap behavior.
[MonoGame] GraphicsDeviceManager.ResetClientBounds: newClientBounds={X:53 Y:0 Width:1334 Height:2892}
[MonoGame] GraphicsDeviceManager.ResetClientBounds: newClientBounds={X:53 Y:0 Width:1334 Height:2892}
[Keycode.Back] set true



author_association: NONE
user: login: dellis1972
created_at: 2024-10-08T06:58:39Z
body: @lanosgnatiuc do you have a simple sample code showing the issue? 
author_association: CONTRIBUTOR
user: login: lanosgnatiuc
created_at: 2024-10-08T07:02:44Z
body: @dellis1972 
The default android project 
author_association: NONE
user: login: lanosgnatiuc
created_at: 2024-10-08T07:05:45Z
body: @dellis1972 
But if you can tell me where else I can look, I can help debug it.
author_association: NONE
user: login: ThomasFOG
created_at: 2024-10-08T07:23:21Z
body: The issue title seems misleading in regard to the description. Are we talking about suspending/resuming the app or about calling ```Game.Exit()```? Because if we're really talking about ```Game.Exit()```, we should assume that anything happening after that is unpredictable and therefore isn't an issue.
author_association: CONTRIBUTOR
user: login: lanosgnatiuc
created_at: 2024-10-08T07:26:09Z
body: That's what the Exit() does - moves the app to background, which effectively suspends the app.
author_association: NONE
user: login: dellis1972
created_at: 2024-10-08T07:36:35Z
body: Calling `Exec` calls `MoveTaskToBack`. See https://github.com/MonoGame/MonoGame/blob/develop/MonoGame.Framework/Platform/Android/AndroidGamePlatform.cs#L44C27-L44C41

At this point it will stop receiving Input calls. 

That said there might be a bug in this area https://github.com/MonoGame/MonoGame/blob/af25f421e45f1e20391d89a3182cfead275727b0/MonoGame.Framework/Platform/Android/MonoGameAndroidGameView.cs#L1168
author_association: CONTRIBUTOR
user: login: ThomasFOG
created_at: 2024-10-08T08:06:42Z
body: ```Game.Exit()``` is not meant to suspend the app and should be prohibited on Android and iOS because app lifecycle is managed by the system and we shouldn't be able to even call ```Game.Exit()``` in that regard.

IMO it should throw an Exception on those platforms.
author_association: CONTRIBUTOR
user: login: dellis1972
created_at: 2024-10-08T08:34:49Z
body: It does not work on iOS at all. Infact the `Exit` method does not exist.
The problem is we have had this call in Android for a long time now. Changing the behaviour will break existing applications, the best thing would be to make it a No-Op on Android and put an `Obsolete` attribute on it. 
author_association: CONTRIBUTOR
user: login: lanosgnatiuc
created_at: 2024-10-08T08:41:11Z
body: The problem is that you keep that static "Keycode Back" variable. It shouldn't exist at all.
author_association: NONE
user: login: lanosgnatiuc
created_at: 2024-10-08T09:34:12Z
body: @dellis1972 
[patch.txt](https://github.com/user-attachments/files/17291217/patch.txt)

That fixes the problem, but the real problem is that a static variable keeps a volatile state.
author_association: NONE
user: login: lanosgnatiuc
created_at: 2024-10-08T09:52:26Z
body: ![Screenshot 2024-10-08 125118](https://github.com/user-attachments/assets/3afbc105-714d-4aa4-b751-706d4b9e74d3)

Here is the commit which made this issue evident.
author_association: NONE
user: login: lanosgnatiuc
created_at: 2024-10-08T10:07:57Z
body: @dellis1972 

This should be put back, because the code was intended to generate just a one time GamePadState, which the if condition suggests.

                if (index == 0 && Back)
                {
                    // Consume state
                    Back = false;
                    state = new GamePadState(new GamePadThumbSticks(), new GamePadTriggers(), new GamePadButtons(Buttons.Back), new GamePadDPad());
                    state.IsConnected = false;
                }
author_association: NONE
user: login: CartBlanche
created_at: 2025-04-02T10:05:49Z
body: @lanosgnatiuc Apoligies for the delay, but we have some nugets with your suggested fix here - https://github.com/MonoGame/MonoGame/actions/runs/14203356436 

If you have time, please confirm this now works.
author_association: CONTRIBUTOR

Diff:
diff --git a/MonoGame.Framework/MonoGame.Framework.Android.csproj b/MonoGame.Framework/MonoGame.Framework.Android.csproj
index 5492620c78a..2ea49b58de0 100644
--- a/MonoGame.Framework/MonoGame.Framework.Android.csproj
+++ b/MonoGame.Framework/MonoGame.Framework.Android.csproj
@@ -76,8 +76,8 @@
     <Compile Include="Platform\Utilities\InteropHelpers.cs" />
     <Compile Include="Platform\Utilities\ReflectionHelpers.Default.cs" />
 
-    <Compile Include="..\ThirdParty\StbImageSharp\src\**\*.cs" LinkBase="Utilities\StbImageSharp"/>
-    <Compile Include="..\ThirdParty\StbImageWriteSharp\src\**\*.cs" LinkBase="Utilities\StbImageWriteSharp"/>
+    <Compile Include="..\ThirdParty\StbImageSharp\src\**\*.cs" LinkBase="Utilities\StbImageSharp" />
+    <Compile Include="..\ThirdParty\StbImageWriteSharp\src\**\*.cs" LinkBase="Utilities\StbImageWriteSharp" />
   </ItemGroup>
 
   <ItemGroup>
diff --git a/MonoGame.Framework/Platform/Input/GamePad.Android.cs b/MonoGame.Framework/Platform/Input/GamePad.Android.cs
index aebd6e65ffc..771ce98cb10 100644
--- a/MonoGame.Framework/Platform/Input/GamePad.Android.cs
+++ b/MonoGame.Framework/Platform/Input/GamePad.Android.cs
@@ -43,7 +43,7 @@ private static GamePadCapabilities CapabilitiesOfDevice(InputDevice device)
                     device.VibratorManager.DefaultVibrator.HasVibrator;
 
             // build out supported inputs from what the gamepad exposes
-            int[] keyMap = new int[16];
+            int[] keyMap = new int[17];
             keyMap[0] = (int)Keycode.ButtonA;
             keyMap[1] = (int)Keycode.ButtonB;
             keyMap[2] = (int)Keycode.ButtonX;
@@ -63,7 +63,9 @@ private static GamePadCapabilities CapabilitiesOfDevice(InputDevice device)
             keyMap[13] = (int)Keycode.DpadUp;
 
             keyMap[14] = (int)Keycode.ButtonStart;
-            keyMap[15] = (int)Keycode.Back;
+            keyMap[15] = (int)Keycode.ButtonSelect;
+
+            keyMap[16] = (int)Keycode.ButtonMode;
 
             // get a bool[] with indices matching the keyMap
             bool[] hasMap = new bool[16];
@@ -140,41 +142,43 @@ private static GamePadCapabilities PlatformGetCapabilities(int index)
 
         private static GamePadState PlatformGetState(int index, GamePadDeadZone leftDeadZoneMode, GamePadDeadZone rightDeadZoneMode)
         {
+            if (index < 0 || index >= GamePads.Length)
+                return GamePadState.Default;
+
             var gamePad = GamePads[index];
-            GamePadState state = GamePadState.Default;
-            if (gamePad != null && gamePad._isConnected)
+
+            if (gamePad == null || !gamePad._isConnected)
             {
-                // Check if the device was disconnected
-                var dvc = InputDevice.GetDevice(gamePad._deviceId);
-                if (dvc == null)
+                if (index == 0 && Back)
                 {
-                    Android.Util.Log.Debug("MonoGame", "Detected controller disconnect [" + index + "] ");
-                    gamePad._isConnected = false;
-                    return state;
+                    Back = false;
+
+                    return new GamePadState(
+                        new GamePadThumbSticks(),
+                        new GamePadTriggers(),
+                        new GamePadButtons(Buttons.Back),
+                        new GamePadDPad())
+                    {
+                        IsConnected = false
+                    };
                 }
 
-                GamePadThumbSticks thumbSticks = new GamePadThumbSticks(gamePad._leftStick, gamePad._rightStick, leftDeadZoneMode, rightDeadZoneMode);
-
-                state = new GamePadState(
-                    thumbSticks,
-                    new GamePadTriggers(gamePad._leftTrigger, gamePad._rightTrigger),
-                    new GamePadButtons(gamePad._buttons),
-                    new GamePadDPad(gamePad._buttons));
+                return GamePadState.Default;
             }
-            // we need to add the default "no gamepad connected but the user hit back"
-            // behaviour here
-            else {
-                if (index == 0 && Back)
-                {
-                    // Consume state
-                    state = new GamePadState(new GamePadThumbSticks(), new GamePadTriggers(), new GamePadButtons(Buttons.Back), new GamePadDPad());
-                    state.IsConnected = false;
-                }
-                else
-                    state = new GamePadState();
+
+            var dvc = InputDevice.GetDevice(gamePad._deviceId);
+            if (dvc == null)
+            {
+                Android.Util.Log.Debug("MonoGame", $"Detected controller disconnect [{index}]");
+                gamePad._isConnected = false;
+                return GamePadState.Default;
             }
 
-            return state;
+            return new GamePadState(
+                new GamePadThumbSticks(gamePad._leftStick, gamePad._rightStick, leftDeadZoneMode, rightDeadZoneMode),
+                new GamePadTriggers(gamePad._leftTrigger, gamePad._rightTrigger),
+                new GamePadButtons(gamePad._buttons),
+                new GamePadDPad(gamePad._buttons));
         }
 
         private static bool PlatformSetVibration(int index, float leftMotor, float rightMotor, float leftTrigger, float rightTrigger)
@@ -283,7 +287,26 @@ internal static bool OnGenericMotionEvent(MotionEvent e)
             gamePad._leftTrigger = e.GetAxisValue(Axis.Brake);
             gamePad._rightTrigger = e.GetAxisValue(Axis.Gas);
 
-            if(!gamePad.DPadButtons)
+            // We set the Trigger buttons here, because it never fires in OnKeyDown
+            if (gamePad._leftTrigger > 0f)
+            {
+                gamePad._buttons |= Buttons.LeftTrigger;
+            }
+            else
+            {
+                gamePad._buttons &= Buttons.LeftTrigger;
+            }
+
+            if (gamePad._rightTrigger > 0f)
+            {
+                gamePad._buttons |= Buttons.RightTrigger;
+            }
+            else
+            {
+                gamePad._buttons &= Buttons.RightTrigger;
+            }
+
+            if (!gamePad.DPadButtons)
             {
                 if(e.GetAxisValue(Axis.HatX) < 0)
                 {
@@ -359,7 +382,7 @@ private static Buttons ButtonForKeyCode(Keycode keyCode)
 
                 case Keycode.ButtonStart:
                     return Buttons.Start;
-                case Keycode.Back:
+                case Keycode.ButtonSelect:
                     return Buttons.Back;
             }
 
diff --git a/MonoGame.Framework/Platform/Input/GamePad.iOS.cs b/MonoGame.Framework/Platform/Input/GamePad.iOS.cs
index e98e578b547..4a602a16a29 100644
--- a/MonoGame.Framework/Platform/Input/GamePad.iOS.cs
+++ b/MonoGame.Framework/Platform/Input/GamePad.iOS.cs
@@ -22,7 +22,7 @@ static bool IndexIsUsed(GCControllerPlayerIndex index)
             return false;
         }
 
-        static void AssingIndex(GCControllerPlayerIndex index)
+        static void AssignIndex(GCControllerPlayerIndex index)
         {
             if (IndexIsUsed(index))
                 return;
@@ -42,7 +42,7 @@ private static GamePadCapabilities PlatformGetCapabilities(int index)
         {
             var ind = (GCControllerPlayerIndex)index;
 
-            AssingIndex(ind);
+            AssignIndex(ind);
 
             foreach (var controller in GCController.Controllers)
             {
@@ -117,7 +117,7 @@ private static GamePadState PlatformGetState(int index, GamePadDeadZone leftDead
             float leftTriggerValue = 0;
             float rightTriggerValue = 0;
 
-            AssingIndex(ind);
+            AssignIndex(ind);
 
             foreach (var controller in GCController.Controllers)
             {
@@ -151,10 +151,16 @@ private static GamePadState PlatformGetState(int index, GamePadDeadZone leftDead
                     if (controller.ExtendedGamepad.RightTrigger.IsPressed)
                         buttons |= Buttons.RightTrigger;
 
-                    if (controller.ExtendedGamepad.ButtonMenu.IsPressed)
+                    if (controller.ExtendedGamepad.ButtonMenu != null
+                    && controller.ExtendedGamepad.ButtonMenu.IsPressed)
+                    {
                         buttons |= Buttons.Start;
+                    }
+                        
                     if (controller.ExtendedGamepad.ButtonOptions?.IsPressed == true)
+                    {
                         buttons |= Buttons.Back;
+                    }
 
                     if (controller.ExtendedGamepad.DPad.Up.IsPressed)
                     {
@@ -177,6 +183,18 @@ private static GamePadState PlatformGetState(int index, GamePadDeadZone leftDead
                         buttons |= Buttons.DPadRight;
                     }
 
+                    if (controller.ExtendedGamepad.LeftThumbstickButton != null
+                    && controller.ExtendedGamepad.LeftThumbstickButton.IsPressed)
+                    {
+                        buttons |= Buttons.LeftStick;
+                    }
+
+                    if (controller.ExtendedGamepad.RightThumbstickButton != null
+                    && controller.ExtendedGamepad.RightThumbstickButton.IsPressed)
+                    {
+                        buttons |= Buttons.RightStick;
+                    }
+
                     leftThumbStickPosition.X = controller.ExtendedGamepad.LeftThumbstick.XAxis.Value;
                     leftThumbStickPosition.Y = controller.ExtendedGamepad.LeftThumbstick.YAxis.Value;
                     rightThumbStickPosition.X = controller.ExtendedGamepad.RightThumbstick.XAxis.Value;


Reviews:
user: login: ThomasFOG
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T12:06:48Z
user: login: squarebananas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T14:55:47Z
user: login: CartBlanche
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T16:42:54Z
user: login: CartBlanche
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T16:44:23Z
user: login: SimonDarksideJ
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T15:07:47Z
user: login: viniciusjarina
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-10T17:48:54Z

Review comments:
diff_hunk: @@ -151,10 +151,16 @@ private static GamePadState PlatformGetState(int index, GamePadDeadZone leftDead
                     if (controller.ExtendedGamepad.RightTrigger.IsPressed)
                         buttons |= Buttons.RightTrigger;
 
-                    if (controller.ExtendedGamepad.ButtonMenu.IsPressed)
+                    if (controller.ExtendedGamepad.ButtonMenu != null
+                    && controller.ExtendedGamepad.ButtonMenu.IsPressed)
path: MonoGame.Framework/Platform/Input/GamePad.iOS.cs
user: login: ThomasFOG
body: If you feel like it, you may write this as ```controller.ExtendedGamepad.ButtonMenu?.IsPressed```. Since the iOS specific code doesn't have to abide to the C# 5 rule, it's fine in this occurrence and won't fail ```ConsoleCheck```.
created_at: 2025-04-01T12:06:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -359,8 +380,10 @@ private static Buttons ButtonForKeyCode(Keycode keyCode)
 
                 case Keycode.ButtonStart:
                     return Buttons.Start;
-                case Keycode.Back:
+                case Keycode.ButtonSelect:
                     return Buttons.Back;
+                case Keycode.ButtonMode:
+                    return Buttons.BigButton;
path: MonoGame.Framework/Platform/Input/GamePad.Android.cs
user: login: squarebananas
body: The `BigButton` isn't actually the Guide button. It was from the Xbox 360 Big Button pads for quiz games. My suggestion would be to comment this in the `Buttons` Enum code, and if additional buttons states are needed then add new Enum values for those.
![bb](https://github.com/user-attachments/assets/6ff83997-3081-4138-9b06-f0b059939d15)


created_at: 2025-04-01T14:55:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -151,10 +151,16 @@ private static GamePadState PlatformGetState(int index, GamePadDeadZone leftDead
                     if (controller.ExtendedGamepad.RightTrigger.IsPressed)
                         buttons |= Buttons.RightTrigger;
 
-                    if (controller.ExtendedGamepad.ButtonMenu.IsPressed)
+                    if (controller.ExtendedGamepad.ButtonMenu != null
+                    && controller.ExtendedGamepad.ButtonMenu.IsPressed)
path: MonoGame.Framework/Platform/Input/GamePad.iOS.cs
user: login: CartBlanche
body: @ThomasFOG the issue we have is that the Xamarin bindings have some properties as nullable, like `controller.ExtendedGamepad.ButtonOptions?`, while others are not. Hence why I  used the above.
created_at: 2025-04-01T16:42:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -359,8 +380,10 @@ private static Buttons ButtonForKeyCode(Keycode keyCode)
 
                 case Keycode.ButtonStart:
                     return Buttons.Start;
-                case Keycode.Back:
+                case Keycode.ButtonSelect:
                     return Buttons.Back;
+                case Keycode.ButtonMode:
+                    return Buttons.BigButton;
path: MonoGame.Framework/Platform/Input/GamePad.Android.cs
user: login: CartBlanche
body: @squarebananas after an internal discussion, I've removed the BigButton mapping, at least for this release.
created_at: 2025-04-01T16:44:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -283,7 +287,26 @@ internal static bool OnGenericMotionEvent(MotionEvent e)
             gamePad._leftTrigger = e.GetAxisValue(Axis.Brake);
             gamePad._rightTrigger = e.GetAxisValue(Axis.Gas);
 
-            if(!gamePad.DPadButtons)
+            // We set the Trigger buttons here, because it never fires in OnKeyDown
+            if (gamePad._leftTrigger > 0f)
+            {
+                gamePad._buttons |= Buttons.LeftTrigger;
+            }
+            else
+            {
+                gamePad._buttons &= Buttons.LeftTrigger;
+            }
+
+            if (gamePad._rightTrigger > 0f)
+            {
+                gamePad._buttons |= Buttons.RightTrigger;
+            }
+            else
+            {
+                gamePad._buttons &= Buttons.RightTrigger;
path: MonoGame.Framework/Platform/Input/GamePad.Android.cs
user: login: viniciusjarina
body: This should be:
```
gamePad._buttons &= ~Buttons.RightTrigger;
```
Otherwise you will reset the state of the other buttons
created_at: 2025-04-10T17:48:54Z
author_association: NONE

PR comments:
user: login: SimonDarksideJ
created_at: 2025-03-31T21:40:29Z
body: P.S. while fixing this @CartBlanche , this issue was also logged related to the Android "Back" button
https://github.com/MonoGame/MonoGame/issues/8522
author_association: CONTRIBUTOR
user: login: ThomasFOG
created_at: 2025-04-01T08:22:13Z
body: For Android back button, I remember that we historically disabled it because Android maps it by default on the system "back", which means that it was basically quitting the app. Or something like that, can't remember exactly but there was something in that line.
author_association: CONTRIBUTOR
user: login: ThomasFOG
created_at: 2025-04-01T08:25:29Z
body: Just looked at this and it seems good to me. And Dom seems to have addressed the back button. It's now "select", which I guess prevents the system back issue.

Seems all fine to me.

(macOS failed because for some reason the Android SDK wasn't installed due to a network outage?)
author_association: CONTRIBUTOR
user: login: CartBlanche
created_at: 2025-04-01T16:52:05Z
body: > P.S. while fixing this @CartBlanche , this issue was also logged related to the Android "Back" button #8522

I've added the suggested fix to this PR.
author_association: CONTRIBUTOR