summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar t8952024-02-16 21:19:17 -0500
committerGravatar t8952024-02-17 12:32:33 -0500
commit50ecad547ea7e88301583f17c9f1eea2cc75b0af (patch)
tree21e6a6669ea19d05b389b097c0d411654aba4fbf
parenthid_core: Prevent crash if we try to iterate through empty color devices list (diff)
downloadyuzu-50ecad547ea7e88301583f17c9f1eea2cc75b0af.tar.gz
yuzu-50ecad547ea7e88301583f17c9f1eea2cc75b0af.tar.xz
yuzu-50ecad547ea7e88301583f17c9f1eea2cc75b0af.zip
android: Input mapping
Diffstat (limited to '')
-rw-r--r--src/android/app/src/main/AndroidManifest.xml1
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt176
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt2
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt39
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/NativeInput.kt416
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuInputDevice.kt93
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuVibrator.kt76
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/AnalogDirection.kt11
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/ButtonName.kt19
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/InputType.kt13
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeAnalog.kt14
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeButton.kt38
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeTrigger.kt10
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NpadStyleIndex.kt30
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/PlayerInput.kt83
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt15
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/AnalogInputSetting.kt31
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ButtonInputSetting.kt29
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputProfileSetting.kt32
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputSetting.kt134
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/IntSingleChoiceSetting.kt38
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ModifierInputSetting.kt31
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt2
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputDialogFragment.kt300
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileAdapter.kt68
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileDialogFragment.kt155
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/NewInputProfileDialogFragment.kt79
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt3
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt247
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt (renamed from src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt)76
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt86
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt730
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsSearchFragment.kt (renamed from src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt)6
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsViewModel.kt (renamed from src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt)43
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputProfileViewHolder.kt33
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputViewHolder.kt71
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt3
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt9
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt14
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt99
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt7
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt25
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt15
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt456
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt15
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NfcReader.kt6
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ParamPackage.kt141
-rw-r--r--src/android/app/src/main/jni/CMakeLists.txt1
-rw-r--r--src/android/app/src/main/jni/android_config.cpp141
-rw-r--r--src/android/app/src/main/jni/android_config.h7
-rw-r--r--src/android/app/src/main/jni/emu_window/emu_window.cpp49
-rw-r--r--src/android/app/src/main/jni/emu_window/emu_window.h15
-rw-r--r--src/android/app/src/main/jni/native.cpp167
-rw-r--r--src/android/app/src/main/jni/native.h5
-rw-r--r--src/android/app/src/main/jni/native_config.cpp117
-rw-r--r--src/android/app/src/main/jni/native_input.cpp631
-rw-r--r--src/android/app/src/main/res/drawable/button_anim.xml142
-rw-r--r--src/android/app/src/main/res/drawable/ic_controller_disconnected.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_more_vert.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_new_label.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_overlay.xml21
-rw-r--r--src/android/app/src/main/res/drawable/ic_share.xml9
-rw-r--r--src/android/app/src/main/res/drawable/stick_one_direction_anim.xml118
-rw-r--r--src/android/app/src/main/res/drawable/stick_two_direction_anim.xml173
-rw-r--r--src/android/app/src/main/res/layout-ldrtl/list_item_setting_input.xml63
-rw-r--r--src/android/app/src/main/res/layout/dialog_input_profiles.xml6
-rw-r--r--src/android/app/src/main/res/layout/dialog_mapping.xml26
-rw-r--r--src/android/app/src/main/res/layout/list_item_input_profile.xml74
-rw-r--r--src/android/app/src/main/res/layout/list_item_setting_input.xml63
-rw-r--r--src/android/app/src/main/res/menu/menu_in_game.xml7
-rw-r--r--src/android/app/src/main/res/menu/menu_input_options.xml34
-rw-r--r--src/android/app/src/main/res/navigation/settings_navigation.xml2
-rw-r--r--src/android/app/src/main/res/values-w600dp/dimens.xml2
-rw-r--r--src/android/app/src/main/res/values/dimens.xml2
-rw-r--r--src/android/app/src/main/res/values/strings.xml93
-rw-r--r--src/common/android/id_cache.cpp163
-rw-r--r--src/common/android/id_cache.h24
-rw-r--r--src/common/settings_input.h4
-rw-r--r--src/hid_core/frontend/emulated_controller.cpp4
-rw-r--r--src/input_common/CMakeLists.txt10
-rw-r--r--src/input_common/drivers/android.cpp324
-rw-r--r--src/input_common/drivers/android.h123
-rw-r--r--src/input_common/main.cpp28
83 files changed, 5706 insertions, 989 deletions
diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml
index 7890b30ca..b037fc055 100644
--- a/src/android/app/src/main/AndroidManifest.xml
+++ b/src/android/app/src/main/AndroidManifest.xml
@@ -14,6 +14,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
14 <uses-permission android:name="android.permission.INTERNET" /> 14 <uses-permission android:name="android.permission.INTERNET" />
15 <uses-permission android:name="android.permission.NFC" /> 15 <uses-permission android:name="android.permission.NFC" />
16 <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> 16 <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
17 <uses-permission android:name="android.permission.VIBRATE" />
17 18
18 <application 19 <application
19 android:name="org.yuzu.yuzu_emu.YuzuApplication" 20 android:name="org.yuzu.yuzu_emu.YuzuApplication"
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
index 6ebb46af7..fd229c855 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
@@ -30,34 +30,6 @@ import org.yuzu.yuzu_emu.model.GameVerificationResult
30 * with the native side of the Yuzu code. 30 * with the native side of the Yuzu code.
31 */ 31 */
32object NativeLibrary { 32object NativeLibrary {
33 /**
34 * Default controller id for each device
35 */
36 const val Player1Device = 0
37 const val Player2Device = 1
38 const val Player3Device = 2
39 const val Player4Device = 3
40 const val Player5Device = 4
41 const val Player6Device = 5
42 const val Player7Device = 6
43 const val Player8Device = 7
44 const val ConsoleDevice = 8
45
46 /**
47 * Controller type for each device
48 */
49 const val ProController = 3
50 const val Handheld = 4
51 const val JoyconDual = 5
52 const val JoyconLeft = 6
53 const val JoyconRight = 7
54 const val GameCube = 8
55 const val Pokeball = 9
56 const val NES = 10
57 const val SNES = 11
58 const val N64 = 12
59 const val SegaGenesis = 13
60
61 @JvmField 33 @JvmField
62 var sEmulationActivity = WeakReference<EmulationActivity?>(null) 34 var sEmulationActivity = WeakReference<EmulationActivity?>(null)
63 35
@@ -127,112 +99,6 @@ object NativeLibrary {
127 FileUtil.getFilename(Uri.parse(path)) 99 FileUtil.getFilename(Uri.parse(path))
128 } 100 }
129 101
130 /**
131 * Returns true if pro controller isn't available and handheld is
132 */
133 external fun isHandheldOnly(): Boolean
134
135 /**
136 * Changes controller type for a specific device.
137 *
138 * @param Device The input descriptor of the gamepad.
139 * @param Type The NpadStyleIndex of the gamepad.
140 */
141 external fun setDeviceType(Device: Int, Type: Int): Boolean
142
143 /**
144 * Handles event when a gamepad is connected.
145 *
146 * @param Device The input descriptor of the gamepad.
147 */
148 external fun onGamePadConnectEvent(Device: Int): Boolean
149
150 /**
151 * Handles event when a gamepad is disconnected.
152 *
153 * @param Device The input descriptor of the gamepad.
154 */
155 external fun onGamePadDisconnectEvent(Device: Int): Boolean
156
157 /**
158 * Handles button press events for a gamepad.
159 *
160 * @param Device The input descriptor of the gamepad.
161 * @param Button Key code identifying which button was pressed.
162 * @param Action Mask identifying which action is happening (button pressed down, or button released).
163 * @return If we handled the button press.
164 */
165 external fun onGamePadButtonEvent(Device: Int, Button: Int, Action: Int): Boolean
166
167 /**
168 * Handles joystick movement events.
169 *
170 * @param Device The device ID of the gamepad.
171 * @param Axis The axis ID
172 * @param x_axis The value of the x-axis represented by the given ID.
173 * @param y_axis The value of the y-axis represented by the given ID.
174 */
175 external fun onGamePadJoystickEvent(
176 Device: Int,
177 Axis: Int,
178 x_axis: Float,
179 y_axis: Float
180 ): Boolean
181
182 /**
183 * Handles motion events.
184 *
185 * @param delta_timestamp The finger id corresponding to this event
186 * @param gyro_x,gyro_y,gyro_z The value of the accelerometer sensor.
187 * @param accel_x,accel_y,accel_z The value of the y-axis
188 */
189 external fun onGamePadMotionEvent(
190 Device: Int,
191 delta_timestamp: Long,
192 gyro_x: Float,
193 gyro_y: Float,
194 gyro_z: Float,
195 accel_x: Float,
196 accel_y: Float,
197 accel_z: Float
198 ): Boolean
199
200 /**
201 * Signals and load a nfc tag
202 *
203 * @param data Byte array containing all the data from a nfc tag
204 */
205 external fun onReadNfcTag(data: ByteArray?): Boolean
206
207 /**
208 * Removes current loaded nfc tag
209 */
210 external fun onRemoveNfcTag(): Boolean
211
212 /**
213 * Handles touch press events.
214 *
215 * @param finger_id The finger id corresponding to this event
216 * @param x_axis The value of the x-axis.
217 * @param y_axis The value of the y-axis.
218 */
219 external fun onTouchPressed(finger_id: Int, x_axis: Float, y_axis: Float)
220
221 /**
222 * Handles touch movement.
223 *
224 * @param x_axis The value of the instantaneous x-axis.
225 * @param y_axis The value of the instantaneous y-axis.
226 */
227 external fun onTouchMoved(finger_id: Int, x_axis: Float, y_axis: Float)
228
229 /**
230 * Handles touch release events.
231 *
232 * @param finger_id The finger id corresponding to this event
233 */
234 external fun onTouchReleased(finger_id: Int)
235
236 external fun setAppDirectory(directory: String) 102 external fun setAppDirectory(directory: String)
237 103
238 /** 104 /**
@@ -629,46 +495,4 @@ object NativeLibrary {
629 * Checks if all necessary keys are present for decryption 495 * Checks if all necessary keys are present for decryption
630 */ 496 */
631 external fun areKeysPresent(): Boolean 497 external fun areKeysPresent(): Boolean
632
633 /**
634 * Button type for use in onTouchEvent
635 */
636 object ButtonType {
637 const val BUTTON_A = 0
638 const val BUTTON_B = 1
639 const val BUTTON_X = 2
640 const val BUTTON_Y = 3
641 const val STICK_L = 4
642 const val STICK_R = 5
643 const val TRIGGER_L = 6
644 const val TRIGGER_R = 7
645 const val TRIGGER_ZL = 8
646 const val TRIGGER_ZR = 9
647 const val BUTTON_PLUS = 10
648 const val BUTTON_MINUS = 11
649 const val DPAD_LEFT = 12
650 const val DPAD_UP = 13
651 const val DPAD_RIGHT = 14
652 const val DPAD_DOWN = 15
653 const val BUTTON_SL = 16
654 const val BUTTON_SR = 17
655 const val BUTTON_HOME = 18
656 const val BUTTON_CAPTURE = 19
657 }
658
659 /**
660 * Stick type for use in onTouchEvent
661 */
662 object StickType {
663 const val STICK_L = 0
664 const val STICK_R = 1
665 }
666
667 /**
668 * Button states
669 */
670 object ButtonState {
671 const val RELEASED = 0
672 const val PRESSED = 1
673 }
674} 498}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt
index 76778c10a..72943f33e 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt
@@ -7,6 +7,7 @@ import android.app.Application
7import android.app.NotificationChannel 7import android.app.NotificationChannel
8import android.app.NotificationManager 8import android.app.NotificationManager
9import android.content.Context 9import android.content.Context
10import org.yuzu.yuzu_emu.features.input.NativeInput
10import java.io.File 11import java.io.File
11import org.yuzu.yuzu_emu.utils.DirectoryInitialization 12import org.yuzu.yuzu_emu.utils.DirectoryInitialization
12import org.yuzu.yuzu_emu.utils.DocumentsTree 13import org.yuzu.yuzu_emu.utils.DocumentsTree
@@ -37,6 +38,7 @@ class YuzuApplication : Application() {
37 documentsTree = DocumentsTree() 38 documentsTree = DocumentsTree()
38 DirectoryInitialization.start() 39 DirectoryInitialization.start()
39 GpuDriverHelper.initializeDriverParameters() 40 GpuDriverHelper.initializeDriverParameters()
41 NativeInput.reloadInputDevices()
40 NativeLibrary.logDeviceInfo() 42 NativeLibrary.logDeviceInfo()
41 Log.logDeviceInfo() 43 Log.logDeviceInfo()
42 44
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
index 7a8d03610..0b70fccec 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
@@ -39,6 +39,7 @@ import org.yuzu.yuzu_emu.NativeLibrary
39import org.yuzu.yuzu_emu.R 39import org.yuzu.yuzu_emu.R
40import org.yuzu.yuzu_emu.YuzuApplication 40import org.yuzu.yuzu_emu.YuzuApplication
41import org.yuzu.yuzu_emu.databinding.ActivityEmulationBinding 41import org.yuzu.yuzu_emu.databinding.ActivityEmulationBinding
42import org.yuzu.yuzu_emu.features.input.NativeInput
42import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting 43import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
43import org.yuzu.yuzu_emu.features.settings.model.IntSetting 44import org.yuzu.yuzu_emu.features.settings.model.IntSetting
44import org.yuzu.yuzu_emu.features.settings.model.Settings 45import org.yuzu.yuzu_emu.features.settings.model.Settings
@@ -47,7 +48,9 @@ import org.yuzu.yuzu_emu.model.Game
47import org.yuzu.yuzu_emu.utils.InputHandler 48import org.yuzu.yuzu_emu.utils.InputHandler
48import org.yuzu.yuzu_emu.utils.Log 49import org.yuzu.yuzu_emu.utils.Log
49import org.yuzu.yuzu_emu.utils.MemoryUtil 50import org.yuzu.yuzu_emu.utils.MemoryUtil
51import org.yuzu.yuzu_emu.utils.NativeConfig
50import org.yuzu.yuzu_emu.utils.NfcReader 52import org.yuzu.yuzu_emu.utils.NfcReader
53import org.yuzu.yuzu_emu.utils.ParamPackage
51import org.yuzu.yuzu_emu.utils.ThemeHelper 54import org.yuzu.yuzu_emu.utils.ThemeHelper
52import java.text.NumberFormat 55import java.text.NumberFormat
53import kotlin.math.roundToInt 56import kotlin.math.roundToInt
@@ -63,8 +66,6 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
63 private var motionTimestamp: Long = 0 66 private var motionTimestamp: Long = 0
64 private var flipMotionOrientation: Boolean = false 67 private var flipMotionOrientation: Boolean = false
65 68
66 private var controllerIds = InputHandler.getGameControllerIds()
67
68 private val actionPause = "ACTION_EMULATOR_PAUSE" 69 private val actionPause = "ACTION_EMULATOR_PAUSE"
69 private val actionPlay = "ACTION_EMULATOR_PLAY" 70 private val actionPlay = "ACTION_EMULATOR_PLAY"
70 private val actionMute = "ACTION_EMULATOR_MUTE" 71 private val actionMute = "ACTION_EMULATOR_MUTE"
@@ -78,6 +79,27 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
78 79
79 super.onCreate(savedInstanceState) 80 super.onCreate(savedInstanceState)
80 81
82 InputHandler.updateControllerData()
83 val playerOne = NativeConfig.getInputSettings(true)[0]
84 if (!playerOne.hasMapping() && InputHandler.androidControllers.isNotEmpty()) {
85 var params: ParamPackage? = null
86 for (controller in InputHandler.registeredControllers) {
87 if (controller.get("port", -1) == 0) {
88 params = controller
89 break
90 }
91 }
92
93 if (params != null) {
94 NativeInput.updateMappingsWithDefault(
95 0,
96 params,
97 params.get("display", getString(R.string.unknown))
98 )
99 NativeConfig.saveGlobalConfig()
100 }
101 }
102
81 binding = ActivityEmulationBinding.inflate(layoutInflater) 103 binding = ActivityEmulationBinding.inflate(layoutInflater)
82 setContentView(binding.root) 104 setContentView(binding.root)
83 105
@@ -95,8 +117,6 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
95 nfcReader = NfcReader(this) 117 nfcReader = NfcReader(this)
96 nfcReader.initialize() 118 nfcReader.initialize()
97 119
98 InputHandler.initialize()
99
100 val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) 120 val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
101 if (!preferences.getBoolean(Settings.PREF_MEMORY_WARNING_SHOWN, false)) { 121 if (!preferences.getBoolean(Settings.PREF_MEMORY_WARNING_SHOWN, false)) {
102 if (MemoryUtil.isLessThan(MemoryUtil.REQUIRED_MEMORY, MemoryUtil.totalMemory)) { 122 if (MemoryUtil.isLessThan(MemoryUtil.REQUIRED_MEMORY, MemoryUtil.totalMemory)) {
@@ -147,7 +167,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
147 super.onResume() 167 super.onResume()
148 nfcReader.startScanning() 168 nfcReader.startScanning()
149 startMotionSensorListener() 169 startMotionSensorListener()
150 InputHandler.updateControllerIds() 170 InputHandler.updateControllerData()
151 171
152 buildPictureInPictureParams() 172 buildPictureInPictureParams()
153 } 173 }
@@ -172,6 +192,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
172 super.onNewIntent(intent) 192 super.onNewIntent(intent)
173 setIntent(intent) 193 setIntent(intent)
174 nfcReader.onNewIntent(intent) 194 nfcReader.onNewIntent(intent)
195 InputHandler.updateControllerData()
175 } 196 }
176 197
177 override fun dispatchKeyEvent(event: KeyEvent): Boolean { 198 override fun dispatchKeyEvent(event: KeyEvent): Boolean {
@@ -244,8 +265,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
244 } 265 }
245 val deltaTimestamp = (event.timestamp - motionTimestamp) / 1000 266 val deltaTimestamp = (event.timestamp - motionTimestamp) / 1000
246 motionTimestamp = event.timestamp 267 motionTimestamp = event.timestamp
247 NativeLibrary.onGamePadMotionEvent( 268 NativeInput.onDeviceMotionEvent(
248 NativeLibrary.Player1Device, 269 NativeInput.Player1Device,
249 deltaTimestamp, 270 deltaTimestamp,
250 gyro[0], 271 gyro[0],
251 gyro[1], 272 gyro[1],
@@ -254,8 +275,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
254 accel[1], 275 accel[1],
255 accel[2] 276 accel[2]
256 ) 277 )
257 NativeLibrary.onGamePadMotionEvent( 278 NativeInput.onDeviceMotionEvent(
258 NativeLibrary.ConsoleDevice, 279 NativeInput.ConsoleDevice,
259 deltaTimestamp, 280 deltaTimestamp,
260 gyro[0], 281 gyro[0],
261 gyro[1], 282 gyro[1],
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/NativeInput.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/NativeInput.kt
new file mode 100644
index 000000000..15d776311
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/NativeInput.kt
@@ -0,0 +1,416 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.input
5
6import org.yuzu.yuzu_emu.features.input.model.NativeButton
7import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
8import org.yuzu.yuzu_emu.features.input.model.InputType
9import org.yuzu.yuzu_emu.features.input.model.ButtonName
10import org.yuzu.yuzu_emu.features.input.model.NpadStyleIndex
11import org.yuzu.yuzu_emu.utils.NativeConfig
12import org.yuzu.yuzu_emu.utils.ParamPackage
13import android.view.InputDevice
14
15object NativeInput {
16 /**
17 * Default controller id for each device
18 */
19 const val Player1Device = 0
20 const val Player2Device = 1
21 const val Player3Device = 2
22 const val Player4Device = 3
23 const val Player5Device = 4
24 const val Player6Device = 5
25 const val Player7Device = 6
26 const val Player8Device = 7
27 const val ConsoleDevice = 8
28
29 /**
30 * Button states
31 */
32 object ButtonState {
33 const val RELEASED = 0
34 const val PRESSED = 1
35 }
36
37 /**
38 * Returns true if pro controller isn't available and handheld is.
39 * Intended to check where the input overlay should direct its inputs.
40 */
41 external fun isHandheldOnly(): Boolean
42
43 /**
44 * Handles button press events for a gamepad.
45 * @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
46 * @param port Port determined by controller connection order.
47 * @param buttonId The Android Keycode corresponding to this event.
48 * @param action Mask identifying which action is happening (button pressed down, or button released).
49 */
50 external fun onGamePadButtonEvent(
51 guid: String,
52 port: Int,
53 buttonId: Int,
54 action: Int
55 )
56
57 /**
58 * Handles axis movement events.
59 * @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
60 * @param port Port determined by controller connection order.
61 * @param axis The axis ID.
62 * @param value Value along the given axis.
63 */
64 external fun onGamePadAxisEvent(guid: String, port: Int, axis: Int, value: Float)
65
66 /**
67 * Handles motion events.
68 * @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
69 * @param port Port determined by controller connection order.
70 * @param deltaTimestamp The finger id corresponding to this event.
71 * @param xGyro The value of the x-axis for the gyroscope.
72 * @param yGyro The value of the y-axis for the gyroscope.
73 * @param zGyro The value of the z-axis for the gyroscope.
74 * @param xAccel The value of the x-axis for the accelerometer.
75 * @param yAccel The value of the y-axis for the accelerometer.
76 * @param zAccel The value of the z-axis for the accelerometer.
77 */
78 external fun onGamePadMotionEvent(
79 guid: String,
80 port: Int,
81 deltaTimestamp: Long,
82 xGyro: Float,
83 yGyro: Float,
84 zGyro: Float,
85 xAccel: Float,
86 yAccel: Float,
87 zAccel: Float
88 )
89
90 /**
91 * Signals and load a nfc tag
92 * @param data Byte array containing all the data from a nfc tag.
93 */
94 external fun onReadNfcTag(data: ByteArray?)
95
96 /**
97 * Removes current loaded nfc tag.
98 */
99 external fun onRemoveNfcTag()
100
101 /**
102 * Handles touch press events.
103 * @param fingerId The finger id corresponding to this event.
104 * @param xAxis The value of the x-axis on the touchscreen.
105 * @param yAxis The value of the y-axis on the touchscreen.
106 */
107 external fun onTouchPressed(fingerId: Int, xAxis: Float, yAxis: Float)
108
109 /**
110 * Handles touch movement.
111 * @param fingerId The finger id corresponding to this event.
112 * @param xAxis The value of the x-axis on the touchscreen.
113 * @param yAxis The value of the y-axis on the touchscreen.
114 */
115 external fun onTouchMoved(fingerId: Int, xAxis: Float, yAxis: Float)
116
117 /**
118 * Handles touch release events.
119 * @param fingerId The finger id corresponding to this event
120 */
121 external fun onTouchReleased(fingerId: Int)
122
123 /**
124 * Sends a button input to the global virtual controllers.
125 * @param port Port determined by controller connection order.
126 * @param button The [NativeButton] corresponding to this event.
127 * @param action Mask identifying which action is happening (button pressed down, or button released).
128 */
129 fun onOverlayButtonEvent(port: Int, button: NativeButton, action: Int) =
130 onOverlayButtonEventImpl(port, button.int, action)
131
132 private external fun onOverlayButtonEventImpl(port: Int, buttonId: Int, action: Int)
133
134 /**
135 * Sends a joystick input to the global virtual controllers.
136 * @param port Port determined by controller connection order.
137 * @param stick The [NativeAnalog] corresponding to this event.
138 * @param xAxis Value along the X axis.
139 * @param yAxis Value along the Y axis.
140 */
141 fun onOverlayJoystickEvent(port: Int, stick: NativeAnalog, xAxis: Float, yAxis: Float) =
142 onOverlayJoystickEventImpl(port, stick.int, xAxis, yAxis)
143
144 private external fun onOverlayJoystickEventImpl(
145 port: Int,
146 stickId: Int,
147 xAxis: Float,
148 yAxis: Float
149 )
150
151 /**
152 * Handles motion events for the global virtual controllers.
153 * @param port Port determined by controller connection order
154 * @param deltaTimestamp The finger id corresponding to this event.
155 * @param xGyro The value of the x-axis for the gyroscope.
156 * @param yGyro The value of the y-axis for the gyroscope.
157 * @param zGyro The value of the z-axis for the gyroscope.
158 * @param xAccel The value of the x-axis for the accelerometer.
159 * @param yAccel The value of the y-axis for the accelerometer.
160 * @param zAccel The value of the z-axis for the accelerometer.
161 */
162 external fun onDeviceMotionEvent(
163 port: Int,
164 deltaTimestamp: Long,
165 xGyro: Float,
166 yGyro: Float,
167 zGyro: Float,
168 xAccel: Float,
169 yAccel: Float,
170 zAccel: Float
171 )
172
173 /**
174 * Reloads all input devices from the currently loaded Settings::values.players into HID Core
175 */
176 external fun reloadInputDevices()
177
178 /**
179 * Registers a controller to be used with mapping
180 * @param device An [InputDevice] or the input overlay wrapped with [YuzuInputDevice]
181 */
182 external fun registerController(device: YuzuInputDevice)
183
184 /**
185 * Gets the names of input devices that have been registered with the input subsystem via [registerController]
186 */
187 external fun getInputDevices(): Array<String>
188
189 /**
190 * Reads all input profiles from disk. Must be called before creating a profile picker.
191 */
192 external fun loadInputProfiles()
193
194 /**
195 * Gets the names of each available input profile.
196 */
197 external fun getInputProfileNames(): Array<String>
198
199 /**
200 * Checks if the user-provided name for an input profile is valid.
201 * @param name User-provided name for an input profile.
202 * @return Whether [name] is valid or not.
203 */
204 external fun isProfileNameValid(name: String): Boolean
205
206 /**
207 * Creates a new input profile.
208 * @param name The new profile's name.
209 * @param playerIndex Index of the player that's currently being edited. Used to write the profile
210 * name to this player's config.
211 * @return Whether creating the profile was successful or not.
212 */
213 external fun createProfile(name: String, playerIndex: Int): Boolean
214
215 /**
216 * Deletes an input profile.
217 * @param name Name of the profile to delete.
218 * @param playerIndex Index of the player that's currently being edited. Used to remove the profile
219 * name from this player's config if they have it loaded.
220 * @return Whether deleting this profile was successful or not.
221 */
222 external fun deleteProfile(name: String, playerIndex: Int): Boolean
223
224 /**
225 * Loads an input profile.
226 * @param name Name of the input profile to load.
227 * @param playerIndex Index of the player that will have this profile loaded.
228 * @return Whether loading this profile was successful or not.
229 */
230 external fun loadProfile(name: String, playerIndex: Int): Boolean
231
232 /**
233 * Saves an input profile.
234 * @param name Name of the profile to save.
235 * @param playerIndex Index of the player that's currently being edited. Used to write the profile
236 * name to this player's config.
237 * @return Whether saving the profile was successful or not.
238 */
239 external fun saveProfile(name: String, playerIndex: Int): Boolean
240
241 /**
242 * Intended to be used immediately before a call to [NativeConfig.saveControlPlayerValues]
243 * Must be used while per-game config is loaded.
244 */
245 external fun loadPerGameConfiguration(
246 playerIndex: Int,
247 selectedIndex: Int,
248 selectedProfileName: String
249 )
250
251 /**
252 * Tells the input subsystem to start listening for inputs to map.
253 * @param type Type of input to map as shown by the int property in each [InputType].
254 */
255 external fun beginMapping(type: Int)
256
257 /**
258 * Gets an input's [ParamPackage] as a serialized string. Used for input verification before mapping.
259 * Must be run after [beginMapping] and before [stopMapping].
260 */
261 external fun getNextInput(): String
262
263 /**
264 * Tells the input subsystem to stop listening for inputs to map.
265 */
266 external fun stopMapping()
267
268 /**
269 * Updates a controller's mappings with auto-mapping params.
270 * @param playerIndex Index of the player to auto-map.
271 * @param deviceParams [ParamPackage] representing the device to auto-map as received
272 * from [getInputDevices].
273 * @param displayName Name of the device to auto-map as received from the "display" param in [deviceParams].
274 * Intended to be a way to provide a default name for a controller if the "display" param is empty.
275 */
276 fun updateMappingsWithDefault(
277 playerIndex: Int,
278 deviceParams: ParamPackage,
279 displayName: String
280 ) = updateMappingsWithDefaultImpl(playerIndex, deviceParams.serialize(), displayName)
281
282 private external fun updateMappingsWithDefaultImpl(
283 playerIndex: Int,
284 deviceParams: String,
285 displayName: String
286 )
287
288 /**
289 * Gets the params for a specific button.
290 * @param playerIndex Index of the player to get params from.
291 * @param button The [NativeButton] to get params for.
292 * @return A [ParamPackage] representing a player's specific button.
293 */
294 fun getButtonParam(playerIndex: Int, button: NativeButton): ParamPackage =
295 ParamPackage(getButtonParamImpl(playerIndex, button.int))
296
297 private external fun getButtonParamImpl(playerIndex: Int, buttonId: Int): String
298
299 /**
300 * Sets the params for a specific button.
301 * @param playerIndex Index of the player to set params for.
302 * @param button The [NativeButton] to set params for.
303 * @param param A [ParamPackage] to set.
304 */
305 fun setButtonParam(playerIndex: Int, button: NativeButton, param: ParamPackage) =
306 setButtonParamImpl(playerIndex, button.int, param.serialize())
307
308 private external fun setButtonParamImpl(playerIndex: Int, buttonId: Int, param: String)
309
310 /**
311 * Gets the params for a specific stick.
312 * @param playerIndex Index of the player to get params from.
313 * @param stick The [NativeAnalog] to get params for.
314 * @return A [ParamPackage] representing a player's specific stick.
315 */
316 fun getStickParam(playerIndex: Int, stick: NativeAnalog): ParamPackage =
317 ParamPackage(getStickParamImpl(playerIndex, stick.int))
318
319 private external fun getStickParamImpl(playerIndex: Int, stickId: Int): String
320
321 /**
322 * Sets the params for a specific stick.
323 * @param playerIndex Index of the player to set params for.
324 * @param stick The [NativeAnalog] to set params for.
325 * @param param A [ParamPackage] to set.
326 */
327 fun setStickParam(playerIndex: Int, stick: NativeAnalog, param: ParamPackage) =
328 setStickParamImpl(playerIndex, stick.int, param.serialize())
329
330 private external fun setStickParamImpl(playerIndex: Int, stickId: Int, param: String)
331
332 /**
333 * Gets the int representation of a [ButtonName]. Tells you what to show as the mapped input for
334 * a button/analog/other.
335 * @param param A [ParamPackage] that represents a specific button's params.
336 * @return The [ButtonName] for [param].
337 */
338 fun getButtonName(param: ParamPackage): ButtonName =
339 ButtonName.from(getButtonNameImpl(param.serialize()))
340
341 private external fun getButtonNameImpl(param: String): Int
342
343 /**
344 * Gets each supported [NpadStyleIndex] for a given player.
345 * @param playerIndex Index of the player to get supported indexes for.
346 * @return List of each supported [NpadStyleIndex].
347 */
348 fun getSupportedStyleTags(playerIndex: Int): List<NpadStyleIndex> =
349 getSupportedStyleTagsImpl(playerIndex).map { NpadStyleIndex.from(it) }
350
351 private external fun getSupportedStyleTagsImpl(playerIndex: Int): IntArray
352
353 /**
354 * Gets the [NpadStyleIndex] for a given player.
355 * @param playerIndex Index of the player to get an [NpadStyleIndex] from.
356 * @return The [NpadStyleIndex] for a given player.
357 */
358 fun getStyleIndex(playerIndex: Int): NpadStyleIndex =
359 NpadStyleIndex.from(getStyleIndexImpl(playerIndex))
360
361 private external fun getStyleIndexImpl(playerIndex: Int): Int
362
363 /**
364 * Sets the [NpadStyleIndex] for a given player.
365 * @param playerIndex Index of the player to change.
366 * @param style The new style to set.
367 */
368 fun setStyleIndex(playerIndex: Int, style: NpadStyleIndex) =
369 setStyleIndexImpl(playerIndex, style.int)
370
371 private external fun setStyleIndexImpl(playerIndex: Int, styleIndex: Int)
372
373 /**
374 * Checks if a device is a controller.
375 * @param params [ParamPackage] for an input device retrieved from [getInputDevices]
376 * @return Whether the device is a controller or not.
377 */
378 fun isController(params: ParamPackage): Boolean = isControllerImpl(params.serialize())
379
380 private external fun isControllerImpl(params: String): Boolean
381
382 /**
383 * Checks if a controller is connected
384 * @param playerIndex Index of the player to check.
385 * @return Whether the player is connected or not.
386 */
387 external fun getIsConnected(playerIndex: Int): Boolean
388
389 /**
390 * Connects/disconnects a controller and ensures that connection order stays in-tact.
391 * @param playerIndex Index of the player to connect/disconnect.
392 * @param connected Whether to connect or disconnect this controller.
393 */
394 fun connectControllers(playerIndex: Int, connected: Boolean = true) {
395 val connectedControllers = mutableListOf<Boolean>().apply {
396 if (connected) {
397 for (i in 0 until 8) {
398 add(i <= playerIndex)
399 }
400 } else {
401 for (i in 0 until 8) {
402 add(i < playerIndex)
403 }
404 }
405 }
406 connectControllersImpl(connectedControllers.toBooleanArray())
407 }
408
409 private external fun connectControllersImpl(connected: BooleanArray)
410
411 /**
412 * Resets all of the button and analog mappings for a player.
413 * @param playerIndex Index of the player that will have its mappings reset.
414 */
415 external fun resetControllerMappings(playerIndex: Int)
416}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuInputDevice.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuInputDevice.kt
new file mode 100644
index 000000000..15cc38c7f
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuInputDevice.kt
@@ -0,0 +1,93 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.input
5
6import android.view.InputDevice
7import androidx.annotation.Keep
8import org.yuzu.yuzu_emu.YuzuApplication
9import org.yuzu.yuzu_emu.R
10import org.yuzu.yuzu_emu.utils.InputHandler.getGUID
11
12@Keep
13interface YuzuInputDevice {
14 fun getName(): String
15
16 fun getGUID(): String
17
18 fun getPort(): Int
19
20 fun getSupportsVibration(): Boolean
21
22 fun vibrate(intensity: Float)
23
24 fun getAxes(): Array<Int> = arrayOf()
25 fun hasKeys(keys: IntArray): BooleanArray = BooleanArray(0)
26}
27
28class YuzuPhysicalDevice(
29 private val device: InputDevice,
30 private val port: Int,
31 useSystemVibrator: Boolean
32) : YuzuInputDevice {
33 private val vibrator = if (useSystemVibrator) {
34 YuzuVibrator.getSystemVibrator()
35 } else {
36 YuzuVibrator.getControllerVibrator(device)
37 }
38
39 override fun getName(): String {
40 return device.name
41 }
42
43 override fun getGUID(): String {
44 return device.getGUID()
45 }
46
47 override fun getPort(): Int {
48 return port
49 }
50
51 override fun getSupportsVibration(): Boolean {
52 return vibrator.supportsVibration()
53 }
54
55 override fun vibrate(intensity: Float) {
56 vibrator.vibrate(intensity)
57 }
58
59 override fun getAxes(): Array<Int> = device.motionRanges.map { it.axis }.toTypedArray()
60 override fun hasKeys(keys: IntArray): BooleanArray = device.hasKeys(*keys)
61}
62
63class YuzuInputOverlayDevice(
64 private val vibration: Boolean,
65 private val port: Int
66) : YuzuInputDevice {
67 private val vibrator = YuzuVibrator.getSystemVibrator()
68
69 override fun getName(): String {
70 return YuzuApplication.appContext.getString(R.string.input_overlay)
71 }
72
73 override fun getGUID(): String {
74 return "00000000000000000000000000000000"
75 }
76
77 override fun getPort(): Int {
78 return port
79 }
80
81 override fun getSupportsVibration(): Boolean {
82 if (vibration) {
83 return vibrator.supportsVibration()
84 }
85 return false
86 }
87
88 override fun vibrate(intensity: Float) {
89 if (vibration) {
90 vibrator.vibrate(intensity)
91 }
92 }
93}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuVibrator.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuVibrator.kt
new file mode 100644
index 000000000..aac49ecae
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuVibrator.kt
@@ -0,0 +1,76 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.input
5
6import android.content.Context
7import android.os.Build
8import android.os.CombinedVibration
9import android.os.VibrationEffect
10import android.os.Vibrator
11import android.os.VibratorManager
12import android.view.InputDevice
13import androidx.annotation.Keep
14import androidx.annotation.RequiresApi
15import org.yuzu.yuzu_emu.YuzuApplication
16
17@Keep
18@Suppress("DEPRECATION")
19interface YuzuVibrator {
20 fun supportsVibration(): Boolean
21
22 fun vibrate(intensity: Float)
23
24 companion object {
25 fun getControllerVibrator(device: InputDevice): YuzuVibrator =
26 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
27 YuzuVibratorManager(device.vibratorManager)
28 } else {
29 YuzuVibratorManagerCompat(device.vibrator)
30 }
31
32 fun getSystemVibrator(): YuzuVibrator =
33 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
34 val vibratorManager = YuzuApplication.appContext
35 .getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
36 YuzuVibratorManager(vibratorManager)
37 } else {
38 val vibrator = YuzuApplication.appContext
39 .getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
40 YuzuVibratorManagerCompat(vibrator)
41 }
42
43 fun getVibrationEffect(intensity: Float): VibrationEffect? {
44 if (intensity > 0f) {
45 return VibrationEffect.createOneShot(
46 50,
47 (255.0 * intensity).toInt().coerceIn(1, 255)
48 )
49 }
50 return null
51 }
52 }
53}
54
55@RequiresApi(Build.VERSION_CODES.S)
56class YuzuVibratorManager(private val vibratorManager: VibratorManager) : YuzuVibrator {
57 override fun supportsVibration(): Boolean {
58 return vibratorManager.vibratorIds.isNotEmpty()
59 }
60
61 override fun vibrate(intensity: Float) {
62 val vibration = YuzuVibrator.getVibrationEffect(intensity) ?: return
63 vibratorManager.vibrate(CombinedVibration.createParallel(vibration))
64 }
65}
66
67class YuzuVibratorManagerCompat(private val vibrator: Vibrator) : YuzuVibrator {
68 override fun supportsVibration(): Boolean {
69 return vibrator.hasVibrator()
70 }
71
72 override fun vibrate(intensity: Float) {
73 val vibration = YuzuVibrator.getVibrationEffect(intensity) ?: return
74 vibrator.vibrate(vibration)
75 }
76}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/AnalogDirection.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/AnalogDirection.kt
new file mode 100644
index 000000000..0a5fab2ae
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/AnalogDirection.kt
@@ -0,0 +1,11 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.input.model
5
6enum class AnalogDirection(val int: Int, val param: String) {
7 Up(0, "up"),
8 Down(1, "down"),
9 Left(2, "left"),
10 Right(3, "right")
11}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/ButtonName.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/ButtonName.kt
new file mode 100644
index 000000000..b8846ecad
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/ButtonName.kt
@@ -0,0 +1,19 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.input.model
5
6// Loosely matches the enum in common/input.h
7enum class ButtonName(val int: Int) {
8 Invalid(1),
9
10 // This will display the engine name instead of the button name
11 Engine(2),
12
13 // This will display the button by value instead of the button name
14 Value(3);
15
16 companion object {
17 fun from(int: Int): ButtonName = entries.firstOrNull { it.int == int } ?: Invalid
18 }
19}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/InputType.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/InputType.kt
new file mode 100644
index 000000000..f725231cb
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/InputType.kt
@@ -0,0 +1,13 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.input.model
5
6// Must match the corresponding enum in input_common/main.h
7enum class InputType(val int: Int) {
8 None(0),
9 Button(1),
10 Stick(2),
11 Motion(3),
12 Touch(4)
13}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeAnalog.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeAnalog.kt
new file mode 100644
index 000000000..c3b7a785d
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeAnalog.kt
@@ -0,0 +1,14 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.input.model
5
6// Must match enum in src/common/settings_input.h
7enum class NativeAnalog(val int: Int) {
8 LStick(0),
9 RStick(1);
10
11 companion object {
12 fun from(int: Int): NativeAnalog = entries.firstOrNull { it.int == int } ?: LStick
13 }
14}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeButton.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeButton.kt
new file mode 100644
index 000000000..c5ccd7115
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeButton.kt
@@ -0,0 +1,38 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.input.model
5
6// Must match enum in src/common/settings_input.h
7enum class NativeButton(val int: Int) {
8 A(0),
9 B(1),
10 X(2),
11 Y(3),
12 LStick(4),
13 RStick(5),
14 L(6),
15 R(7),
16 ZL(8),
17 ZR(9),
18 Plus(10),
19 Minus(11),
20
21 DLeft(12),
22 DUp(13),
23 DRight(14),
24 DDown(15),
25
26 SLLeft(16),
27 SRLeft(17),
28
29 Home(18),
30 Capture(19),
31
32 SLRight(20),
33 SRRight(21);
34
35 companion object {
36 fun from(int: Int): NativeButton = entries.firstOrNull { it.int == int } ?: A
37 }
38}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeTrigger.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeTrigger.kt
new file mode 100644
index 000000000..625f352b4
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeTrigger.kt
@@ -0,0 +1,10 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.input.model
5
6// Must match enum in src/common/settings_input.h
7enum class NativeTrigger(val int: Int) {
8 LTrigger(0),
9 RTrigger(1)
10}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NpadStyleIndex.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NpadStyleIndex.kt
new file mode 100644
index 000000000..e2a3d7aff
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NpadStyleIndex.kt
@@ -0,0 +1,30 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.input.model
5
6import androidx.annotation.StringRes
7import org.yuzu.yuzu_emu.R
8
9// Must match enum in src/core/hid/hid_types.h
10enum class NpadStyleIndex(val int: Int, @StringRes val nameId: Int = 0) {
11 None(0),
12 Fullkey(3, R.string.pro_controller),
13 Handheld(4, R.string.handheld),
14 HandheldNES(4),
15 JoyconDual(5, R.string.dual_joycons),
16 JoyconLeft(6, R.string.left_joycon),
17 JoyconRight(7, R.string.right_joycon),
18 GameCube(8, R.string.gamecube_controller),
19 Pokeball(9),
20 NES(10),
21 SNES(12),
22 N64(13),
23 SegaGenesis(14),
24 SystemExt(32),
25 System(33);
26
27 companion object {
28 fun from(int: Int): NpadStyleIndex = entries.firstOrNull { it.int == int } ?: None
29 }
30}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/PlayerInput.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/PlayerInput.kt
new file mode 100644
index 000000000..d35de80c4
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/PlayerInput.kt
@@ -0,0 +1,83 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.input.model
5
6import androidx.annotation.Keep
7
8@Keep
9data class PlayerInput(
10 var connected: Boolean,
11 var buttons: Array<String>,
12 var analogs: Array<String>,
13 var motions: Array<String>,
14
15 var vibrationEnabled: Boolean,
16 var vibrationStrength: Int,
17
18 var bodyColorLeft: Long,
19 var bodyColorRight: Long,
20 var buttonColorLeft: Long,
21 var buttonColorRight: Long,
22 var profileName: String,
23
24 var useSystemVibrator: Boolean
25) {
26 // It's recommended to use the generated equals() and hashCode() methods
27 // when using arrays in a data class
28 override fun equals(other: Any?): Boolean {
29 if (this === other) return true
30 if (javaClass != other?.javaClass) return false
31
32 other as PlayerInput
33
34 if (connected != other.connected) return false
35 if (!buttons.contentEquals(other.buttons)) return false
36 if (!analogs.contentEquals(other.analogs)) return false
37 if (!motions.contentEquals(other.motions)) return false
38 if (vibrationEnabled != other.vibrationEnabled) return false
39 if (vibrationStrength != other.vibrationStrength) return false
40 if (bodyColorLeft != other.bodyColorLeft) return false
41 if (bodyColorRight != other.bodyColorRight) return false
42 if (buttonColorLeft != other.buttonColorLeft) return false
43 if (buttonColorRight != other.buttonColorRight) return false
44 if (profileName != other.profileName) return false
45 return useSystemVibrator == other.useSystemVibrator
46 }
47
48 override fun hashCode(): Int {
49 var result = connected.hashCode()
50 result = 31 * result + buttons.contentHashCode()
51 result = 31 * result + analogs.contentHashCode()
52 result = 31 * result + motions.contentHashCode()
53 result = 31 * result + vibrationEnabled.hashCode()
54 result = 31 * result + vibrationStrength
55 result = 31 * result + bodyColorLeft.hashCode()
56 result = 31 * result + bodyColorRight.hashCode()
57 result = 31 * result + buttonColorLeft.hashCode()
58 result = 31 * result + buttonColorRight.hashCode()
59 result = 31 * result + profileName.hashCode()
60 result = 31 * result + useSystemVibrator.hashCode()
61 return result
62 }
63
64 fun hasMapping(): Boolean {
65 var hasMapping = false
66 buttons.forEach {
67 if (it != "[empty]") {
68 hasMapping = true
69 }
70 }
71 analogs.forEach {
72 if (it != "[empty]") {
73 hasMapping = true
74 }
75 }
76 motions.forEach {
77 if (it != "[empty]") {
78 hasMapping = true
79 }
80 }
81 return hasMapping
82 }
83}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
index 862c6c483..4f6b93bd2 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
@@ -4,17 +4,30 @@
4package org.yuzu.yuzu_emu.features.settings.model 4package org.yuzu.yuzu_emu.features.settings.model
5 5
6import org.yuzu.yuzu_emu.R 6import org.yuzu.yuzu_emu.R
7import org.yuzu.yuzu_emu.YuzuApplication
7 8
8object Settings { 9object Settings {
9 enum class MenuTag(val titleId: Int) { 10 enum class MenuTag(val titleId: Int = 0) {
10 SECTION_ROOT(R.string.advanced_settings), 11 SECTION_ROOT(R.string.advanced_settings),
11 SECTION_SYSTEM(R.string.preferences_system), 12 SECTION_SYSTEM(R.string.preferences_system),
12 SECTION_RENDERER(R.string.preferences_graphics), 13 SECTION_RENDERER(R.string.preferences_graphics),
13 SECTION_AUDIO(R.string.preferences_audio), 14 SECTION_AUDIO(R.string.preferences_audio),
15 SECTION_INPUT(R.string.preferences_controls),
16 SECTION_INPUT_PLAYER_ONE,
17 SECTION_INPUT_PLAYER_TWO,
18 SECTION_INPUT_PLAYER_THREE,
19 SECTION_INPUT_PLAYER_FOUR,
20 SECTION_INPUT_PLAYER_FIVE,
21 SECTION_INPUT_PLAYER_SIX,
22 SECTION_INPUT_PLAYER_SEVEN,
23 SECTION_INPUT_PLAYER_EIGHT,
14 SECTION_THEME(R.string.preferences_theme), 24 SECTION_THEME(R.string.preferences_theme),
15 SECTION_DEBUG(R.string.preferences_debug); 25 SECTION_DEBUG(R.string.preferences_debug);
16 } 26 }
17 27
28 fun getPlayerString(player: Int): String =
29 YuzuApplication.appContext.getString(R.string.preferences_player, player)
30
18 const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch" 31 const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch"
19 const val PREF_MEMORY_WARNING_SHOWN = "MemoryWarningShown" 32 const val PREF_MEMORY_WARNING_SHOWN = "MemoryWarningShown"
20 33
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/AnalogInputSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/AnalogInputSetting.kt
new file mode 100644
index 000000000..a2996725e
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/AnalogInputSetting.kt
@@ -0,0 +1,31 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.model.view
5
6import androidx.annotation.StringRes
7import org.yuzu.yuzu_emu.features.input.NativeInput
8import org.yuzu.yuzu_emu.features.input.model.AnalogDirection
9import org.yuzu.yuzu_emu.features.input.model.InputType
10import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
11import org.yuzu.yuzu_emu.utils.ParamPackage
12
13class AnalogInputSetting(
14 override val playerIndex: Int,
15 val nativeAnalog: NativeAnalog,
16 val analogDirection: AnalogDirection,
17 @StringRes titleId: Int = 0,
18 titleString: String = ""
19) : InputSetting(titleId, titleString) {
20 override val type = TYPE_INPUT
21 override val inputType = InputType.Stick
22
23 override fun getSelectedValue(): String {
24 val params = NativeInput.getStickParam(playerIndex, nativeAnalog)
25 val analog = analogToText(params, analogDirection.param)
26 return getDisplayString(params, analog)
27 }
28
29 override fun setSelectedValue(param: ParamPackage) =
30 NativeInput.setStickParam(playerIndex, nativeAnalog, param)
31}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ButtonInputSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ButtonInputSetting.kt
new file mode 100644
index 000000000..786d09a7a
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ButtonInputSetting.kt
@@ -0,0 +1,29 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.model.view
5
6import androidx.annotation.StringRes
7import org.yuzu.yuzu_emu.utils.ParamPackage
8import org.yuzu.yuzu_emu.features.input.NativeInput
9import org.yuzu.yuzu_emu.features.input.model.InputType
10import org.yuzu.yuzu_emu.features.input.model.NativeButton
11
12class ButtonInputSetting(
13 override val playerIndex: Int,
14 val nativeButton: NativeButton,
15 @StringRes titleId: Int = 0,
16 titleString: String = ""
17) : InputSetting(titleId, titleString) {
18 override val type = TYPE_INPUT
19 override val inputType = InputType.Button
20
21 override fun getSelectedValue(): String {
22 val params = NativeInput.getButtonParam(playerIndex, nativeButton)
23 val button = buttonToText(params)
24 return getDisplayString(params, button)
25 }
26
27 override fun setSelectedValue(param: ParamPackage) =
28 NativeInput.setButtonParam(playerIndex, nativeButton, param)
29}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputProfileSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputProfileSetting.kt
new file mode 100644
index 000000000..c46de08c5
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputProfileSetting.kt
@@ -0,0 +1,32 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.model.view
5
6import org.yuzu.yuzu_emu.R
7import org.yuzu.yuzu_emu.features.input.NativeInput
8import org.yuzu.yuzu_emu.utils.NativeConfig
9
10class InputProfileSetting(private val playerIndex: Int) :
11 SettingsItem(emptySetting, R.string.profile, "", 0, "") {
12 override val type = TYPE_INPUT_PROFILE
13
14 fun getCurrentProfile(): String =
15 NativeConfig.getInputSettings(true)[playerIndex].profileName
16
17 fun getProfileNames(): Array<String> = NativeInput.getInputProfileNames()
18
19 fun isProfileNameValid(name: String): Boolean = NativeInput.isProfileNameValid(name)
20
21 fun createProfile(name: String): Boolean = NativeInput.createProfile(name, playerIndex)
22
23 fun deleteProfile(name: String): Boolean = NativeInput.deleteProfile(name, playerIndex)
24
25 fun loadProfile(name: String): Boolean {
26 val result = NativeInput.loadProfile(name, playerIndex)
27 NativeInput.reloadInputDevices()
28 return result
29 }
30
31 fun saveProfile(name: String): Boolean = NativeInput.saveProfile(name, playerIndex)
32}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputSetting.kt
new file mode 100644
index 000000000..2d118bff3
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputSetting.kt
@@ -0,0 +1,134 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.model.view
5
6import androidx.annotation.StringRes
7import org.yuzu.yuzu_emu.R
8import org.yuzu.yuzu_emu.YuzuApplication
9import org.yuzu.yuzu_emu.features.input.NativeInput
10import org.yuzu.yuzu_emu.features.input.model.ButtonName
11import org.yuzu.yuzu_emu.features.input.model.InputType
12import org.yuzu.yuzu_emu.utils.ParamPackage
13
14sealed class InputSetting(
15 @StringRes titleId: Int,
16 titleString: String
17) : SettingsItem(emptySetting, titleId, titleString, 0, "") {
18 override val type = TYPE_INPUT
19 abstract val inputType: InputType
20 abstract val playerIndex: Int
21
22 protected val context get() = YuzuApplication.appContext
23
24 abstract fun getSelectedValue(): String
25
26 abstract fun setSelectedValue(param: ParamPackage)
27
28 protected fun getDisplayString(params: ParamPackage, control: String): String {
29 val deviceName = params.get("display", "")
30 deviceName.ifEmpty {
31 return context.getString(R.string.not_set)
32 }
33 return "$deviceName: $control"
34 }
35
36 private fun getDirectionName(direction: String): String =
37 when (direction) {
38 "up" -> context.getString(R.string.up)
39 "down" -> context.getString(R.string.down)
40 "left" -> context.getString(R.string.left)
41 "right" -> context.getString(R.string.right)
42 else -> direction
43 }
44
45 protected fun buttonToText(param: ParamPackage): String {
46 if (!param.has("engine")) {
47 return context.getString(R.string.not_set)
48 }
49
50 val toggle = if (param.get("toggle", false)) "~" else ""
51 val inverted = if (param.get("inverted", false)) "!" else ""
52 val invert = if (param.get("invert", "+") == "-") "-" else ""
53 val turbo = if (param.get("turbo", false)) "$" else ""
54 val commonButtonName = NativeInput.getButtonName(param)
55
56 if (commonButtonName == ButtonName.Invalid) {
57 return context.getString(R.string.invalid)
58 }
59
60 if (commonButtonName == ButtonName.Engine) {
61 return param.get("engine", "")
62 }
63
64 if (commonButtonName == ButtonName.Value) {
65 if (param.has("hat")) {
66 val hat = getDirectionName(param.get("direction", ""))
67 return context.getString(R.string.qualified_hat, turbo, toggle, inverted, hat)
68 }
69 if (param.has("axis")) {
70 val axis = param.get("axis", "")
71 return context.getString(
72 R.string.qualified_button_stick_axis,
73 toggle,
74 inverted,
75 invert,
76 axis
77 )
78 }
79 if (param.has("button")) {
80 val button = param.get("button", "")
81 return context.getString(R.string.qualified_button, turbo, toggle, inverted, button)
82 }
83 }
84
85 return context.getString(R.string.unknown)
86 }
87
88 protected fun analogToText(param: ParamPackage, direction: String): String {
89 if (!param.has("engine")) {
90 return context.getString(R.string.not_set)
91 }
92
93 if (param.get("engine", "") == "analog_from_button") {
94 return buttonToText(ParamPackage(param.get(direction, "")))
95 }
96
97 if (!param.has("axis_x") || !param.has("axis_y")) {
98 return context.getString(R.string.unknown)
99 }
100
101 val xAxis = param.get("axis_x", "")
102 val yAxis = param.get("axis_y", "")
103 val xInvert = param.get("invert_x", "+") == "-"
104 val yInvert = param.get("invert_y", "+") == "-"
105
106 if (direction == "modifier") {
107 return context.getString(R.string.unused)
108 }
109
110 when (direction) {
111 "up" -> {
112 val yInvertString = if (yInvert) "+" else "-"
113 return context.getString(R.string.qualified_axis, yAxis, yInvertString)
114 }
115
116 "down" -> {
117 val yInvertString = if (yInvert) "-" else "+"
118 return context.getString(R.string.qualified_axis, yAxis, yInvertString)
119 }
120
121 "left" -> {
122 val xInvertString = if (xInvert) "+" else "-"
123 return context.getString(R.string.qualified_axis, xAxis, xInvertString)
124 }
125
126 "right" -> {
127 val xInvertString = if (xInvert) "-" else "+"
128 return context.getString(R.string.qualified_axis, xAxis, xInvertString)
129 }
130 }
131
132 return context.getString(R.string.unknown)
133 }
134}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/IntSingleChoiceSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/IntSingleChoiceSetting.kt
new file mode 100644
index 000000000..e024c793a
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/IntSingleChoiceSetting.kt
@@ -0,0 +1,38 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.model.view
5
6import androidx.annotation.StringRes
7import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
8
9class IntSingleChoiceSetting(
10 private val intSetting: AbstractIntSetting,
11 @StringRes titleId: Int = 0,
12 titleString: String = "",
13 @StringRes descriptionId: Int = 0,
14 descriptionString: String = "",
15 val choices: Array<String>,
16 val values: Array<Int>
17) : SettingsItem(intSetting, titleId, titleString, descriptionId, descriptionString) {
18 override val type = TYPE_INT_SINGLE_CHOICE
19
20 fun getValueAt(index: Int): Int =
21 if (values.indices.contains(index)) values[index] else -1
22
23 fun getChoiceAt(index: Int): String =
24 if (choices.indices.contains(index)) choices[index] else ""
25
26 fun getSelectedValue(needsGlobal: Boolean = false) = intSetting.getInt(needsGlobal)
27 fun setSelectedValue(value: Int) = intSetting.setInt(value)
28
29 val selectedValueIndex: Int
30 get() {
31 for (i in values.indices) {
32 if (values[i] == getSelectedValue()) {
33 return i
34 }
35 }
36 return -1
37 }
38}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ModifierInputSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ModifierInputSetting.kt
new file mode 100644
index 000000000..a1db3cc87
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ModifierInputSetting.kt
@@ -0,0 +1,31 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.model.view
5
6import androidx.annotation.StringRes
7import org.yuzu.yuzu_emu.features.input.NativeInput
8import org.yuzu.yuzu_emu.features.input.model.InputType
9import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
10import org.yuzu.yuzu_emu.utils.ParamPackage
11
12class ModifierInputSetting(
13 override val playerIndex: Int,
14 val nativeAnalog: NativeAnalog,
15 @StringRes titleId: Int = 0,
16 titleString: String = ""
17) : InputSetting(titleId, titleString) {
18 override val inputType = InputType.Button
19
20 override fun getSelectedValue(): String {
21 val analogParam = NativeInput.getStickParam(playerIndex, nativeAnalog)
22 val modifierParam = ParamPackage(analogParam.get("modifier", ""))
23 return buttonToText(modifierParam)
24 }
25
26 override fun setSelectedValue(param: ParamPackage) {
27 val newParam = NativeInput.getStickParam(playerIndex, nativeAnalog)
28 newParam.set("modifier", param.serialize())
29 NativeInput.setStickParam(playerIndex, nativeAnalog, newParam)
30 }
31}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt
index 1005a2b7d..06f607424 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt
@@ -7,11 +7,11 @@ import androidx.annotation.DrawableRes
7import androidx.annotation.StringRes 7import androidx.annotation.StringRes
8 8
9class RunnableSetting( 9class RunnableSetting(
10 val isRuntimeRunnable: Boolean,
11 @StringRes titleId: Int = 0, 10 @StringRes titleId: Int = 0,
12 titleString: String = "", 11 titleString: String = "",
13 @StringRes descriptionId: Int = 0, 12 @StringRes descriptionId: Int = 0,
14 descriptionString: String = "", 13 descriptionString: String = "",
14 val isRunnable: Boolean,
15 @DrawableRes val iconId: Int = 0, 15 @DrawableRes val iconId: Int = 0,
16 val runnable: () -> Unit 16 val runnable: () -> Unit
17) : SettingsItem(emptySetting, titleId, titleString, descriptionId, descriptionString) { 17) : SettingsItem(emptySetting, titleId, titleString, descriptionId, descriptionString) {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputDialogFragment.kt
new file mode 100644
index 000000000..16a1d0504
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputDialogFragment.kt
@@ -0,0 +1,300 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.ui
5
6import android.app.Dialog
7import android.graphics.drawable.Animatable2
8import android.graphics.drawable.AnimatedVectorDrawable
9import android.graphics.drawable.Drawable
10import android.os.Bundle
11import android.view.InputDevice
12import android.view.KeyEvent
13import android.view.LayoutInflater
14import android.view.MotionEvent
15import android.view.View
16import android.view.ViewGroup
17import androidx.fragment.app.DialogFragment
18import androidx.fragment.app.activityViewModels
19import com.google.android.material.dialog.MaterialAlertDialogBuilder
20import org.yuzu.yuzu_emu.R
21import org.yuzu.yuzu_emu.databinding.DialogMappingBinding
22import org.yuzu.yuzu_emu.features.input.NativeInput
23import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
24import org.yuzu.yuzu_emu.features.input.model.NativeButton
25import org.yuzu.yuzu_emu.features.settings.model.view.AnalogInputSetting
26import org.yuzu.yuzu_emu.features.settings.model.view.ButtonInputSetting
27import org.yuzu.yuzu_emu.features.settings.model.view.InputSetting
28import org.yuzu.yuzu_emu.features.settings.model.view.ModifierInputSetting
29import org.yuzu.yuzu_emu.utils.InputHandler
30import org.yuzu.yuzu_emu.utils.ParamPackage
31
32class InputDialogFragment : DialogFragment() {
33 private var inputAccepted = false
34
35 private var position: Int = 0
36
37 private lateinit var inputSetting: InputSetting
38
39 private lateinit var binding: DialogMappingBinding
40
41 private val settingsViewModel: SettingsViewModel by activityViewModels()
42
43 override fun onCreate(savedInstanceState: Bundle?) {
44 super.onCreate(savedInstanceState)
45 if (settingsViewModel.clickedItem == null) dismiss()
46
47 position = requireArguments().getInt(POSITION)
48
49 InputHandler.updateControllerData()
50 }
51
52 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
53 inputSetting = settingsViewModel.clickedItem as InputSetting
54 binding = DialogMappingBinding.inflate(layoutInflater)
55
56 val builder = MaterialAlertDialogBuilder(requireContext())
57 .setPositiveButton(android.R.string.cancel) { _, _ ->
58 NativeInput.stopMapping()
59 dismiss()
60 }
61 .setView(binding.root)
62
63 val playButtonMapAnimation = { twoDirections: Boolean ->
64 val stickAnimation: AnimatedVectorDrawable
65 val buttonAnimation: AnimatedVectorDrawable
66 binding.imageStickAnimation.apply {
67 val anim = if (twoDirections) {
68 R.drawable.stick_two_direction_anim
69 } else {
70 R.drawable.stick_one_direction_anim
71 }
72 setBackgroundResource(anim)
73 stickAnimation = background as AnimatedVectorDrawable
74 }
75 binding.imageButtonAnimation.apply {
76 setBackgroundResource(R.drawable.button_anim)
77 buttonAnimation = background as AnimatedVectorDrawable
78 }
79 stickAnimation.registerAnimationCallback(object : Animatable2.AnimationCallback() {
80 override fun onAnimationEnd(drawable: Drawable?) {
81 buttonAnimation.start()
82 }
83 })
84 buttonAnimation.registerAnimationCallback(object : Animatable2.AnimationCallback() {
85 override fun onAnimationEnd(drawable: Drawable?) {
86 stickAnimation.start()
87 }
88 })
89 stickAnimation.start()
90 }
91
92 when (val setting = inputSetting) {
93 is AnalogInputSetting -> {
94 when (setting.nativeAnalog) {
95 NativeAnalog.LStick -> builder.setTitle(
96 getString(R.string.map_control, getString(R.string.left_stick))
97 )
98
99 NativeAnalog.RStick -> builder.setTitle(
100 getString(R.string.map_control, getString(R.string.right_stick))
101 )
102 }
103
104 builder.setMessage(R.string.stick_map_description)
105
106 playButtonMapAnimation.invoke(true)
107 }
108
109 is ModifierInputSetting -> {
110 builder.setTitle(getString(R.string.map_control, setting.title))
111 .setMessage(R.string.button_map_description)
112 playButtonMapAnimation.invoke(false)
113 }
114
115 is ButtonInputSetting -> {
116 if (setting.nativeButton == NativeButton.DUp ||
117 setting.nativeButton == NativeButton.DDown ||
118 setting.nativeButton == NativeButton.DLeft ||
119 setting.nativeButton == NativeButton.DRight
120 ) {
121 builder.setTitle(getString(R.string.map_dpad_direction, setting.title))
122 } else {
123 builder.setTitle(getString(R.string.map_control, setting.title))
124 }
125 builder.setMessage(R.string.button_map_description)
126 playButtonMapAnimation.invoke(false)
127 }
128 }
129
130 return builder.create()
131 }
132
133 override fun onCreateView(
134 inflater: LayoutInflater,
135 container: ViewGroup?,
136 savedInstanceState: Bundle?
137 ): View {
138 return binding.root
139 }
140
141 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
142 super.onViewCreated(view, savedInstanceState)
143 view.requestFocus()
144 view.setOnFocusChangeListener { v, hasFocus -> if (!hasFocus) v.requestFocus() }
145 dialog?.setOnKeyListener { _, _, keyEvent -> onKeyEvent(keyEvent) }
146 binding.root.setOnGenericMotionListener { _, motionEvent -> onMotionEvent(motionEvent) }
147 NativeInput.beginMapping(inputSetting.inputType.int)
148 }
149
150 private fun onKeyEvent(event: KeyEvent): Boolean {
151 if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK &&
152 event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD
153 ) {
154 return false
155 }
156
157 val action = when (event.action) {
158 KeyEvent.ACTION_DOWN -> NativeInput.ButtonState.PRESSED
159 KeyEvent.ACTION_UP -> NativeInput.ButtonState.RELEASED
160 else -> return false
161 }
162 val controllerData =
163 InputHandler.androidControllers[event.device.controllerNumber] ?: return false
164 NativeInput.onGamePadButtonEvent(
165 controllerData.getGUID(),
166 controllerData.getPort(),
167 event.keyCode,
168 action
169 )
170 onInputReceived(event.device)
171 return true
172 }
173
174 private fun onMotionEvent(event: MotionEvent): Boolean {
175 if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK &&
176 event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD
177 ) {
178 return false
179 }
180
181 // Temp workaround for DPads that give both axis and button input. The input system can't
182 // take in a specific axis direction for a binding so you lose half of the directions for a DPad.
183
184 val controllerData =
185 InputHandler.androidControllers[event.device.controllerNumber] ?: return false
186 event.device.motionRanges.forEach {
187 NativeInput.onGamePadAxisEvent(
188 controllerData.getGUID(),
189 controllerData.getPort(),
190 it.axis,
191 event.getAxisValue(it.axis)
192 )
193 onInputReceived(event.device)
194 }
195 return true
196 }
197
198 private fun onInputReceived(device: InputDevice) {
199 val params = ParamPackage(NativeInput.getNextInput())
200 if (params.has("engine") && isInputAcceptable(params) && !inputAccepted) {
201 inputAccepted = true
202 setResult(params, device)
203 }
204 }
205
206 private fun setResult(params: ParamPackage, device: InputDevice) {
207 NativeInput.stopMapping()
208 params.set("display", "${device.name} ${params.get("port", 0)}")
209 when (val item = settingsViewModel.clickedItem as InputSetting) {
210 is ModifierInputSetting,
211 is ButtonInputSetting -> {
212 // Invert DPad up and left bindings by default
213 val tempSetting = inputSetting as? ButtonInputSetting
214 if (tempSetting != null) {
215 if (tempSetting.nativeButton == NativeButton.DUp ||
216 tempSetting.nativeButton == NativeButton.DLeft &&
217 params.has("axis")
218 ) {
219 params.set("invert", "-")
220 }
221 }
222
223 item.setSelectedValue(params)
224 settingsViewModel.setAdapterItemChanged(position)
225 }
226
227 is AnalogInputSetting -> {
228 var analogParam = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
229 analogParam = adjustAnalogParam(params, analogParam, item.analogDirection.param)
230
231 // Invert Y-Axis by default
232 analogParam.set("invert_y", "-")
233
234 item.setSelectedValue(analogParam)
235 settingsViewModel.setReloadListAndNotifyDataset(true)
236 }
237 }
238 dismiss()
239 }
240
241 private fun adjustAnalogParam(
242 inputParam: ParamPackage,
243 analogParam: ParamPackage,
244 buttonName: String
245 ): ParamPackage {
246 // The poller returned a complete axis, so set all the buttons
247 if (inputParam.has("axis_x") && inputParam.has("axis_y")) {
248 return inputParam
249 }
250
251 // Check if the current configuration has either no engine or an axis binding.
252 // Clears out the old binding and adds one with analog_from_button.
253 if (!analogParam.has("engine") || analogParam.has("axis_x") || analogParam.has("axis_y")) {
254 analogParam.clear()
255 analogParam.set("engine", "analog_from_button")
256 }
257 analogParam.set(buttonName, inputParam.serialize())
258 return analogParam
259 }
260
261 private fun isInputAcceptable(params: ParamPackage): Boolean {
262 if (InputHandler.registeredControllers.size == 1) {
263 return true
264 }
265
266 if (params.has("motion")) {
267 return true
268 }
269
270 val currentDevice = settingsViewModel.getCurrentDeviceParams(params)
271 if (currentDevice.get("engine", "any") == "any") {
272 return true
273 }
274
275 val guidMatch = params.get("guid", "") == currentDevice.get("guid", "") ||
276 params.get("guid", "") == currentDevice.get("guid2", "")
277 return params.get("engine", "") == currentDevice.get("engine", "") &&
278 guidMatch &&
279 params.get("port", 0) == currentDevice.get("port", 0)
280 }
281
282 companion object {
283 const val TAG = "InputDialogFragment"
284
285 const val POSITION = "Position"
286
287 fun newInstance(
288 inputMappingViewModel: SettingsViewModel,
289 setting: InputSetting,
290 position: Int
291 ): InputDialogFragment {
292 inputMappingViewModel.clickedItem = setting
293 val args = Bundle()
294 args.putInt(POSITION, position)
295 val fragment = InputDialogFragment()
296 fragment.arguments = args
297 return fragment
298 }
299 }
300}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileAdapter.kt
new file mode 100644
index 000000000..5656e9d8d
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileAdapter.kt
@@ -0,0 +1,68 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.ui
5
6import android.view.LayoutInflater
7import android.view.View
8import android.view.ViewGroup
9import org.yuzu.yuzu_emu.YuzuApplication
10import org.yuzu.yuzu_emu.adapters.AbstractListAdapter
11import org.yuzu.yuzu_emu.databinding.ListItemInputProfileBinding
12import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
13import org.yuzu.yuzu_emu.R
14
15class InputProfileAdapter(options: List<ProfileItem>) :
16 AbstractListAdapter<ProfileItem, AbstractViewHolder<ProfileItem>>(options) {
17 override fun onCreateViewHolder(
18 parent: ViewGroup,
19 viewType: Int
20 ): AbstractViewHolder<ProfileItem> {
21 ListItemInputProfileBinding.inflate(LayoutInflater.from(parent.context), parent, false)
22 .also { return InputProfileViewHolder(it) }
23 }
24
25 inner class InputProfileViewHolder(val binding: ListItemInputProfileBinding) :
26 AbstractViewHolder<ProfileItem>(binding) {
27 override fun bind(model: ProfileItem) {
28 when (model) {
29 is ExistingProfileItem -> {
30 binding.title.text = model.name
31 binding.buttonNew.visibility = View.GONE
32 binding.buttonDelete.visibility = View.VISIBLE
33 binding.buttonDelete.setOnClickListener { model.deleteProfile.invoke() }
34 binding.buttonSave.visibility = View.VISIBLE
35 binding.buttonSave.setOnClickListener { model.saveProfile.invoke() }
36 binding.buttonLoad.visibility = View.VISIBLE
37 binding.buttonLoad.setOnClickListener { model.loadProfile.invoke() }
38 }
39
40 is NewProfileItem -> {
41 binding.title.text = model.name
42 binding.buttonNew.visibility = View.VISIBLE
43 binding.buttonNew.setOnClickListener { model.createNewProfile.invoke() }
44 binding.buttonSave.visibility = View.GONE
45 binding.buttonDelete.visibility = View.GONE
46 binding.buttonLoad.visibility = View.GONE
47 }
48 }
49 }
50 }
51}
52
53sealed interface ProfileItem {
54 val name: String
55}
56
57data class NewProfileItem(
58 val createNewProfile: () -> Unit
59) : ProfileItem {
60 override val name: String = YuzuApplication.appContext.getString(R.string.create_new_profile)
61}
62
63data class ExistingProfileItem(
64 override val name: String,
65 val deleteProfile: () -> Unit,
66 val saveProfile: () -> Unit,
67 val loadProfile: () -> Unit
68) : ProfileItem
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileDialogFragment.kt
new file mode 100644
index 000000000..9b24d41c1
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileDialogFragment.kt
@@ -0,0 +1,155 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.ui
5
6import android.app.Dialog
7import android.os.Bundle
8import android.view.LayoutInflater
9import android.view.View
10import android.view.ViewGroup
11import android.widget.Toast
12import androidx.fragment.app.DialogFragment
13import androidx.fragment.app.activityViewModels
14import androidx.lifecycle.Lifecycle
15import androidx.lifecycle.lifecycleScope
16import androidx.lifecycle.repeatOnLifecycle
17import androidx.recyclerview.widget.LinearLayoutManager
18import com.google.android.material.dialog.MaterialAlertDialogBuilder
19import kotlinx.coroutines.launch
20import org.yuzu.yuzu_emu.R
21import org.yuzu.yuzu_emu.databinding.DialogInputProfilesBinding
22import org.yuzu.yuzu_emu.features.settings.model.view.InputProfileSetting
23import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
24
25class InputProfileDialogFragment : DialogFragment() {
26 private var position = 0
27
28 private val settingsViewModel: SettingsViewModel by activityViewModels()
29
30 private lateinit var binding: DialogInputProfilesBinding
31
32 private lateinit var setting: InputProfileSetting
33
34 override fun onCreate(savedInstanceState: Bundle?) {
35 super.onCreate(savedInstanceState)
36 position = requireArguments().getInt(POSITION)
37 }
38
39 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
40 binding = DialogInputProfilesBinding.inflate(layoutInflater)
41
42 setting = settingsViewModel.clickedItem as InputProfileSetting
43 val options = mutableListOf<ProfileItem>().apply {
44 add(
45 NewProfileItem(
46 createNewProfile = {
47 NewInputProfileDialogFragment.newInstance(
48 settingsViewModel,
49 setting,
50 position
51 ).show(parentFragmentManager, NewInputProfileDialogFragment.TAG)
52 dismiss()
53 }
54 )
55 )
56
57 val onActionDismiss = {
58 settingsViewModel.setReloadListAndNotifyDataset(true)
59 dismiss()
60 }
61 setting.getProfileNames().forEach {
62 add(
63 ExistingProfileItem(
64 it,
65 deleteProfile = {
66 settingsViewModel.setShouldShowDeleteProfileDialog(it)
67 },
68 saveProfile = {
69 if (!setting.saveProfile(it)) {
70 Toast.makeText(
71 requireContext(),
72 R.string.failed_to_save_profile,
73 Toast.LENGTH_SHORT
74 ).show()
75 }
76 onActionDismiss.invoke()
77 },
78 loadProfile = {
79 if (!setting.loadProfile(it)) {
80 Toast.makeText(
81 requireContext(),
82 R.string.failed_to_load_profile,
83 Toast.LENGTH_SHORT
84 ).show()
85 }
86 onActionDismiss.invoke()
87 }
88 )
89 )
90 }
91 }
92 binding.listProfiles.apply {
93 layoutManager = LinearLayoutManager(requireContext())
94 adapter = InputProfileAdapter(options)
95 }
96
97 return MaterialAlertDialogBuilder(requireContext())
98 .setView(binding.root)
99 .create()
100 }
101
102 override fun onCreateView(
103 inflater: LayoutInflater,
104 container: ViewGroup?,
105 savedInstanceState: Bundle?
106 ): View {
107 return binding.root
108 }
109
110 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
111 super.onViewCreated(view, savedInstanceState)
112
113 viewLifecycleOwner.lifecycleScope.launch {
114 repeatOnLifecycle(Lifecycle.State.CREATED) {
115 settingsViewModel.shouldShowDeleteProfileDialog.collect {
116 if (it.isNotEmpty()) {
117 MessageDialogFragment.newInstance(
118 activity = requireActivity(),
119 titleId = R.string.delete_input_profile,
120 descriptionId = R.string.delete_input_profile_description,
121 positiveAction = {
122 setting.deleteProfile(it)
123 settingsViewModel.setReloadListAndNotifyDataset(true)
124 },
125 negativeAction = {},
126 negativeButtonTitleId = android.R.string.cancel
127 ).show(parentFragmentManager, MessageDialogFragment.TAG)
128 settingsViewModel.setShouldShowDeleteProfileDialog("")
129 dismiss()
130 }
131 }
132 }
133 }
134 }
135
136 companion object {
137 const val TAG = "InputProfileDialogFragment"
138
139 const val POSITION = "Position"
140
141 fun newInstance(
142 settingsViewModel: SettingsViewModel,
143 profileSetting: InputProfileSetting,
144 position: Int
145 ): InputProfileDialogFragment {
146 settingsViewModel.clickedItem = profileSetting
147
148 val args = Bundle()
149 args.putInt(POSITION, position)
150 val fragment = InputProfileDialogFragment()
151 fragment.arguments = args
152 return fragment
153 }
154 }
155}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/NewInputProfileDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/NewInputProfileDialogFragment.kt
new file mode 100644
index 000000000..6e52bea80
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/NewInputProfileDialogFragment.kt
@@ -0,0 +1,79 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.ui
5
6import android.app.Dialog
7import android.os.Bundle
8import android.widget.Toast
9import androidx.fragment.app.DialogFragment
10import androidx.fragment.app.activityViewModels
11import com.google.android.material.dialog.MaterialAlertDialogBuilder
12import org.yuzu.yuzu_emu.databinding.DialogEditTextBinding
13import org.yuzu.yuzu_emu.features.settings.model.view.InputProfileSetting
14import org.yuzu.yuzu_emu.R
15
16class NewInputProfileDialogFragment : DialogFragment() {
17 private var position = 0
18
19 private val settingsViewModel: SettingsViewModel by activityViewModels()
20
21 private lateinit var binding: DialogEditTextBinding
22
23 override fun onCreate(savedInstanceState: Bundle?) {
24 super.onCreate(savedInstanceState)
25 position = requireArguments().getInt(POSITION)
26 }
27
28 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
29 binding = DialogEditTextBinding.inflate(layoutInflater)
30
31 val setting = settingsViewModel.clickedItem as InputProfileSetting
32 return MaterialAlertDialogBuilder(requireContext())
33 .setTitle(R.string.enter_profile_name)
34 .setPositiveButton(android.R.string.ok) { _, _ ->
35 val profileName = binding.editText.text.toString()
36 if (!setting.isProfileNameValid(profileName)) {
37 Toast.makeText(
38 requireContext(),
39 R.string.invalid_profile_name,
40 Toast.LENGTH_SHORT
41 ).show()
42 return@setPositiveButton
43 }
44
45 if (!setting.createProfile(profileName)) {
46 Toast.makeText(
47 requireContext(),
48 R.string.profile_name_already_exists,
49 Toast.LENGTH_SHORT
50 ).show()
51 } else {
52 settingsViewModel.setAdapterItemChanged(position)
53 }
54 }
55 .setNegativeButton(android.R.string.cancel, null)
56 .setView(binding.root)
57 .show()
58 }
59
60 companion object {
61 const val TAG = "NewInputProfileDialogFragment"
62
63 const val POSITION = "Position"
64
65 fun newInstance(
66 settingsViewModel: SettingsViewModel,
67 profileSetting: InputProfileSetting,
68 position: Int
69 ): NewInputProfileDialogFragment {
70 settingsViewModel.clickedItem = profileSetting
71
72 val args = Bundle()
73 args.putInt(POSITION, position)
74 val fragment = NewInputProfileDialogFragment()
75 fragment.arguments = args
76 return fragment
77 }
78 }
79}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt
index 6f072241a..681a18b3b 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt
@@ -25,9 +25,9 @@ import org.yuzu.yuzu_emu.NativeLibrary
25import java.io.IOException 25import java.io.IOException
26import org.yuzu.yuzu_emu.R 26import org.yuzu.yuzu_emu.R
27import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding 27import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding
28import org.yuzu.yuzu_emu.features.input.NativeInput
28import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile 29import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
29import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment 30import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment
30import org.yuzu.yuzu_emu.model.SettingsViewModel
31import org.yuzu.yuzu_emu.utils.* 31import org.yuzu.yuzu_emu.utils.*
32 32
33class SettingsActivity : AppCompatActivity() { 33class SettingsActivity : AppCompatActivity() {
@@ -137,6 +137,7 @@ class SettingsActivity : AppCompatActivity() {
137 super.onStop() 137 super.onStop()
138 Log.info("[SettingsActivity] Settings activity stopping. Saving settings to INI...") 138 Log.info("[SettingsActivity] Settings activity stopping. Saving settings to INI...")
139 if (isFinishing) { 139 if (isFinishing) {
140 NativeInput.reloadInputDevices()
140 NativeLibrary.applySettings() 141 NativeLibrary.applySettings()
141 if (args.game == null) { 142 if (args.game == null) {
142 NativeConfig.saveGlobalConfig() 143 NativeConfig.saveGlobalConfig()
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
index be9b3031b..45c8faa10 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
@@ -8,12 +8,11 @@ import android.icu.util.Calendar
8import android.icu.util.TimeZone 8import android.icu.util.TimeZone
9import android.text.format.DateFormat 9import android.text.format.DateFormat
10import android.view.LayoutInflater 10import android.view.LayoutInflater
11import android.view.View
11import android.view.ViewGroup 12import android.view.ViewGroup
13import android.widget.PopupMenu
12import androidx.fragment.app.Fragment 14import androidx.fragment.app.Fragment
13import androidx.lifecycle.Lifecycle
14import androidx.lifecycle.ViewModelProvider 15import androidx.lifecycle.ViewModelProvider
15import androidx.lifecycle.lifecycleScope
16import androidx.lifecycle.repeatOnLifecycle
17import androidx.navigation.findNavController 16import androidx.navigation.findNavController
18import androidx.recyclerview.widget.AsyncDifferConfig 17import androidx.recyclerview.widget.AsyncDifferConfig
19import androidx.recyclerview.widget.DiffUtil 18import androidx.recyclerview.widget.DiffUtil
@@ -21,16 +20,18 @@ import androidx.recyclerview.widget.ListAdapter
21import com.google.android.material.datepicker.MaterialDatePicker 20import com.google.android.material.datepicker.MaterialDatePicker
22import com.google.android.material.timepicker.MaterialTimePicker 21import com.google.android.material.timepicker.MaterialTimePicker
23import com.google.android.material.timepicker.TimeFormat 22import com.google.android.material.timepicker.TimeFormat
24import kotlinx.coroutines.launch
25import org.yuzu.yuzu_emu.R 23import org.yuzu.yuzu_emu.R
26import org.yuzu.yuzu_emu.SettingsNavigationDirections 24import org.yuzu.yuzu_emu.SettingsNavigationDirections
27import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding 25import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
26import org.yuzu.yuzu_emu.databinding.ListItemSettingInputBinding
28import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding 27import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding
29import org.yuzu.yuzu_emu.databinding.ListItemSettingsHeaderBinding 28import org.yuzu.yuzu_emu.databinding.ListItemSettingsHeaderBinding
29import org.yuzu.yuzu_emu.features.input.NativeInput
30import org.yuzu.yuzu_emu.features.input.model.AnalogDirection
31import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
30import org.yuzu.yuzu_emu.features.settings.model.view.* 32import org.yuzu.yuzu_emu.features.settings.model.view.*
31import org.yuzu.yuzu_emu.features.settings.ui.viewholder.* 33import org.yuzu.yuzu_emu.features.settings.ui.viewholder.*
32import org.yuzu.yuzu_emu.fragments.SettingsDialogFragment 34import org.yuzu.yuzu_emu.utils.ParamPackage
33import org.yuzu.yuzu_emu.model.SettingsViewModel
34 35
35class SettingsAdapter( 36class SettingsAdapter(
36 private val fragment: Fragment, 37 private val fragment: Fragment,
@@ -41,19 +42,6 @@ class SettingsAdapter(
41 private val settingsViewModel: SettingsViewModel 42 private val settingsViewModel: SettingsViewModel
42 get() = ViewModelProvider(fragment.requireActivity())[SettingsViewModel::class.java] 43 get() = ViewModelProvider(fragment.requireActivity())[SettingsViewModel::class.java]
43 44
44 init {
45 fragment.viewLifecycleOwner.lifecycleScope.launch {
46 fragment.repeatOnLifecycle(Lifecycle.State.STARTED) {
47 settingsViewModel.adapterItemChanged.collect {
48 if (it != -1) {
49 notifyItemChanged(it)
50 settingsViewModel.setAdapterItemChanged(-1)
51 }
52 }
53 }
54 }
55 }
56
57 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder { 45 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder {
58 val inflater = LayoutInflater.from(parent.context) 46 val inflater = LayoutInflater.from(parent.context)
59 return when (viewType) { 47 return when (viewType) {
@@ -85,8 +73,19 @@ class SettingsAdapter(
85 RunnableViewHolder(ListItemSettingBinding.inflate(inflater), this) 73 RunnableViewHolder(ListItemSettingBinding.inflate(inflater), this)
86 } 74 }
87 75
76 SettingsItem.TYPE_INPUT -> {
77 InputViewHolder(ListItemSettingInputBinding.inflate(inflater), this)
78 }
79
80 SettingsItem.TYPE_INT_SINGLE_CHOICE -> {
81 SingleChoiceViewHolder(ListItemSettingBinding.inflate(inflater), this)
82 }
83
84 SettingsItem.TYPE_INPUT_PROFILE -> {
85 InputProfileViewHolder(ListItemSettingBinding.inflate(inflater), this)
86 }
87
88 else -> { 88 else -> {
89 // TODO: Create an error view since we can't return null now
90 HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this) 89 HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this)
91 } 90 }
92 } 91 }
@@ -126,6 +125,15 @@ class SettingsAdapter(
126 ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG) 125 ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG)
127 } 126 }
128 127
128 fun onIntSingleChoiceClick(item: IntSingleChoiceSetting, position: Int) {
129 SettingsDialogFragment.newInstance(
130 settingsViewModel,
131 item,
132 SettingsItem.TYPE_INT_SINGLE_CHOICE,
133 position
134 ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG)
135 }
136
129 fun onDateTimeClick(item: DateTimeSetting, position: Int) { 137 fun onDateTimeClick(item: DateTimeSetting, position: Int) {
130 val storedTime = item.getValue() * 1000 138 val storedTime = item.getValue() * 1000
131 139
@@ -185,6 +193,205 @@ class SettingsAdapter(
185 fragment.view?.findNavController()?.navigate(action) 193 fragment.view?.findNavController()?.navigate(action)
186 } 194 }
187 195
196 fun onInputProfileClick(item: InputProfileSetting, position: Int) {
197 InputProfileDialogFragment.newInstance(
198 settingsViewModel,
199 item,
200 position
201 ).show(fragment.childFragmentManager, InputProfileDialogFragment.TAG)
202 }
203
204 fun onInputClick(item: InputSetting, position: Int) {
205 InputDialogFragment.newInstance(
206 settingsViewModel,
207 item,
208 position
209 ).show(fragment.childFragmentManager, InputDialogFragment.TAG)
210 }
211
212 fun onInputOptionsClick(anchor: View, item: InputSetting, position: Int) {
213 val popup = PopupMenu(context, anchor)
214 popup.menuInflater.inflate(R.menu.menu_input_options, popup.menu)
215
216 popup.menu.apply {
217 val invertAxis = findItem(R.id.invert_axis)
218 val invertButton = findItem(R.id.invert_button)
219 val toggleButton = findItem(R.id.toggle_button)
220 val turboButton = findItem(R.id.turbo_button)
221 val setThreshold = findItem(R.id.set_threshold)
222 val toggleAxis = findItem(R.id.toggle_axis)
223 when (item) {
224 is AnalogInputSetting -> {
225 val params = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
226
227 invertAxis.isVisible = true
228 invertAxis.isCheckable = true
229 invertAxis.isChecked = when (item.analogDirection) {
230 AnalogDirection.Left, AnalogDirection.Right -> {
231 params.get("invert_x", "+") == "-"
232 }
233
234 AnalogDirection.Up, AnalogDirection.Down -> {
235 params.get("invert_y", "+") == "-"
236 }
237 }
238 invertAxis.setOnMenuItemClickListener {
239 if (item.analogDirection == AnalogDirection.Left ||
240 item.analogDirection == AnalogDirection.Right
241 ) {
242 val invertValue = params.get("invert_x", "+") == "-"
243 val invertString = if (invertValue) "+" else "-"
244 params.set("invert_x", invertString)
245 } else if (
246 item.analogDirection == AnalogDirection.Up ||
247 item.analogDirection == AnalogDirection.Down
248 ) {
249 val invertValue = params.get("invert_y", "+") == "-"
250 val invertString = if (invertValue) "+" else "-"
251 params.set("invert_y", invertString)
252 }
253 true
254 }
255
256 popup.setOnDismissListener {
257 NativeInput.setStickParam(item.playerIndex, item.nativeAnalog, params)
258 settingsViewModel.setDatasetChanged(true)
259 }
260 }
261
262 is ButtonInputSetting -> {
263 val params = NativeInput.getButtonParam(item.playerIndex, item.nativeButton)
264 if (params.has("code") || params.has("button") || params.has("hat")) {
265 val buttonInvert = params.get("inverted", false)
266 invertButton.isVisible = true
267 invertButton.isCheckable = true
268 invertButton.isChecked = buttonInvert
269 invertButton.setOnMenuItemClickListener {
270 params.set("inverted", !buttonInvert)
271 true
272 }
273
274 val toggle = params.get("toggle", false)
275 toggleButton.isVisible = true
276 toggleButton.isCheckable = true
277 toggleButton.isChecked = toggle
278 toggleButton.setOnMenuItemClickListener {
279 params.set("toggle", !toggle)
280 true
281 }
282
283 val turbo = params.get("turbo", false)
284 turboButton.isVisible = true
285 turboButton.isCheckable = true
286 turboButton.isChecked = turbo
287 turboButton.setOnMenuItemClickListener {
288 params.set("turbo", !turbo)
289 true
290 }
291 } else if (params.has("axis")) {
292 val axisInvert = params.get("invert", "+") == "-"
293 invertAxis.isVisible = true
294 invertAxis.isCheckable = true
295 invertAxis.isChecked = axisInvert
296 invertAxis.setOnMenuItemClickListener {
297 params.set("invert", if (!axisInvert) "-" else "+")
298 true
299 }
300
301 val buttonInvert = params.get("inverted", false)
302 invertButton.isVisible = true
303 invertButton.isCheckable = true
304 invertButton.isChecked = buttonInvert
305 invertButton.setOnMenuItemClickListener {
306 params.set("inverted", !buttonInvert)
307 true
308 }
309
310 setThreshold.isVisible = true
311 val thresholdSetting = object : AbstractIntSetting {
312 override val key = ""
313
314 override fun getInt(needsGlobal: Boolean): Int =
315 (params.get("threshold", 0.5f) * 100).toInt()
316
317 override fun setInt(value: Int) {
318 params.set("threshold", value.toFloat() / 100)
319 NativeInput.setButtonParam(
320 item.playerIndex,
321 item.nativeButton,
322 params
323 )
324 }
325
326 override val defaultValue = 50
327
328 override fun getValueAsString(needsGlobal: Boolean): String =
329 getInt(needsGlobal).toString()
330
331 override fun reset() = setInt(defaultValue)
332 }
333 setThreshold.setOnMenuItemClickListener {
334 onSliderClick(
335 SliderSetting(thresholdSetting, R.string.set_threshold),
336 position
337 )
338 true
339 }
340
341 val axisToggle = params.get("toggle", false)
342 toggleAxis.isVisible = true
343 toggleAxis.isCheckable = true
344 toggleAxis.isChecked = axisToggle
345 toggleAxis.setOnMenuItemClickListener {
346 params.set("toggle", !axisToggle)
347 true
348 }
349 }
350
351 popup.setOnDismissListener {
352 NativeInput.setButtonParam(item.playerIndex, item.nativeButton, params)
353 settingsViewModel.setAdapterItemChanged(position)
354 }
355 }
356
357 is ModifierInputSetting -> {
358 val stickParams = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
359 val modifierParams = ParamPackage(stickParams.get("modifier", ""))
360
361 val invert = modifierParams.get("inverted", false)
362 invertButton.isVisible = true
363 invertButton.isCheckable = true
364 invertButton.isChecked = invert
365 invertButton.setOnMenuItemClickListener {
366 modifierParams.set("inverted", !invert)
367 stickParams.set("modifier", modifierParams.serialize())
368 true
369 }
370
371 val toggle = modifierParams.get("toggle", false)
372 toggleButton.isVisible = true
373 toggleButton.isCheckable = true
374 toggleButton.isChecked = toggle
375 toggleButton.setOnMenuItemClickListener {
376 modifierParams.set("toggle", !toggle)
377 stickParams.set("modifier", modifierParams.serialize())
378 true
379 }
380
381 popup.setOnDismissListener {
382 NativeInput.setStickParam(
383 item.playerIndex,
384 item.nativeAnalog,
385 stickParams
386 )
387 settingsViewModel.setAdapterItemChanged(position)
388 }
389 }
390 }
391 }
392 popup.show()
393 }
394
188 fun onLongClick(item: SettingsItem, position: Int): Boolean { 395 fun onLongClick(item: SettingsItem, position: Int): Boolean {
189 SettingsDialogFragment.newInstance( 396 SettingsDialogFragment.newInstance(
190 settingsViewModel, 397 settingsViewModel,
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt
index 60e029f34..5d1ea5d29 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt
@@ -1,7 +1,7 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project 1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later 2// SPDX-License-Identifier: GPL-2.0-or-later
3 3
4package org.yuzu.yuzu_emu.fragments 4package org.yuzu.yuzu_emu.features.settings.ui
5 5
6import android.app.Dialog 6import android.app.Dialog
7import android.content.DialogInterface 7import android.content.DialogInterface
@@ -19,11 +19,16 @@ import com.google.android.material.slider.Slider
19import kotlinx.coroutines.launch 19import kotlinx.coroutines.launch
20import org.yuzu.yuzu_emu.R 20import org.yuzu.yuzu_emu.R
21import org.yuzu.yuzu_emu.databinding.DialogSliderBinding 21import org.yuzu.yuzu_emu.databinding.DialogSliderBinding
22import org.yuzu.yuzu_emu.features.input.NativeInput
23import org.yuzu.yuzu_emu.features.input.model.AnalogDirection
24import org.yuzu.yuzu_emu.features.settings.model.view.AnalogInputSetting
25import org.yuzu.yuzu_emu.features.settings.model.view.ButtonInputSetting
26import org.yuzu.yuzu_emu.features.settings.model.view.IntSingleChoiceSetting
22import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem 27import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
23import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting 28import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting
24import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting 29import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting
25import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting 30import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting
26import org.yuzu.yuzu_emu.model.SettingsViewModel 31import org.yuzu.yuzu_emu.utils.ParamPackage
27 32
28class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener { 33class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener {
29 private var type = 0 34 private var type = 0
@@ -50,8 +55,49 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
50 MaterialAlertDialogBuilder(requireContext()) 55 MaterialAlertDialogBuilder(requireContext())
51 .setMessage(R.string.reset_setting_confirmation) 56 .setMessage(R.string.reset_setting_confirmation)
52 .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> 57 .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
53 settingsViewModel.clickedItem!!.setting.reset() 58 when (val item = settingsViewModel.clickedItem) {
54 settingsViewModel.setAdapterItemChanged(position) 59 is AnalogInputSetting -> {
60 val stickParam = NativeInput.getStickParam(
61 item.playerIndex,
62 item.nativeAnalog
63 )
64 if (stickParam.get("engine", "") == "analog_from_button") {
65 when (item.analogDirection) {
66 AnalogDirection.Up -> stickParam.erase("up")
67 AnalogDirection.Down -> stickParam.erase("down")
68 AnalogDirection.Left -> stickParam.erase("left")
69 AnalogDirection.Right -> stickParam.erase("right")
70 }
71 NativeInput.setStickParam(
72 item.playerIndex,
73 item.nativeAnalog,
74 stickParam
75 )
76 settingsViewModel.setAdapterItemChanged(position)
77 } else {
78 NativeInput.setStickParam(
79 item.playerIndex,
80 item.nativeAnalog,
81 ParamPackage()
82 )
83 settingsViewModel.setDatasetChanged(true)
84 }
85 }
86
87 is ButtonInputSetting -> {
88 NativeInput.setButtonParam(
89 item.playerIndex,
90 item.nativeButton,
91 ParamPackage()
92 )
93 settingsViewModel.setAdapterItemChanged(position)
94 }
95
96 else -> {
97 settingsViewModel.clickedItem!!.setting.reset()
98 settingsViewModel.setAdapterItemChanged(position)
99 }
100 }
55 } 101 }
56 .setNegativeButton(android.R.string.cancel, null) 102 .setNegativeButton(android.R.string.cancel, null)
57 .create() 103 .create()
@@ -61,7 +107,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
61 val item = settingsViewModel.clickedItem as SingleChoiceSetting 107 val item = settingsViewModel.clickedItem as SingleChoiceSetting
62 val value = getSelectionForSingleChoiceValue(item) 108 val value = getSelectionForSingleChoiceValue(item)
63 MaterialAlertDialogBuilder(requireContext()) 109 MaterialAlertDialogBuilder(requireContext())
64 .setTitle(item.nameId) 110 .setTitle(item.title)
65 .setSingleChoiceItems(item.choicesId, value, this) 111 .setSingleChoiceItems(item.choicesId, value, this)
66 .create() 112 .create()
67 } 113 }
@@ -81,7 +127,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
81 } 127 }
82 128
83 MaterialAlertDialogBuilder(requireContext()) 129 MaterialAlertDialogBuilder(requireContext())
84 .setTitle(item.nameId) 130 .setTitle(item.title)
85 .setView(sliderBinding.root) 131 .setView(sliderBinding.root)
86 .setPositiveButton(android.R.string.ok, this) 132 .setPositiveButton(android.R.string.ok, this)
87 .setNegativeButton(android.R.string.cancel, defaultCancelListener) 133 .setNegativeButton(android.R.string.cancel, defaultCancelListener)
@@ -91,8 +137,16 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
91 SettingsItem.TYPE_STRING_SINGLE_CHOICE -> { 137 SettingsItem.TYPE_STRING_SINGLE_CHOICE -> {
92 val item = settingsViewModel.clickedItem as StringSingleChoiceSetting 138 val item = settingsViewModel.clickedItem as StringSingleChoiceSetting
93 MaterialAlertDialogBuilder(requireContext()) 139 MaterialAlertDialogBuilder(requireContext())
94 .setTitle(item.nameId) 140 .setTitle(item.title)
95 .setSingleChoiceItems(item.choices, item.selectValueIndex, this) 141 .setSingleChoiceItems(item.choices, item.selectedValueIndex, this)
142 .create()
143 }
144
145 SettingsItem.TYPE_INT_SINGLE_CHOICE -> {
146 val item = settingsViewModel.clickedItem as IntSingleChoiceSetting
147 MaterialAlertDialogBuilder(requireContext())
148 .setTitle(item.title)
149 .setSingleChoiceItems(item.choices, item.selectedValueIndex, this)
96 .create() 150 .create()
97 } 151 }
98 152
@@ -145,6 +199,12 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
145 scSetting.setSelectedValue(value) 199 scSetting.setSelectedValue(value)
146 } 200 }
147 201
202 is IntSingleChoiceSetting -> {
203 val scSetting = settingsViewModel.clickedItem as IntSingleChoiceSetting
204 val value = scSetting.getValueAt(which)
205 scSetting.setSelectedValue(value)
206 }
207
148 is SliderSetting -> { 208 is SliderSetting -> {
149 val sliderSetting = settingsViewModel.clickedItem as SliderSetting 209 val sliderSetting = settingsViewModel.clickedItem as SliderSetting
150 sliderSetting.setSelectedValue(settingsViewModel.sliderProgress.value) 210 sliderSetting.setSelectedValue(settingsViewModel.sliderProgress.value)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt
index 6f6e7be10..0cf944b43 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt
@@ -24,8 +24,9 @@ import kotlinx.coroutines.flow.collectLatest
24import kotlinx.coroutines.launch 24import kotlinx.coroutines.launch
25import org.yuzu.yuzu_emu.R 25import org.yuzu.yuzu_emu.R
26import org.yuzu.yuzu_emu.databinding.FragmentSettingsBinding 26import org.yuzu.yuzu_emu.databinding.FragmentSettingsBinding
27import org.yuzu.yuzu_emu.features.input.NativeInput
27import org.yuzu.yuzu_emu.features.settings.model.Settings 28import org.yuzu.yuzu_emu.features.settings.model.Settings
28import org.yuzu.yuzu_emu.model.SettingsViewModel 29import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
29import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins 30import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
30 31
31class SettingsFragment : Fragment() { 32class SettingsFragment : Fragment() {
@@ -45,6 +46,12 @@ class SettingsFragment : Fragment() {
45 returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) 46 returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
46 reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) 47 reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
47 exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) 48 exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
49
50 val playerIndex = getPlayerIndex()
51 if (playerIndex != -1) {
52 NativeInput.loadInputProfiles()
53 NativeInput.reloadInputDevices()
54 }
48 } 55 }
49 56
50 override fun onCreateView( 57 override fun onCreateView(
@@ -57,8 +64,9 @@ class SettingsFragment : Fragment() {
57 } 64 }
58 65
59 // This is using the correct scope, lint is just acting up 66 // This is using the correct scope, lint is just acting up
60 @SuppressLint("UnsafeRepeatOnLifecycleDetector") 67 @SuppressLint("UnsafeRepeatOnLifecycleDetector", "NotifyDataSetChanged")
61 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 68 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
69 super.onViewCreated(view, savedInstanceState)
62 settingsAdapter = SettingsAdapter(this, requireContext()) 70 settingsAdapter = SettingsAdapter(this, requireContext())
63 presenter = SettingsFragmentPresenter( 71 presenter = SettingsFragmentPresenter(
64 settingsViewModel, 72 settingsViewModel,
@@ -71,7 +79,17 @@ class SettingsFragment : Fragment() {
71 ) { 79 ) {
72 args.game!!.title 80 args.game!!.title
73 } else { 81 } else {
74 getString(args.menuTag.titleId) 82 when (args.menuTag) {
83 Settings.MenuTag.SECTION_INPUT_PLAYER_ONE -> Settings.getPlayerString(1)
84 Settings.MenuTag.SECTION_INPUT_PLAYER_TWO -> Settings.getPlayerString(2)
85 Settings.MenuTag.SECTION_INPUT_PLAYER_THREE -> Settings.getPlayerString(3)
86 Settings.MenuTag.SECTION_INPUT_PLAYER_FOUR -> Settings.getPlayerString(4)
87 Settings.MenuTag.SECTION_INPUT_PLAYER_FIVE -> Settings.getPlayerString(5)
88 Settings.MenuTag.SECTION_INPUT_PLAYER_SIX -> Settings.getPlayerString(6)
89 Settings.MenuTag.SECTION_INPUT_PLAYER_SEVEN -> Settings.getPlayerString(7)
90 Settings.MenuTag.SECTION_INPUT_PLAYER_EIGHT -> Settings.getPlayerString(8)
91 else -> getString(args.menuTag.titleId)
92 }
75 } 93 }
76 binding.listSettings.apply { 94 binding.listSettings.apply {
77 adapter = settingsAdapter 95 adapter = settingsAdapter
@@ -93,6 +111,55 @@ class SettingsFragment : Fragment() {
93 } 111 }
94 } 112 }
95 } 113 }
114 launch {
115 repeatOnLifecycle(Lifecycle.State.STARTED) {
116 settingsViewModel.adapterItemChanged.collect {
117 if (it != -1) {
118 settingsAdapter?.notifyItemChanged(it)
119 settingsViewModel.setAdapterItemChanged(-1)
120 }
121 }
122 }
123 }
124 launch {
125 repeatOnLifecycle(Lifecycle.State.STARTED) {
126 settingsViewModel.datasetChanged.collect {
127 if (it) {
128 settingsAdapter?.notifyDataSetChanged()
129 settingsViewModel.setDatasetChanged(false)
130 }
131 }
132 }
133 }
134 launch {
135 repeatOnLifecycle(Lifecycle.State.CREATED) {
136 settingsViewModel.reloadListAndNotifyDataset.collectLatest {
137 if (it) {
138 settingsViewModel.setReloadListAndNotifyDataset(false)
139 presenter.loadSettingsList(true)
140 }
141 }
142 }
143 }
144 launch {
145 repeatOnLifecycle(Lifecycle.State.CREATED) {
146 settingsViewModel.shouldShowResetInputDialog.collectLatest {
147 if (it) {
148 MessageDialogFragment.newInstance(
149 activity = requireActivity(),
150 titleId = R.string.reset_mapping,
151 descriptionId = R.string.reset_mapping_description,
152 positiveAction = {
153 NativeInput.resetControllerMappings(getPlayerIndex())
154 settingsViewModel.setReloadListAndNotifyDataset(true)
155 },
156 negativeAction = {}
157 ).show(parentFragmentManager, MessageDialogFragment.TAG)
158 settingsViewModel.setShouldShowResetInputDialog(false)
159 }
160 }
161 }
162 }
96 } 163 }
97 164
98 if (args.menuTag == Settings.MenuTag.SECTION_ROOT) { 165 if (args.menuTag == Settings.MenuTag.SECTION_ROOT) {
@@ -115,6 +182,19 @@ class SettingsFragment : Fragment() {
115 setInsets() 182 setInsets()
116 } 183 }
117 184
185 private fun getPlayerIndex(): Int =
186 when (args.menuTag) {
187 Settings.MenuTag.SECTION_INPUT_PLAYER_ONE -> 0
188 Settings.MenuTag.SECTION_INPUT_PLAYER_TWO -> 1
189 Settings.MenuTag.SECTION_INPUT_PLAYER_THREE -> 2
190 Settings.MenuTag.SECTION_INPUT_PLAYER_FOUR -> 3
191 Settings.MenuTag.SECTION_INPUT_PLAYER_FIVE -> 4
192 Settings.MenuTag.SECTION_INPUT_PLAYER_SIX -> 5
193 Settings.MenuTag.SECTION_INPUT_PLAYER_SEVEN -> 6
194 Settings.MenuTag.SECTION_INPUT_PLAYER_EIGHT -> 7
195 else -> -1
196 }
197
118 private fun setInsets() { 198 private fun setInsets() {
119 ViewCompat.setOnApplyWindowInsetsListener( 199 ViewCompat.setOnApplyWindowInsetsListener(
120 binding.root 200 binding.root
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
index 5d495a7ca..e491c29a2 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
@@ -3,11 +3,17 @@
3 3
4package org.yuzu.yuzu_emu.features.settings.ui 4package org.yuzu.yuzu_emu.features.settings.ui
5 5
6import android.annotation.SuppressLint
6import android.os.Build 7import android.os.Build
7import android.widget.Toast 8import android.widget.Toast
8import org.yuzu.yuzu_emu.NativeLibrary 9import org.yuzu.yuzu_emu.NativeLibrary
9import org.yuzu.yuzu_emu.R 10import org.yuzu.yuzu_emu.R
10import org.yuzu.yuzu_emu.YuzuApplication 11import org.yuzu.yuzu_emu.YuzuApplication
12import org.yuzu.yuzu_emu.features.input.NativeInput
13import org.yuzu.yuzu_emu.features.input.model.AnalogDirection
14import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
15import org.yuzu.yuzu_emu.features.input.model.NativeButton
16import org.yuzu.yuzu_emu.features.input.model.NpadStyleIndex
11import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting 17import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
12import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting 18import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
13import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting 19import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
@@ -15,18 +21,21 @@ import org.yuzu.yuzu_emu.features.settings.model.ByteSetting
15import org.yuzu.yuzu_emu.features.settings.model.IntSetting 21import org.yuzu.yuzu_emu.features.settings.model.IntSetting
16import org.yuzu.yuzu_emu.features.settings.model.LongSetting 22import org.yuzu.yuzu_emu.features.settings.model.LongSetting
17import org.yuzu.yuzu_emu.features.settings.model.Settings 23import org.yuzu.yuzu_emu.features.settings.model.Settings
24import org.yuzu.yuzu_emu.features.settings.model.Settings.MenuTag
18import org.yuzu.yuzu_emu.features.settings.model.ShortSetting 25import org.yuzu.yuzu_emu.features.settings.model.ShortSetting
19import org.yuzu.yuzu_emu.features.settings.model.view.* 26import org.yuzu.yuzu_emu.features.settings.model.view.*
20import org.yuzu.yuzu_emu.model.SettingsViewModel 27import org.yuzu.yuzu_emu.utils.InputHandler
21import org.yuzu.yuzu_emu.utils.NativeConfig 28import org.yuzu.yuzu_emu.utils.NativeConfig
22 29
23class SettingsFragmentPresenter( 30class SettingsFragmentPresenter(
24 private val settingsViewModel: SettingsViewModel, 31 private val settingsViewModel: SettingsViewModel,
25 private val adapter: SettingsAdapter, 32 private val adapter: SettingsAdapter,
26 private var menuTag: Settings.MenuTag 33 private var menuTag: MenuTag
27) { 34) {
28 private var settingsList = ArrayList<SettingsItem>() 35 private var settingsList = ArrayList<SettingsItem>()
29 36
37 private val context get() = YuzuApplication.appContext
38
30 // Extension for altering settings list based on each setting's properties 39 // Extension for altering settings list based on each setting's properties
31 fun ArrayList<SettingsItem>.add(key: String) { 40 fun ArrayList<SettingsItem>.add(key: String) {
32 val item = SettingsItem.settingsItems[key]!! 41 val item = SettingsItem.settingsItems[key]!!
@@ -53,31 +62,48 @@ class SettingsFragmentPresenter(
53 add(item) 62 add(item)
54 } 63 }
55 64
65 // Allows you to show/hide abstract settings based on the paired setting key
66 fun ArrayList<SettingsItem>.addAbstract(item: SettingsItem) {
67 val pairedSettingKey = item.setting.pairedSettingKey
68 if (pairedSettingKey.isNotEmpty()) {
69 val pairedSettingsItem =
70 this.firstOrNull { it.setting.key == pairedSettingKey } ?: return
71 val pairedSetting = pairedSettingsItem.setting as AbstractBooleanSetting
72 if (!pairedSetting.getBoolean(!NativeConfig.isPerGameConfigLoaded())) return
73 }
74 add(item)
75 }
76
56 fun onViewCreated() { 77 fun onViewCreated() {
57 loadSettingsList() 78 loadSettingsList()
58 } 79 }
59 80
60 fun loadSettingsList() { 81 @SuppressLint("NotifyDataSetChanged")
82 fun loadSettingsList(notifyDataSetChanged: Boolean = false) {
61 val sl = ArrayList<SettingsItem>() 83 val sl = ArrayList<SettingsItem>()
62 when (menuTag) { 84 when (menuTag) {
63 Settings.MenuTag.SECTION_ROOT -> addConfigSettings(sl) 85 MenuTag.SECTION_ROOT -> addConfigSettings(sl)
64 Settings.MenuTag.SECTION_SYSTEM -> addSystemSettings(sl) 86 MenuTag.SECTION_SYSTEM -> addSystemSettings(sl)
65 Settings.MenuTag.SECTION_RENDERER -> addGraphicsSettings(sl) 87 MenuTag.SECTION_RENDERER -> addGraphicsSettings(sl)
66 Settings.MenuTag.SECTION_AUDIO -> addAudioSettings(sl) 88 MenuTag.SECTION_AUDIO -> addAudioSettings(sl)
67 Settings.MenuTag.SECTION_THEME -> addThemeSettings(sl) 89 MenuTag.SECTION_INPUT -> addInputSettings(sl)
68 Settings.MenuTag.SECTION_DEBUG -> addDebugSettings(sl) 90 MenuTag.SECTION_INPUT_PLAYER_ONE -> addInputPlayer(sl, 0)
69 else -> { 91 MenuTag.SECTION_INPUT_PLAYER_TWO -> addInputPlayer(sl, 1)
70 val context = YuzuApplication.appContext 92 MenuTag.SECTION_INPUT_PLAYER_THREE -> addInputPlayer(sl, 2)
71 Toast.makeText( 93 MenuTag.SECTION_INPUT_PLAYER_FOUR -> addInputPlayer(sl, 3)
72 context, 94 MenuTag.SECTION_INPUT_PLAYER_FIVE -> addInputPlayer(sl, 4)
73 context.getString(R.string.unimplemented_menu), 95 MenuTag.SECTION_INPUT_PLAYER_SIX -> addInputPlayer(sl, 5)
74 Toast.LENGTH_SHORT 96 MenuTag.SECTION_INPUT_PLAYER_SEVEN -> addInputPlayer(sl, 6)
75 ).show() 97 MenuTag.SECTION_INPUT_PLAYER_EIGHT -> addInputPlayer(sl, 7)
76 return 98 MenuTag.SECTION_THEME -> addThemeSettings(sl)
77 } 99 MenuTag.SECTION_DEBUG -> addDebugSettings(sl)
78 } 100 }
79 settingsList = sl 101 settingsList = sl
80 adapter.submitList(settingsList) 102 adapter.submitList(settingsList) {
103 if (notifyDataSetChanged) {
104 adapter.notifyDataSetChanged()
105 }
106 }
81 } 107 }
82 108
83 private fun addConfigSettings(sl: ArrayList<SettingsItem>) { 109 private fun addConfigSettings(sl: ArrayList<SettingsItem>) {
@@ -118,6 +144,7 @@ class SettingsFragmentPresenter(
118 RunnableSetting( 144 RunnableSetting(
119 titleId = R.string.reset_to_default, 145 titleId = R.string.reset_to_default,
120 descriptionId = R.string.reset_to_default_description, 146 descriptionId = R.string.reset_to_default_description,
147 isRunnable = !NativeLibrary.isRunning(),
121 iconId = R.drawable.ic_restore 148 iconId = R.drawable.ic_restore
122 ) { settingsViewModel.setShouldShowResetSettingsDialog(true) } 149 ) { settingsViewModel.setShouldShowResetSettingsDialog(true) }
123 ) 150 )
@@ -163,6 +190,671 @@ class SettingsFragmentPresenter(
163 } 190 }
164 } 191 }
165 192
193 private fun addInputSettings(sl: ArrayList<SettingsItem>) {
194 settingsViewModel.currentDevice = 0
195
196 if (NativeConfig.isPerGameConfigLoaded()) {
197 NativeInput.loadInputProfiles()
198 val profiles = NativeInput.getInputProfileNames().toMutableList()
199 profiles.add(0, "")
200 val prettyProfiles = profiles.toTypedArray()
201 prettyProfiles[0] =
202 context.getString(R.string.use_global_input_configuration)
203 sl.apply {
204 for (i in 0 until 8) {
205 add(
206 IntSingleChoiceSetting(
207 getPerGameProfileSetting(profiles, i),
208 titleString = getPlayerProfileString(i + 1),
209 choices = prettyProfiles,
210 values = IntArray(profiles.size) { it }.toTypedArray()
211 )
212 )
213 }
214 }
215 return
216 }
217
218 val getConnectedIcon: (Int) -> Int = { playerIndex: Int ->
219 if (NativeInput.getIsConnected(playerIndex)) {
220 R.drawable.ic_controller
221 } else {
222 R.drawable.ic_controller_disconnected
223 }
224 }
225
226 val inputSettings = NativeConfig.getInputSettings(true)
227 sl.apply {
228 add(
229 SubmenuSetting(
230 titleString = Settings.getPlayerString(1),
231 descriptionString = inputSettings[0].profileName,
232 menuKey = MenuTag.SECTION_INPUT_PLAYER_ONE,
233 iconId = getConnectedIcon(0)
234 )
235 )
236 add(
237 SubmenuSetting(
238 titleString = Settings.getPlayerString(2),
239 descriptionString = inputSettings[1].profileName,
240 menuKey = MenuTag.SECTION_INPUT_PLAYER_TWO,
241 iconId = getConnectedIcon(1)
242 )
243 )
244 add(
245 SubmenuSetting(
246 titleString = Settings.getPlayerString(3),
247 descriptionString = inputSettings[2].profileName,
248 menuKey = MenuTag.SECTION_INPUT_PLAYER_THREE,
249 iconId = getConnectedIcon(2)
250 )
251 )
252 add(
253 SubmenuSetting(
254 titleString = Settings.getPlayerString(4),
255 descriptionString = inputSettings[3].profileName,
256 menuKey = MenuTag.SECTION_INPUT_PLAYER_FOUR,
257 iconId = getConnectedIcon(3)
258 )
259 )
260 add(
261 SubmenuSetting(
262 titleString = Settings.getPlayerString(5),
263 descriptionString = inputSettings[4].profileName,
264 menuKey = MenuTag.SECTION_INPUT_PLAYER_FIVE,
265 iconId = getConnectedIcon(4)
266 )
267 )
268 add(
269 SubmenuSetting(
270 titleString = Settings.getPlayerString(6),
271 descriptionString = inputSettings[5].profileName,
272 menuKey = MenuTag.SECTION_INPUT_PLAYER_SIX,
273 iconId = getConnectedIcon(5)
274 )
275 )
276 add(
277 SubmenuSetting(
278 titleString = Settings.getPlayerString(7),
279 descriptionString = inputSettings[6].profileName,
280 menuKey = MenuTag.SECTION_INPUT_PLAYER_SEVEN,
281 iconId = getConnectedIcon(6)
282 )
283 )
284 add(
285 SubmenuSetting(
286 titleString = Settings.getPlayerString(8),
287 descriptionString = inputSettings[7].profileName,
288 menuKey = MenuTag.SECTION_INPUT_PLAYER_EIGHT,
289 iconId = getConnectedIcon(7)
290 )
291 )
292 }
293 }
294
295 private fun getPlayerProfileString(player: Int): String =
296 context.getString(R.string.player_num_profile, player)
297
298 private fun getPerGameProfileSetting(
299 profiles: List<String>,
300 playerIndex: Int
301 ): AbstractIntSetting {
302 return object : AbstractIntSetting {
303 private val players
304 get() = NativeConfig.getInputSettings(false)
305
306 override val key = ""
307
308 override fun getInt(needsGlobal: Boolean): Int {
309 val currentProfile = players[playerIndex].profileName
310 profiles.forEachIndexed { i, profile ->
311 if (profile == currentProfile) {
312 return i
313 }
314 }
315 return 0
316 }
317
318 override fun setInt(value: Int) {
319 NativeInput.loadPerGameConfiguration(playerIndex, value, profiles[value])
320 NativeInput.connectControllers(playerIndex)
321 NativeConfig.saveControlPlayerValues()
322 }
323
324 override val defaultValue = 0
325
326 override fun getValueAsString(needsGlobal: Boolean): String = getInt().toString()
327
328 override fun reset() = setInt(defaultValue)
329
330 override var global = true
331
332 override val isRuntimeModifiable = true
333
334 override val isSaveable = true
335 }
336 }
337
338 private fun addInputPlayer(sl: ArrayList<SettingsItem>, playerIndex: Int) {
339 sl.apply {
340 val connectedSetting = object : AbstractBooleanSetting {
341 override val key = "connected"
342
343 override fun getBoolean(needsGlobal: Boolean): Boolean =
344 NativeInput.getIsConnected(playerIndex)
345
346 override fun setBoolean(value: Boolean) =
347 NativeInput.connectControllers(playerIndex, value)
348
349 override val defaultValue = playerIndex == 0
350
351 override fun getValueAsString(needsGlobal: Boolean): String =
352 getBoolean(needsGlobal).toString()
353
354 override fun reset() = setBoolean(defaultValue)
355 }
356 add(SwitchSetting(connectedSetting, R.string.connected))
357
358 val styleTags = NativeInput.getSupportedStyleTags(playerIndex)
359 val npadType = object : AbstractIntSetting {
360 override val key = "npad_type"
361 override fun getInt(needsGlobal: Boolean): Int {
362 val styleIndex = NativeInput.getStyleIndex(playerIndex)
363 return styleTags.indexOfFirst { it == styleIndex }
364 }
365
366 override fun setInt(value: Int) {
367 NativeInput.setStyleIndex(playerIndex, styleTags[value])
368 settingsViewModel.setReloadListAndNotifyDataset(true)
369 }
370
371 override val defaultValue = NpadStyleIndex.Fullkey.int
372 override fun getValueAsString(needsGlobal: Boolean): String = getInt().toString()
373 override fun reset() = setInt(defaultValue)
374 override val pairedSettingKey: String = "connected"
375 }
376 addAbstract(
377 IntSingleChoiceSetting(
378 npadType,
379 titleId = R.string.controller_type,
380 choices = styleTags.map { context.getString(it.nameId) }
381 .toTypedArray(),
382 values = IntArray(styleTags.size) { it }.toTypedArray()
383 )
384 )
385
386 InputHandler.updateControllerData()
387
388 val autoMappingSetting = object : AbstractIntSetting {
389 override val key = "auto_mapping_device"
390
391 override fun getInt(needsGlobal: Boolean): Int = -1
392
393 override fun setInt(value: Int) {
394 val registeredController = InputHandler.registeredControllers[value + 1]
395 val displayName = registeredController.get(
396 "display",
397 context.getString(R.string.unknown)
398 )
399 NativeInput.updateMappingsWithDefault(
400 playerIndex,
401 registeredController,
402 displayName
403 )
404 Toast.makeText(
405 context,
406 context.getString(R.string.attempted_auto_map, displayName),
407 Toast.LENGTH_SHORT
408 ).show()
409 settingsViewModel.setReloadListAndNotifyDataset(true)
410 }
411
412 override val defaultValue = -1
413
414 override fun getValueAsString(needsGlobal: Boolean) = getInt().toString()
415
416 override fun reset() = setInt(defaultValue)
417
418 override val isRuntimeModifiable: Boolean = true
419 }
420
421 val unknownString = context.getString(R.string.unknown)
422 val prettyAutoMappingControllerList = InputHandler.registeredControllers.mapNotNull {
423 val port = it.get("port", -1)
424 return@mapNotNull if (port == 100 || port == -1) {
425 null
426 } else {
427 it.get("display", unknownString)
428 }
429 }.toTypedArray()
430 add(
431 IntSingleChoiceSetting(
432 autoMappingSetting,
433 titleId = R.string.auto_map,
434 descriptionId = R.string.auto_map_description,
435 choices = prettyAutoMappingControllerList,
436 values = IntArray(prettyAutoMappingControllerList.size) { it }.toTypedArray()
437 )
438 )
439
440 val mappingFilterSetting = object : AbstractIntSetting {
441 override val key = "mapping_filter"
442
443 override fun getInt(needsGlobal: Boolean): Int = settingsViewModel.currentDevice
444
445 override fun setInt(value: Int) {
446 settingsViewModel.currentDevice = value
447 }
448
449 override val defaultValue = 0
450
451 override fun getValueAsString(needsGlobal: Boolean) = getInt().toString()
452
453 override fun reset() = setInt(defaultValue)
454
455 override val isRuntimeModifiable: Boolean = true
456 }
457
458 val prettyControllerList = InputHandler.registeredControllers.mapNotNull {
459 return@mapNotNull if (it.get("port", 0) == 100) {
460 null
461 } else {
462 it.get("display", unknownString)
463 }
464 }.toTypedArray()
465 add(
466 IntSingleChoiceSetting(
467 mappingFilterSetting,
468 titleId = R.string.input_mapping_filter,
469 descriptionId = R.string.input_mapping_filter_description,
470 choices = prettyControllerList,
471 values = IntArray(prettyControllerList.size) { it }.toTypedArray()
472 )
473 )
474
475 add(InputProfileSetting(playerIndex))
476 add(
477 RunnableSetting(titleId = R.string.reset_to_default, isRunnable = true) {
478 settingsViewModel.setShouldShowResetInputDialog(true)
479 }
480 )
481
482 val styleIndex = NativeInput.getStyleIndex(playerIndex)
483
484 // Buttons
485 when (styleIndex) {
486 NpadStyleIndex.Fullkey,
487 NpadStyleIndex.Handheld,
488 NpadStyleIndex.JoyconDual -> {
489 add(HeaderSetting(R.string.buttons))
490 add(ButtonInputSetting(playerIndex, NativeButton.A, R.string.button_a))
491 add(ButtonInputSetting(playerIndex, NativeButton.B, R.string.button_b))
492 add(ButtonInputSetting(playerIndex, NativeButton.X, R.string.button_x))
493 add(ButtonInputSetting(playerIndex, NativeButton.Y, R.string.button_y))
494 add(ButtonInputSetting(playerIndex, NativeButton.Plus, R.string.button_plus))
495 add(ButtonInputSetting(playerIndex, NativeButton.Minus, R.string.button_minus))
496 add(ButtonInputSetting(playerIndex, NativeButton.Home, R.string.button_home))
497 add(
498 ButtonInputSetting(
499 playerIndex,
500 NativeButton.Capture,
501 R.string.button_capture
502 )
503 )
504 }
505
506 NpadStyleIndex.JoyconLeft -> {
507 add(HeaderSetting(R.string.buttons))
508 add(ButtonInputSetting(playerIndex, NativeButton.Minus, R.string.button_minus))
509 add(
510 ButtonInputSetting(
511 playerIndex,
512 NativeButton.Capture,
513 R.string.button_capture
514 )
515 )
516 }
517
518 NpadStyleIndex.JoyconRight -> {
519 add(HeaderSetting(R.string.buttons))
520 add(ButtonInputSetting(playerIndex, NativeButton.A, R.string.button_a))
521 add(ButtonInputSetting(playerIndex, NativeButton.B, R.string.button_b))
522 add(ButtonInputSetting(playerIndex, NativeButton.X, R.string.button_x))
523 add(ButtonInputSetting(playerIndex, NativeButton.Y, R.string.button_y))
524 add(ButtonInputSetting(playerIndex, NativeButton.Plus, R.string.button_plus))
525 add(ButtonInputSetting(playerIndex, NativeButton.Home, R.string.button_home))
526 }
527
528 NpadStyleIndex.GameCube -> {
529 add(HeaderSetting(R.string.buttons))
530 add(ButtonInputSetting(playerIndex, NativeButton.A, R.string.button_a))
531 add(ButtonInputSetting(playerIndex, NativeButton.B, R.string.button_b))
532 add(ButtonInputSetting(playerIndex, NativeButton.X, R.string.button_x))
533 add(ButtonInputSetting(playerIndex, NativeButton.Y, R.string.button_y))
534 add(ButtonInputSetting(playerIndex, NativeButton.Plus, R.string.start_pause))
535 }
536
537 else -> {
538 // No-op
539 }
540 }
541
542 when (styleIndex) {
543 NpadStyleIndex.Fullkey,
544 NpadStyleIndex.Handheld,
545 NpadStyleIndex.JoyconDual,
546 NpadStyleIndex.JoyconLeft -> {
547 add(HeaderSetting(R.string.dpad))
548 add(ButtonInputSetting(playerIndex, NativeButton.DUp, R.string.up))
549 add(ButtonInputSetting(playerIndex, NativeButton.DDown, R.string.down))
550 add(ButtonInputSetting(playerIndex, NativeButton.DLeft, R.string.left))
551 add(ButtonInputSetting(playerIndex, NativeButton.DRight, R.string.right))
552 }
553
554 else -> {
555 // No-op
556 }
557 }
558
559 // Left stick
560 when (styleIndex) {
561 NpadStyleIndex.Fullkey,
562 NpadStyleIndex.Handheld,
563 NpadStyleIndex.JoyconDual,
564 NpadStyleIndex.JoyconLeft -> {
565 add(HeaderSetting(R.string.left_stick))
566 addAll(getStickDirections(playerIndex, NativeAnalog.LStick))
567 add(ButtonInputSetting(playerIndex, NativeButton.LStick, R.string.pressed))
568 addAll(getExtraStickSettings(playerIndex, NativeAnalog.LStick))
569 }
570
571 NpadStyleIndex.GameCube -> {
572 add(HeaderSetting(R.string.control_stick))
573 addAll(getStickDirections(playerIndex, NativeAnalog.LStick))
574 addAll(getExtraStickSettings(playerIndex, NativeAnalog.LStick))
575 }
576
577 else -> {
578 // No-op
579 }
580 }
581
582 // Right stick
583 when (styleIndex) {
584 NpadStyleIndex.Fullkey,
585 NpadStyleIndex.Handheld,
586 NpadStyleIndex.JoyconDual,
587 NpadStyleIndex.JoyconRight -> {
588 add(HeaderSetting(R.string.right_stick))
589 addAll(getStickDirections(playerIndex, NativeAnalog.RStick))
590 add(ButtonInputSetting(playerIndex, NativeButton.RStick, R.string.pressed))
591 addAll(getExtraStickSettings(playerIndex, NativeAnalog.RStick))
592 }
593
594 NpadStyleIndex.GameCube -> {
595 add(HeaderSetting(R.string.c_stick))
596 addAll(getStickDirections(playerIndex, NativeAnalog.RStick))
597 addAll(getExtraStickSettings(playerIndex, NativeAnalog.RStick))
598 }
599
600 else -> {
601 // No-op
602 }
603 }
604
605 // L/R, ZL/ZR, and SL/SR
606 when (styleIndex) {
607 NpadStyleIndex.Fullkey,
608 NpadStyleIndex.Handheld -> {
609 add(HeaderSetting(R.string.triggers))
610 add(ButtonInputSetting(playerIndex, NativeButton.L, R.string.button_l))
611 add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_r))
612 add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_zl))
613 add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_zr))
614 }
615
616 NpadStyleIndex.JoyconDual -> {
617 add(HeaderSetting(R.string.triggers))
618 add(ButtonInputSetting(playerIndex, NativeButton.L, R.string.button_l))
619 add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_r))
620 add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_zl))
621 add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_zr))
622 add(
623 ButtonInputSetting(
624 playerIndex,
625 NativeButton.SLLeft,
626 R.string.button_sl_left
627 )
628 )
629 add(
630 ButtonInputSetting(
631 playerIndex,
632 NativeButton.SRLeft,
633 R.string.button_sr_left
634 )
635 )
636 add(
637 ButtonInputSetting(
638 playerIndex,
639 NativeButton.SLRight,
640 R.string.button_sl_right
641 )
642 )
643 add(
644 ButtonInputSetting(
645 playerIndex,
646 NativeButton.SRRight,
647 R.string.button_sr_right
648 )
649 )
650 }
651
652 NpadStyleIndex.JoyconLeft -> {
653 add(HeaderSetting(R.string.triggers))
654 add(ButtonInputSetting(playerIndex, NativeButton.L, R.string.button_l))
655 add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_zl))
656 add(
657 ButtonInputSetting(
658 playerIndex,
659 NativeButton.SLLeft,
660 R.string.button_sl_left
661 )
662 )
663 add(
664 ButtonInputSetting(
665 playerIndex,
666 NativeButton.SRLeft,
667 R.string.button_sr_left
668 )
669 )
670 }
671
672 NpadStyleIndex.JoyconRight -> {
673 add(HeaderSetting(R.string.triggers))
674 add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_r))
675 add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_zr))
676 add(
677 ButtonInputSetting(
678 playerIndex,
679 NativeButton.SLRight,
680 R.string.button_sl_right
681 )
682 )
683 add(
684 ButtonInputSetting(
685 playerIndex,
686 NativeButton.SRRight,
687 R.string.button_sr_right
688 )
689 )
690 }
691
692 NpadStyleIndex.GameCube -> {
693 add(HeaderSetting(R.string.triggers))
694 add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_z))
695 add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_l))
696 add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_r))
697 }
698
699 else -> {
700 // No-op
701 }
702 }
703
704 add(HeaderSetting(R.string.vibration))
705 val vibrationEnabledSetting = object : AbstractBooleanSetting {
706 override val key = "vibration"
707
708 override fun getBoolean(needsGlobal: Boolean): Boolean =
709 NativeConfig.getInputSettings(true)[playerIndex].vibrationEnabled
710
711 override fun setBoolean(value: Boolean) {
712 val settings = NativeConfig.getInputSettings(true)
713 settings[playerIndex].vibrationEnabled = value
714 NativeConfig.setInputSettings(settings, true)
715 }
716
717 override val defaultValue = true
718
719 override fun getValueAsString(needsGlobal: Boolean): String =
720 getBoolean(needsGlobal).toString()
721
722 override fun reset() = setBoolean(defaultValue)
723 }
724 add(SwitchSetting(vibrationEnabledSetting, R.string.vibration))
725
726 val useSystemVibratorSetting = object : AbstractBooleanSetting {
727 override val key = ""
728
729 override fun getBoolean(needsGlobal: Boolean): Boolean =
730 NativeConfig.getInputSettings(true)[playerIndex].useSystemVibrator
731
732 override fun setBoolean(value: Boolean) {
733 val settings = NativeConfig.getInputSettings(true)
734 settings[playerIndex].useSystemVibrator = value
735 NativeConfig.setInputSettings(settings, true)
736 }
737
738 override val defaultValue = playerIndex == 0
739
740 override fun getValueAsString(needsGlobal: Boolean): String =
741 getBoolean(needsGlobal).toString()
742
743 override fun reset() = setBoolean(defaultValue)
744
745 override val pairedSettingKey: String = "vibration"
746 }
747 addAbstract(SwitchSetting(useSystemVibratorSetting, R.string.use_system_vibrator))
748
749 val vibrationStrengthSetting = object : AbstractIntSetting {
750 override val key = ""
751
752 override fun getInt(needsGlobal: Boolean): Int =
753 NativeConfig.getInputSettings(true)[playerIndex].vibrationStrength
754
755 override fun setInt(value: Int) {
756 val settings = NativeConfig.getInputSettings(true)
757 settings[playerIndex].vibrationStrength = value
758 NativeConfig.setInputSettings(settings, true)
759 }
760
761 override val defaultValue = 100
762
763 override fun getValueAsString(needsGlobal: Boolean): String =
764 getInt(needsGlobal).toString()
765
766 override fun reset() = setInt(defaultValue)
767
768 override val pairedSettingKey: String = "vibration"
769 }
770 addAbstract(
771 SliderSetting(vibrationStrengthSetting, R.string.vibration_strength, units = "%")
772 )
773 }
774 }
775
776 // Convenience function for creating AbstractIntSettings for modifier range/stick range/stick deadzones
777 private fun getStickIntSettingFromParam(
778 playerIndex: Int,
779 paramName: String,
780 stick: NativeAnalog,
781 defaultValue: Int
782 ): AbstractIntSetting =
783 object : AbstractIntSetting {
784 val params get() = NativeInput.getStickParam(playerIndex, stick)
785
786 override val key = ""
787
788 override fun getInt(needsGlobal: Boolean): Int =
789 (params.get(paramName, 0.15f) * 100).toInt()
790
791 override fun setInt(value: Int) {
792 val tempParams = params
793 tempParams.set(paramName, value.toFloat() / 100)
794 NativeInput.setStickParam(playerIndex, stick, tempParams)
795 }
796
797 override val defaultValue = defaultValue
798
799 override fun getValueAsString(needsGlobal: Boolean): String =
800 getInt(needsGlobal).toString()
801
802 override fun reset() = setInt(defaultValue)
803 }
804
805 private fun getExtraStickSettings(
806 playerIndex: Int,
807 nativeAnalog: NativeAnalog
808 ): List<SettingsItem> {
809 val stickIsController =
810 NativeInput.isController(NativeInput.getStickParam(playerIndex, nativeAnalog))
811 val modifierRangeSetting =
812 getStickIntSettingFromParam(playerIndex, "modifier_scale", nativeAnalog, 50)
813 val stickRangeSetting =
814 getStickIntSettingFromParam(playerIndex, "range", nativeAnalog, 95)
815 val stickDeadzoneSetting =
816 getStickIntSettingFromParam(playerIndex, "deadzone", nativeAnalog, 15)
817
818 val out = mutableListOf<SettingsItem>().apply {
819 if (stickIsController) {
820 add(SliderSetting(stickRangeSetting, titleId = R.string.range, min = 25, max = 150))
821 add(SliderSetting(stickDeadzoneSetting, R.string.deadzone))
822 } else {
823 add(ModifierInputSetting(playerIndex, NativeAnalog.LStick, R.string.modifier))
824 add(SliderSetting(modifierRangeSetting, R.string.modifier_range))
825 }
826 }
827 return out
828 }
829
830 private fun getStickDirections(player: Int, stick: NativeAnalog): List<AnalogInputSetting> =
831 listOf(
832 AnalogInputSetting(
833 player,
834 stick,
835 AnalogDirection.Up,
836 R.string.up
837 ),
838 AnalogInputSetting(
839 player,
840 stick,
841 AnalogDirection.Down,
842 R.string.down
843 ),
844 AnalogInputSetting(
845 player,
846 stick,
847 AnalogDirection.Left,
848 R.string.left
849 ),
850 AnalogInputSetting(
851 player,
852 stick,
853 AnalogDirection.Right,
854 R.string.right
855 )
856 )
857
166 private fun addThemeSettings(sl: ArrayList<SettingsItem>) { 858 private fun addThemeSettings(sl: ArrayList<SettingsItem>) {
167 sl.apply { 859 sl.apply {
168 val theme: AbstractIntSetting = object : AbstractIntSetting { 860 val theme: AbstractIntSetting = object : AbstractIntSetting {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsSearchFragment.kt
index a135b80b4..51740a2ac 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsSearchFragment.kt
@@ -1,7 +1,7 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project 1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later 2// SPDX-License-Identifier: GPL-2.0-or-later
3 3
4package org.yuzu.yuzu_emu.fragments 4package org.yuzu.yuzu_emu.features.settings.ui
5 5
6import android.content.Context 6import android.content.Context
7import android.os.Bundle 7import android.os.Bundle
@@ -26,8 +26,6 @@ import kotlinx.coroutines.launch
26import org.yuzu.yuzu_emu.R 26import org.yuzu.yuzu_emu.R
27import org.yuzu.yuzu_emu.databinding.FragmentSettingsSearchBinding 27import org.yuzu.yuzu_emu.databinding.FragmentSettingsSearchBinding
28import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem 28import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
29import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
30import org.yuzu.yuzu_emu.model.SettingsViewModel
31import org.yuzu.yuzu_emu.utils.NativeConfig 29import org.yuzu.yuzu_emu.utils.NativeConfig
32import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins 30import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
33 31
@@ -119,7 +117,7 @@ class SettingsSearchFragment : Fragment() {
119 val baseList = SettingsItem.settingsItems 117 val baseList = SettingsItem.settingsItems
120 val similarityAlgorithm = if (searchTerm.length > 2) Cosine() else Cosine(1) 118 val similarityAlgorithm = if (searchTerm.length > 2) Cosine() else Cosine(1)
121 val sortedList: List<SettingsItem> = baseList.mapNotNull { item -> 119 val sortedList: List<SettingsItem> = baseList.mapNotNull { item ->
122 val title = getString(item.value.nameId).lowercase() 120 val title = item.value.title.lowercase()
123 val similarity = similarityAlgorithm.similarity(searchTerm, title) 121 val similarity = similarityAlgorithm.similarity(searchTerm, title)
124 if (similarity > 0.08) { 122 if (similarity > 0.08) {
125 Pair(similarity, item) 123 Pair(similarity, item)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsViewModel.kt
index 5cb6a5d57..fbdca04e9 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsViewModel.kt
@@ -1,20 +1,26 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project 1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later 2// SPDX-License-Identifier: GPL-2.0-or-later
3 3
4package org.yuzu.yuzu_emu.model 4package org.yuzu.yuzu_emu.features.settings.ui
5 5
6import androidx.lifecycle.ViewModel 6import androidx.lifecycle.ViewModel
7import kotlinx.coroutines.flow.MutableStateFlow 7import kotlinx.coroutines.flow.MutableStateFlow
8import kotlinx.coroutines.flow.StateFlow 8import kotlinx.coroutines.flow.StateFlow
9import kotlinx.coroutines.flow.asStateFlow
9import org.yuzu.yuzu_emu.R 10import org.yuzu.yuzu_emu.R
10import org.yuzu.yuzu_emu.YuzuApplication 11import org.yuzu.yuzu_emu.YuzuApplication
11import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem 12import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
13import org.yuzu.yuzu_emu.model.Game
14import org.yuzu.yuzu_emu.utils.InputHandler
15import org.yuzu.yuzu_emu.utils.ParamPackage
12 16
13class SettingsViewModel : ViewModel() { 17class SettingsViewModel : ViewModel() {
14 var game: Game? = null 18 var game: Game? = null
15 19
16 var clickedItem: SettingsItem? = null 20 var clickedItem: SettingsItem? = null
17 21
22 var currentDevice = 0
23
18 val shouldRecreate: StateFlow<Boolean> get() = _shouldRecreate 24 val shouldRecreate: StateFlow<Boolean> get() = _shouldRecreate
19 private val _shouldRecreate = MutableStateFlow(false) 25 private val _shouldRecreate = MutableStateFlow(false)
20 26
@@ -36,6 +42,18 @@ class SettingsViewModel : ViewModel() {
36 val adapterItemChanged: StateFlow<Int> get() = _adapterItemChanged 42 val adapterItemChanged: StateFlow<Int> get() = _adapterItemChanged
37 private val _adapterItemChanged = MutableStateFlow(-1) 43 private val _adapterItemChanged = MutableStateFlow(-1)
38 44
45 private val _datasetChanged = MutableStateFlow(false)
46 val datasetChanged = _datasetChanged.asStateFlow()
47
48 private val _reloadListAndNotifyDataset = MutableStateFlow(false)
49 val reloadListAndNotifyDataset = _reloadListAndNotifyDataset.asStateFlow()
50
51 private val _shouldShowDeleteProfileDialog = MutableStateFlow("")
52 val shouldShowDeleteProfileDialog = _shouldShowDeleteProfileDialog.asStateFlow()
53
54 private val _shouldShowResetInputDialog = MutableStateFlow(false)
55 val shouldShowResetInputDialog = _shouldShowResetInputDialog.asStateFlow()
56
39 fun setShouldRecreate(value: Boolean) { 57 fun setShouldRecreate(value: Boolean) {
40 _shouldRecreate.value = value 58 _shouldRecreate.value = value
41 } 59 }
@@ -68,4 +86,27 @@ class SettingsViewModel : ViewModel() {
68 fun setAdapterItemChanged(value: Int) { 86 fun setAdapterItemChanged(value: Int) {
69 _adapterItemChanged.value = value 87 _adapterItemChanged.value = value
70 } 88 }
89
90 fun setDatasetChanged(value: Boolean) {
91 _datasetChanged.value = value
92 }
93
94 fun setReloadListAndNotifyDataset(value: Boolean) {
95 _reloadListAndNotifyDataset.value = value
96 }
97
98 fun setShouldShowDeleteProfileDialog(profile: String) {
99 _shouldShowDeleteProfileDialog.value = profile
100 }
101
102 fun setShouldShowResetInputDialog(value: Boolean) {
103 _shouldShowResetInputDialog.value = value
104 }
105
106 fun getCurrentDeviceParams(defaultParams: ParamPackage): ParamPackage =
107 try {
108 InputHandler.registeredControllers[currentDevice]
109 } catch (e: IndexOutOfBoundsException) {
110 defaultParams
111 }
71} 112}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputProfileViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputProfileViewHolder.kt
new file mode 100644
index 000000000..81161d5d3
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputProfileViewHolder.kt
@@ -0,0 +1,33 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.ui.viewholder
5
6import android.view.View
7import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
8import org.yuzu.yuzu_emu.features.settings.model.view.InputProfileSetting
9import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
10import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
11import org.yuzu.yuzu_emu.R
12
13class InputProfileViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
14 SettingViewHolder(binding.root, adapter) {
15 private lateinit var setting: InputProfileSetting
16
17 override fun bind(item: SettingsItem) {
18 setting = item as InputProfileSetting
19 binding.textSettingName.text = setting.title
20 binding.textSettingValue.text =
21 setting.getCurrentProfile().ifEmpty { binding.root.context.getString(R.string.not_set) }
22
23 binding.textSettingDescription.visibility = View.GONE
24 binding.buttonClear.visibility = View.GONE
25 binding.icon.visibility = View.GONE
26 binding.buttonClear.visibility = View.GONE
27 }
28
29 override fun onClick(clicked: View) =
30 adapter.onInputProfileClick(setting, bindingAdapterPosition)
31
32 override fun onLongClick(clicked: View): Boolean = false
33}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputViewHolder.kt
new file mode 100644
index 000000000..1f1f08190
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputViewHolder.kt
@@ -0,0 +1,71 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.ui.viewholder
5
6import android.view.View
7import org.yuzu.yuzu_emu.databinding.ListItemSettingInputBinding
8import org.yuzu.yuzu_emu.features.input.NativeInput
9import org.yuzu.yuzu_emu.features.settings.model.view.AnalogInputSetting
10import org.yuzu.yuzu_emu.features.settings.model.view.ButtonInputSetting
11import org.yuzu.yuzu_emu.features.settings.model.view.InputSetting
12import org.yuzu.yuzu_emu.features.settings.model.view.ModifierInputSetting
13import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
14import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
15
16class InputViewHolder(val binding: ListItemSettingInputBinding, adapter: SettingsAdapter) :
17 SettingViewHolder(binding.root, adapter) {
18 private lateinit var setting: InputSetting
19
20 override fun bind(item: SettingsItem) {
21 setting = item as InputSetting
22 binding.textSettingName.text = setting.title
23 binding.textSettingValue.text = setting.getSelectedValue()
24
25 binding.buttonOptions.visibility = when (item) {
26 is AnalogInputSetting -> {
27 val param = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
28 if (
29 param.get("engine", "") == "analog_from_button" ||
30 param.has("axis_x") || param.has("axis_y")
31 ) {
32 View.VISIBLE
33 } else {
34 View.GONE
35 }
36 }
37
38 is ButtonInputSetting -> {
39 val param = NativeInput.getButtonParam(item.playerIndex, item.nativeButton)
40 if (
41 param.has("code") || param.has("button") || param.has("hat") ||
42 param.has("axis")
43 ) {
44 View.VISIBLE
45 } else {
46 View.GONE
47 }
48 }
49
50 is ModifierInputSetting -> {
51 val params = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
52 if (params.has("modifier")) {
53 View.VISIBLE
54 } else {
55 View.GONE
56 }
57 }
58 }
59
60 binding.buttonOptions.setOnClickListener(null)
61 binding.buttonOptions.setOnClickListener {
62 adapter.onInputOptionsClick(binding.buttonOptions, setting, bindingAdapterPosition)
63 }
64 }
65
66 override fun onClick(clicked: View) =
67 adapter.onInputClick(setting, bindingAdapterPosition)
68
69 override fun onLongClick(clicked: View): Boolean =
70 adapter.onLongClick(setting, bindingAdapterPosition)
71}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt
index 2cecede48..9705d428c 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt
@@ -47,6 +47,9 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
47 binding.textSettingValue.text = item.getChoiceAt(item.getSelectedValue()) 47 binding.textSettingValue.text = item.getChoiceAt(item.getSelectedValue())
48 } 48 }
49 } 49 }
50 if (binding.textSettingValue.text.isEmpty()) {
51 binding.textSettingValue.visibility = View.GONE
52 }
50 53
51 binding.buttonClear.visibility = if (setting.setting.global || 54 binding.buttonClear.visibility = if (setting.setting.global ||
52 !NativeConfig.isPerGameConfigLoaded() 55 !NativeConfig.isPerGameConfigLoaded()
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
index 6b25cc525..c737ed5e8 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
@@ -277,6 +277,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
277 true 277 true
278 } 278 }
279 279
280 R.id.menu_controls -> {
281 val action = HomeNavigationDirections.actionGlobalSettingsActivity(
282 null,
283 Settings.MenuTag.SECTION_INPUT
284 )
285 binding.root.findNavController().navigate(action)
286 true
287 }
288
280 R.id.menu_overlay_controls -> { 289 R.id.menu_overlay_controls -> {
281 showOverlayOptions() 290 showOverlayOptions()
282 true 291 true
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
index 87e130d3e..14a2504b6 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
@@ -91,6 +91,20 @@ class HomeSettingsFragment : Fragment() {
91 ) 91 )
92 add( 92 add(
93 HomeSetting( 93 HomeSetting(
94 R.string.preferences_controls,
95 R.string.preferences_controls_description,
96 R.drawable.ic_controller,
97 {
98 val action = HomeNavigationDirections.actionGlobalSettingsActivity(
99 null,
100 Settings.MenuTag.SECTION_INPUT
101 )
102 binding.root.findNavController().navigate(action)
103 }
104 )
105 )
106 add(
107 HomeSetting(
94 R.string.gpu_driver_manager, 108 R.string.gpu_driver_manager,
95 R.string.install_gpu_driver_description, 109 R.string.install_gpu_driver_description,
96 R.drawable.ic_build, 110 R.drawable.ic_build,
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt
index c87486c90..66907085a 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt
@@ -24,10 +24,10 @@ import androidx.core.content.ContextCompat
24import androidx.window.layout.WindowMetricsCalculator 24import androidx.window.layout.WindowMetricsCalculator
25import kotlin.math.max 25import kotlin.math.max
26import kotlin.math.min 26import kotlin.math.min
27import org.yuzu.yuzu_emu.NativeLibrary 27import org.yuzu.yuzu_emu.features.input.NativeInput
28import org.yuzu.yuzu_emu.NativeLibrary.ButtonType
29import org.yuzu.yuzu_emu.NativeLibrary.StickType
30import org.yuzu.yuzu_emu.R 28import org.yuzu.yuzu_emu.R
29import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
30import org.yuzu.yuzu_emu.features.input.model.NativeButton
31import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting 31import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
32import org.yuzu.yuzu_emu.features.settings.model.IntSetting 32import org.yuzu.yuzu_emu.features.settings.model.IntSetting
33import org.yuzu.yuzu_emu.overlay.model.OverlayControl 33import org.yuzu.yuzu_emu.overlay.model.OverlayControl
@@ -100,19 +100,19 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
100 100
101 var shouldUpdateView = false 101 var shouldUpdateView = false
102 val playerIndex = 102 val playerIndex =
103 if (NativeLibrary.isHandheldOnly()) { 103 if (NativeInput.isHandheldOnly()) {
104 NativeLibrary.ConsoleDevice 104 NativeInput.ConsoleDevice
105 } else { 105 } else {
106 NativeLibrary.Player1Device 106 NativeInput.Player1Device
107 } 107 }
108 108
109 for (button in overlayButtons) { 109 for (button in overlayButtons) {
110 if (!button.updateStatus(event)) { 110 if (!button.updateStatus(event)) {
111 continue 111 continue
112 } 112 }
113 NativeLibrary.onGamePadButtonEvent( 113 NativeInput.onOverlayButtonEvent(
114 playerIndex, 114 playerIndex,
115 button.buttonId, 115 button.button,
116 button.status 116 button.status
117 ) 117 )
118 playHaptics(event) 118 playHaptics(event)
@@ -123,24 +123,24 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
123 if (!dpad.updateStatus(event, BooleanSetting.DPAD_SLIDE.getBoolean())) { 123 if (!dpad.updateStatus(event, BooleanSetting.DPAD_SLIDE.getBoolean())) {
124 continue 124 continue
125 } 125 }
126 NativeLibrary.onGamePadButtonEvent( 126 NativeInput.onOverlayButtonEvent(
127 playerIndex, 127 playerIndex,
128 dpad.upId, 128 dpad.up,
129 dpad.upStatus 129 dpad.upStatus
130 ) 130 )
131 NativeLibrary.onGamePadButtonEvent( 131 NativeInput.onOverlayButtonEvent(
132 playerIndex, 132 playerIndex,
133 dpad.downId, 133 dpad.down,
134 dpad.downStatus 134 dpad.downStatus
135 ) 135 )
136 NativeLibrary.onGamePadButtonEvent( 136 NativeInput.onOverlayButtonEvent(
137 playerIndex, 137 playerIndex,
138 dpad.leftId, 138 dpad.left,
139 dpad.leftStatus 139 dpad.leftStatus
140 ) 140 )
141 NativeLibrary.onGamePadButtonEvent( 141 NativeInput.onOverlayButtonEvent(
142 playerIndex, 142 playerIndex,
143 dpad.rightId, 143 dpad.right,
144 dpad.rightStatus 144 dpad.rightStatus
145 ) 145 )
146 playHaptics(event) 146 playHaptics(event)
@@ -151,16 +151,15 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
151 if (!joystick.updateStatus(event)) { 151 if (!joystick.updateStatus(event)) {
152 continue 152 continue
153 } 153 }
154 val axisID = joystick.joystickId 154 NativeInput.onOverlayJoystickEvent(
155 NativeLibrary.onGamePadJoystickEvent(
156 playerIndex, 155 playerIndex,
157 axisID, 156 joystick.joystick,
158 joystick.xAxis, 157 joystick.xAxis,
159 joystick.realYAxis 158 joystick.realYAxis
160 ) 159 )
161 NativeLibrary.onGamePadButtonEvent( 160 NativeInput.onOverlayButtonEvent(
162 playerIndex, 161 playerIndex,
163 joystick.buttonId, 162 joystick.button,
164 joystick.buttonStatus 163 joystick.buttonStatus
165 ) 164 )
166 playHaptics(event) 165 playHaptics(event)
@@ -187,7 +186,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
187 motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP 186 motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP
188 187
189 if (isActionDown && !isTouchInputConsumed(pointerId)) { 188 if (isActionDown && !isTouchInputConsumed(pointerId)) {
190 NativeLibrary.onTouchPressed(pointerId, xPosition.toFloat(), yPosition.toFloat()) 189 NativeInput.onTouchPressed(pointerId, xPosition.toFloat(), yPosition.toFloat())
191 } 190 }
192 191
193 if (isActionMove) { 192 if (isActionMove) {
@@ -196,12 +195,12 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
196 if (isTouchInputConsumed(fingerId)) { 195 if (isTouchInputConsumed(fingerId)) {
197 continue 196 continue
198 } 197 }
199 NativeLibrary.onTouchMoved(fingerId, event.getX(i), event.getY(i)) 198 NativeInput.onTouchMoved(fingerId, event.getX(i), event.getY(i))
200 } 199 }
201 } 200 }
202 201
203 if (isActionUp && !isTouchInputConsumed(pointerId)) { 202 if (isActionUp && !isTouchInputConsumed(pointerId)) {
204 NativeLibrary.onTouchReleased(pointerId) 203 NativeInput.onTouchReleased(pointerId)
205 } 204 }
206 205
207 return true 206 return true
@@ -359,7 +358,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
359 windowSize, 358 windowSize,
360 R.drawable.facebutton_a, 359 R.drawable.facebutton_a,
361 R.drawable.facebutton_a_depressed, 360 R.drawable.facebutton_a_depressed,
362 ButtonType.BUTTON_A, 361 NativeButton.A,
363 data, 362 data,
364 position 363 position
365 ) 364 )
@@ -373,7 +372,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
373 windowSize, 372 windowSize,
374 R.drawable.facebutton_b, 373 R.drawable.facebutton_b,
375 R.drawable.facebutton_b_depressed, 374 R.drawable.facebutton_b_depressed,
376 ButtonType.BUTTON_B, 375 NativeButton.B,
377 data, 376 data,
378 position 377 position
379 ) 378 )
@@ -387,7 +386,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
387 windowSize, 386 windowSize,
388 R.drawable.facebutton_x, 387 R.drawable.facebutton_x,
389 R.drawable.facebutton_x_depressed, 388 R.drawable.facebutton_x_depressed,
390 ButtonType.BUTTON_X, 389 NativeButton.X,
391 data, 390 data,
392 position 391 position
393 ) 392 )
@@ -401,7 +400,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
401 windowSize, 400 windowSize,
402 R.drawable.facebutton_y, 401 R.drawable.facebutton_y,
403 R.drawable.facebutton_y_depressed, 402 R.drawable.facebutton_y_depressed,
404 ButtonType.BUTTON_Y, 403 NativeButton.Y,
405 data, 404 data,
406 position 405 position
407 ) 406 )
@@ -415,7 +414,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
415 windowSize, 414 windowSize,
416 R.drawable.facebutton_plus, 415 R.drawable.facebutton_plus,
417 R.drawable.facebutton_plus_depressed, 416 R.drawable.facebutton_plus_depressed,
418 ButtonType.BUTTON_PLUS, 417 NativeButton.Plus,
419 data, 418 data,
420 position 419 position
421 ) 420 )
@@ -429,7 +428,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
429 windowSize, 428 windowSize,
430 R.drawable.facebutton_minus, 429 R.drawable.facebutton_minus,
431 R.drawable.facebutton_minus_depressed, 430 R.drawable.facebutton_minus_depressed,
432 ButtonType.BUTTON_MINUS, 431 NativeButton.Minus,
433 data, 432 data,
434 position 433 position
435 ) 434 )
@@ -443,7 +442,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
443 windowSize, 442 windowSize,
444 R.drawable.facebutton_home, 443 R.drawable.facebutton_home,
445 R.drawable.facebutton_home_depressed, 444 R.drawable.facebutton_home_depressed,
446 ButtonType.BUTTON_HOME, 445 NativeButton.Home,
447 data, 446 data,
448 position 447 position
449 ) 448 )
@@ -457,7 +456,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
457 windowSize, 456 windowSize,
458 R.drawable.facebutton_screenshot, 457 R.drawable.facebutton_screenshot,
459 R.drawable.facebutton_screenshot_depressed, 458 R.drawable.facebutton_screenshot_depressed,
460 ButtonType.BUTTON_CAPTURE, 459 NativeButton.Capture,
461 data, 460 data,
462 position 461 position
463 ) 462 )
@@ -471,7 +470,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
471 windowSize, 470 windowSize,
472 R.drawable.l_shoulder, 471 R.drawable.l_shoulder,
473 R.drawable.l_shoulder_depressed, 472 R.drawable.l_shoulder_depressed,
474 ButtonType.TRIGGER_L, 473 NativeButton.L,
475 data, 474 data,
476 position 475 position
477 ) 476 )
@@ -485,7 +484,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
485 windowSize, 484 windowSize,
486 R.drawable.r_shoulder, 485 R.drawable.r_shoulder,
487 R.drawable.r_shoulder_depressed, 486 R.drawable.r_shoulder_depressed,
488 ButtonType.TRIGGER_R, 487 NativeButton.R,
489 data, 488 data,
490 position 489 position
491 ) 490 )
@@ -499,7 +498,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
499 windowSize, 498 windowSize,
500 R.drawable.zl_trigger, 499 R.drawable.zl_trigger,
501 R.drawable.zl_trigger_depressed, 500 R.drawable.zl_trigger_depressed,
502 ButtonType.TRIGGER_ZL, 501 NativeButton.ZL,
503 data, 502 data,
504 position 503 position
505 ) 504 )
@@ -513,7 +512,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
513 windowSize, 512 windowSize,
514 R.drawable.zr_trigger, 513 R.drawable.zr_trigger,
515 R.drawable.zr_trigger_depressed, 514 R.drawable.zr_trigger_depressed,
516 ButtonType.TRIGGER_ZR, 515 NativeButton.ZR,
517 data, 516 data,
518 position 517 position
519 ) 518 )
@@ -527,7 +526,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
527 windowSize, 526 windowSize,
528 R.drawable.button_l3, 527 R.drawable.button_l3,
529 R.drawable.button_l3_depressed, 528 R.drawable.button_l3_depressed,
530 ButtonType.STICK_L, 529 NativeButton.LStick,
531 data, 530 data,
532 position 531 position
533 ) 532 )
@@ -541,7 +540,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
541 windowSize, 540 windowSize,
542 R.drawable.button_r3, 541 R.drawable.button_r3,
543 R.drawable.button_r3_depressed, 542 R.drawable.button_r3_depressed,
544 ButtonType.STICK_R, 543 NativeButton.RStick,
545 data, 544 data,
546 position 545 position
547 ) 546 )
@@ -556,8 +555,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
556 R.drawable.joystick_range, 555 R.drawable.joystick_range,
557 R.drawable.joystick, 556 R.drawable.joystick,
558 R.drawable.joystick_depressed, 557 R.drawable.joystick_depressed,
559 StickType.STICK_L, 558 NativeAnalog.LStick,
560 ButtonType.STICK_L, 559 NativeButton.LStick,
561 data, 560 data,
562 position 561 position
563 ) 562 )
@@ -572,8 +571,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
572 R.drawable.joystick_range, 571 R.drawable.joystick_range,
573 R.drawable.joystick, 572 R.drawable.joystick,
574 R.drawable.joystick_depressed, 573 R.drawable.joystick_depressed,
575 StickType.STICK_R, 574 NativeAnalog.RStick,
576 ButtonType.STICK_R, 575 NativeButton.RStick,
577 data, 576 data,
578 position 577 position
579 ) 578 )
@@ -835,7 +834,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
835 windowSize: Pair<Point, Point>, 834 windowSize: Pair<Point, Point>,
836 defaultResId: Int, 835 defaultResId: Int,
837 pressedResId: Int, 836 pressedResId: Int,
838 buttonId: Int, 837 button: NativeButton,
839 overlayControlData: OverlayControlData, 838 overlayControlData: OverlayControlData,
840 position: Pair<Double, Double> 839 position: Pair<Double, Double>
841 ): InputOverlayDrawableButton { 840 ): InputOverlayDrawableButton {
@@ -869,7 +868,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
869 res, 868 res,
870 defaultStateBitmap, 869 defaultStateBitmap,
871 pressedStateBitmap, 870 pressedStateBitmap,
872 buttonId, 871 button,
873 overlayControlData 872 overlayControlData
874 ) 873 )
875 874
@@ -940,11 +939,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
940 res, 939 res,
941 defaultStateBitmap, 940 defaultStateBitmap,
942 pressedOneDirectionStateBitmap, 941 pressedOneDirectionStateBitmap,
943 pressedTwoDirectionsStateBitmap, 942 pressedTwoDirectionsStateBitmap
944 ButtonType.DPAD_UP,
945 ButtonType.DPAD_DOWN,
946 ButtonType.DPAD_LEFT,
947 ButtonType.DPAD_RIGHT
948 ) 943 )
949 944
950 // Get the minimum and maximum coordinates of the screen where the button can be placed. 945 // Get the minimum and maximum coordinates of the screen where the button can be placed.
@@ -993,8 +988,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
993 resOuter: Int, 988 resOuter: Int,
994 defaultResInner: Int, 989 defaultResInner: Int,
995 pressedResInner: Int, 990 pressedResInner: Int,
996 joystick: Int, 991 joystick: NativeAnalog,
997 buttonId: Int, 992 button: NativeButton,
998 overlayControlData: OverlayControlData, 993 overlayControlData: OverlayControlData,
999 position: Pair<Double, Double> 994 position: Pair<Double, Double>
1000 ): InputOverlayDrawableJoystick { 995 ): InputOverlayDrawableJoystick {
@@ -1042,7 +1037,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
1042 outerRect, 1037 outerRect,
1043 innerRect, 1038 innerRect,
1044 joystick, 1039 joystick,
1045 buttonId, 1040 button,
1046 overlayControlData.id 1041 overlayControlData.id
1047 ) 1042 )
1048 1043
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt
index b14a4f96e..fee3d04ee 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt
@@ -9,7 +9,8 @@ import android.graphics.Canvas
9import android.graphics.Rect 9import android.graphics.Rect
10import android.graphics.drawable.BitmapDrawable 10import android.graphics.drawable.BitmapDrawable
11import android.view.MotionEvent 11import android.view.MotionEvent
12import org.yuzu.yuzu_emu.NativeLibrary.ButtonState 12import org.yuzu.yuzu_emu.features.input.NativeInput.ButtonState
13import org.yuzu.yuzu_emu.features.input.model.NativeButton
13import org.yuzu.yuzu_emu.overlay.model.OverlayControlData 14import org.yuzu.yuzu_emu.overlay.model.OverlayControlData
14 15
15/** 16/**
@@ -19,13 +20,13 @@ import org.yuzu.yuzu_emu.overlay.model.OverlayControlData
19 * @param res [Resources] instance. 20 * @param res [Resources] instance.
20 * @param defaultStateBitmap [Bitmap] to use with the default state Drawable. 21 * @param defaultStateBitmap [Bitmap] to use with the default state Drawable.
21 * @param pressedStateBitmap [Bitmap] to use with the pressed state Drawable. 22 * @param pressedStateBitmap [Bitmap] to use with the pressed state Drawable.
22 * @param buttonId Identifier for this type of button. 23 * @param button [NativeButton] for this type of button.
23 */ 24 */
24class InputOverlayDrawableButton( 25class InputOverlayDrawableButton(
25 res: Resources, 26 res: Resources,
26 defaultStateBitmap: Bitmap, 27 defaultStateBitmap: Bitmap,
27 pressedStateBitmap: Bitmap, 28 pressedStateBitmap: Bitmap,
28 val buttonId: Int, 29 val button: NativeButton,
29 val overlayControlData: OverlayControlData 30 val overlayControlData: OverlayControlData
30) { 31) {
31 // The ID value what motion event is tracking 32 // The ID value what motion event is tracking
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt
index 8aef6f5a5..0cb6ff244 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt
@@ -9,7 +9,8 @@ import android.graphics.Canvas
9import android.graphics.Rect 9import android.graphics.Rect
10import android.graphics.drawable.BitmapDrawable 10import android.graphics.drawable.BitmapDrawable
11import android.view.MotionEvent 11import android.view.MotionEvent
12import org.yuzu.yuzu_emu.NativeLibrary.ButtonState 12import org.yuzu.yuzu_emu.features.input.NativeInput.ButtonState
13import org.yuzu.yuzu_emu.features.input.model.NativeButton
13 14
14/** 15/**
15 * Custom [BitmapDrawable] that is capable 16 * Custom [BitmapDrawable] that is capable
@@ -19,20 +20,12 @@ import org.yuzu.yuzu_emu.NativeLibrary.ButtonState
19 * @param defaultStateBitmap [Bitmap] of the default state. 20 * @param defaultStateBitmap [Bitmap] of the default state.
20 * @param pressedOneDirectionStateBitmap [Bitmap] of the pressed state in one direction. 21 * @param pressedOneDirectionStateBitmap [Bitmap] of the pressed state in one direction.
21 * @param pressedTwoDirectionsStateBitmap [Bitmap] of the pressed state in two direction. 22 * @param pressedTwoDirectionsStateBitmap [Bitmap] of the pressed state in two direction.
22 * @param buttonUp Identifier for the up button.
23 * @param buttonDown Identifier for the down button.
24 * @param buttonLeft Identifier for the left button.
25 * @param buttonRight Identifier for the right button.
26 */ 23 */
27class InputOverlayDrawableDpad( 24class InputOverlayDrawableDpad(
28 res: Resources, 25 res: Resources,
29 defaultStateBitmap: Bitmap, 26 defaultStateBitmap: Bitmap,
30 pressedOneDirectionStateBitmap: Bitmap, 27 pressedOneDirectionStateBitmap: Bitmap,
31 pressedTwoDirectionsStateBitmap: Bitmap, 28 pressedTwoDirectionsStateBitmap: Bitmap
32 buttonUp: Int,
33 buttonDown: Int,
34 buttonLeft: Int,
35 buttonRight: Int
36) { 29) {
37 /** 30 /**
38 * Gets one of the InputOverlayDrawableDpad's button IDs. 31 * Gets one of the InputOverlayDrawableDpad's button IDs.
@@ -40,10 +33,10 @@ class InputOverlayDrawableDpad(
40 * @return the requested InputOverlayDrawableDpad's button ID. 33 * @return the requested InputOverlayDrawableDpad's button ID.
41 */ 34 */
42 // The ID identifying what type of button this Drawable represents. 35 // The ID identifying what type of button this Drawable represents.
43 val upId: Int 36 val up = NativeButton.DUp
44 val downId: Int 37 val down = NativeButton.DDown
45 val leftId: Int 38 val left = NativeButton.DLeft
46 val rightId: Int 39 val right = NativeButton.DRight
47 var trackId: Int 40 var trackId: Int
48 41
49 val width: Int 42 val width: Int
@@ -69,10 +62,6 @@ class InputOverlayDrawableDpad(
69 this.pressedTwoDirectionsStateBitmap = BitmapDrawable(res, pressedTwoDirectionsStateBitmap) 62 this.pressedTwoDirectionsStateBitmap = BitmapDrawable(res, pressedTwoDirectionsStateBitmap)
70 width = this.defaultStateBitmap.intrinsicWidth 63 width = this.defaultStateBitmap.intrinsicWidth
71 height = this.defaultStateBitmap.intrinsicHeight 64 height = this.defaultStateBitmap.intrinsicHeight
72 upId = buttonUp
73 downId = buttonDown
74 leftId = buttonLeft
75 rightId = buttonRight
76 trackId = -1 65 trackId = -1
77 } 66 }
78 67
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt
index 113bf7c24..4b07107fc 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt
@@ -13,7 +13,9 @@ import kotlin.math.atan2
13import kotlin.math.cos 13import kotlin.math.cos
14import kotlin.math.sin 14import kotlin.math.sin
15import kotlin.math.sqrt 15import kotlin.math.sqrt
16import org.yuzu.yuzu_emu.NativeLibrary 16import org.yuzu.yuzu_emu.features.input.NativeInput.ButtonState
17import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
18import org.yuzu.yuzu_emu.features.input.model.NativeButton
17import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting 19import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
18 20
19/** 21/**
@@ -26,8 +28,8 @@ import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
26 * @param bitmapInnerPressed [Bitmap] which represents the pressed inner movable part of the joystick. 28 * @param bitmapInnerPressed [Bitmap] which represents the pressed inner movable part of the joystick.
27 * @param rectOuter [Rect] which represents the outer joystick bounds. 29 * @param rectOuter [Rect] which represents the outer joystick bounds.
28 * @param rectInner [Rect] which represents the inner joystick bounds. 30 * @param rectInner [Rect] which represents the inner joystick bounds.
29 * @param joystickId The ID value what type of joystick this Drawable represents. 31 * @param joystick The [NativeAnalog] this Drawable represents.
30 * @param buttonId The ID value what type of button this Drawable represents. 32 * @param button The [NativeButton] this Drawable represents.
31 */ 33 */
32class InputOverlayDrawableJoystick( 34class InputOverlayDrawableJoystick(
33 res: Resources, 35 res: Resources,
@@ -36,8 +38,8 @@ class InputOverlayDrawableJoystick(
36 bitmapInnerPressed: Bitmap, 38 bitmapInnerPressed: Bitmap,
37 rectOuter: Rect, 39 rectOuter: Rect,
38 rectInner: Rect, 40 rectInner: Rect,
39 val joystickId: Int, 41 val joystick: NativeAnalog,
40 val buttonId: Int, 42 val button: NativeButton,
41 val prefId: String 43 val prefId: String
42) { 44) {
43 // The ID value what motion event is tracking 45 // The ID value what motion event is tracking
@@ -69,8 +71,7 @@ class InputOverlayDrawableJoystick(
69 71
70 // TODO: Add button support 72 // TODO: Add button support
71 val buttonStatus: Int 73 val buttonStatus: Int
72 get() = 74 get() = ButtonState.RELEASED
73 NativeLibrary.ButtonState.RELEASED
74 var bounds: Rect 75 var bounds: Rect
75 get() = outerBitmap.bounds 76 get() = outerBitmap.bounds
76 set(bounds) { 77 set(bounds) {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt
index e63382e1d..2c7356e6a 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt
@@ -6,439 +6,89 @@ package org.yuzu.yuzu_emu.utils
6import android.view.InputDevice 6import android.view.InputDevice
7import android.view.KeyEvent 7import android.view.KeyEvent
8import android.view.MotionEvent 8import android.view.MotionEvent
9import kotlin.math.sqrt 9import org.yuzu.yuzu_emu.features.input.NativeInput
10import org.yuzu.yuzu_emu.NativeLibrary 10import org.yuzu.yuzu_emu.features.input.YuzuInputOverlayDevice
11import org.yuzu.yuzu_emu.features.input.YuzuPhysicalDevice
11 12
12object InputHandler { 13object InputHandler {
13 private var controllerIds = getGameControllerIds() 14 var androidControllers = mapOf<Int, YuzuPhysicalDevice>()
14 15 var registeredControllers = mutableListOf<ParamPackage>()
15 fun initialize() {
16 // Connect first controller
17 NativeLibrary.onGamePadConnectEvent(getPlayerNumber(NativeLibrary.Player1Device))
18 }
19
20 fun updateControllerIds() {
21 controllerIds = getGameControllerIds()
22 }
23 16
24 fun dispatchKeyEvent(event: KeyEvent): Boolean { 17 fun dispatchKeyEvent(event: KeyEvent): Boolean {
25 val button: Int = when (event.device.vendorId) {
26 0x045E -> getInputXboxButtonKey(event.keyCode)
27 0x054C -> getInputDS5ButtonKey(event.keyCode)
28 0x057E -> getInputJoyconButtonKey(event.keyCode)
29 0x1532 -> getInputRazerButtonKey(event.keyCode)
30 0x3537 -> getInputRedmagicButtonKey(event.keyCode)
31 0x358A -> getInputBackboneLabsButtonKey(event.keyCode)
32 else -> getInputGenericButtonKey(event.keyCode)
33 }
34
35 val action = when (event.action) { 18 val action = when (event.action) {
36 KeyEvent.ACTION_DOWN -> NativeLibrary.ButtonState.PRESSED 19 KeyEvent.ACTION_DOWN -> NativeInput.ButtonState.PRESSED
37 KeyEvent.ACTION_UP -> NativeLibrary.ButtonState.RELEASED 20 KeyEvent.ACTION_UP -> NativeInput.ButtonState.RELEASED
38 else -> return false 21 else -> return false
39 } 22 }
40 23
41 // Ignore invalid buttons 24 var controllerData = androidControllers[event.device.controllerNumber]
42 if (button < 0) { 25 if (controllerData == null) {
43 return false 26 updateControllerData()
27 controllerData = androidControllers[event.device.controllerNumber] ?: return false
44 } 28 }
45 29
46 return NativeLibrary.onGamePadButtonEvent( 30 NativeInput.onGamePadButtonEvent(
47 getPlayerNumber(event.device.controllerNumber, event.deviceId), 31 controllerData.getGUID(),
48 button, 32 controllerData.getPort(),
33 event.keyCode,
49 action 34 action
50 ) 35 )
36 return true
51 } 37 }
52 38
53 fun dispatchGenericMotionEvent(event: MotionEvent): Boolean { 39 fun dispatchGenericMotionEvent(event: MotionEvent): Boolean {
54 val device = event.device 40 val controllerData =
55 // Check every axis input available on the controller 41 androidControllers[event.device.controllerNumber] ?: return false
56 for (range in device.motionRanges) { 42 event.device.motionRanges.forEach {
57 val axis = range.axis 43 NativeInput.onGamePadAxisEvent(
58 when (device.vendorId) { 44 controllerData.getGUID(),
59 0x045E -> setGenericAxisInput(event, axis) 45 controllerData.getPort(),
60 0x054C -> setGenericAxisInput(event, axis) 46 it.axis,
61 0x057E -> setJoyconAxisInput(event, axis) 47 event.getAxisValue(it.axis)
62 0x1532 -> setRazerAxisInput(event, axis) 48 )
63 else -> setGenericAxisInput(event, axis)
64 }
65 } 49 }
66
67 return true 50 return true
68 } 51 }
69 52
70 private fun getPlayerNumber(index: Int, deviceId: Int = -1): Int { 53 fun getDevices(): Map<Int, YuzuPhysicalDevice> {
71 var deviceIndex = index 54 val gameControllerDeviceIds = mutableMapOf<Int, YuzuPhysicalDevice>()
72 if (deviceId != -1) {
73 deviceIndex = controllerIds[deviceId] ?: 0
74 }
75
76 // TODO: Joycons are handled as different controllers. Find a way to merge them.
77 return when (deviceIndex) {
78 2 -> NativeLibrary.Player2Device
79 3 -> NativeLibrary.Player3Device
80 4 -> NativeLibrary.Player4Device
81 5 -> NativeLibrary.Player5Device
82 6 -> NativeLibrary.Player6Device
83 7 -> NativeLibrary.Player7Device
84 8 -> NativeLibrary.Player8Device
85 else -> if (NativeLibrary.isHandheldOnly()) {
86 NativeLibrary.ConsoleDevice
87 } else {
88 NativeLibrary.Player1Device
89 }
90 }
91 }
92
93 private fun setStickState(playerNumber: Int, index: Int, xAxis: Float, yAxis: Float) {
94 // Calculate vector size
95 val r2 = xAxis * xAxis + yAxis * yAxis
96 var r = sqrt(r2.toDouble()).toFloat()
97
98 // Adjust range of joystick
99 val deadzone = 0.15f
100 var x = xAxis
101 var y = yAxis
102
103 if (r > deadzone) {
104 val deadzoneFactor = 1.0f / r * (r - deadzone) / (1.0f - deadzone)
105 x *= deadzoneFactor
106 y *= deadzoneFactor
107 r *= deadzoneFactor
108 } else {
109 x = 0.0f
110 y = 0.0f
111 }
112
113 // Normalize joystick
114 if (r > 1.0f) {
115 x /= r
116 y /= r
117 }
118
119 NativeLibrary.onGamePadJoystickEvent(
120 playerNumber,
121 index,
122 x,
123 -y
124 )
125 }
126
127 private fun getAxisToButton(axis: Float): Int {
128 return if (axis > 0.5f) {
129 NativeLibrary.ButtonState.PRESSED
130 } else {
131 NativeLibrary.ButtonState.RELEASED
132 }
133 }
134
135 private fun setAxisDpadState(playerNumber: Int, xAxis: Float, yAxis: Float) {
136 NativeLibrary.onGamePadButtonEvent(
137 playerNumber,
138 NativeLibrary.ButtonType.DPAD_UP,
139 getAxisToButton(-yAxis)
140 )
141 NativeLibrary.onGamePadButtonEvent(
142 playerNumber,
143 NativeLibrary.ButtonType.DPAD_DOWN,
144 getAxisToButton(yAxis)
145 )
146 NativeLibrary.onGamePadButtonEvent(
147 playerNumber,
148 NativeLibrary.ButtonType.DPAD_LEFT,
149 getAxisToButton(-xAxis)
150 )
151 NativeLibrary.onGamePadButtonEvent(
152 playerNumber,
153 NativeLibrary.ButtonType.DPAD_RIGHT,
154 getAxisToButton(xAxis)
155 )
156 }
157
158 private fun getInputDS5ButtonKey(key: Int): Int {
159 // The missing ds5 buttons are axis
160 return when (key) {
161 KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B
162 KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A
163 KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y
164 KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X
165 KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
166 KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
167 KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
168 KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
169 KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
170 KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
171 else -> -1
172 }
173 }
174
175 private fun getInputJoyconButtonKey(key: Int): Int {
176 // Joycon support is half dead. A lot of buttons can't be mapped
177 return when (key) {
178 KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B
179 KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A
180 KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_X
181 KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_Y
182 KeyEvent.KEYCODE_DPAD_UP -> NativeLibrary.ButtonType.DPAD_UP
183 KeyEvent.KEYCODE_DPAD_DOWN -> NativeLibrary.ButtonType.DPAD_DOWN
184 KeyEvent.KEYCODE_DPAD_LEFT -> NativeLibrary.ButtonType.DPAD_LEFT
185 KeyEvent.KEYCODE_DPAD_RIGHT -> NativeLibrary.ButtonType.DPAD_RIGHT
186 KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
187 KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
188 KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL
189 KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR
190 KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
191 KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
192 KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
193 KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
194 else -> -1
195 }
196 }
197
198 private fun getInputXboxButtonKey(key: Int): Int {
199 // The missing xbox buttons are axis
200 return when (key) {
201 KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_A
202 KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_B
203 KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_X
204 KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_Y
205 KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
206 KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
207 KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
208 KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
209 KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
210 KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
211 else -> -1
212 }
213 }
214
215 private fun getInputRazerButtonKey(key: Int): Int {
216 // The missing xbox buttons are axis
217 return when (key) {
218 KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B
219 KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A
220 KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y
221 KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X
222 KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
223 KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
224 KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
225 KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
226 KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
227 KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
228 else -> -1
229 }
230 }
231
232 private fun getInputRedmagicButtonKey(key: Int): Int {
233 return when (key) {
234 KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B
235 KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A
236 KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y
237 KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X
238 KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
239 KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
240 KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL
241 KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR
242 KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
243 KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
244 KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
245 KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
246 else -> -1
247 }
248 }
249
250 private fun getInputBackboneLabsButtonKey(key: Int): Int {
251 return when (key) {
252 KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B
253 KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A
254 KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y
255 KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X
256 KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
257 KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
258 KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL
259 KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR
260 KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
261 KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
262 KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
263 KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
264 else -> -1
265 }
266 }
267
268 private fun getInputGenericButtonKey(key: Int): Int {
269 return when (key) {
270 KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_A
271 KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_B
272 KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_X
273 KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_Y
274 KeyEvent.KEYCODE_DPAD_UP -> NativeLibrary.ButtonType.DPAD_UP
275 KeyEvent.KEYCODE_DPAD_DOWN -> NativeLibrary.ButtonType.DPAD_DOWN
276 KeyEvent.KEYCODE_DPAD_LEFT -> NativeLibrary.ButtonType.DPAD_LEFT
277 KeyEvent.KEYCODE_DPAD_RIGHT -> NativeLibrary.ButtonType.DPAD_RIGHT
278 KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
279 KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
280 KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL
281 KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR
282 KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
283 KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
284 KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
285 KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
286 else -> -1
287 }
288 }
289
290 private fun setGenericAxisInput(event: MotionEvent, axis: Int) {
291 val playerNumber = getPlayerNumber(event.device.controllerNumber, event.deviceId)
292
293 when (axis) {
294 MotionEvent.AXIS_X, MotionEvent.AXIS_Y ->
295 setStickState(
296 playerNumber,
297 NativeLibrary.StickType.STICK_L,
298 event.getAxisValue(MotionEvent.AXIS_X),
299 event.getAxisValue(MotionEvent.AXIS_Y)
300 )
301 MotionEvent.AXIS_RX, MotionEvent.AXIS_RY ->
302 setStickState(
303 playerNumber,
304 NativeLibrary.StickType.STICK_R,
305 event.getAxisValue(MotionEvent.AXIS_RX),
306 event.getAxisValue(MotionEvent.AXIS_RY)
307 )
308 MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ ->
309 setStickState(
310 playerNumber,
311 NativeLibrary.StickType.STICK_R,
312 event.getAxisValue(MotionEvent.AXIS_Z),
313 event.getAxisValue(MotionEvent.AXIS_RZ)
314 )
315 MotionEvent.AXIS_LTRIGGER ->
316 NativeLibrary.onGamePadButtonEvent(
317 playerNumber,
318 NativeLibrary.ButtonType.TRIGGER_ZL,
319 getAxisToButton(event.getAxisValue(MotionEvent.AXIS_LTRIGGER))
320 )
321 MotionEvent.AXIS_BRAKE ->
322 NativeLibrary.onGamePadButtonEvent(
323 playerNumber,
324 NativeLibrary.ButtonType.TRIGGER_ZL,
325 getAxisToButton(event.getAxisValue(MotionEvent.AXIS_BRAKE))
326 )
327 MotionEvent.AXIS_RTRIGGER ->
328 NativeLibrary.onGamePadButtonEvent(
329 playerNumber,
330 NativeLibrary.ButtonType.TRIGGER_ZR,
331 getAxisToButton(event.getAxisValue(MotionEvent.AXIS_RTRIGGER))
332 )
333 MotionEvent.AXIS_GAS ->
334 NativeLibrary.onGamePadButtonEvent(
335 playerNumber,
336 NativeLibrary.ButtonType.TRIGGER_ZR,
337 getAxisToButton(event.getAxisValue(MotionEvent.AXIS_GAS))
338 )
339 MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y ->
340 setAxisDpadState(
341 playerNumber,
342 event.getAxisValue(MotionEvent.AXIS_HAT_X),
343 event.getAxisValue(MotionEvent.AXIS_HAT_Y)
344 )
345 }
346 }
347
348 private fun setJoyconAxisInput(event: MotionEvent, axis: Int) {
349 // Joycon support is half dead. Right joystick doesn't work
350 val playerNumber = getPlayerNumber(event.device.controllerNumber, event.deviceId)
351
352 when (axis) {
353 MotionEvent.AXIS_X, MotionEvent.AXIS_Y ->
354 setStickState(
355 playerNumber,
356 NativeLibrary.StickType.STICK_L,
357 event.getAxisValue(MotionEvent.AXIS_X),
358 event.getAxisValue(MotionEvent.AXIS_Y)
359 )
360 MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ ->
361 setStickState(
362 playerNumber,
363 NativeLibrary.StickType.STICK_R,
364 event.getAxisValue(MotionEvent.AXIS_Z),
365 event.getAxisValue(MotionEvent.AXIS_RZ)
366 )
367 MotionEvent.AXIS_RX, MotionEvent.AXIS_RY ->
368 setStickState(
369 playerNumber,
370 NativeLibrary.StickType.STICK_R,
371 event.getAxisValue(MotionEvent.AXIS_RX),
372 event.getAxisValue(MotionEvent.AXIS_RY)
373 )
374 }
375 }
376
377 private fun setRazerAxisInput(event: MotionEvent, axis: Int) {
378 val playerNumber = getPlayerNumber(event.device.controllerNumber, event.deviceId)
379
380 when (axis) {
381 MotionEvent.AXIS_X, MotionEvent.AXIS_Y ->
382 setStickState(
383 playerNumber,
384 NativeLibrary.StickType.STICK_L,
385 event.getAxisValue(MotionEvent.AXIS_X),
386 event.getAxisValue(MotionEvent.AXIS_Y)
387 )
388 MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ ->
389 setStickState(
390 playerNumber,
391 NativeLibrary.StickType.STICK_R,
392 event.getAxisValue(MotionEvent.AXIS_Z),
393 event.getAxisValue(MotionEvent.AXIS_RZ)
394 )
395 MotionEvent.AXIS_BRAKE ->
396 NativeLibrary.onGamePadButtonEvent(
397 playerNumber,
398 NativeLibrary.ButtonType.TRIGGER_ZL,
399 getAxisToButton(event.getAxisValue(MotionEvent.AXIS_BRAKE))
400 )
401 MotionEvent.AXIS_GAS ->
402 NativeLibrary.onGamePadButtonEvent(
403 playerNumber,
404 NativeLibrary.ButtonType.TRIGGER_ZR,
405 getAxisToButton(event.getAxisValue(MotionEvent.AXIS_GAS))
406 )
407 MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y ->
408 setAxisDpadState(
409 playerNumber,
410 event.getAxisValue(MotionEvent.AXIS_HAT_X),
411 event.getAxisValue(MotionEvent.AXIS_HAT_Y)
412 )
413 }
414 }
415
416 fun getGameControllerIds(): Map<Int, Int> {
417 val gameControllerDeviceIds = mutableMapOf<Int, Int>()
418 val deviceIds = InputDevice.getDeviceIds() 55 val deviceIds = InputDevice.getDeviceIds()
419 var controllerSlot = 1 56 var port = 0
57 val inputSettings = NativeConfig.getInputSettings(true)
420 deviceIds.forEach { deviceId -> 58 deviceIds.forEach { deviceId ->
421 InputDevice.getDevice(deviceId)?.apply { 59 InputDevice.getDevice(deviceId)?.apply {
422 // Don't over-assign controllers
423 if (controllerSlot >= 8) {
424 return gameControllerDeviceIds
425 }
426
427 // Verify that the device has gamepad buttons, control sticks, or both. 60 // Verify that the device has gamepad buttons, control sticks, or both.
428 if (sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD || 61 if (sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD ||
429 sources and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK 62 sources and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK
430 ) { 63 ) {
431 // This device is a game controller. Store its device ID. 64 if (!gameControllerDeviceIds.contains(controllerNumber)) {
432 if (deviceId and id and vendorId and productId != 0) { 65 gameControllerDeviceIds[controllerNumber] = YuzuPhysicalDevice(
433 // Additionally filter out devices that have no ID 66 this,
434 gameControllerDeviceIds 67 port,
435 .takeIf { !it.contains(deviceId) } 68 inputSettings[port].useSystemVibrator
436 ?.put(deviceId, controllerSlot) 69 )
437 controllerSlot++
438 } 70 }
71 port++
439 } 72 }
440 } 73 }
441 } 74 }
442 return gameControllerDeviceIds 75 return gameControllerDeviceIds
443 } 76 }
77
78 fun updateControllerData() {
79 androidControllers = getDevices()
80 androidControllers.forEach {
81 NativeInput.registerController(it.value)
82 }
83
84 // Register the input overlay on a dedicated port for all player 1 vibrations
85 NativeInput.registerController(YuzuInputOverlayDevice(androidControllers.isEmpty(), 100))
86 registeredControllers.clear()
87 NativeInput.getInputDevices().forEach {
88 registeredControllers.add(ParamPackage(it))
89 }
90 registeredControllers.sortBy { it.get("port", 0) }
91 }
92
93 fun InputDevice.getGUID(): String = String.format("%016x%016x", productId, vendorId)
444} 94}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt
index a4c14b3a7..7228f25d2 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt
@@ -6,6 +6,8 @@ package org.yuzu.yuzu_emu.utils
6import org.yuzu.yuzu_emu.model.GameDir 6import org.yuzu.yuzu_emu.model.GameDir
7import org.yuzu.yuzu_emu.overlay.model.OverlayControlData 7import org.yuzu.yuzu_emu.overlay.model.OverlayControlData
8 8
9import org.yuzu.yuzu_emu.features.input.model.PlayerInput
10
9object NativeConfig { 11object NativeConfig {
10 /** 12 /**
11 * Loads global config. 13 * Loads global config.
@@ -168,4 +170,17 @@ object NativeConfig {
168 */ 170 */
169 @Synchronized 171 @Synchronized
170 external fun setOverlayControlData(overlayControlData: Array<OverlayControlData>) 172 external fun setOverlayControlData(overlayControlData: Array<OverlayControlData>)
173
174 @Synchronized
175 external fun getInputSettings(global: Boolean): Array<PlayerInput>
176
177 @Synchronized
178 external fun setInputSettings(value: Array<PlayerInput>, global: Boolean)
179
180 /**
181 * Saves control values for a specific player
182 * Must be used when per game config is loaded
183 */
184 @Synchronized
185 external fun saveControlPlayerValues()
171} 186}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NfcReader.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NfcReader.kt
index 68ed66565..331b7ddca 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NfcReader.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NfcReader.kt
@@ -14,7 +14,7 @@ import android.os.Build
14import android.os.Handler 14import android.os.Handler
15import android.os.Looper 15import android.os.Looper
16import java.io.IOException 16import java.io.IOException
17import org.yuzu.yuzu_emu.NativeLibrary 17import org.yuzu.yuzu_emu.features.input.NativeInput
18 18
19class NfcReader(private val activity: Activity) { 19class NfcReader(private val activity: Activity) {
20 private var nfcAdapter: NfcAdapter? = null 20 private var nfcAdapter: NfcAdapter? = null
@@ -76,12 +76,12 @@ class NfcReader(private val activity: Activity) {
76 amiibo.connect() 76 amiibo.connect()
77 77
78 val tagData = ntag215ReadAll(amiibo) ?: return 78 val tagData = ntag215ReadAll(amiibo) ?: return
79 NativeLibrary.onReadNfcTag(tagData) 79 NativeInput.onReadNfcTag(tagData)
80 80
81 nfcAdapter?.ignore( 81 nfcAdapter?.ignore(
82 tag, 82 tag,
83 1000, 83 1000,
84 { NativeLibrary.onRemoveNfcTag() }, 84 { NativeInput.onRemoveNfcTag() },
85 Handler(Looper.getMainLooper()) 85 Handler(Looper.getMainLooper())
86 ) 86 )
87 } 87 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ParamPackage.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ParamPackage.kt
new file mode 100644
index 000000000..83fc7da3c
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ParamPackage.kt
@@ -0,0 +1,141 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.utils
5
6// Kotlin version of src/common/param_package.h
7class ParamPackage(serialized: String = "") {
8 private val KEY_VALUE_SEPARATOR = ":"
9 private val PARAM_SEPARATOR = ","
10
11 private val ESCAPE_CHARACTER = "$"
12 private val KEY_VALUE_SEPARATOR_ESCAPE = "$0"
13 private val PARAM_SEPARATOR_ESCAPE = "$1"
14 private val ESCAPE_CHARACTER_ESCAPE = "$2"
15
16 private val EMPTY_PLACEHOLDER = "[empty]"
17
18 val data = mutableMapOf<String, String>()
19
20 init {
21 val pairs = serialized.split(PARAM_SEPARATOR)
22 for (pair in pairs) {
23 val keyValue = pair.split(KEY_VALUE_SEPARATOR).toMutableList()
24 if (keyValue.size != 2) {
25 Log.error("[ParamPackage] Invalid key pair $keyValue")
26 continue
27 }
28
29 keyValue.forEachIndexed { i: Int, _: String ->
30 keyValue[i] = keyValue[i].replace(KEY_VALUE_SEPARATOR_ESCAPE, KEY_VALUE_SEPARATOR)
31 keyValue[i] = keyValue[i].replace(PARAM_SEPARATOR_ESCAPE, PARAM_SEPARATOR)
32 keyValue[i] = keyValue[i].replace(ESCAPE_CHARACTER_ESCAPE, ESCAPE_CHARACTER)
33 }
34
35 set(keyValue[0], keyValue[1])
36 }
37 }
38
39 constructor(params: List<Pair<String, String>>) : this() {
40 params.forEach {
41 data[it.first] = it.second
42 }
43 }
44
45 fun serialize(): String {
46 if (data.isEmpty()) {
47 return EMPTY_PLACEHOLDER
48 }
49
50 val result = StringBuilder()
51 data.forEach {
52 val keyValue = mutableListOf(it.key, it.value)
53 keyValue.forEachIndexed { i, _ ->
54 keyValue[i] = keyValue[i].replace(ESCAPE_CHARACTER, ESCAPE_CHARACTER_ESCAPE)
55 keyValue[i] = keyValue[i].replace(PARAM_SEPARATOR, PARAM_SEPARATOR_ESCAPE)
56 keyValue[i] = keyValue[i].replace(KEY_VALUE_SEPARATOR, KEY_VALUE_SEPARATOR_ESCAPE)
57 }
58 result.append("${keyValue[0]}$KEY_VALUE_SEPARATOR${keyValue[1]}$PARAM_SEPARATOR")
59 }
60 return result.removeSuffix(PARAM_SEPARATOR).toString()
61 }
62
63 fun get(key: String, defaultValue: String): String =
64 if (has(key)) {
65 data[key]!!
66 } else {
67 Log.debug("[ParamPackage] key $key not found")
68 defaultValue
69 }
70
71 fun get(key: String, defaultValue: Int): Int =
72 if (has(key)) {
73 try {
74 data[key]!!.toInt()
75 } catch (e: NumberFormatException) {
76 Log.debug("[ParamPackage] failed to convert ${data[key]!!} to int")
77 defaultValue
78 }
79 } else {
80 Log.debug("[ParamPackage] key $key not found")
81 defaultValue
82 }
83
84 private fun Int.toBoolean(): Boolean =
85 if (this == 1) {
86 true
87 } else if (this == 0) {
88 false
89 } else {
90 throw Exception("Tried to convert a value to a boolean that was not 0 or 1!")
91 }
92
93 fun get(key: String, defaultValue: Boolean): Boolean =
94 if (has(key)) {
95 try {
96 get(key, if (defaultValue) 1 else 0).toBoolean()
97 } catch (e: Exception) {
98 Log.debug("[ParamPackage] failed to convert ${data[key]!!} to boolean")
99 defaultValue
100 }
101 } else {
102 Log.debug("[ParamPackage] key $key not found")
103 defaultValue
104 }
105
106 fun get(key: String, defaultValue: Float): Float =
107 if (has(key)) {
108 try {
109 data[key]!!.toFloat()
110 } catch (e: NumberFormatException) {
111 Log.debug("[ParamPackage] failed to convert ${data[key]!!} to float")
112 defaultValue
113 }
114 } else {
115 Log.debug("[ParamPackage] key $key not found")
116 defaultValue
117 }
118
119 fun set(key: String, value: String) {
120 data[key] = value
121 }
122
123 fun set(key: String, value: Int) {
124 data[key] = value.toString()
125 }
126
127 fun Boolean.toInt(): Int = if (this) 1 else 0
128 fun set(key: String, value: Boolean) {
129 data[key] = value.toInt().toString()
130 }
131
132 fun set(key: String, value: Float) {
133 data[key] = value.toString()
134 }
135
136 fun has(key: String): Boolean = data.containsKey(key)
137
138 fun erase(key: String) = data.remove(key)
139
140 fun clear() = data.clear()
141}
diff --git a/src/android/app/src/main/jni/CMakeLists.txt b/src/android/app/src/main/jni/CMakeLists.txt
index 20b319c12..ec8ae5c57 100644
--- a/src/android/app/src/main/jni/CMakeLists.txt
+++ b/src/android/app/src/main/jni/CMakeLists.txt
@@ -12,6 +12,7 @@ add_library(yuzu-android SHARED
12 native_log.cpp 12 native_log.cpp
13 android_config.cpp 13 android_config.cpp
14 android_config.h 14 android_config.h
15 native_input.cpp
15) 16)
16 17
17set_property(TARGET yuzu-android PROPERTY IMPORTED_LOCATION ${FFmpeg_LIBRARY_DIR}) 18set_property(TARGET yuzu-android PROPERTY IMPORTED_LOCATION ${FFmpeg_LIBRARY_DIR})
diff --git a/src/android/app/src/main/jni/android_config.cpp b/src/android/app/src/main/jni/android_config.cpp
index e147560c3..a79a64afb 100644
--- a/src/android/app/src/main/jni/android_config.cpp
+++ b/src/android/app/src/main/jni/android_config.cpp
@@ -1,6 +1,8 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project 1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later 2// SPDX-License-Identifier: GPL-2.0-or-later
3 3
4#include <common/logging/log.h>
5#include <input_common/main.h>
4#include "android_config.h" 6#include "android_config.h"
5#include "android_settings.h" 7#include "android_settings.h"
6#include "common/settings_setting.h" 8#include "common/settings_setting.h"
@@ -32,6 +34,7 @@ void AndroidConfig::ReadAndroidValues() {
32 ReadOverlayValues(); 34 ReadOverlayValues();
33 } 35 }
34 ReadDriverValues(); 36 ReadDriverValues();
37 ReadAndroidControlValues();
35} 38}
36 39
37void AndroidConfig::ReadAndroidUIValues() { 40void AndroidConfig::ReadAndroidUIValues() {
@@ -107,6 +110,76 @@ void AndroidConfig::ReadOverlayValues() {
107 EndGroup(); 110 EndGroup();
108} 111}
109 112
113void AndroidConfig::ReadAndroidPlayerValues(std::size_t player_index) {
114 std::string player_prefix;
115 if (type != ConfigType::InputProfile) {
116 player_prefix.append("player_").append(ToString(player_index)).append("_");
117 }
118
119 auto& player = Settings::values.players.GetValue()[player_index];
120 if (IsCustomConfig()) {
121 const auto profile_name =
122 ReadStringSetting(std::string(player_prefix).append("profile_name"));
123 if (profile_name.empty()) {
124 // Use the global input config
125 player = Settings::values.players.GetValue(true)[player_index];
126 player.profile_name = "";
127 return;
128 }
129 }
130
131 // Android doesn't have default options for controllers. We have the input overlay for that.
132 for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) {
133 const std::string default_param;
134 auto& player_buttons = player.buttons[i];
135
136 player_buttons = ReadStringSetting(
137 std::string(player_prefix).append(Settings::NativeButton::mapping[i]), default_param);
138 if (player_buttons.empty()) {
139 player_buttons = default_param;
140 }
141 }
142 for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) {
143 const std::string default_param;
144 auto& player_analogs = player.analogs[i];
145
146 player_analogs = ReadStringSetting(
147 std::string(player_prefix).append(Settings::NativeAnalog::mapping[i]), default_param);
148 if (player_analogs.empty()) {
149 player_analogs = default_param;
150 }
151 }
152 for (int i = 0; i < Settings::NativeMotion::NumMotions; ++i) {
153 const std::string default_param;
154 auto& player_motions = player.motions[i];
155
156 player_motions = ReadStringSetting(
157 std::string(player_prefix).append(Settings::NativeMotion::mapping[i]), default_param);
158 if (player_motions.empty()) {
159 player_motions = default_param;
160 }
161 }
162 player.use_system_vibrator = ReadBooleanSetting(
163 std::string(player_prefix).append("use_system_vibrator"), player_index == 0);
164}
165
166void AndroidConfig::ReadAndroidControlValues() {
167 BeginGroup(Settings::TranslateCategory(Settings::Category::Controls));
168
169 Settings::values.players.SetGlobal(!IsCustomConfig());
170 for (std::size_t p = 0; p < Settings::values.players.GetValue().size(); ++p) {
171 ReadAndroidPlayerValues(p);
172 }
173 if (IsCustomConfig()) {
174 EndGroup();
175 return;
176 }
177 // ReadDebugControlValues();
178 // ReadHidbusValues();
179
180 EndGroup();
181}
182
110void AndroidConfig::SaveAndroidValues() { 183void AndroidConfig::SaveAndroidValues() {
111 if (global) { 184 if (global) {
112 SaveAndroidUIValues(); 185 SaveAndroidUIValues();
@@ -114,6 +187,7 @@ void AndroidConfig::SaveAndroidValues() {
114 SaveOverlayValues(); 187 SaveOverlayValues();
115 } 188 }
116 SaveDriverValues(); 189 SaveDriverValues();
190 SaveAndroidControlValues();
117 191
118 WriteToIni(); 192 WriteToIni();
119} 193}
@@ -187,6 +261,52 @@ void AndroidConfig::SaveOverlayValues() {
187 EndGroup(); 261 EndGroup();
188} 262}
189 263
264void AndroidConfig::SaveAndroidPlayerValues(std::size_t player_index) {
265 std::string player_prefix;
266 if (type != ConfigType::InputProfile) {
267 player_prefix = std::string("player_").append(ToString(player_index)).append("_");
268 }
269
270 const auto& player = Settings::values.players.GetValue()[player_index];
271 if (IsCustomConfig() && player.profile_name.empty()) {
272 // No custom profile selected
273 return;
274 }
275
276 const std::string default_param;
277 for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) {
278 WriteStringSetting(std::string(player_prefix).append(Settings::NativeButton::mapping[i]),
279 player.buttons[i], std::make_optional(default_param));
280 }
281 for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) {
282 WriteStringSetting(std::string(player_prefix).append(Settings::NativeAnalog::mapping[i]),
283 player.analogs[i], std::make_optional(default_param));
284 }
285 for (int i = 0; i < Settings::NativeMotion::NumMotions; ++i) {
286 WriteStringSetting(std::string(player_prefix).append(Settings::NativeMotion::mapping[i]),
287 player.motions[i], std::make_optional(default_param));
288 }
289 WriteBooleanSetting(std::string(player_prefix).append("use_system_vibrator"),
290 player.use_system_vibrator, std::make_optional(player_index == 0));
291}
292
293void AndroidConfig::SaveAndroidControlValues() {
294 BeginGroup(Settings::TranslateCategory(Settings::Category::Controls));
295
296 Settings::values.players.SetGlobal(!IsCustomConfig());
297 for (std::size_t p = 0; p < Settings::values.players.GetValue().size(); ++p) {
298 SaveAndroidPlayerValues(p);
299 }
300 if (IsCustomConfig()) {
301 EndGroup();
302 return;
303 }
304 // SaveDebugControlValues();
305 // SaveHidbusValues();
306
307 EndGroup();
308}
309
190std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::Category category) { 310std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::Category category) {
191 auto& map = Settings::values.linkage.by_category; 311 auto& map = Settings::values.linkage.by_category;
192 if (map.contains(category)) { 312 if (map.contains(category)) {
@@ -194,3 +314,24 @@ std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::
194 } 314 }
195 return AndroidSettings::values.linkage.by_category[category]; 315 return AndroidSettings::values.linkage.by_category[category];
196} 316}
317
318void AndroidConfig::ReadAndroidControlPlayerValues(std::size_t player_index) {
319 BeginGroup(Settings::TranslateCategory(Settings::Category::Controls));
320
321 ReadPlayerValues(player_index);
322 ReadAndroidPlayerValues(player_index);
323
324 EndGroup();
325}
326
327void AndroidConfig::SaveAndroidControlPlayerValues(std::size_t player_index) {
328 BeginGroup(Settings::TranslateCategory(Settings::Category::Controls));
329
330 LOG_DEBUG(Config, "Saving players control configuration values");
331 SavePlayerValues(player_index);
332 SaveAndroidPlayerValues(player_index);
333
334 EndGroup();
335
336 WriteToIni();
337}
diff --git a/src/android/app/src/main/jni/android_config.h b/src/android/app/src/main/jni/android_config.h
index 693e1e3f0..28ef5d0a8 100644
--- a/src/android/app/src/main/jni/android_config.h
+++ b/src/android/app/src/main/jni/android_config.h
@@ -13,7 +13,12 @@ public:
13 void ReloadAllValues() override; 13 void ReloadAllValues() override;
14 void SaveAllValues() override; 14 void SaveAllValues() override;
15 15
16 void ReadAndroidControlPlayerValues(std::size_t player_index);
17 void SaveAndroidControlPlayerValues(std::size_t player_index);
18
16protected: 19protected:
20 void ReadAndroidPlayerValues(std::size_t player_index);
21 void ReadAndroidControlValues();
17 void ReadAndroidValues(); 22 void ReadAndroidValues();
18 void ReadAndroidUIValues(); 23 void ReadAndroidUIValues();
19 void ReadDriverValues(); 24 void ReadDriverValues();
@@ -27,6 +32,8 @@ protected:
27 void ReadUILayoutValues() override {} 32 void ReadUILayoutValues() override {}
28 void ReadMultiplayerValues() override {} 33 void ReadMultiplayerValues() override {}
29 34
35 void SaveAndroidPlayerValues(std::size_t player_index);
36 void SaveAndroidControlValues();
30 void SaveAndroidValues(); 37 void SaveAndroidValues();
31 void SaveAndroidUIValues(); 38 void SaveAndroidUIValues();
32 void SaveDriverValues(); 39 void SaveDriverValues();
diff --git a/src/android/app/src/main/jni/emu_window/emu_window.cpp b/src/android/app/src/main/jni/emu_window/emu_window.cpp
index c927cddda..2768a01c9 100644
--- a/src/android/app/src/main/jni/emu_window/emu_window.cpp
+++ b/src/android/app/src/main/jni/emu_window/emu_window.cpp
@@ -5,6 +5,7 @@
5 5
6#include "common/android/id_cache.h" 6#include "common/android/id_cache.h"
7#include "common/logging/log.h" 7#include "common/logging/log.h"
8#include "input_common/drivers/android.h"
8#include "input_common/drivers/touch_screen.h" 9#include "input_common/drivers/touch_screen.h"
9#include "input_common/drivers/virtual_amiibo.h" 10#include "input_common/drivers/virtual_amiibo.h"
10#include "input_common/drivers/virtual_gamepad.h" 11#include "input_common/drivers/virtual_gamepad.h"
@@ -22,43 +23,6 @@ void EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) {
22 window_info.render_surface = reinterpret_cast<void*>(surface); 23 window_info.render_surface = reinterpret_cast<void*>(surface);
23} 24}
24 25
25void EmuWindow_Android::OnTouchPressed(int id, float x, float y) {
26 const auto [touch_x, touch_y] = MapToTouchScreen(x, y);
27 m_input_subsystem->GetTouchScreen()->TouchPressed(touch_x, touch_y, id);
28}
29
30void EmuWindow_Android::OnTouchMoved(int id, float x, float y) {
31 const auto [touch_x, touch_y] = MapToTouchScreen(x, y);
32 m_input_subsystem->GetTouchScreen()->TouchMoved(touch_x, touch_y, id);
33}
34
35void EmuWindow_Android::OnTouchReleased(int id) {
36 m_input_subsystem->GetTouchScreen()->TouchReleased(id);
37}
38
39void EmuWindow_Android::OnGamepadButtonEvent(int player_index, int button_id, bool pressed) {
40 m_input_subsystem->GetVirtualGamepad()->SetButtonState(player_index, button_id, pressed);
41}
42
43void EmuWindow_Android::OnGamepadJoystickEvent(int player_index, int stick_id, float x, float y) {
44 m_input_subsystem->GetVirtualGamepad()->SetStickPosition(player_index, stick_id, x, y);
45}
46
47void EmuWindow_Android::OnGamepadMotionEvent(int player_index, u64 delta_timestamp, float gyro_x,
48 float gyro_y, float gyro_z, float accel_x,
49 float accel_y, float accel_z) {
50 m_input_subsystem->GetVirtualGamepad()->SetMotionState(
51 player_index, delta_timestamp, gyro_x, gyro_y, gyro_z, accel_x, accel_y, accel_z);
52}
53
54void EmuWindow_Android::OnReadNfcTag(std::span<u8> data) {
55 m_input_subsystem->GetVirtualAmiibo()->LoadAmiibo(data);
56}
57
58void EmuWindow_Android::OnRemoveNfcTag() {
59 m_input_subsystem->GetVirtualAmiibo()->CloseAmiibo();
60}
61
62void EmuWindow_Android::OnFrameDisplayed() { 26void EmuWindow_Android::OnFrameDisplayed() {
63 if (!m_first_frame) { 27 if (!m_first_frame) {
64 Common::Android::RunJNIOnFiber<void>( 28 Common::Android::RunJNIOnFiber<void>(
@@ -67,10 +31,9 @@ void EmuWindow_Android::OnFrameDisplayed() {
67 } 31 }
68} 32}
69 33
70EmuWindow_Android::EmuWindow_Android(InputCommon::InputSubsystem* input_subsystem, 34EmuWindow_Android::EmuWindow_Android(ANativeWindow* surface,
71 ANativeWindow* surface,
72 std::shared_ptr<Common::DynamicLibrary> driver_library) 35 std::shared_ptr<Common::DynamicLibrary> driver_library)
73 : m_input_subsystem{input_subsystem}, m_driver_library{driver_library} { 36 : m_driver_library{driver_library} {
74 LOG_INFO(Frontend, "initializing"); 37 LOG_INFO(Frontend, "initializing");
75 38
76 if (!surface) { 39 if (!surface) {
@@ -80,10 +43,4 @@ EmuWindow_Android::EmuWindow_Android(InputCommon::InputSubsystem* input_subsyste
80 43
81 OnSurfaceChanged(surface); 44 OnSurfaceChanged(surface);
82 window_info.type = Core::Frontend::WindowSystemType::Android; 45 window_info.type = Core::Frontend::WindowSystemType::Android;
83
84 m_input_subsystem->Initialize();
85}
86
87EmuWindow_Android::~EmuWindow_Android() {
88 m_input_subsystem->Shutdown();
89} 46}
diff --git a/src/android/app/src/main/jni/emu_window/emu_window.h b/src/android/app/src/main/jni/emu_window/emu_window.h
index a34a0e479..34704ae95 100644
--- a/src/android/app/src/main/jni/emu_window/emu_window.h
+++ b/src/android/app/src/main/jni/emu_window/emu_window.h
@@ -30,21 +30,12 @@ private:
30class EmuWindow_Android final : public Core::Frontend::EmuWindow { 30class EmuWindow_Android final : public Core::Frontend::EmuWindow {
31 31
32public: 32public:
33 EmuWindow_Android(InputCommon::InputSubsystem* input_subsystem, ANativeWindow* surface, 33 EmuWindow_Android(ANativeWindow* surface,
34 std::shared_ptr<Common::DynamicLibrary> driver_library); 34 std::shared_ptr<Common::DynamicLibrary> driver_library);
35 35
36 ~EmuWindow_Android(); 36 ~EmuWindow_Android() = default;
37 37
38 void OnSurfaceChanged(ANativeWindow* surface); 38 void OnSurfaceChanged(ANativeWindow* surface);
39 void OnTouchPressed(int id, float x, float y);
40 void OnTouchMoved(int id, float x, float y);
41 void OnTouchReleased(int id);
42 void OnGamepadButtonEvent(int player_index, int button_id, bool pressed);
43 void OnGamepadJoystickEvent(int player_index, int stick_id, float x, float y);
44 void OnGamepadMotionEvent(int player_index, u64 delta_timestamp, float gyro_x, float gyro_y,
45 float gyro_z, float accel_x, float accel_y, float accel_z);
46 void OnReadNfcTag(std::span<u8> data);
47 void OnRemoveNfcTag();
48 void OnFrameDisplayed() override; 39 void OnFrameDisplayed() override;
49 40
50 std::unique_ptr<Core::Frontend::GraphicsContext> CreateSharedContext() const override { 41 std::unique_ptr<Core::Frontend::GraphicsContext> CreateSharedContext() const override {
@@ -55,8 +46,6 @@ public:
55 }; 46 };
56 47
57private: 48private:
58 InputCommon::InputSubsystem* m_input_subsystem{};
59
60 float m_window_width{}; 49 float m_window_width{};
61 float m_window_height{}; 50 float m_window_height{};
62 51
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp
index a4d8454e8..50cef5d2a 100644
--- a/src/android/app/src/main/jni/native.cpp
+++ b/src/android/app/src/main/jni/native.cpp
@@ -88,6 +88,10 @@ FileSys::ManualContentProvider* EmulationSession::GetContentProvider() {
88 return m_manual_provider.get(); 88 return m_manual_provider.get();
89} 89}
90 90
91InputCommon::InputSubsystem& EmulationSession::GetInputSubsystem() {
92 return m_input_subsystem;
93}
94
91const EmuWindow_Android& EmulationSession::Window() const { 95const EmuWindow_Android& EmulationSession::Window() const {
92 return *m_window; 96 return *m_window;
93} 97}
@@ -198,6 +202,8 @@ void EmulationSession::InitializeSystem(bool reload) {
198 Common::Log::Initialize(); 202 Common::Log::Initialize();
199 Common::Log::SetColorConsoleBackendEnabled(true); 203 Common::Log::SetColorConsoleBackendEnabled(true);
200 Common::Log::Start(); 204 Common::Log::Start();
205
206 m_input_subsystem.Initialize();
201 } 207 }
202 208
203 // Initialize filesystem. 209 // Initialize filesystem.
@@ -222,8 +228,7 @@ Core::SystemResultStatus EmulationSession::InitializeEmulation(const std::string
222 std::scoped_lock lock(m_mutex); 228 std::scoped_lock lock(m_mutex);
223 229
224 // Create the render window. 230 // Create the render window.
225 m_window = 231 m_window = std::make_unique<EmuWindow_Android>(m_native_window, m_vulkan_library);
226 std::make_unique<EmuWindow_Android>(&m_input_subsystem, m_native_window, m_vulkan_library);
227 232
228 // Initialize system. 233 // Initialize system.
229 jauto android_keyboard = std::make_unique<Common::Android::SoftwareKeyboard::AndroidKeyboard>(); 234 jauto android_keyboard = std::make_unique<Common::Android::SoftwareKeyboard::AndroidKeyboard>();
@@ -355,60 +360,6 @@ void EmulationSession::RunEmulation() {
355 m_applet_id = static_cast<int>(Service::AM::AppletId::Application); 360 m_applet_id = static_cast<int>(Service::AM::AppletId::Application);
356} 361}
357 362
358bool EmulationSession::IsHandheldOnly() {
359 jconst npad_style_set = m_system.HIDCore().GetSupportedStyleTag();
360
361 if (npad_style_set.fullkey == 1) {
362 return false;
363 }
364
365 if (npad_style_set.handheld == 0) {
366 return false;
367 }
368
369 return !Settings::IsDockedMode();
370}
371
372void EmulationSession::SetDeviceType([[maybe_unused]] int index, int type) {
373 jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index);
374 controller->SetNpadStyleIndex(static_cast<Core::HID::NpadStyleIndex>(type));
375}
376
377void EmulationSession::OnGamepadConnectEvent([[maybe_unused]] int index) {
378 jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index);
379
380 // Ensure that player1 is configured correctly and handheld disconnected
381 if (controller->GetNpadIdType() == Core::HID::NpadIdType::Player1) {
382 jauto handheld = m_system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Handheld);
383
384 if (controller->GetNpadStyleIndex() == Core::HID::NpadStyleIndex::Handheld) {
385 handheld->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Fullkey);
386 controller->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Fullkey);
387 handheld->Disconnect();
388 }
389 }
390
391 // Ensure that handheld is configured correctly and player 1 disconnected
392 if (controller->GetNpadIdType() == Core::HID::NpadIdType::Handheld) {
393 jauto player1 = m_system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1);
394
395 if (controller->GetNpadStyleIndex() != Core::HID::NpadStyleIndex::Handheld) {
396 player1->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Handheld);
397 controller->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Handheld);
398 player1->Disconnect();
399 }
400 }
401
402 if (!controller->IsConnected()) {
403 controller->Connect();
404 }
405}
406
407void EmulationSession::OnGamepadDisconnectEvent([[maybe_unused]] int index) {
408 jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index);
409 controller->Disconnect();
410}
411
412Common::Android::SoftwareKeyboard::AndroidKeyboard* EmulationSession::SoftwareKeyboard() { 363Common::Android::SoftwareKeyboard::AndroidKeyboard* EmulationSession::SoftwareKeyboard() {
413 return m_software_keyboard; 364 return m_software_keyboard;
414} 365}
@@ -574,14 +525,14 @@ jobjectArray Java_org_yuzu_yuzu_1emu_utils_GpuDriverHelper_getSystemDriverInfo(
574 nullptr, nullptr, file_redirect_dir_, nullptr); 525 nullptr, nullptr, file_redirect_dir_, nullptr);
575 auto driver_library = std::make_shared<Common::DynamicLibrary>(handle); 526 auto driver_library = std::make_shared<Common::DynamicLibrary>(handle);
576 InputCommon::InputSubsystem input_subsystem; 527 InputCommon::InputSubsystem input_subsystem;
577 auto m_window = std::make_unique<EmuWindow_Android>( 528 auto window =
578 &input_subsystem, ANativeWindow_fromSurface(env, j_surf), driver_library); 529 std::make_unique<EmuWindow_Android>(ANativeWindow_fromSurface(env, j_surf), driver_library);
579 530
580 Vulkan::vk::InstanceDispatch dld; 531 Vulkan::vk::InstanceDispatch dld;
581 Vulkan::vk::Instance vk_instance = Vulkan::CreateInstance( 532 Vulkan::vk::Instance vk_instance = Vulkan::CreateInstance(
582 *driver_library, dld, VK_API_VERSION_1_1, Core::Frontend::WindowSystemType::Android); 533 *driver_library, dld, VK_API_VERSION_1_1, Core::Frontend::WindowSystemType::Android);
583 534
584 auto surface = Vulkan::CreateSurface(vk_instance, m_window->GetWindowInfo()); 535 auto surface = Vulkan::CreateSurface(vk_instance, window->GetWindowInfo());
585 536
586 auto device = Vulkan::CreateDevice(vk_instance, dld, *surface); 537 auto device = Vulkan::CreateDevice(vk_instance, dld, *surface);
587 538
@@ -622,103 +573,6 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isPaused(JNIEnv* env, jclass claz
622 return static_cast<jboolean>(EmulationSession::GetInstance().IsPaused()); 573 return static_cast<jboolean>(EmulationSession::GetInstance().IsPaused());
623} 574}
624 575
625jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isHandheldOnly(JNIEnv* env, jclass clazz) {
626 return EmulationSession::GetInstance().IsHandheldOnly();
627}
628
629jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_setDeviceType(JNIEnv* env, jclass clazz,
630 jint j_device, jint j_type) {
631 if (EmulationSession::GetInstance().IsRunning()) {
632 EmulationSession::GetInstance().SetDeviceType(j_device, j_type);
633 }
634 return static_cast<jboolean>(true);
635}
636
637jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadConnectEvent(JNIEnv* env, jclass clazz,
638 jint j_device) {
639 if (EmulationSession::GetInstance().IsRunning()) {
640 EmulationSession::GetInstance().OnGamepadConnectEvent(j_device);
641 }
642 return static_cast<jboolean>(true);
643}
644
645jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadDisconnectEvent(JNIEnv* env, jclass clazz,
646 jint j_device) {
647 if (EmulationSession::GetInstance().IsRunning()) {
648 EmulationSession::GetInstance().OnGamepadDisconnectEvent(j_device);
649 }
650 return static_cast<jboolean>(true);
651}
652jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadButtonEvent(JNIEnv* env, jclass clazz,
653 jint j_device, jint j_button,
654 jint action) {
655 if (EmulationSession::GetInstance().IsRunning()) {
656 // Ensure gamepad is connected
657 EmulationSession::GetInstance().OnGamepadConnectEvent(j_device);
658 EmulationSession::GetInstance().Window().OnGamepadButtonEvent(j_device, j_button,
659 action != 0);
660 }
661 return static_cast<jboolean>(true);
662}
663
664jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadJoystickEvent(JNIEnv* env, jclass clazz,
665 jint j_device, jint stick_id,
666 jfloat x, jfloat y) {
667 if (EmulationSession::GetInstance().IsRunning()) {
668 EmulationSession::GetInstance().Window().OnGamepadJoystickEvent(j_device, stick_id, x, y);
669 }
670 return static_cast<jboolean>(true);
671}
672
673jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadMotionEvent(
674 JNIEnv* env, jclass clazz, jint j_device, jlong delta_timestamp, jfloat gyro_x, jfloat gyro_y,
675 jfloat gyro_z, jfloat accel_x, jfloat accel_y, jfloat accel_z) {
676 if (EmulationSession::GetInstance().IsRunning()) {
677 EmulationSession::GetInstance().Window().OnGamepadMotionEvent(
678 j_device, delta_timestamp, gyro_x, gyro_y, gyro_z, accel_x, accel_y, accel_z);
679 }
680 return static_cast<jboolean>(true);
681}
682
683jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onReadNfcTag(JNIEnv* env, jclass clazz,
684 jbyteArray j_data) {
685 jboolean isCopy{false};
686 std::span<u8> data(reinterpret_cast<u8*>(env->GetByteArrayElements(j_data, &isCopy)),
687 static_cast<size_t>(env->GetArrayLength(j_data)));
688
689 if (EmulationSession::GetInstance().IsRunning()) {
690 EmulationSession::GetInstance().Window().OnReadNfcTag(data);
691 }
692 return static_cast<jboolean>(true);
693}
694
695jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onRemoveNfcTag(JNIEnv* env, jclass clazz) {
696 if (EmulationSession::GetInstance().IsRunning()) {
697 EmulationSession::GetInstance().Window().OnRemoveNfcTag();
698 }
699 return static_cast<jboolean>(true);
700}
701
702void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchPressed(JNIEnv* env, jclass clazz, jint id,
703 jfloat x, jfloat y) {
704 if (EmulationSession::GetInstance().IsRunning()) {
705 EmulationSession::GetInstance().Window().OnTouchPressed(id, x, y);
706 }
707}
708
709void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchMoved(JNIEnv* env, jclass clazz, jint id,
710 jfloat x, jfloat y) {
711 if (EmulationSession::GetInstance().IsRunning()) {
712 EmulationSession::GetInstance().Window().OnTouchMoved(id, x, y);
713 }
714}
715
716void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchReleased(JNIEnv* env, jclass clazz, jint id) {
717 if (EmulationSession::GetInstance().IsRunning()) {
718 EmulationSession::GetInstance().Window().OnTouchReleased(id);
719 }
720}
721
722void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeSystem(JNIEnv* env, jclass clazz, 576void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeSystem(JNIEnv* env, jclass clazz,
723 jboolean reload) { 577 jboolean reload) {
724 // Initialize the emulated system. 578 // Initialize the emulated system.
@@ -759,6 +613,7 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getGpuDriver(JNIEnv* env, jobject
759 613
760void Java_org_yuzu_yuzu_1emu_NativeLibrary_applySettings(JNIEnv* env, jobject jobj) { 614void Java_org_yuzu_yuzu_1emu_NativeLibrary_applySettings(JNIEnv* env, jobject jobj) {
761 EmulationSession::GetInstance().System().ApplySettings(); 615 EmulationSession::GetInstance().System().ApplySettings();
616 EmulationSession::GetInstance().System().HIDCore().ReloadInputDevices();
762} 617}
763 618
764void Java_org_yuzu_yuzu_1emu_NativeLibrary_logSettings(JNIEnv* env, jobject jobj) { 619void Java_org_yuzu_yuzu_1emu_NativeLibrary_logSettings(JNIEnv* env, jobject jobj) {
diff --git a/src/android/app/src/main/jni/native.h b/src/android/app/src/main/jni/native.h
index 47936e305..6a4551ada 100644
--- a/src/android/app/src/main/jni/native.h
+++ b/src/android/app/src/main/jni/native.h
@@ -23,6 +23,7 @@ public:
23 const Core::System& System() const; 23 const Core::System& System() const;
24 Core::System& System(); 24 Core::System& System();
25 FileSys::ManualContentProvider* GetContentProvider(); 25 FileSys::ManualContentProvider* GetContentProvider();
26 InputCommon::InputSubsystem& GetInputSubsystem();
26 27
27 const EmuWindow_Android& Window() const; 28 const EmuWindow_Android& Window() const;
28 EmuWindow_Android& Window(); 29 EmuWindow_Android& Window();
@@ -50,10 +51,6 @@ public:
50 const std::size_t program_index, 51 const std::size_t program_index,
51 const bool frontend_initiated); 52 const bool frontend_initiated);
52 53
53 bool IsHandheldOnly();
54 void SetDeviceType([[maybe_unused]] int index, int type);
55 void OnGamepadConnectEvent([[maybe_unused]] int index);
56 void OnGamepadDisconnectEvent([[maybe_unused]] int index);
57 Common::Android::SoftwareKeyboard::AndroidKeyboard* SoftwareKeyboard(); 54 Common::Android::SoftwareKeyboard::AndroidKeyboard* SoftwareKeyboard();
58 55
59 static void OnEmulationStarted(); 56 static void OnEmulationStarted();
diff --git a/src/android/app/src/main/jni/native_config.cpp b/src/android/app/src/main/jni/native_config.cpp
index 8ae10fbc7..0b26280c6 100644
--- a/src/android/app/src/main/jni/native_config.cpp
+++ b/src/android/app/src/main/jni/native_config.cpp
@@ -3,7 +3,6 @@
3 3
4#include <string> 4#include <string>
5 5
6#include <common/fs/fs_util.h>
7#include <jni.h> 6#include <jni.h>
8 7
9#include "android_config.h" 8#include "android_config.h"
@@ -425,4 +424,120 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setOverlayControlData(
425 } 424 }
426} 425}
427 426
427jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getInputSettings(JNIEnv* env, jobject obj,
428 jboolean j_global) {
429 Settings::values.players.SetGlobal(static_cast<bool>(j_global));
430 auto& players = Settings::values.players.GetValue();
431 jobjectArray j_input_settings =
432 env->NewObjectArray(players.size(), Common::Android::GetPlayerInputClass(), nullptr);
433 for (size_t i = 0; i < players.size(); ++i) {
434 auto j_connected = static_cast<jboolean>(players[i].connected);
435
436 jobjectArray j_buttons = env->NewObjectArray(
437 players[i].buttons.size(), Common::Android::GetStringClass(), env->NewStringUTF(""));
438 for (size_t j = 0; j < players[i].buttons.size(); ++j) {
439 env->SetObjectArrayElement(j_buttons, j,
440 Common::Android::ToJString(env, players[i].buttons[j]));
441 }
442 jobjectArray j_analogs = env->NewObjectArray(
443 players[i].analogs.size(), Common::Android::GetStringClass(), env->NewStringUTF(""));
444 for (size_t j = 0; j < players[i].analogs.size(); ++j) {
445 env->SetObjectArrayElement(j_analogs, j,
446 Common::Android::ToJString(env, players[i].analogs[j]));
447 }
448 jobjectArray j_motions = env->NewObjectArray(
449 players[i].motions.size(), Common::Android::GetStringClass(), env->NewStringUTF(""));
450 for (size_t j = 0; j < players[i].motions.size(); ++j) {
451 env->SetObjectArrayElement(j_motions, j,
452 Common::Android::ToJString(env, players[i].motions[j]));
453 }
454
455 auto j_vibration_enabled = static_cast<jboolean>(players[i].vibration_enabled);
456 auto j_vibration_strength = static_cast<jint>(players[i].vibration_strength);
457
458 auto j_body_color_left = static_cast<jlong>(players[i].body_color_left);
459 auto j_body_color_right = static_cast<jlong>(players[i].body_color_right);
460 auto j_button_color_left = static_cast<jlong>(players[i].button_color_left);
461 auto j_button_color_right = static_cast<jlong>(players[i].button_color_right);
462
463 auto j_profile_name = Common::Android::ToJString(env, players[i].profile_name);
464
465 auto j_use_system_vibrator = players[i].use_system_vibrator;
466
467 jobject playerInput = env->NewObject(
468 Common::Android::GetPlayerInputClass(), Common::Android::GetPlayerInputConstructor(),
469 j_connected, j_buttons, j_analogs, j_motions, j_vibration_enabled, j_vibration_strength,
470 j_body_color_left, j_body_color_right, j_button_color_left, j_button_color_right,
471 j_profile_name, j_use_system_vibrator);
472 env->SetObjectArrayElement(j_input_settings, i, playerInput);
473 }
474 return j_input_settings;
475}
476
477void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setInputSettings(JNIEnv* env, jobject obj,
478 jobjectArray j_value,
479 jboolean j_global) {
480 auto& players = Settings::values.players.GetValue(static_cast<bool>(j_global));
481 int playersSize = env->GetArrayLength(j_value);
482 for (int i = 0; i < playersSize; ++i) {
483 jobject jplayer = env->GetObjectArrayElement(j_value, i);
484
485 players[i].connected = static_cast<bool>(
486 env->GetBooleanField(jplayer, Common::Android::GetPlayerInputConnectedField()));
487
488 auto j_buttons_array = static_cast<jobjectArray>(
489 env->GetObjectField(jplayer, Common::Android::GetPlayerInputButtonsField()));
490 int buttons_size = env->GetArrayLength(j_buttons_array);
491 for (int j = 0; j < buttons_size; ++j) {
492 auto button = static_cast<jstring>(env->GetObjectArrayElement(j_buttons_array, j));
493 players[i].buttons[j] = Common::Android::GetJString(env, button);
494 }
495 auto j_analogs_array = static_cast<jobjectArray>(
496 env->GetObjectField(jplayer, Common::Android::GetPlayerInputAnalogsField()));
497 int analogs_size = env->GetArrayLength(j_analogs_array);
498 for (int j = 0; j < analogs_size; ++j) {
499 auto analog = static_cast<jstring>(env->GetObjectArrayElement(j_analogs_array, j));
500 players[i].analogs[j] = Common::Android::GetJString(env, analog);
501 }
502 auto j_motions_array = static_cast<jobjectArray>(
503 env->GetObjectField(jplayer, Common::Android::GetPlayerInputMotionsField()));
504 int motions_size = env->GetArrayLength(j_motions_array);
505 for (int j = 0; j < motions_size; ++j) {
506 auto motion = static_cast<jstring>(env->GetObjectArrayElement(j_motions_array, j));
507 players[i].motions[j] = Common::Android::GetJString(env, motion);
508 }
509
510 players[i].vibration_enabled = static_cast<bool>(
511 env->GetBooleanField(jplayer, Common::Android::GetPlayerInputVibrationEnabledField()));
512 players[i].vibration_strength = static_cast<int>(
513 env->GetIntField(jplayer, Common::Android::GetPlayerInputVibrationStrengthField()));
514
515 players[i].body_color_left = static_cast<u32>(
516 env->GetLongField(jplayer, Common::Android::GetPlayerInputBodyColorLeftField()));
517 players[i].body_color_right = static_cast<u32>(
518 env->GetLongField(jplayer, Common::Android::GetPlayerInputBodyColorRightField()));
519 players[i].button_color_left = static_cast<u32>(
520 env->GetLongField(jplayer, Common::Android::GetPlayerInputButtonColorLeftField()));
521 players[i].button_color_right = static_cast<u32>(
522 env->GetLongField(jplayer, Common::Android::GetPlayerInputButtonColorRightField()));
523
524 auto profileName = static_cast<jstring>(
525 env->GetObjectField(jplayer, Common::Android::GetPlayerInputProfileNameField()));
526 players[i].profile_name = Common::Android::GetJString(env, profileName);
527
528 players[i].use_system_vibrator =
529 env->GetBooleanField(jplayer, Common::Android::GetPlayerInputUseSystemVibratorField());
530 }
531}
532
533void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_saveControlPlayerValues(JNIEnv* env, jobject obj) {
534 Settings::values.players.SetGlobal(false);
535
536 // Clear all controls from the config in case the user reverted back to globals
537 per_game_config->ClearControlPlayerValues();
538 for (size_t index = 0; index < Settings::values.players.GetValue().size(); ++index) {
539 per_game_config->SaveAndroidControlPlayerValues(index);
540 }
541}
542
428} // extern "C" 543} // extern "C"
diff --git a/src/android/app/src/main/jni/native_input.cpp b/src/android/app/src/main/jni/native_input.cpp
new file mode 100644
index 000000000..ddf2f297b
--- /dev/null
+++ b/src/android/app/src/main/jni/native_input.cpp
@@ -0,0 +1,631 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include <common/fs/fs.h>
5#include <common/fs/path_util.h>
6#include <common/settings.h>
7#include <hid_core/hid_types.h>
8#include <jni.h>
9
10#include "android_config.h"
11#include "common/android/android_common.h"
12#include "common/android/id_cache.h"
13#include "hid_core/frontend/emulated_controller.h"
14#include "hid_core/hid_core.h"
15#include "input_common/drivers/android.h"
16#include "input_common/drivers/touch_screen.h"
17#include "input_common/drivers/virtual_amiibo.h"
18#include "input_common/drivers/virtual_gamepad.h"
19#include "native.h"
20
21std::unordered_map<std::string, std::unique_ptr<AndroidConfig>> map_profiles;
22
23bool IsHandheldOnly() {
24 const auto npad_style_set =
25 EmulationSession::GetInstance().System().HIDCore().GetSupportedStyleTag();
26
27 if (npad_style_set.fullkey == 1) {
28 return false;
29 }
30
31 if (npad_style_set.handheld == 0) {
32 return false;
33 }
34
35 return !Settings::IsDockedMode();
36}
37
38std::filesystem::path GetNameWithoutExtension(std::filesystem::path filename) {
39 return filename.replace_extension();
40}
41
42bool IsProfileNameValid(std::string_view profile_name) {
43 return profile_name.find_first_of("<>:;\"/\\|,.!?*") == std::string::npos;
44}
45
46bool ProfileExistsInFilesystem(std::string_view profile_name) {
47 return Common::FS::Exists(Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir) / "input" /
48 fmt::format("{}.ini", profile_name));
49}
50
51bool ProfileExistsInMap(const std::string& profile_name) {
52 return map_profiles.find(profile_name) != map_profiles.end();
53}
54
55bool SaveProfile(const std::string& profile_name, std::size_t player_index) {
56 if (!ProfileExistsInMap(profile_name)) {
57 return false;
58 }
59
60 Settings::values.players.GetValue()[player_index].profile_name = profile_name;
61 map_profiles[profile_name]->SaveAndroidControlPlayerValues(player_index);
62 return true;
63}
64
65bool LoadProfile(std::string& profile_name, std::size_t player_index) {
66 if (!ProfileExistsInMap(profile_name)) {
67 return false;
68 }
69
70 if (!ProfileExistsInFilesystem(profile_name)) {
71 map_profiles.erase(profile_name);
72 return false;
73 }
74
75 LOG_INFO(Config, "Loading input profile `{}`", profile_name);
76
77 Settings::values.players.GetValue()[player_index].profile_name = profile_name;
78 map_profiles[profile_name]->ReadAndroidControlPlayerValues(player_index);
79 return true;
80}
81
82void ApplyControllerConfig(size_t player_index,
83 const std::function<void(Core::HID::EmulatedController*)>& apply) {
84 auto& hid_core = EmulationSession::GetInstance().System().HIDCore();
85 if (player_index == 0) {
86 auto* handheld = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld);
87 auto* player_one = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1);
88 handheld->EnableConfiguration();
89 player_one->EnableConfiguration();
90 apply(handheld);
91 apply(player_one);
92 handheld->DisableConfiguration();
93 player_one->DisableConfiguration();
94 handheld->SaveCurrentConfig();
95 player_one->SaveCurrentConfig();
96 } else {
97 auto* controller = hid_core.GetEmulatedControllerByIndex(player_index);
98 controller->EnableConfiguration();
99 apply(controller);
100 controller->DisableConfiguration();
101 controller->SaveCurrentConfig();
102 }
103}
104
105void ConnectController(size_t player_index, bool connected) {
106 auto& hid_core = EmulationSession::GetInstance().System().HIDCore();
107 if (player_index == 0) {
108 auto* handheld = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld);
109 auto* player_one = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1);
110 handheld->EnableConfiguration();
111 player_one->EnableConfiguration();
112 if (player_one->GetNpadStyleIndex(true) == Core::HID::NpadStyleIndex::Handheld) {
113 if (connected) {
114 handheld->Connect();
115 } else {
116 handheld->Disconnect();
117 }
118 player_one->Disconnect();
119 } else {
120 if (connected) {
121 player_one->Connect();
122 } else {
123 player_one->Disconnect();
124 }
125 handheld->Disconnect();
126 }
127 handheld->DisableConfiguration();
128 player_one->DisableConfiguration();
129 handheld->SaveCurrentConfig();
130 player_one->SaveCurrentConfig();
131 } else {
132 auto* controller = hid_core.GetEmulatedControllerByIndex(player_index);
133 controller->EnableConfiguration();
134 if (connected) {
135 controller->Connect();
136 } else {
137 controller->Disconnect();
138 }
139 controller->DisableConfiguration();
140 controller->SaveCurrentConfig();
141 }
142}
143
144extern "C" {
145
146jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_isHandheldOnly(JNIEnv* env,
147 jobject j_obj) {
148 return IsHandheldOnly();
149}
150
151void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onGamePadButtonEvent(
152 JNIEnv* env, jobject j_obj, jstring j_guid, jint j_port, jint j_button_id, jint j_action) {
153 EmulationSession::GetInstance().GetInputSubsystem().GetAndroid()->SetButtonState(
154 Common::Android::GetJString(env, j_guid), j_port, j_button_id, j_action != 0);
155}
156
157void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onGamePadAxisEvent(
158 JNIEnv* env, jobject j_obj, jstring j_guid, jint j_port, jint j_stick_id, jfloat j_value) {
159 EmulationSession::GetInstance().GetInputSubsystem().GetAndroid()->SetAxisPosition(
160 Common::Android::GetJString(env, j_guid), j_port, j_stick_id, j_value);
161}
162
163void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onGamePadMotionEvent(
164 JNIEnv* env, jobject j_obj, jstring j_guid, jint j_port, jlong j_delta_timestamp,
165 jfloat j_x_gyro, jfloat j_y_gyro, jfloat j_z_gyro, jfloat j_x_accel, jfloat j_y_accel,
166 jfloat j_z_accel) {
167 EmulationSession::GetInstance().GetInputSubsystem().GetAndroid()->SetMotionState(
168 Common::Android::GetJString(env, j_guid), j_port, j_delta_timestamp, j_x_gyro, j_y_gyro,
169 j_z_gyro, j_x_accel, j_y_accel, j_z_accel);
170}
171
172void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onReadNfcTag(JNIEnv* env, jobject j_obj,
173 jbyteArray j_data) {
174 jboolean isCopy{false};
175 std::span<u8> data(reinterpret_cast<u8*>(env->GetByteArrayElements(j_data, &isCopy)),
176 static_cast<size_t>(env->GetArrayLength(j_data)));
177
178 if (EmulationSession::GetInstance().IsRunning()) {
179 EmulationSession::GetInstance().GetInputSubsystem().GetVirtualAmiibo()->LoadAmiibo(data);
180 }
181}
182
183void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onRemoveNfcTag(JNIEnv* env, jobject j_obj) {
184 if (EmulationSession::GetInstance().IsRunning()) {
185 EmulationSession::GetInstance().GetInputSubsystem().GetVirtualAmiibo()->CloseAmiibo();
186 }
187}
188
189void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onTouchPressed(JNIEnv* env, jobject j_obj,
190 jint j_id, jfloat j_x_axis,
191 jfloat j_y_axis) {
192 if (EmulationSession::GetInstance().IsRunning()) {
193 EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchPressed(
194 j_id, j_x_axis, j_y_axis);
195 }
196}
197
198void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onTouchMoved(JNIEnv* env, jobject j_obj,
199 jint j_id, jfloat j_x_axis,
200 jfloat j_y_axis) {
201 if (EmulationSession::GetInstance().IsRunning()) {
202 EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchMoved(
203 j_id, j_x_axis, j_y_axis);
204 }
205}
206
207void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onTouchReleased(JNIEnv* env, jobject j_obj,
208 jint j_id) {
209 if (EmulationSession::GetInstance().IsRunning()) {
210 EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchReleased(j_id);
211 }
212}
213
214void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onOverlayButtonEventImpl(
215 JNIEnv* env, jobject j_obj, jint j_port, jint j_button_id, jint j_action) {
216 if (EmulationSession::GetInstance().IsRunning()) {
217 EmulationSession::GetInstance().GetInputSubsystem().GetVirtualGamepad()->SetButtonState(
218 j_port, j_button_id, j_action == 1);
219 }
220}
221
222void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onOverlayJoystickEventImpl(
223 JNIEnv* env, jobject j_obj, jint j_port, jint j_stick_id, jfloat j_x_axis, jfloat j_y_axis) {
224 if (EmulationSession::GetInstance().IsRunning()) {
225 EmulationSession::GetInstance().GetInputSubsystem().GetVirtualGamepad()->SetStickPosition(
226 j_port, j_stick_id, j_x_axis, j_y_axis);
227 }
228}
229
230void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onDeviceMotionEvent(
231 JNIEnv* env, jobject j_obj, jint j_port, jlong j_delta_timestamp, jfloat j_x_gyro,
232 jfloat j_y_gyro, jfloat j_z_gyro, jfloat j_x_accel, jfloat j_y_accel, jfloat j_z_accel) {
233 if (EmulationSession::GetInstance().IsRunning()) {
234 EmulationSession::GetInstance().GetInputSubsystem().GetVirtualGamepad()->SetMotionState(
235 j_port, j_delta_timestamp, j_x_gyro, j_y_gyro, j_z_gyro, j_x_accel, j_y_accel,
236 j_z_accel);
237 }
238}
239
240void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_reloadInputDevices(JNIEnv* env,
241 jobject j_obj) {
242 EmulationSession::GetInstance().System().HIDCore().ReloadInputDevices();
243}
244
245void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_registerController(JNIEnv* env,
246 jobject j_obj,
247 jobject j_device) {
248 EmulationSession::GetInstance().GetInputSubsystem().GetAndroid()->RegisterController(j_device);
249}
250
251jobjectArray Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getInputDevices(JNIEnv* env,
252 jobject j_obj) {
253 auto devices = EmulationSession::GetInstance().GetInputSubsystem().GetInputDevices();
254 jobjectArray jdevices = env->NewObjectArray(devices.size(), Common::Android::GetStringClass(),
255 Common::Android::ToJString(env, ""));
256 for (size_t i = 0; i < devices.size(); ++i) {
257 env->SetObjectArrayElement(jdevices, i,
258 Common::Android::ToJString(env, devices[i].Serialize()));
259 }
260 return jdevices;
261}
262
263void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_loadInputProfiles(JNIEnv* env,
264 jobject j_obj) {
265 map_profiles.clear();
266 const auto input_profile_loc =
267 Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir) / "input";
268
269 if (Common::FS::IsDir(input_profile_loc)) {
270 Common::FS::IterateDirEntries(
271 input_profile_loc,
272 [&](const std::filesystem::path& full_path) {
273 const auto filename = full_path.filename();
274 const auto name_without_ext =
275 Common::FS::PathToUTF8String(GetNameWithoutExtension(filename));
276
277 if (filename.extension() == ".ini" && IsProfileNameValid(name_without_ext)) {
278 map_profiles.insert_or_assign(
279 name_without_ext, std::make_unique<AndroidConfig>(
280 name_without_ext, Config::ConfigType::InputProfile));
281 }
282
283 return true;
284 },
285 Common::FS::DirEntryFilter::File);
286 }
287}
288
289jobjectArray Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getInputProfileNames(
290 JNIEnv* env, jobject j_obj) {
291 std::vector<std::string> profile_names;
292 profile_names.reserve(map_profiles.size());
293
294 auto it = map_profiles.cbegin();
295 while (it != map_profiles.cend()) {
296 const auto& [profile_name, config] = *it;
297 if (!ProfileExistsInFilesystem(profile_name)) {
298 it = map_profiles.erase(it);
299 continue;
300 }
301
302 profile_names.push_back(profile_name);
303 ++it;
304 }
305
306 std::stable_sort(profile_names.begin(), profile_names.end());
307
308 jobjectArray j_profile_names =
309 env->NewObjectArray(profile_names.size(), Common::Android::GetStringClass(),
310 Common::Android::ToJString(env, ""));
311 for (size_t i = 0; i < profile_names.size(); ++i) {
312 env->SetObjectArrayElement(j_profile_names, i,
313 Common::Android::ToJString(env, profile_names[i]));
314 }
315
316 return j_profile_names;
317}
318
319jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_isProfileNameValid(JNIEnv* env,
320 jobject j_obj,
321 jstring j_name) {
322 return Common::Android::GetJString(env, j_name).find_first_of("<>:;\"/\\|,.!?*") ==
323 std::string::npos;
324}
325
326jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_createProfile(JNIEnv* env,
327 jobject j_obj,
328 jstring j_name,
329 jint j_player_index) {
330 auto profile_name = Common::Android::GetJString(env, j_name);
331 if (ProfileExistsInMap(profile_name)) {
332 return false;
333 }
334
335 map_profiles.insert_or_assign(
336 profile_name,
337 std::make_unique<AndroidConfig>(profile_name, Config::ConfigType::InputProfile));
338
339 return SaveProfile(profile_name, j_player_index);
340}
341
342jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_deleteProfile(JNIEnv* env,
343 jobject j_obj,
344 jstring j_name,
345 jint j_player_index) {
346 auto profile_name = Common::Android::GetJString(env, j_name);
347 if (!ProfileExistsInMap(profile_name)) {
348 return false;
349 }
350
351 if (!ProfileExistsInFilesystem(profile_name) ||
352 Common::FS::RemoveFile(map_profiles[profile_name]->GetConfigFilePath())) {
353 map_profiles.erase(profile_name);
354 }
355
356 Settings::values.players.GetValue()[j_player_index].profile_name = "";
357 return !ProfileExistsInMap(profile_name) && !ProfileExistsInFilesystem(profile_name);
358}
359
360jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_loadProfile(JNIEnv* env, jobject j_obj,
361 jstring j_name,
362 jint j_player_index) {
363 auto profile_name = Common::Android::GetJString(env, j_name);
364 return LoadProfile(profile_name, j_player_index);
365}
366
367jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_saveProfile(JNIEnv* env, jobject j_obj,
368 jstring j_name,
369 jint j_player_index) {
370 auto profile_name = Common::Android::GetJString(env, j_name);
371 return SaveProfile(profile_name, j_player_index);
372}
373
374void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_loadPerGameConfiguration(
375 JNIEnv* env, jobject j_obj, jint j_player_index, jint j_selected_index,
376 jstring j_selected_profile_name) {
377 static constexpr size_t HANDHELD_INDEX = 8;
378
379 auto& hid_core = EmulationSession::GetInstance().System().HIDCore();
380 Settings::values.players.SetGlobal(false);
381
382 auto profile_name = Common::Android::GetJString(env, j_selected_profile_name);
383 auto* emulated_controller = hid_core.GetEmulatedControllerByIndex(j_player_index);
384
385 if (j_selected_index == 0) {
386 Settings::values.players.GetValue()[j_player_index].profile_name = "";
387 if (j_player_index == 0) {
388 Settings::values.players.GetValue()[HANDHELD_INDEX] = {};
389 }
390 Settings::values.players.SetGlobal(true);
391 emulated_controller->ReloadFromSettings();
392 return;
393 }
394 if (profile_name.empty()) {
395 return;
396 }
397 auto& player = Settings::values.players.GetValue()[j_player_index];
398 auto& global_player = Settings::values.players.GetValue(true)[j_player_index];
399 player.profile_name = profile_name;
400 global_player.profile_name = profile_name;
401 // Read from the profile into the custom player settings
402 LoadProfile(profile_name, j_player_index);
403 // Make sure the controller is connected
404 player.connected = true;
405
406 emulated_controller->ReloadFromSettings();
407
408 if (j_player_index > 0) {
409 return;
410 }
411 // Handle Handheld cases
412 auto& handheld_player = Settings::values.players.GetValue()[HANDHELD_INDEX];
413 auto* handheld_controller = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld);
414 if (player.controller_type == Settings::ControllerType::Handheld) {
415 handheld_player = player;
416 } else {
417 handheld_player = {};
418 }
419 handheld_controller->ReloadFromSettings();
420}
421
422void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_beginMapping(JNIEnv* env, jobject j_obj,
423 jint jtype) {
424 EmulationSession::GetInstance().GetInputSubsystem().BeginMapping(
425 static_cast<InputCommon::Polling::InputType>(jtype));
426}
427
428jstring Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getNextInput(JNIEnv* env,
429 jobject j_obj) {
430 return Common::Android::ToJString(
431 env, EmulationSession::GetInstance().GetInputSubsystem().GetNextInput().Serialize());
432}
433
434void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_stopMapping(JNIEnv* env, jobject j_obj) {
435 EmulationSession::GetInstance().GetInputSubsystem().StopMapping();
436}
437
438void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_updateMappingsWithDefaultImpl(
439 JNIEnv* env, jobject j_obj, jint j_player_index, jstring j_device_params,
440 jstring j_display_name) {
441 auto& input_subsystem = EmulationSession::GetInstance().GetInputSubsystem();
442
443 // Clear all previous mappings
444 for (int button_id = 0; button_id < Settings::NativeButton::NumButtons; ++button_id) {
445 ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
446 controller->SetButtonParam(button_id, {});
447 });
448 }
449 for (int analog_id = 0; analog_id < Settings::NativeAnalog::NumAnalogs; ++analog_id) {
450 ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
451 controller->SetStickParam(analog_id, {});
452 });
453 }
454
455 // Apply new mappings
456 auto device = Common::ParamPackage(Common::Android::GetJString(env, j_device_params));
457 auto button_mappings = input_subsystem.GetButtonMappingForDevice(device);
458 auto analog_mappings = input_subsystem.GetAnalogMappingForDevice(device);
459 auto display_name = Common::Android::GetJString(env, j_display_name);
460 for (const auto& button_mapping : button_mappings) {
461 const std::size_t index = button_mapping.first;
462 auto named_mapping = button_mapping.second;
463 named_mapping.Set("display", display_name);
464 ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
465 controller->SetButtonParam(index, named_mapping);
466 });
467 }
468 for (const auto& analog_mapping : analog_mappings) {
469 const std::size_t index = analog_mapping.first;
470 auto named_mapping = analog_mapping.second;
471 named_mapping.Set("display", display_name);
472 ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
473 controller->SetStickParam(index, named_mapping);
474 });
475 }
476}
477
478jstring Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getButtonParamImpl(JNIEnv* env,
479 jobject j_obj,
480 jint j_player_index,
481 jint j_button) {
482 return Common::Android::ToJString(env, EmulationSession::GetInstance()
483 .System()
484 .HIDCore()
485 .GetEmulatedControllerByIndex(j_player_index)
486 ->GetButtonParam(j_button)
487 .Serialize());
488}
489
490void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_setButtonParamImpl(
491 JNIEnv* env, jobject j_obj, jint j_player_index, jint j_button_id, jstring j_param) {
492 ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
493 controller->SetButtonParam(j_button_id,
494 Common::ParamPackage(Common::Android::GetJString(env, j_param)));
495 });
496}
497
498jstring Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getStickParamImpl(JNIEnv* env,
499 jobject j_obj,
500 jint j_player_index,
501 jint j_stick) {
502 return Common::Android::ToJString(env, EmulationSession::GetInstance()
503 .System()
504 .HIDCore()
505 .GetEmulatedControllerByIndex(j_player_index)
506 ->GetStickParam(j_stick)
507 .Serialize());
508}
509
510void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_setStickParamImpl(
511 JNIEnv* env, jobject j_obj, jint j_player_index, jint j_stick_id, jstring j_param) {
512 ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
513 controller->SetStickParam(j_stick_id,
514 Common::ParamPackage(Common::Android::GetJString(env, j_param)));
515 });
516}
517
518jint Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getButtonNameImpl(JNIEnv* env,
519 jobject j_obj,
520 jstring j_param) {
521 return static_cast<jint>(EmulationSession::GetInstance().GetInputSubsystem().GetButtonName(
522 Common::ParamPackage(Common::Android::GetJString(env, j_param))));
523}
524
525jintArray Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getSupportedStyleTagsImpl(
526 JNIEnv* env, jobject j_obj, jint j_player_index) {
527 auto& hid_core = EmulationSession::GetInstance().System().HIDCore();
528 const auto npad_style_set = hid_core.GetSupportedStyleTag();
529 std::vector<s32> supported_indexes;
530 if (npad_style_set.fullkey == 1) {
531 supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::Fullkey));
532 }
533
534 if (npad_style_set.joycon_dual == 1) {
535 supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::JoyconDual));
536 }
537
538 if (npad_style_set.joycon_left == 1) {
539 supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::JoyconLeft));
540 }
541
542 if (npad_style_set.joycon_right == 1) {
543 supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::JoyconRight));
544 }
545
546 if (j_player_index == 0 && npad_style_set.handheld == 1) {
547 supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::Handheld));
548 }
549
550 if (npad_style_set.gamecube == 1) {
551 supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::GameCube));
552 }
553
554 jintArray j_supported_indexes = env->NewIntArray(supported_indexes.size());
555 env->SetIntArrayRegion(j_supported_indexes, 0, supported_indexes.size(),
556 supported_indexes.data());
557 return j_supported_indexes;
558}
559
560jint Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getStyleIndexImpl(JNIEnv* env,
561 jobject j_obj,
562 jint j_player_index) {
563 return static_cast<s32>(EmulationSession::GetInstance()
564 .System()
565 .HIDCore()
566 .GetEmulatedControllerByIndex(j_player_index)
567 ->GetNpadStyleIndex(true));
568}
569
570void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_setStyleIndexImpl(JNIEnv* env,
571 jobject j_obj,
572 jint j_player_index,
573 jint j_style_index) {
574 auto& hid_core = EmulationSession::GetInstance().System().HIDCore();
575 auto type = static_cast<Core::HID::NpadStyleIndex>(j_style_index);
576 ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
577 controller->SetNpadStyleIndex(type);
578 });
579 if (j_player_index == 0) {
580 auto* handheld = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld);
581 auto* player_one = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1);
582 ConnectController(j_player_index,
583 player_one->IsConnected(true) || handheld->IsConnected(true));
584 }
585}
586
587jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_isControllerImpl(JNIEnv* env,
588 jobject j_obj,
589 jstring jparams) {
590 return static_cast<jint>(EmulationSession::GetInstance().GetInputSubsystem().IsController(
591 Common::ParamPackage(Common::Android::GetJString(env, jparams))));
592}
593
594jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getIsConnected(JNIEnv* env,
595 jobject j_obj,
596 jint j_player_index) {
597 auto& hid_core = EmulationSession::GetInstance().System().HIDCore();
598 auto* controller = hid_core.GetEmulatedControllerByIndex(static_cast<size_t>(j_player_index));
599 if (j_player_index == 0 &&
600 controller->GetNpadStyleIndex(true) == Core::HID::NpadStyleIndex::Handheld) {
601 return hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld)->IsConnected(true);
602 }
603 return controller->IsConnected(true);
604}
605
606void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_connectControllersImpl(
607 JNIEnv* env, jobject j_obj, jbooleanArray j_connected) {
608 jboolean isCopy = false;
609 auto j_connected_array_size = env->GetArrayLength(j_connected);
610 jboolean* j_connected_array = env->GetBooleanArrayElements(j_connected, &isCopy);
611 for (int i = 0; i < j_connected_array_size; ++i) {
612 ConnectController(i, j_connected_array[i]);
613 }
614}
615
616void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_resetControllerMappings(
617 JNIEnv* env, jobject j_obj, jint j_player_index) {
618 // Clear all previous mappings
619 for (int button_id = 0; button_id < Settings::NativeButton::NumButtons; ++button_id) {
620 ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
621 controller->SetButtonParam(button_id, {});
622 });
623 }
624 for (int analog_id = 0; analog_id < Settings::NativeAnalog::NumAnalogs; ++analog_id) {
625 ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
626 controller->SetStickParam(analog_id, {});
627 });
628 }
629}
630
631} // extern "C"
diff --git a/src/android/app/src/main/res/drawable/button_anim.xml b/src/android/app/src/main/res/drawable/button_anim.xml
new file mode 100644
index 000000000..ccdc5ca6a
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/button_anim.xml
@@ -0,0 +1,142 @@
1<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
2 xmlns:aapt="http://schemas.android.com/aapt">
3 <aapt:attr name="android:drawable">
4 <vector
5 android:width="1000dp"
6 android:height="1000dp"
7 android:viewportWidth="1000"
8 android:viewportHeight="1000">
9 <group android:name="_R_G">
10 <group
11 android:name="_R_G_L_0_G"
12 android:pivotX="100"
13 android:pivotY="100"
14 android:scaleX="4.5"
15 android:scaleY="4.5"
16 android:translateX="400"
17 android:translateY="400">
18 <path
19 android:name="_R_G_L_0_G_D_0_P_0"
20 android:fillAlpha="1"
21 android:fillColor="?attr/colorSecondaryContainer"
22 android:fillType="nonZero"
23 android:pathData=" M198.56 100 C198.56,154.43 154.43,198.56 100,198.56 C45.57,198.56 1.44,154.43 1.44,100 C1.44,45.57 45.57,1.44 100,1.44 C154.43,1.44 198.56,45.57 198.56,100c " />
24 <path
25 android:name="_R_G_L_0_G_D_2_P_0"
26 android:fillAlpha="0.8"
27 android:fillColor="?attr/colorOnSecondaryContainer"
28 android:fillType="nonZero"
29 android:pathData=" M50.14 151.21 C50.53,150.18 89.6,49.87 90.1,48.63 C90.1,48.63 90.67,47.2 90.67,47.2 C90.67,47.2 101.67,47.2 101.67,47.2 C101.67,47.2 112.67,47.2 112.67,47.2 C112.67,47.2 133.47,99.12 133.47,99.12 C144.91,127.68 154.32,151.17 154.38,151.33 C154.47,151.56 152.2,151.6 143.14,151.55 C143.14,151.55 131.79,151.48 131.79,151.48 C131.79,151.48 127.22,139.57 127.22,139.57 C127.22,139.57 122.65,127.66 122.65,127.66 C122.65,127.66 101.68,127.73 101.68,127.73 C101.68,127.73 80.71,127.8 80.71,127.8 C80.71,127.8 76.38,139.71 76.38,139.71 C76.38,139.71 72.06,151.62 72.06,151.62 C72.06,151.62 61.02,151.62 61.02,151.62 C50.61,151.62 50,151.55 50.14,151.22 C50.14,151.22 50.14,151.21 50.14,151.21c M115.86 110.06 C115.8,109.91 112.55,101.13 108.62,90.56 C104.7,80 101.42,71.43 101.34,71.53 C101.22,71.66 92.84,94.61 87.25,110.06 C87.17,110.29 90.13,110.34 101.56,110.34 C113,110.34 115.95,110.28 115.86,110.06c " />
30 </group>
31 </group>
32 <group android:name="time_group" />
33 </vector>
34 </aapt:attr>
35 <target android:name="_R_G_L_0_G">
36 <aapt:attr name="android:animation">
37 <set android:ordering="together">
38 <objectAnimator
39 android:duration="100"
40 android:propertyName="scaleX"
41 android:startOffset="0"
42 android:valueFrom="4.5"
43 android:valueTo="3.75"
44 android:valueType="floatType">
45 <aapt:attr name="android:interpolator">
46 <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
47 </aapt:attr>
48 </objectAnimator>
49 <objectAnimator
50 android:duration="100"
51 android:propertyName="scaleY"
52 android:startOffset="0"
53 android:valueFrom="4.5"
54 android:valueTo="3.75"
55 android:valueType="floatType">
56 <aapt:attr name="android:interpolator">
57 <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
58 </aapt:attr>
59 </objectAnimator>
60 <objectAnimator
61 android:duration="234"
62 android:propertyName="scaleX"
63 android:startOffset="100"
64 android:valueFrom="3.75"
65 android:valueTo="3.75"
66 android:valueType="floatType">
67 <aapt:attr name="android:interpolator">
68 <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
69 </aapt:attr>
70 </objectAnimator>
71 <objectAnimator
72 android:duration="234"
73 android:propertyName="scaleY"
74 android:startOffset="100"
75 android:valueFrom="3.75"
76 android:valueTo="3.75"
77 android:valueType="floatType">
78 <aapt:attr name="android:interpolator">
79 <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
80 </aapt:attr>
81 </objectAnimator>
82 <objectAnimator
83 android:duration="167"
84 android:propertyName="scaleX"
85 android:startOffset="334"
86 android:valueFrom="3.75"
87 android:valueTo="4.75"
88 android:valueType="floatType">
89 <aapt:attr name="android:interpolator">
90 <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
91 </aapt:attr>
92 </objectAnimator>
93 <objectAnimator
94 android:duration="167"
95 android:propertyName="scaleY"
96 android:startOffset="334"
97 android:valueFrom="3.75"
98 android:valueTo="4.75"
99 android:valueType="floatType">
100 <aapt:attr name="android:interpolator">
101 <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
102 </aapt:attr>
103 </objectAnimator>
104 <objectAnimator
105 android:duration="67"
106 android:propertyName="scaleX"
107 android:startOffset="501"
108 android:valueFrom="4.75"
109 android:valueTo="4.5"
110 android:valueType="floatType">
111 <aapt:attr name="android:interpolator">
112 <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
113 </aapt:attr>
114 </objectAnimator>
115 <objectAnimator
116 android:duration="67"
117 android:propertyName="scaleY"
118 android:startOffset="501"
119 android:valueFrom="4.75"
120 android:valueTo="4.5"
121 android:valueType="floatType">
122 <aapt:attr name="android:interpolator">
123 <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
124 </aapt:attr>
125 </objectAnimator>
126 </set>
127 </aapt:attr>
128 </target>
129 <target android:name="time_group">
130 <aapt:attr name="android:animation">
131 <set android:ordering="together">
132 <objectAnimator
133 android:duration="1034"
134 android:propertyName="translateX"
135 android:startOffset="0"
136 android:valueFrom="0"
137 android:valueTo="1"
138 android:valueType="floatType" />
139 </set>
140 </aapt:attr>
141 </target>
142</animated-vector>
diff --git a/src/android/app/src/main/res/drawable/ic_controller_disconnected.xml b/src/android/app/src/main/res/drawable/ic_controller_disconnected.xml
new file mode 100644
index 000000000..8e3c66f74
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_controller_disconnected.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="960"
5 android:viewportHeight="960">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M700,480q-25,0 -42.5,-17.5T640,420q0,-25 17.5,-42.5T700,360q25,0 42.5,17.5T760,420q0,25 -17.5,42.5T700,480ZM366,480ZM280,600v-80h-80v-80h80v-80h80v80h80v80h-80v80h-80ZM160,720q-33,0 -56.5,-23.5T80,640v-320q0,-34 24,-57.5t58,-23.5h77l81,81L160,320v320h366L55,169l57,-57 736,736 -57,57 -185,-185L160,720ZM880,640q0,26 -14,46t-37,29l-29,-29v-366L434,320l-80,-80h446q33,0 56.5,23.5T880,320v320ZM617,503Z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_more_vert.xml b/src/android/app/src/main/res/drawable/ic_more_vert.xml
new file mode 100644
index 000000000..9f62ac595
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_more_vert.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:height="24dp"
3 android:viewportHeight="24"
4 android:viewportWidth="24"
5 android:width="24dp">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_new_label.xml b/src/android/app/src/main/res/drawable/ic_new_label.xml
new file mode 100644
index 000000000..fac562c26
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_new_label.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M21,12l-4.37,6.16C16.26,18.68 15.65,19 15,19h-3l0,-6H9v-3H3V7c0,-1.1 0.9,-2 2,-2h10c0.65,0 1.26,0.31 1.63,0.84L21,12zM10,15H7v-3H5v3H2v2h3v3h2v-3h3V15z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_overlay.xml b/src/android/app/src/main/res/drawable/ic_overlay.xml
new file mode 100644
index 000000000..c7986c5a2
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_overlay.xml
@@ -0,0 +1,21 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M21,5H3C1.9,5 1,5.9 1,7v10c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2V7C23,5.9 22.1,5 21,5zM18,17H6V7h12V17z" />
9 <path
10 android:fillColor="?attr/colorControlNormal"
11 android:pathData="M15,11.25h1.5v1.5h-1.5z" />
12 <path
13 android:fillColor="?attr/colorControlNormal"
14 android:pathData="M12.5,11.25h1.5v1.5h-1.5z" />
15 <path
16 android:fillColor="?attr/colorControlNormal"
17 android:pathData="M10,11.25h1.5v1.5h-1.5z" />
18 <path
19 android:fillColor="?attr/colorControlNormal"
20 android:pathData="M7.5,11.25h1.5v1.5h-1.5z" />
21</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_share.xml b/src/android/app/src/main/res/drawable/ic_share.xml
new file mode 100644
index 000000000..3fc2f3c99
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_share.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/stick_one_direction_anim.xml b/src/android/app/src/main/res/drawable/stick_one_direction_anim.xml
new file mode 100644
index 000000000..a1da1316f
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/stick_one_direction_anim.xml
@@ -0,0 +1,118 @@
1<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
2 xmlns:aapt="http://schemas.android.com/aapt">
3 <aapt:attr name="android:drawable">
4 <vector
5 android:width="1000dp"
6 android:height="1000dp"
7 android:viewportWidth="1000"
8 android:viewportHeight="1000">
9 <group android:name="_R_G">
10 <group
11 android:name="_R_G_L_1_G"
12 android:pivotX="100"
13 android:pivotY="100"
14 android:scaleX="5"
15 android:scaleY="5"
16 android:translateX="400"
17 android:translateY="400">
18 <path
19 android:name="_R_G_L_1_G_D_0_P_0"
20 android:pathData=" M100 199.39 C59.8,199.39 23.56,175.17 8.18,138.04 C-7.2,100.9 1.3,58.15 29.73,29.72 C58.15,1.3 100.9,-7.21 138.04,8.18 C175.18,23.56 199.39,59.8 199.39,100 C199.33,154.87 154.87,199.33 100,199.39c "
21 android:strokeWidth="1"
22 android:strokeAlpha="0.6"
23 android:strokeColor="?attr/colorOutline"
24 android:strokeLineCap="round"
25 android:strokeLineJoin="round" />
26 </group>
27 <group
28 android:name="_R_G_L_0_G_T_1"
29 android:scaleX="5"
30 android:scaleY="5"
31 android:translateX="500"
32 android:translateY="500">
33 <group
34 android:name="_R_G_L_0_G"
35 android:translateX="-100"
36 android:translateY="-100">
37 <path
38 android:name="_R_G_L_0_G_D_0_P_0"
39 android:fillAlpha="1"
40 android:fillColor="?attr/colorSecondaryContainer"
41 android:fillType="nonZero"
42 android:pathData=" M100.45 28.02 C140.63,28.02 173.2,60.59 173.2,100.77 C173.2,140.95 140.63,173.52 100.45,173.52 C60.27,173.52 27.7,140.95 27.7,100.77 C27.7,60.59 60.27,28.02 100.45,28.02c " />
43 <path
44 android:name="_R_G_L_0_G_D_2_P_0"
45 android:fillAlpha="0.8"
46 android:fillColor="?attr/colorOnSecondaryContainer"
47 android:fillType="nonZero"
48 android:pathData=" M100.45 50.26 C128.62,50.26 151.46,73.1 151.46,101.28 C151.46,129.45 128.62,152.29 100.45,152.29 C72.27,152.29 49.43,129.45 49.43,101.28 C49.43,73.1 72.27,50.26 100.45,50.26c " />
49 </group>
50 </group>
51 </group>
52 <group android:name="time_group" />
53 </vector>
54 </aapt:attr>
55 <target android:name="_R_G_L_0_G_T_1">
56 <aapt:attr name="android:animation">
57 <set android:ordering="together">
58 <objectAnimator
59 android:duration="267"
60 android:pathData="M 500,500C 500,500 364,500 364,500"
61 android:propertyName="translateXY"
62 android:propertyXName="translateX"
63 android:propertyYName="translateY"
64 android:startOffset="0">
65 <aapt:attr name="android:interpolator">
66 <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
67 </aapt:attr>
68 </objectAnimator>
69 <objectAnimator
70 android:duration="234"
71 android:pathData="M 364,500C 364,500 364,500 364,500"
72 android:propertyName="translateXY"
73 android:propertyXName="translateX"
74 android:propertyYName="translateY"
75 android:startOffset="267">
76 <aapt:attr name="android:interpolator">
77 <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0.333 0.667,0.667 1.0,1.0" />
78 </aapt:attr>
79 </objectAnimator>
80 <objectAnimator
81 android:duration="133"
82 android:pathData="M 364,500C 364,500 525,500 525,500"
83 android:propertyName="translateXY"
84 android:propertyXName="translateX"
85 android:propertyYName="translateY"
86 android:startOffset="501">
87 <aapt:attr name="android:interpolator">
88 <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
89 </aapt:attr>
90 </objectAnimator>
91 <objectAnimator
92 android:duration="100"
93 android:pathData="M 525,500C 525,500 500,500 500,500"
94 android:propertyName="translateXY"
95 android:propertyXName="translateX"
96 android:propertyYName="translateY"
97 android:startOffset="634">
98 <aapt:attr name="android:interpolator">
99 <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
100 </aapt:attr>
101 </objectAnimator>
102 </set>
103 </aapt:attr>
104 </target>
105 <target android:name="time_group">
106 <aapt:attr name="android:animation">
107 <set android:ordering="together">
108 <objectAnimator
109 android:duration="968"
110 android:propertyName="translateX"
111 android:startOffset="0"
112 android:valueFrom="0"
113 android:valueTo="1"
114 android:valueType="floatType" />
115 </set>
116 </aapt:attr>
117 </target>
118</animated-vector>
diff --git a/src/android/app/src/main/res/drawable/stick_two_direction_anim.xml b/src/android/app/src/main/res/drawable/stick_two_direction_anim.xml
new file mode 100644
index 000000000..bc71adcbd
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/stick_two_direction_anim.xml
@@ -0,0 +1,173 @@
1<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
2 xmlns:aapt="http://schemas.android.com/aapt">
3 <aapt:attr name="android:drawable">
4 <vector
5 android:width="1000dp"
6 android:height="1000dp"
7 android:viewportWidth="1000"
8 android:viewportHeight="1000">
9 <group android:name="_R_G">
10 <group
11 android:name="_R_G_L_1_G"
12 android:pivotX="100"
13 android:pivotY="100"
14 android:scaleX="5"
15 android:scaleY="5"
16 android:translateX="400"
17 android:translateY="400">
18 <path
19 android:name="_R_G_L_1_G_D_0_P_0"
20 android:pathData=" M100 199.39 C59.8,199.39 23.56,175.17 8.18,138.04 C-7.2,100.9 1.3,58.15 29.73,29.72 C58.15,1.3 100.9,-7.21 138.04,8.18 C175.18,23.56 199.39,59.8 199.39,100 C199.33,154.87 154.87,199.33 100,199.39c "
21 android:strokeWidth="1"
22 android:strokeAlpha="0.6"
23 android:strokeColor="?attr/colorOutline"
24 android:strokeLineCap="round"
25 android:strokeLineJoin="round" />
26 </group>
27 <group
28 android:name="_R_G_L_0_G_T_1"
29 android:scaleX="5"
30 android:scaleY="5"
31 android:translateX="500"
32 android:translateY="500">
33 <group
34 android:name="_R_G_L_0_G"
35 android:translateX="-100"
36 android:translateY="-100">
37 <path
38 android:name="_R_G_L_0_G_D_0_P_0"
39 android:fillAlpha="1"
40 android:fillColor="?attr/colorSecondaryContainer"
41 android:fillType="nonZero"
42 android:pathData=" M100.45 28.02 C140.63,28.02 173.2,60.59 173.2,100.77 C173.2,140.95 140.63,173.52 100.45,173.52 C60.27,173.52 27.7,140.95 27.7,100.77 C27.7,60.59 60.27,28.02 100.45,28.02c " />
43 <path
44 android:name="_R_G_L_0_G_D_2_P_0"
45 android:fillAlpha="0.8"
46 android:fillColor="?attr/colorOnSecondaryContainer"
47 android:fillType="nonZero"
48 android:pathData=" M100.45 50.26 C128.62,50.26 151.46,73.1 151.46,101.28 C151.46,129.45 128.62,152.29 100.45,152.29 C72.27,152.29 49.43,129.45 49.43,101.28 C49.43,73.1 72.27,50.26 100.45,50.26c " />
49 </group>
50 </group>
51 </group>
52 <group android:name="time_group" />
53 </vector>
54 </aapt:attr>
55 <target android:name="_R_G_L_0_G_T_1">
56 <aapt:attr name="android:animation">
57 <set android:ordering="together">
58 <objectAnimator
59 android:duration="267"
60 android:pathData="M 500,500C 500,500 364,500 364,500"
61 android:propertyName="translateXY"
62 android:propertyXName="translateX"
63 android:propertyYName="translateY"
64 android:startOffset="0">
65 <aapt:attr name="android:interpolator">
66 <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
67 </aapt:attr>
68 </objectAnimator>
69 <objectAnimator
70 android:duration="234"
71 android:pathData="M 364,500C 364,500 364,500 364,500"
72 android:propertyName="translateXY"
73 android:propertyXName="translateX"
74 android:propertyYName="translateY"
75 android:startOffset="267">
76 <aapt:attr name="android:interpolator">
77 <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0.333 0.667,0.667 1.0,1.0" />
78 </aapt:attr>
79 </objectAnimator>
80 <objectAnimator
81 android:duration="133"
82 android:pathData="M 364,500C 364,500 525,500 525,500"
83 android:propertyName="translateXY"
84 android:propertyXName="translateX"
85 android:propertyYName="translateY"
86 android:startOffset="501">
87 <aapt:attr name="android:interpolator">
88 <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
89 </aapt:attr>
90 </objectAnimator>
91 <objectAnimator
92 android:duration="100"
93 android:pathData="M 525,500C 525,500 500,500 500,500"
94 android:propertyName="translateXY"
95 android:propertyXName="translateX"
96 android:propertyYName="translateY"
97 android:startOffset="634">
98 <aapt:attr name="android:interpolator">
99 <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
100 </aapt:attr>
101 </objectAnimator>
102 <objectAnimator
103 android:duration="400"
104 android:pathData="M 500,500C 500,500 500,500 500,500"
105 android:propertyName="translateXY"
106 android:propertyXName="translateX"
107 android:propertyYName="translateY"
108 android:startOffset="734">
109 <aapt:attr name="android:interpolator">
110 <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0.333 0.667,0.667 1.0,1.0" />
111 </aapt:attr>
112 </objectAnimator>
113 <objectAnimator
114 android:duration="267"
115 android:pathData="M 500,500C 500,500 500,364 500,364"
116 android:propertyName="translateXY"
117 android:propertyXName="translateX"
118 android:propertyYName="translateY"
119 android:startOffset="1134">
120 <aapt:attr name="android:interpolator">
121 <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
122 </aapt:attr>
123 </objectAnimator>
124 <objectAnimator
125 android:duration="234"
126 android:pathData="M 500,364C 500,364 500,364 500,364"
127 android:propertyName="translateXY"
128 android:propertyXName="translateX"
129 android:propertyYName="translateY"
130 android:startOffset="1401">
131 <aapt:attr name="android:interpolator">
132 <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0.333 0.667,0.667 1.0,1.0" />
133 </aapt:attr>
134 </objectAnimator>
135 <objectAnimator
136 android:duration="133"
137 android:pathData="M 500,364C 500,364 500,535 500,535"
138 android:propertyName="translateXY"
139 android:propertyXName="translateX"
140 android:propertyYName="translateY"
141 android:startOffset="1635">
142 <aapt:attr name="android:interpolator">
143 <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
144 </aapt:attr>
145 </objectAnimator>
146 <objectAnimator
147 android:duration="100"
148 android:pathData="M 500,535C 500,535 500,500 500,500"
149 android:propertyName="translateXY"
150 android:propertyXName="translateX"
151 android:propertyYName="translateY"
152 android:startOffset="1768">
153 <aapt:attr name="android:interpolator">
154 <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
155 </aapt:attr>
156 </objectAnimator>
157 </set>
158 </aapt:attr>
159 </target>
160 <target android:name="time_group">
161 <aapt:attr name="android:animation">
162 <set android:ordering="together">
163 <objectAnimator
164 android:duration="2269"
165 android:propertyName="translateX"
166 android:startOffset="0"
167 android:valueFrom="0"
168 android:valueTo="1"
169 android:valueType="floatType" />
170 </set>
171 </aapt:attr>
172 </target>
173</animated-vector>
diff --git a/src/android/app/src/main/res/layout-ldrtl/list_item_setting_input.xml b/src/android/app/src/main/res/layout-ldrtl/list_item_setting_input.xml
new file mode 100644
index 000000000..583620dc6
--- /dev/null
+++ b/src/android/app/src/main/res/layout-ldrtl/list_item_setting_input.xml
@@ -0,0 +1,63 @@
1<?xml version="1.0" encoding="utf-8"?>
2<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 xmlns:tools="http://schemas.android.com/tools"
5 android:id="@+id/setting_body"
6 android:layout_width="match_parent"
7 android:layout_height="wrap_content"
8 android:background="?android:attr/selectableItemBackground"
9 android:clickable="true"
10 android:focusable="true"
11 android:gravity="center_vertical"
12 android:minHeight="72dp"
13 android:padding="16dp"
14 android:nextFocusLeft="@id/button_options">
15
16 <LinearLayout
17 android:layout_width="match_parent"
18 android:layout_height="wrap_content"
19 android:gravity="center_vertical"
20 android:orientation="horizontal">
21
22 <LinearLayout
23 android:layout_width="0dp"
24 android:layout_height="wrap_content"
25 android:orientation="vertical"
26 android:layout_weight="1">
27
28 <com.google.android.material.textview.MaterialTextView
29 android:id="@+id/text_setting_name"
30 style="@style/TextAppearance.Material3.HeadlineMedium"
31 android:layout_width="match_parent"
32 android:layout_height="wrap_content"
33 android:textAlignment="viewStart"
34 android:textSize="17sp"
35 app:lineHeight="22dp"
36 tools:text="Setting Name" />
37
38 <com.google.android.material.textview.MaterialTextView
39 android:id="@+id/text_setting_value"
40 style="@style/TextAppearance.Material3.LabelMedium"
41 android:layout_width="match_parent"
42 android:layout_height="wrap_content"
43 android:layout_marginTop="@dimen/spacing_small"
44 android:textAlignment="viewStart"
45 android:textStyle="bold"
46 android:textSize="13sp"
47 tools:text="1x" />
48
49 </LinearLayout>
50
51 <Button
52 android:id="@+id/button_options"
53 style="?attr/materialIconButtonStyle"
54 android:layout_width="wrap_content"
55 android:layout_height="wrap_content"
56 android:nextFocusRight="@id/setting_body"
57 app:icon="@drawable/ic_more_vert"
58 app:iconSize="24dp"
59 app:iconTint="?attr/colorOnSurface" />
60
61 </LinearLayout>
62
63</RelativeLayout>
diff --git a/src/android/app/src/main/res/layout/dialog_input_profiles.xml b/src/android/app/src/main/res/layout/dialog_input_profiles.xml
new file mode 100644
index 000000000..6ad76fe41
--- /dev/null
+++ b/src/android/app/src/main/res/layout/dialog_input_profiles.xml
@@ -0,0 +1,6 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
3 android:id="@+id/list_profiles"
4 android:layout_width="match_parent"
5 android:layout_height="wrap_content"
6 android:fadeScrollbars="false" />
diff --git a/src/android/app/src/main/res/layout/dialog_mapping.xml b/src/android/app/src/main/res/layout/dialog_mapping.xml
new file mode 100644
index 000000000..06190b8d2
--- /dev/null
+++ b/src/android/app/src/main/res/layout/dialog_mapping.xml
@@ -0,0 +1,26 @@
1<?xml version="1.0" encoding="utf-8"?>
2<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 android:layout_width="match_parent"
4 android:layout_height="wrap_content"
5 xmlns:tools="http://schemas.android.com/tools"
6 android:defaultFocusHighlightEnabled="false"
7 android:focusable="true"
8 android:focusableInTouchMode="true"
9 android:focusedByDefault="true"
10 android:orientation="horizontal"
11 android:gravity="center">
12
13 <ImageView
14 android:id="@+id/image_stick_animation"
15 android:layout_width="@dimen/mapping_anim_size"
16 android:layout_height="@dimen/mapping_anim_size"
17 tools:src="@drawable/stick_two_direction_anim" />
18
19 <ImageView
20 android:id="@+id/image_button_animation"
21 android:layout_width="@dimen/mapping_anim_size"
22 android:layout_height="@dimen/mapping_anim_size"
23 android:layout_marginStart="48dp"
24 tools:src="@drawable/button_anim" />
25
26</LinearLayout>
diff --git a/src/android/app/src/main/res/layout/list_item_input_profile.xml b/src/android/app/src/main/res/layout/list_item_input_profile.xml
new file mode 100644
index 000000000..a08dccf0c
--- /dev/null
+++ b/src/android/app/src/main/res/layout/list_item_input_profile.xml
@@ -0,0 +1,74 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 xmlns:tools="http://schemas.android.com/tools"
5 android:layout_width="match_parent"
6 android:layout_height="wrap_content"
7 android:focusable="false"
8 android:paddingHorizontal="20dp"
9 android:paddingVertical="16dp">
10
11 <com.google.android.material.textview.MaterialTextView
12 android:id="@+id/title"
13 style="@style/TextAppearance.Material3.HeadlineMedium"
14 android:layout_width="0dp"
15 android:layout_height="0dp"
16 android:textAlignment="viewStart"
17 android:gravity="start|center_vertical"
18 android:textSize="17sp"
19 android:layout_marginEnd="16dp"
20 app:layout_constraintBottom_toBottomOf="@+id/button_layout"
21 app:layout_constraintEnd_toStartOf="@+id/button_layout"
22 app:layout_constraintStart_toStartOf="parent"
23 app:layout_constraintTop_toTopOf="parent"
24 app:lineHeight="28dp"
25 tools:text="My profile" />
26
27 <LinearLayout
28 android:id="@+id/button_layout"
29 android:layout_width="wrap_content"
30 android:layout_height="wrap_content"
31 android:gravity="center_vertical"
32 android:orientation="horizontal"
33 app:layout_constraintEnd_toEndOf="parent"
34 app:layout_constraintTop_toTopOf="parent">
35
36 <Button
37 android:id="@+id/button_new"
38 style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
39 android:layout_width="wrap_content"
40 android:layout_height="wrap_content"
41 android:contentDescription="@string/create_new_profile"
42 android:tooltipText="@string/create_new_profile"
43 app:icon="@drawable/ic_new_label" />
44
45 <Button
46 android:id="@+id/button_delete"
47 style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
48 android:layout_width="wrap_content"
49 android:layout_height="wrap_content"
50 android:contentDescription="@string/delete"
51 android:tooltipText="@string/delete"
52 app:icon="@drawable/ic_delete" />
53
54 <Button
55 android:id="@+id/button_save"
56 style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
57 android:layout_width="wrap_content"
58 android:layout_height="wrap_content"
59 android:contentDescription="@string/save"
60 android:tooltipText="@string/save"
61 app:icon="@drawable/ic_save" />
62
63 <Button
64 android:id="@+id/button_load"
65 style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
66 android:layout_width="wrap_content"
67 android:layout_height="wrap_content"
68 android:contentDescription="@string/load"
69 android:tooltipText="@string/load"
70 app:icon="@drawable/ic_import" />
71
72 </LinearLayout>
73
74</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/src/android/app/src/main/res/layout/list_item_setting_input.xml b/src/android/app/src/main/res/layout/list_item_setting_input.xml
new file mode 100644
index 000000000..d67cbe245
--- /dev/null
+++ b/src/android/app/src/main/res/layout/list_item_setting_input.xml
@@ -0,0 +1,63 @@
1<?xml version="1.0" encoding="utf-8"?>
2<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 xmlns:tools="http://schemas.android.com/tools"
5 android:id="@+id/setting_body"
6 android:layout_width="match_parent"
7 android:layout_height="wrap_content"
8 android:background="?android:attr/selectableItemBackground"
9 android:clickable="true"
10 android:focusable="true"
11 android:gravity="center_vertical"
12 android:minHeight="72dp"
13 android:padding="16dp"
14 android:nextFocusRight="@id/button_options">
15
16 <LinearLayout
17 android:layout_width="match_parent"
18 android:layout_height="wrap_content"
19 android:gravity="center_vertical"
20 android:orientation="horizontal">
21
22 <LinearLayout
23 android:layout_width="0dp"
24 android:layout_height="wrap_content"
25 android:orientation="vertical"
26 android:layout_weight="1">
27
28 <com.google.android.material.textview.MaterialTextView
29 android:id="@+id/text_setting_name"
30 style="@style/TextAppearance.Material3.HeadlineMedium"
31 android:layout_width="match_parent"
32 android:layout_height="wrap_content"
33 android:textAlignment="viewStart"
34 android:textSize="17sp"
35 app:lineHeight="22dp"
36 tools:text="Setting Name" />
37
38 <com.google.android.material.textview.MaterialTextView
39 android:id="@+id/text_setting_value"
40 style="@style/TextAppearance.Material3.LabelMedium"
41 android:layout_width="match_parent"
42 android:layout_height="wrap_content"
43 android:layout_marginTop="@dimen/spacing_small"
44 android:textAlignment="viewStart"
45 android:textStyle="bold"
46 android:textSize="13sp"
47 tools:text="1x" />
48
49 </LinearLayout>
50
51 <Button
52 android:id="@+id/button_options"
53 style="?attr/materialIconButtonStyle"
54 android:layout_width="wrap_content"
55 android:layout_height="wrap_content"
56 android:nextFocusLeft="@id/setting_body"
57 app:icon="@drawable/ic_more_vert"
58 app:iconSize="24dp"
59 app:iconTint="?attr/colorOnSurface" />
60
61 </LinearLayout>
62
63</RelativeLayout>
diff --git a/src/android/app/src/main/res/menu/menu_in_game.xml b/src/android/app/src/main/res/menu/menu_in_game.xml
index eecb0563b..867197ebc 100644
--- a/src/android/app/src/main/res/menu/menu_in_game.xml
+++ b/src/android/app/src/main/res/menu/menu_in_game.xml
@@ -17,8 +17,13 @@
17 android:title="@string/per_game_settings" /> 17 android:title="@string/per_game_settings" />
18 18
19 <item 19 <item
20 android:id="@+id/menu_overlay_controls" 20 android:id="@+id/menu_controls"
21 android:icon="@drawable/ic_controller" 21 android:icon="@drawable/ic_controller"
22 android:title="@string/preferences_controls" />
23
24 <item
25 android:id="@+id/menu_overlay_controls"
26 android:icon="@drawable/ic_overlay"
22 android:title="@string/emulation_input_overlay" /> 27 android:title="@string/emulation_input_overlay" />
23 28
24 <item 29 <item
diff --git a/src/android/app/src/main/res/menu/menu_input_options.xml b/src/android/app/src/main/res/menu/menu_input_options.xml
new file mode 100644
index 000000000..81ea5043f
--- /dev/null
+++ b/src/android/app/src/main/res/menu/menu_input_options.xml
@@ -0,0 +1,34 @@
1<?xml version="1.0" encoding="utf-8"?>
2<menu xmlns:android="http://schemas.android.com/apk/res/android">
3
4 <item
5 android:id="@+id/invert_axis"
6 android:title="@string/invert_axis"
7 android:visible="false" />
8
9 <item
10 android:id="@+id/invert_button"
11 android:title="@string/invert_button"
12 android:visible="false" />
13
14 <item
15 android:id="@+id/toggle_button"
16 android:title="@string/toggle_button"
17 android:visible="false" />
18
19 <item
20 android:id="@+id/turbo_button"
21 android:title="@string/turbo_button"
22 android:visible="false" />
23
24 <item
25 android:id="@+id/set_threshold"
26 android:title="@string/set_threshold"
27 android:visible="false" />
28
29 <item
30 android:id="@+id/toggle_axis"
31 android:title="@string/toggle_axis"
32 android:visible="false" />
33
34</menu>
diff --git a/src/android/app/src/main/res/navigation/settings_navigation.xml b/src/android/app/src/main/res/navigation/settings_navigation.xml
index 1d87d36b3..e4c66e7d5 100644
--- a/src/android/app/src/main/res/navigation/settings_navigation.xml
+++ b/src/android/app/src/main/res/navigation/settings_navigation.xml
@@ -26,7 +26,7 @@
26 26
27 <fragment 27 <fragment
28 android:id="@+id/settingsSearchFragment" 28 android:id="@+id/settingsSearchFragment"
29 android:name="org.yuzu.yuzu_emu.fragments.SettingsSearchFragment" 29 android:name="org.yuzu.yuzu_emu.features.settings.ui.SettingsSearchFragment"
30 android:label="SettingsSearchFragment" /> 30 android:label="SettingsSearchFragment" />
31 31
32</navigation> 32</navigation>
diff --git a/src/android/app/src/main/res/values-w600dp/dimens.xml b/src/android/app/src/main/res/values-w600dp/dimens.xml
index 128319e27..0e2d40876 100644
--- a/src/android/app/src/main/res/values-w600dp/dimens.xml
+++ b/src/android/app/src/main/res/values-w600dp/dimens.xml
@@ -2,4 +2,6 @@
2<resources> 2<resources>
3 <dimen name="spacing_navigation">0dp</dimen> 3 <dimen name="spacing_navigation">0dp</dimen>
4 <dimen name="spacing_navigation_rail">80dp</dimen> 4 <dimen name="spacing_navigation_rail">80dp</dimen>
5
6 <dimen name="mapping_anim_size">100dp</dimen>
5</resources> 7</resources>
diff --git a/src/android/app/src/main/res/values/dimens.xml b/src/android/app/src/main/res/values/dimens.xml
index 992b5ae44..bf733637f 100644
--- a/src/android/app/src/main/res/values/dimens.xml
+++ b/src/android/app/src/main/res/values/dimens.xml
@@ -18,4 +18,6 @@
18 18
19 <dimen name="dialog_margin">20dp</dimen> 19 <dimen name="dialog_margin">20dp</dimen>
20 <dimen name="elevated_app_bar">3dp</dimen> 20 <dimen name="elevated_app_bar">3dp</dimen>
21
22 <dimen name="mapping_anim_size">75dp</dimen>
21</resources> 23</resources>
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index 78a4c958a..6a631f664 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -255,6 +255,92 @@
255 <string name="audio_volume">Volume</string> 255 <string name="audio_volume">Volume</string>
256 <string name="audio_volume_description">Specifies the volume of audio output.</string> 256 <string name="audio_volume_description">Specifies the volume of audio output.</string>
257 257
258 <!-- Input strings -->
259 <string name="buttons">Buttons</string>
260 <string name="button_a">A</string>
261 <string name="button_b">B</string>
262 <string name="button_x">X</string>
263 <string name="button_y">Y</string>
264 <string name="button_plus">Plus</string>
265 <string name="button_minus">Minus</string>
266 <string name="button_home">Home</string>
267 <string name="button_capture">Capture</string>
268 <string name="start_pause">Start/Pause</string>
269 <string name="dpad">D-Pad</string>
270 <string name="up">Up</string>
271 <string name="down">Down</string>
272 <string name="left">Left</string>
273 <string name="right">Right</string>
274 <string name="left_stick">Left stick</string>
275 <string name="control_stick">Control stick</string>
276 <string name="right_stick">Right stick</string>
277 <string name="c_stick">C-Stick</string>
278 <string name="pressed">Pressed</string>
279 <string name="range">Range</string>
280 <string name="deadzone">Deadzone</string>
281 <string name="modifier">Modifier</string>
282 <string name="modifier_range">Modifier range</string>
283 <string name="triggers">Triggers</string>
284 <string name="button_l">L</string>
285 <string name="button_r">R</string>
286 <string name="button_zl">ZL</string>
287 <string name="button_zr">ZR</string>
288 <string name="button_sl_left">Left SL</string>
289 <string name="button_sr_left">Left SR</string>
290 <string name="button_sl_right">Right SL</string>
291 <string name="button_sr_right">Right SR</string>
292 <string name="button_z">Z</string>
293 <string name="invalid">Invalid</string>
294 <string name="not_set">Not set</string>
295 <string name="unknown">Unknown</string>
296 <string name="qualified_hat">%1$s%2$s%3$sHat %4$s</string>
297 <string name="qualified_button_stick_axis">%1$s%2$s%3$sAxis %4$s</string>
298 <string name="qualified_button">%1$s%2$s%3$sButton %4$s</string>
299 <string name="qualified_axis">Axis %1$s%2$s</string>
300 <string name="unused">Unused</string>
301 <string name="input_prompt">Move or press an input</string>
302 <string name="unsupported_input">Unsupported input type</string>
303 <string name="input_mapping_filter">Input mapping filter</string>
304 <string name="input_mapping_filter_description">Select a device to filter mapping inputs</string>
305 <string name="auto_map">Auto-map a controller</string>
306 <string name="auto_map_description">Select a device to attempt auto-mapping</string>
307 <string name="attempted_auto_map">Attempted auto-map with %1$s</string>
308 <string name="controller_type">Controller type</string>
309 <string name="pro_controller">Pro Controller</string>
310 <string name="handheld">Handheld</string>
311 <string name="dual_joycons">Dual Joycons</string>
312 <string name="left_joycon">Left Joycon</string>
313 <string name="right_joycon">Right Joycon</string>
314 <string name="gamecube_controller">GameCube Controller</string>
315 <string name="invert_axis">Invert axis</string>
316 <string name="invert_button">Invert button</string>
317 <string name="toggle_button">Toggle button</string>
318 <string name="turbo_button">Turbo button</string>
319 <string name="set_threshold">Set threshold</string>
320 <string name="toggle_axis">Toggle axis</string>
321 <string name="connected">Connected</string>
322 <string name="use_system_vibrator">Use system vibrator</string>
323 <string name="input_overlay">Input overlay</string>
324 <string name="vibration">Vibration</string>
325 <string name="vibration_strength">Vibration strength</string>
326 <string name="profile">Profile</string>
327 <string name="create_new_profile">Create new profile</string>
328 <string name="enter_profile_name">Enter profile name</string>
329 <string name="profile_name_already_exists">Profile name already exists</string>
330 <string name="invalid_profile_name">Invalid profile name</string>
331 <string name="use_global_input_configuration">Use global input configuration</string>
332 <string name="player_num_profile">Player %d profile</string>
333 <string name="delete_input_profile">Delete input profile</string>
334 <string name="delete_input_profile_description">Are you sure that you want to delete this profile? This is not recoverable.</string>
335 <string name="stick_map_description">Move a stick left and then up or press a button</string>
336 <string name="button_map_description">Press a button or move a trigger/stick</string>
337 <string name="map_dpad_direction">Map to D-Pad %1$s</string>
338 <string name="map_control">Map to %1$s</string>
339 <string name="failed_to_load_profile">Failed to load profile</string>
340 <string name="failed_to_save_profile">Failed to save profile</string>
341 <string name="reset_mapping">Reset mappings</string>
342 <string name="reset_mapping_description">Are you sure that you want to reset all mappings for this controller to default? This cannot be undone.</string>
343
258 <!-- Miscellaneous --> 344 <!-- Miscellaneous -->
259 <string name="slider_default">Default</string> 345 <string name="slider_default">Default</string>
260 <string name="ini_saved">Saved settings</string> 346 <string name="ini_saved">Saved settings</string>
@@ -292,6 +378,10 @@
292 <string name="more_options">More options</string> 378 <string name="more_options">More options</string>
293 <string name="use_global_setting">Use global setting</string> 379 <string name="use_global_setting">Use global setting</string>
294 <string name="operation_completed_successfully">The operation completed successfully</string> 380 <string name="operation_completed_successfully">The operation completed successfully</string>
381 <string name="retry">Retry</string>
382 <string name="confirm">Confirm</string>
383 <string name="load">Load</string>
384 <string name="save">Save</string>
295 385
296 <!-- GPU driver installation --> 386 <!-- GPU driver installation -->
297 <string name="select_gpu_driver">Select GPU driver</string> 387 <string name="select_gpu_driver">Select GPU driver</string>
@@ -313,6 +403,9 @@
313 <string name="preferences_graphics_description">Accuracy level, resolution, shader cache</string> 403 <string name="preferences_graphics_description">Accuracy level, resolution, shader cache</string>
314 <string name="preferences_audio">Audio</string> 404 <string name="preferences_audio">Audio</string>
315 <string name="preferences_audio_description">Output engine, volume</string> 405 <string name="preferences_audio_description">Output engine, volume</string>
406 <string name="preferences_controls">Controls</string>
407 <string name="preferences_controls_description">Map controller input</string>
408 <string name="preferences_player">Player %d</string>
316 <string name="preferences_theme">Theme and color</string> 409 <string name="preferences_theme">Theme and color</string>
317 <string name="preferences_debug">Debug</string> 410 <string name="preferences_debug">Debug</string>
318 <string name="preferences_debug_description">CPU/GPU debugging, graphics API, fastmem</string> 411 <string name="preferences_debug_description">CPU/GPU debugging, graphics API, fastmem</string>
diff --git a/src/common/android/id_cache.cpp b/src/common/android/id_cache.cpp
index f39262db9..1145cbdf2 100644
--- a/src/common/android/id_cache.cpp
+++ b/src/common/android/id_cache.cpp
@@ -65,6 +65,30 @@ static jclass s_boolean_class;
65static jmethodID s_boolean_constructor; 65static jmethodID s_boolean_constructor;
66static jfieldID s_boolean_value_field; 66static jfieldID s_boolean_value_field;
67 67
68static jclass s_player_input_class;
69static jmethodID s_player_input_constructor;
70static jfieldID s_player_input_connected_field;
71static jfieldID s_player_input_buttons_field;
72static jfieldID s_player_input_analogs_field;
73static jfieldID s_player_input_motions_field;
74static jfieldID s_player_input_vibration_enabled_field;
75static jfieldID s_player_input_vibration_strength_field;
76static jfieldID s_player_input_body_color_left_field;
77static jfieldID s_player_input_body_color_right_field;
78static jfieldID s_player_input_button_color_left_field;
79static jfieldID s_player_input_button_color_right_field;
80static jfieldID s_player_input_profile_name_field;
81static jfieldID s_player_input_use_system_vibrator_field;
82
83static jclass s_yuzu_input_device_interface;
84static jmethodID s_yuzu_input_device_get_name;
85static jmethodID s_yuzu_input_device_get_guid;
86static jmethodID s_yuzu_input_device_get_port;
87static jmethodID s_yuzu_input_device_get_supports_vibration;
88static jmethodID s_yuzu_input_device_vibrate;
89static jmethodID s_yuzu_input_device_get_axes;
90static jmethodID s_yuzu_input_device_has_keys;
91
68static constexpr jint JNI_VERSION = JNI_VERSION_1_6; 92static constexpr jint JNI_VERSION = JNI_VERSION_1_6;
69 93
70namespace Common::Android { 94namespace Common::Android {
@@ -276,6 +300,94 @@ jfieldID GetBooleanValueField() {
276 return s_boolean_value_field; 300 return s_boolean_value_field;
277} 301}
278 302
303jclass GetPlayerInputClass() {
304 return s_player_input_class;
305}
306
307jmethodID GetPlayerInputConstructor() {
308 return s_player_input_constructor;
309}
310
311jfieldID GetPlayerInputConnectedField() {
312 return s_player_input_connected_field;
313}
314
315jfieldID GetPlayerInputButtonsField() {
316 return s_player_input_buttons_field;
317}
318
319jfieldID GetPlayerInputAnalogsField() {
320 return s_player_input_analogs_field;
321}
322
323jfieldID GetPlayerInputMotionsField() {
324 return s_player_input_motions_field;
325}
326
327jfieldID GetPlayerInputVibrationEnabledField() {
328 return s_player_input_vibration_enabled_field;
329}
330
331jfieldID GetPlayerInputVibrationStrengthField() {
332 return s_player_input_vibration_strength_field;
333}
334
335jfieldID GetPlayerInputBodyColorLeftField() {
336 return s_player_input_body_color_left_field;
337}
338
339jfieldID GetPlayerInputBodyColorRightField() {
340 return s_player_input_body_color_right_field;
341}
342
343jfieldID GetPlayerInputButtonColorLeftField() {
344 return s_player_input_button_color_left_field;
345}
346
347jfieldID GetPlayerInputButtonColorRightField() {
348 return s_player_input_button_color_right_field;
349}
350
351jfieldID GetPlayerInputProfileNameField() {
352 return s_player_input_profile_name_field;
353}
354
355jfieldID GetPlayerInputUseSystemVibratorField() {
356 return s_player_input_use_system_vibrator_field;
357}
358
359jclass GetYuzuInputDeviceInterface() {
360 return s_yuzu_input_device_interface;
361}
362
363jmethodID GetYuzuDeviceGetName() {
364 return s_yuzu_input_device_get_name;
365}
366
367jmethodID GetYuzuDeviceGetGUID() {
368 return s_yuzu_input_device_get_guid;
369}
370
371jmethodID GetYuzuDeviceGetPort() {
372 return s_yuzu_input_device_get_port;
373}
374
375jmethodID GetYuzuDeviceGetSupportsVibration() {
376 return s_yuzu_input_device_get_supports_vibration;
377}
378
379jmethodID GetYuzuDeviceVibrate() {
380 return s_yuzu_input_device_vibrate;
381}
382
383jmethodID GetYuzuDeviceGetAxes() {
384 return s_yuzu_input_device_get_axes;
385}
386
387jmethodID GetYuzuDeviceHasKeys() {
388 return s_yuzu_input_device_has_keys;
389}
390
279#ifdef __cplusplus 391#ifdef __cplusplus
280extern "C" { 392extern "C" {
281#endif 393#endif
@@ -387,6 +499,55 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
387 s_boolean_value_field = env->GetFieldID(boolean_class, "value", "Z"); 499 s_boolean_value_field = env->GetFieldID(boolean_class, "value", "Z");
388 env->DeleteLocalRef(boolean_class); 500 env->DeleteLocalRef(boolean_class);
389 501
502 const jclass player_input_class =
503 env->FindClass("org/yuzu/yuzu_emu/features/input/model/PlayerInput");
504 s_player_input_class = reinterpret_cast<jclass>(env->NewGlobalRef(player_input_class));
505 s_player_input_constructor = env->GetMethodID(
506 player_input_class, "<init>",
507 "(Z[Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;ZIJJJJLjava/lang/String;Z)V");
508 s_player_input_connected_field = env->GetFieldID(player_input_class, "connected", "Z");
509 s_player_input_buttons_field =
510 env->GetFieldID(player_input_class, "buttons", "[Ljava/lang/String;");
511 s_player_input_analogs_field =
512 env->GetFieldID(player_input_class, "analogs", "[Ljava/lang/String;");
513 s_player_input_motions_field =
514 env->GetFieldID(player_input_class, "motions", "[Ljava/lang/String;");
515 s_player_input_vibration_enabled_field =
516 env->GetFieldID(player_input_class, "vibrationEnabled", "Z");
517 s_player_input_vibration_strength_field =
518 env->GetFieldID(player_input_class, "vibrationStrength", "I");
519 s_player_input_body_color_left_field =
520 env->GetFieldID(player_input_class, "bodyColorLeft", "J");
521 s_player_input_body_color_right_field =
522 env->GetFieldID(player_input_class, "bodyColorRight", "J");
523 s_player_input_button_color_left_field =
524 env->GetFieldID(player_input_class, "buttonColorLeft", "J");
525 s_player_input_button_color_right_field =
526 env->GetFieldID(player_input_class, "buttonColorRight", "J");
527 s_player_input_profile_name_field =
528 env->GetFieldID(player_input_class, "profileName", "Ljava/lang/String;");
529 s_player_input_use_system_vibrator_field =
530 env->GetFieldID(player_input_class, "useSystemVibrator", "Z");
531 env->DeleteLocalRef(player_input_class);
532
533 const jclass yuzu_input_device_interface =
534 env->FindClass("org/yuzu/yuzu_emu/features/input/YuzuInputDevice");
535 s_yuzu_input_device_interface =
536 reinterpret_cast<jclass>(env->NewGlobalRef(yuzu_input_device_interface));
537 s_yuzu_input_device_get_name =
538 env->GetMethodID(yuzu_input_device_interface, "getName", "()Ljava/lang/String;");
539 s_yuzu_input_device_get_guid =
540 env->GetMethodID(yuzu_input_device_interface, "getGUID", "()Ljava/lang/String;");
541 s_yuzu_input_device_get_port = env->GetMethodID(yuzu_input_device_interface, "getPort", "()I");
542 s_yuzu_input_device_get_supports_vibration =
543 env->GetMethodID(yuzu_input_device_interface, "getSupportsVibration", "()Z");
544 s_yuzu_input_device_vibrate = env->GetMethodID(yuzu_input_device_interface, "vibrate", "(F)V");
545 s_yuzu_input_device_get_axes =
546 env->GetMethodID(yuzu_input_device_interface, "getAxes", "()[Ljava/lang/Integer;");
547 s_yuzu_input_device_has_keys =
548 env->GetMethodID(yuzu_input_device_interface, "hasKeys", "([I)[Z");
549 env->DeleteLocalRef(yuzu_input_device_interface);
550
390 // Initialize Android Storage 551 // Initialize Android Storage
391 Common::FS::Android::RegisterCallbacks(env, s_native_library_class); 552 Common::FS::Android::RegisterCallbacks(env, s_native_library_class);
392 553
@@ -416,6 +577,8 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) {
416 env->DeleteGlobalRef(s_double_class); 577 env->DeleteGlobalRef(s_double_class);
417 env->DeleteGlobalRef(s_integer_class); 578 env->DeleteGlobalRef(s_integer_class);
418 env->DeleteGlobalRef(s_boolean_class); 579 env->DeleteGlobalRef(s_boolean_class);
580 env->DeleteGlobalRef(s_player_input_class);
581 env->DeleteGlobalRef(s_yuzu_input_device_interface);
419 582
420 // UnInitialize applets 583 // UnInitialize applets
421 SoftwareKeyboard::CleanupJNI(env); 584 SoftwareKeyboard::CleanupJNI(env);
diff --git a/src/common/android/id_cache.h b/src/common/android/id_cache.h
index 47802f96c..cd2844dcc 100644
--- a/src/common/android/id_cache.h
+++ b/src/common/android/id_cache.h
@@ -85,4 +85,28 @@ jclass GetBooleanClass();
85jmethodID GetBooleanConstructor(); 85jmethodID GetBooleanConstructor();
86jfieldID GetBooleanValueField(); 86jfieldID GetBooleanValueField();
87 87
88jclass GetPlayerInputClass();
89jmethodID GetPlayerInputConstructor();
90jfieldID GetPlayerInputConnectedField();
91jfieldID GetPlayerInputButtonsField();
92jfieldID GetPlayerInputAnalogsField();
93jfieldID GetPlayerInputMotionsField();
94jfieldID GetPlayerInputVibrationEnabledField();
95jfieldID GetPlayerInputVibrationStrengthField();
96jfieldID GetPlayerInputBodyColorLeftField();
97jfieldID GetPlayerInputBodyColorRightField();
98jfieldID GetPlayerInputButtonColorLeftField();
99jfieldID GetPlayerInputButtonColorRightField();
100jfieldID GetPlayerInputProfileNameField();
101jfieldID GetPlayerInputUseSystemVibratorField();
102
103jclass GetYuzuInputDeviceInterface();
104jmethodID GetYuzuDeviceGetName();
105jmethodID GetYuzuDeviceGetGUID();
106jmethodID GetYuzuDeviceGetPort();
107jmethodID GetYuzuDeviceGetSupportsVibration();
108jmethodID GetYuzuDeviceVibrate();
109jmethodID GetYuzuDeviceGetAxes();
110jmethodID GetYuzuDeviceHasKeys();
111
88} // namespace Common::Android 112} // namespace Common::Android
diff --git a/src/common/settings_input.h b/src/common/settings_input.h
index 53a95ef8f..a99bb0892 100644
--- a/src/common/settings_input.h
+++ b/src/common/settings_input.h
@@ -395,6 +395,10 @@ struct PlayerInput {
395 u32 button_color_left; 395 u32 button_color_left;
396 u32 button_color_right; 396 u32 button_color_right;
397 std::string profile_name; 397 std::string profile_name;
398
399 // This is meant to tell the Android frontend whether to use a device's built-in vibration
400 // motor or a controller's vibrations.
401 bool use_system_vibrator;
398}; 402};
399 403
400struct TouchscreenInput { 404struct TouchscreenInput {
diff --git a/src/hid_core/frontend/emulated_controller.cpp b/src/hid_core/frontend/emulated_controller.cpp
index 8b5d0eec6..3fa06d188 100644
--- a/src/hid_core/frontend/emulated_controller.cpp
+++ b/src/hid_core/frontend/emulated_controller.cpp
@@ -1285,9 +1285,7 @@ bool EmulatedController::SetVibration(DeviceIndex device_index, const VibrationV
1285 }; 1285 };
1286 1286
1287 // Send vibrations to Android's input overlay 1287 // Send vibrations to Android's input overlay
1288 if (npad_id_type == NpadIdType::Handheld || npad_id_type == NpadIdType::Player1) { 1288 output_devices[4]->SetVibration(status);
1289 output_devices[4]->SetVibration(status);
1290 }
1291 1289
1292 return output_devices[index]->SetVibration(status) == Common::Input::DriverResult::Success; 1290 return output_devices[index]->SetVibration(status) == Common::Input::DriverResult::Success;
1293} 1291}
diff --git a/src/input_common/CMakeLists.txt b/src/input_common/CMakeLists.txt
index d0a71a15b..d455323e0 100644
--- a/src/input_common/CMakeLists.txt
+++ b/src/input_common/CMakeLists.txt
@@ -2,8 +2,6 @@
2# SPDX-License-Identifier: GPL-2.0-or-later 2# SPDX-License-Identifier: GPL-2.0-or-later
3 3
4add_library(input_common STATIC 4add_library(input_common STATIC
5 drivers/android.cpp
6 drivers/android.h
7 drivers/camera.cpp 5 drivers/camera.cpp
8 drivers/camera.h 6 drivers/camera.h
9 drivers/keyboard.cpp 7 drivers/keyboard.cpp
@@ -94,3 +92,11 @@ target_link_libraries(input_common PUBLIC hid_core PRIVATE common Boost::headers
94if (YUZU_USE_PRECOMPILED_HEADERS) 92if (YUZU_USE_PRECOMPILED_HEADERS)
95 target_precompile_headers(input_common PRIVATE precompiled_headers.h) 93 target_precompile_headers(input_common PRIVATE precompiled_headers.h)
96endif() 94endif()
95
96if (ANDROID)
97 target_sources(input_common PRIVATE
98 drivers/android.cpp
99 drivers/android.h
100 )
101 target_link_libraries(input_common PRIVATE android)
102endif()
diff --git a/src/input_common/drivers/android.cpp b/src/input_common/drivers/android.cpp
index b6a03fdc0..e859cc538 100644
--- a/src/input_common/drivers/android.cpp
+++ b/src/input_common/drivers/android.cpp
@@ -1,30 +1,47 @@
1// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project 1// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-3.0-or-later 2// SPDX-License-Identifier: GPL-3.0-or-later
3 3
4#include <set>
5#include <common/settings_input.h>
6#include <jni.h>
7#include "common/android/android_common.h"
8#include "common/android/id_cache.h"
4#include "input_common/drivers/android.h" 9#include "input_common/drivers/android.h"
5 10
6namespace InputCommon { 11namespace InputCommon {
7 12
8Android::Android(std::string input_engine_) : InputEngine(std::move(input_engine_)) {} 13Android::Android(std::string input_engine_) : InputEngine(std::move(input_engine_)) {}
9 14
10void Android::RegisterController(std::size_t controller_number) { 15void Android::RegisterController(jobject j_input_device) {
11 PreSetController(GetIdentifier(controller_number)); 16 auto env = Common::Android::GetEnvForThread();
17 const std::string guid = Common::Android::GetJString(
18 env, static_cast<jstring>(
19 env->CallObjectMethod(j_input_device, Common::Android::GetYuzuDeviceGetGUID())));
20 const s32 port = env->CallIntMethod(j_input_device, Common::Android::GetYuzuDeviceGetPort());
21 const auto identifier = GetIdentifier(guid, static_cast<size_t>(port));
22 PreSetController(identifier);
23
24 if (input_devices.find(identifier) != input_devices.end()) {
25 env->DeleteGlobalRef(input_devices[identifier]);
26 }
27 auto new_device = env->NewGlobalRef(j_input_device);
28 input_devices[identifier] = new_device;
12} 29}
13 30
14void Android::SetButtonState(std::size_t controller_number, int button_id, bool value) { 31void Android::SetButtonState(std::string guid, size_t port, int button_id, bool value) {
15 const auto identifier = GetIdentifier(controller_number); 32 const auto identifier = GetIdentifier(guid, port);
16 SetButton(identifier, button_id, value); 33 SetButton(identifier, button_id, value);
17} 34}
18 35
19void Android::SetAxisState(std::size_t controller_number, int axis_id, float value) { 36void Android::SetAxisPosition(std::string guid, size_t port, int axis_id, float value) {
20 const auto identifier = GetIdentifier(controller_number); 37 const auto identifier = GetIdentifier(guid, port);
21 SetAxis(identifier, axis_id, value); 38 SetAxis(identifier, axis_id, value);
22} 39}
23 40
24void Android::SetMotionState(std::size_t controller_number, u64 delta_timestamp, float gyro_x, 41void Android::SetMotionState(std::string guid, size_t port, u64 delta_timestamp, float gyro_x,
25 float gyro_y, float gyro_z, float accel_x, float accel_y, 42 float gyro_y, float gyro_z, float accel_x, float accel_y,
26 float accel_z) { 43 float accel_z) {
27 const auto identifier = GetIdentifier(controller_number); 44 const auto identifier = GetIdentifier(guid, port);
28 const BasicMotion motion_data{ 45 const BasicMotion motion_data{
29 .gyro_x = gyro_x, 46 .gyro_x = gyro_x,
30 .gyro_y = gyro_y, 47 .gyro_y = gyro_y,
@@ -37,10 +54,295 @@ void Android::SetMotionState(std::size_t controller_number, u64 delta_timestamp,
37 SetMotion(identifier, 0, motion_data); 54 SetMotion(identifier, 0, motion_data);
38} 55}
39 56
40PadIdentifier Android::GetIdentifier(std::size_t controller_number) const { 57Common::Input::DriverResult Android::SetVibration(
58 [[maybe_unused]] const PadIdentifier& identifier,
59 [[maybe_unused]] const Common::Input::VibrationStatus& vibration) {
60 auto device = input_devices.find(identifier);
61 if (device != input_devices.end()) {
62 Common::Android::RunJNIOnFiber<void>([&](JNIEnv* env) {
63 float average_intensity =
64 static_cast<float>((vibration.high_amplitude + vibration.low_amplitude) / 2.0);
65 env->CallVoidMethod(device->second, Common::Android::GetYuzuDeviceVibrate(),
66 average_intensity);
67 });
68 return Common::Input::DriverResult::Success;
69 }
70 return Common::Input::DriverResult::NotSupported;
71}
72
73bool Android::IsVibrationEnabled([[maybe_unused]] const PadIdentifier& identifier) {
74 auto device = input_devices.find(identifier);
75 if (device != input_devices.end()) {
76 return Common::Android::RunJNIOnFiber<bool>([&](JNIEnv* env) {
77 return static_cast<bool>(env->CallBooleanMethod(
78 device->second, Common::Android::GetYuzuDeviceGetSupportsVibration()));
79 });
80 }
81 return false;
82}
83
84std::vector<Common::ParamPackage> Android::GetInputDevices() const {
85 std::vector<Common::ParamPackage> devices;
86 auto env = Common::Android::GetEnvForThread();
87 for (const auto& [key, value] : input_devices) {
88 auto name_object = static_cast<jstring>(
89 env->CallObjectMethod(value, Common::Android::GetYuzuDeviceGetName()));
90 const std::string name =
91 fmt::format("{} {}", Common::Android::GetJString(env, name_object), key.port);
92 devices.emplace_back(Common::ParamPackage{
93 {"engine", GetEngineName()},
94 {"display", std::move(name)},
95 {"guid", key.guid.RawString()},
96 {"port", std::to_string(key.port)},
97 });
98 }
99 return devices;
100}
101
102std::set<s32> Android::GetDeviceAxes(JNIEnv* env, jobject& j_device) const {
103 auto j_axes = static_cast<jobjectArray>(
104 env->CallObjectMethod(j_device, Common::Android::GetYuzuDeviceGetAxes()));
105 std::set<s32> axes;
106 for (int i = 0; i < env->GetArrayLength(j_axes); ++i) {
107 jobject axis = env->GetObjectArrayElement(j_axes, i);
108 axes.insert(env->GetIntField(axis, Common::Android::GetIntegerValueField()));
109 }
110 return axes;
111}
112
113Common::ParamPackage Android::BuildParamPackageForAnalog(PadIdentifier identifier, int axis_x,
114 int axis_y) const {
115 Common::ParamPackage params;
116 params.Set("engine", GetEngineName());
117 params.Set("port", static_cast<int>(identifier.port));
118 params.Set("guid", identifier.guid.RawString());
119 params.Set("axis_x", axis_x);
120 params.Set("axis_y", axis_y);
121 params.Set("offset_x", 0);
122 params.Set("offset_y", 0);
123 params.Set("invert_x", "+");
124
125 // Invert Y-Axis by default
126 params.Set("invert_y", "-");
127 return params;
128}
129
130Common::ParamPackage Android::BuildAnalogParamPackageForButton(PadIdentifier identifier, s32 axis,
131 bool invert) const {
132 Common::ParamPackage params{};
133 params.Set("engine", GetEngineName());
134 params.Set("port", static_cast<int>(identifier.port));
135 params.Set("guid", identifier.guid.RawString());
136 params.Set("axis", axis);
137 params.Set("threshold", "0.5");
138 params.Set("invert", invert ? "-" : "+");
139 return params;
140}
141
142Common::ParamPackage Android::BuildButtonParamPackageForButton(PadIdentifier identifier,
143 s32 button) const {
144 Common::ParamPackage params{};
145 params.Set("engine", GetEngineName());
146 params.Set("port", static_cast<int>(identifier.port));
147 params.Set("guid", identifier.guid.RawString());
148 params.Set("button", button);
149 return params;
150}
151
152bool Android::MatchVID(Common::UUID device, const std::vector<std::string>& vids) const {
153 for (size_t i = 0; i < vids.size(); ++i) {
154 auto fucker = device.RawString();
155 if (fucker.find(vids[i]) != std::string::npos) {
156 return true;
157 }
158 }
159 return false;
160}
161
162AnalogMapping Android::GetAnalogMappingForDevice(const Common::ParamPackage& params) {
163 if (!params.Has("guid") || !params.Has("port")) {
164 return {};
165 }
166
167 auto identifier =
168 GetIdentifier(params.Get("guid", ""), static_cast<size_t>(params.Get("port", 0)));
169 auto& j_device = input_devices[identifier];
170 if (j_device == nullptr) {
171 return {};
172 }
173
174 auto env = Common::Android::GetEnvForThread();
175 std::set<s32> axes = GetDeviceAxes(env, j_device);
176 if (axes.size() == 0) {
177 return {};
178 }
179
180 AnalogMapping mapping = {};
181 if (axes.find(AXIS_X) != axes.end() && axes.find(AXIS_Y) != axes.end()) {
182 mapping.insert_or_assign(Settings::NativeAnalog::LStick,
183 BuildParamPackageForAnalog(identifier, AXIS_X, AXIS_Y));
184 }
185
186 if (axes.find(AXIS_RX) != axes.end() && axes.find(AXIS_RY) != axes.end()) {
187 mapping.insert_or_assign(Settings::NativeAnalog::RStick,
188 BuildParamPackageForAnalog(identifier, AXIS_RX, AXIS_RY));
189 } else if (axes.find(AXIS_Z) != axes.end() && axes.find(AXIS_RZ) != axes.end()) {
190 mapping.insert_or_assign(Settings::NativeAnalog::RStick,
191 BuildParamPackageForAnalog(identifier, AXIS_Z, AXIS_RZ));
192 }
193 return mapping;
194}
195
196ButtonMapping Android::GetButtonMappingForDevice(const Common::ParamPackage& params) {
197 if (!params.Has("guid") || !params.Has("port")) {
198 return {};
199 }
200
201 auto identifier =
202 GetIdentifier(params.Get("guid", ""), static_cast<size_t>(params.Get("port", 0)));
203 auto& j_device = input_devices[identifier];
204 if (j_device == nullptr) {
205 return {};
206 }
207
208 auto env = Common::Android::GetEnvForThread();
209 jintArray j_keys = env->NewIntArray(static_cast<int>(keycode_ids.size()));
210 env->SetIntArrayRegion(j_keys, 0, static_cast<int>(keycode_ids.size()), keycode_ids.data());
211 auto j_has_keys_object = static_cast<jbooleanArray>(
212 env->CallObjectMethod(j_device, Common::Android::GetYuzuDeviceHasKeys(), j_keys));
213 jboolean isCopy = false;
214 jboolean* j_has_keys = env->GetBooleanArrayElements(j_has_keys_object, &isCopy);
215
216 std::set<s32> available_keys;
217 for (size_t i = 0; i < keycode_ids.size(); ++i) {
218 if (j_has_keys[i]) {
219 available_keys.insert(keycode_ids[i]);
220 }
221 }
222
223 // Some devices use axes instead of buttons for certain controls so we need all the axes here
224 std::set<s32> axes = GetDeviceAxes(env, j_device);
225
226 ButtonMapping mapping = {};
227 if (axes.find(AXIS_HAT_X) != axes.end() && axes.find(AXIS_HAT_Y) != axes.end()) {
228 mapping.insert_or_assign(Settings::NativeButton::DUp,
229 BuildAnalogParamPackageForButton(identifier, AXIS_HAT_Y, true));
230 mapping.insert_or_assign(Settings::NativeButton::DDown,
231 BuildAnalogParamPackageForButton(identifier, AXIS_HAT_Y, false));
232 mapping.insert_or_assign(Settings::NativeButton::DLeft,
233 BuildAnalogParamPackageForButton(identifier, AXIS_HAT_X, true));
234 mapping.insert_or_assign(Settings::NativeButton::DRight,
235 BuildAnalogParamPackageForButton(identifier, AXIS_HAT_X, false));
236 } else if (available_keys.find(KEYCODE_DPAD_UP) != available_keys.end() &&
237 available_keys.find(KEYCODE_DPAD_DOWN) != available_keys.end() &&
238 available_keys.find(KEYCODE_DPAD_LEFT) != available_keys.end() &&
239 available_keys.find(KEYCODE_DPAD_RIGHT) != available_keys.end()) {
240 mapping.insert_or_assign(Settings::NativeButton::DUp,
241 BuildButtonParamPackageForButton(identifier, KEYCODE_DPAD_UP));
242 mapping.insert_or_assign(Settings::NativeButton::DDown,
243 BuildButtonParamPackageForButton(identifier, KEYCODE_DPAD_DOWN));
244 mapping.insert_or_assign(Settings::NativeButton::DLeft,
245 BuildButtonParamPackageForButton(identifier, KEYCODE_DPAD_LEFT));
246 mapping.insert_or_assign(Settings::NativeButton::DRight,
247 BuildButtonParamPackageForButton(identifier, KEYCODE_DPAD_RIGHT));
248 }
249
250 if (axes.find(AXIS_LTRIGGER) != axes.end()) {
251 mapping.insert_or_assign(Settings::NativeButton::ZL, BuildAnalogParamPackageForButton(
252 identifier, AXIS_LTRIGGER, false));
253 } else if (available_keys.find(KEYCODE_BUTTON_L2) != available_keys.end()) {
254 mapping.insert_or_assign(Settings::NativeButton::ZL,
255 BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_L2));
256 }
257
258 if (axes.find(AXIS_RTRIGGER) != axes.end()) {
259 mapping.insert_or_assign(Settings::NativeButton::ZR, BuildAnalogParamPackageForButton(
260 identifier, AXIS_RTRIGGER, false));
261 } else if (available_keys.find(KEYCODE_BUTTON_R2) != available_keys.end()) {
262 mapping.insert_or_assign(Settings::NativeButton::ZR,
263 BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_R2));
264 }
265
266 if (available_keys.find(KEYCODE_BUTTON_A) != available_keys.end()) {
267 if (MatchVID(identifier.guid, flipped_ab_vids)) {
268 mapping.insert_or_assign(Settings::NativeButton::B, BuildButtonParamPackageForButton(
269 identifier, KEYCODE_BUTTON_A));
270 } else {
271 mapping.insert_or_assign(Settings::NativeButton::A, BuildButtonParamPackageForButton(
272 identifier, KEYCODE_BUTTON_A));
273 }
274 }
275 if (available_keys.find(KEYCODE_BUTTON_B) != available_keys.end()) {
276 if (MatchVID(identifier.guid, flipped_ab_vids)) {
277 mapping.insert_or_assign(Settings::NativeButton::A, BuildButtonParamPackageForButton(
278 identifier, KEYCODE_BUTTON_B));
279 } else {
280 mapping.insert_or_assign(Settings::NativeButton::B, BuildButtonParamPackageForButton(
281 identifier, KEYCODE_BUTTON_B));
282 }
283 }
284 if (available_keys.find(KEYCODE_BUTTON_X) != available_keys.end()) {
285 if (MatchVID(identifier.guid, flipped_xy_vids)) {
286 mapping.insert_or_assign(Settings::NativeButton::Y, BuildButtonParamPackageForButton(
287 identifier, KEYCODE_BUTTON_X));
288 } else {
289 mapping.insert_or_assign(Settings::NativeButton::X, BuildButtonParamPackageForButton(
290 identifier, KEYCODE_BUTTON_X));
291 }
292 }
293 if (available_keys.find(KEYCODE_BUTTON_Y) != available_keys.end()) {
294 if (MatchVID(identifier.guid, flipped_xy_vids)) {
295 mapping.insert_or_assign(Settings::NativeButton::X, BuildButtonParamPackageForButton(
296 identifier, KEYCODE_BUTTON_Y));
297 } else {
298 mapping.insert_or_assign(Settings::NativeButton::Y, BuildButtonParamPackageForButton(
299 identifier, KEYCODE_BUTTON_Y));
300 }
301 }
302
303 if (available_keys.find(KEYCODE_BUTTON_L1) != available_keys.end()) {
304 mapping.insert_or_assign(Settings::NativeButton::L,
305 BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_L1));
306 }
307 if (available_keys.find(KEYCODE_BUTTON_R1) != available_keys.end()) {
308 mapping.insert_or_assign(Settings::NativeButton::R,
309 BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_R1));
310 }
311
312 if (available_keys.find(KEYCODE_BUTTON_THUMBL) != available_keys.end()) {
313 mapping.insert_or_assign(
314 Settings::NativeButton::LStick,
315 BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_THUMBL));
316 }
317 if (available_keys.find(KEYCODE_BUTTON_THUMBR) != available_keys.end()) {
318 mapping.insert_or_assign(
319 Settings::NativeButton::RStick,
320 BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_THUMBR));
321 }
322
323 if (available_keys.find(KEYCODE_BUTTON_START) != available_keys.end()) {
324 mapping.insert_or_assign(
325 Settings::NativeButton::Plus,
326 BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_START));
327 }
328 if (available_keys.find(KEYCODE_BUTTON_SELECT) != available_keys.end()) {
329 mapping.insert_or_assign(
330 Settings::NativeButton::Minus,
331 BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_SELECT));
332 }
333
334 return mapping;
335}
336
337Common::Input::ButtonNames Android::GetUIName(
338 [[maybe_unused]] const Common::ParamPackage& params) const {
339 return Common::Input::ButtonNames::Value;
340}
341
342PadIdentifier Android::GetIdentifier(const std::string& guid, size_t port) const {
41 return { 343 return {
42 .guid = Common::UUID{}, 344 .guid = Common::UUID{guid},
43 .port = controller_number, 345 .port = port,
44 .pad = 0, 346 .pad = 0,
45 }; 347 };
46} 348}
diff --git a/src/input_common/drivers/android.h b/src/input_common/drivers/android.h
index 3f01817f6..ac60e3598 100644
--- a/src/input_common/drivers/android.h
+++ b/src/input_common/drivers/android.h
@@ -3,6 +3,8 @@
3 3
4#pragma once 4#pragma once
5 5
6#include <set>
7#include <jni.h>
6#include "input_common/input_engine.h" 8#include "input_common/input_engine.h"
7 9
8namespace InputCommon { 10namespace InputCommon {
@@ -15,40 +17,121 @@ public:
15 explicit Android(std::string input_engine_); 17 explicit Android(std::string input_engine_);
16 18
17 /** 19 /**
18 * Registers controller number to accept new inputs 20 * Registers controller number to accept new inputs.
19 * @param controller_number the controller number that will take this action 21 * @param j_input_device YuzuInputDevice object from the Android frontend to register.
20 */ 22 */
21 void RegisterController(std::size_t controller_number); 23 void RegisterController(jobject j_input_device);
22 24
23 /** 25 /**
24 * Sets the status of all buttons bound with the key to pressed 26 * Sets the status of a button on a specific controller.
25 * @param controller_number the controller number that will take this action 27 * @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
26 * @param button_id the id of the button 28 * @param port Port determined by controller connection order.
27 * @param value indicates if the button is pressed or not 29 * @param button_id The Android Keycode corresponding to this event.
30 * @param value Whether the button is pressed or not.
28 */ 31 */
29 void SetButtonState(std::size_t controller_number, int button_id, bool value); 32 void SetButtonState(std::string guid, size_t port, int button_id, bool value);
30 33
31 /** 34 /**
32 * Sets the status of a analog input to a specific player index 35 * Sets the status of an axis on a specific controller.
33 * @param controller_number the controller number that will take this action 36 * @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
34 * @param axis_id the id of the axis to move 37 * @param port Port determined by controller connection order.
35 * @param value the analog position of the axis 38 * @param axis_id The Android axis ID corresponding to this event.
39 * @param value Value along the given axis.
36 */ 40 */
37 void SetAxisState(std::size_t controller_number, int axis_id, float value); 41 void SetAxisPosition(std::string guid, size_t port, int axis_id, float value);
38 42
39 /** 43 /**
40 * Sets the status of the motion sensor to a specific player index 44 * Sets the status of the motion sensor on a specific controller
41 * @param controller_number the controller number that will take this action 45 * @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
42 * @param delta_timestamp time passed since last reading 46 * @param port Port determined by controller connection order.
43 * @param gyro_x,gyro_y,gyro_z the gyro sensor readings 47 * @param delta_timestamp Time passed since the last read.
44 * @param accel_x,accel_y,accel_z the accelerometer reading 48 * @param gyro_x,gyro_y,gyro_z Gyro sensor readings.
49 * @param accel_x,accel_y,accel_z Accelerometer sensor readings.
45 */ 50 */
46 void SetMotionState(std::size_t controller_number, u64 delta_timestamp, float gyro_x, 51 void SetMotionState(std::string guid, size_t port, u64 delta_timestamp, float gyro_x,
47 float gyro_y, float gyro_z, float accel_x, float accel_y, float accel_z); 52 float gyro_y, float gyro_z, float accel_x, float accel_y, float accel_z);
48 53
54 Common::Input::DriverResult SetVibration(
55 const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) override;
56
57 bool IsVibrationEnabled(const PadIdentifier& identifier) override;
58
59 std::vector<Common::ParamPackage> GetInputDevices() const override;
60
61 /**
62 * Gets the axes reported by the YuzuInputDevice.
63 * @param env JNI environment pointer.
64 * @param j_device YuzuInputDevice from the Android frontend.
65 * @return Set of the axes reported by the underlying Android InputDevice
66 */
67 std::set<s32> GetDeviceAxes(JNIEnv* env, jobject& j_device) const;
68
69 Common::ParamPackage BuildParamPackageForAnalog(PadIdentifier identifier, int axis_x,
70 int axis_y) const;
71
72 Common::ParamPackage BuildAnalogParamPackageForButton(PadIdentifier identifier, s32 axis,
73 bool invert) const;
74
75 Common::ParamPackage BuildButtonParamPackageForButton(PadIdentifier identifier,
76 s32 button) const;
77
78 bool MatchVID(Common::UUID device, const std::vector<std::string>& vids) const;
79
80 AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override;
81
82 ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) override;
83
84 Common::Input::ButtonNames GetUIName(const Common::ParamPackage& params) const override;
85
49private: 86private:
87 std::unordered_map<PadIdentifier, jobject> input_devices;
88
50 /// Returns the correct identifier corresponding to the player index 89 /// Returns the correct identifier corresponding to the player index
51 PadIdentifier GetIdentifier(std::size_t controller_number) const; 90 PadIdentifier GetIdentifier(const std::string& guid, size_t port) const;
91
92 static constexpr s32 AXIS_X = 0;
93 static constexpr s32 AXIS_Y = 1;
94 static constexpr s32 AXIS_Z = 11;
95 static constexpr s32 AXIS_RX = 12;
96 static constexpr s32 AXIS_RY = 13;
97 static constexpr s32 AXIS_RZ = 14;
98 static constexpr s32 AXIS_HAT_X = 15;
99 static constexpr s32 AXIS_HAT_Y = 16;
100 static constexpr s32 AXIS_LTRIGGER = 17;
101 static constexpr s32 AXIS_RTRIGGER = 18;
102
103 static constexpr s32 KEYCODE_DPAD_UP = 19;
104 static constexpr s32 KEYCODE_DPAD_DOWN = 20;
105 static constexpr s32 KEYCODE_DPAD_LEFT = 21;
106 static constexpr s32 KEYCODE_DPAD_RIGHT = 22;
107 static constexpr s32 KEYCODE_BUTTON_A = 96;
108 static constexpr s32 KEYCODE_BUTTON_B = 97;
109 static constexpr s32 KEYCODE_BUTTON_X = 99;
110 static constexpr s32 KEYCODE_BUTTON_Y = 100;
111 static constexpr s32 KEYCODE_BUTTON_L1 = 102;
112 static constexpr s32 KEYCODE_BUTTON_R1 = 103;
113 static constexpr s32 KEYCODE_BUTTON_L2 = 104;
114 static constexpr s32 KEYCODE_BUTTON_R2 = 105;
115 static constexpr s32 KEYCODE_BUTTON_THUMBL = 106;
116 static constexpr s32 KEYCODE_BUTTON_THUMBR = 107;
117 static constexpr s32 KEYCODE_BUTTON_START = 108;
118 static constexpr s32 KEYCODE_BUTTON_SELECT = 109;
119 const std::vector<s32> keycode_ids{
120 KEYCODE_DPAD_UP, KEYCODE_DPAD_DOWN, KEYCODE_DPAD_LEFT, KEYCODE_DPAD_RIGHT,
121 KEYCODE_BUTTON_A, KEYCODE_BUTTON_B, KEYCODE_BUTTON_X, KEYCODE_BUTTON_Y,
122 KEYCODE_BUTTON_L1, KEYCODE_BUTTON_R1, KEYCODE_BUTTON_L2, KEYCODE_BUTTON_R2,
123 KEYCODE_BUTTON_THUMBL, KEYCODE_BUTTON_THUMBR, KEYCODE_BUTTON_START, KEYCODE_BUTTON_SELECT,
124 };
125
126 const std::string sony_vid{"054c"};
127 const std::string nintendo_vid{"057e"};
128 const std::string razer_vid{"1532"};
129 const std::string redmagic_vid{"3537"};
130 const std::string backbone_labs_vid{"358a"};
131 const std::vector<std::string> flipped_ab_vids{sony_vid, nintendo_vid, razer_vid, redmagic_vid,
132 backbone_labs_vid};
133 const std::vector<std::string> flipped_xy_vids{sony_vid, razer_vid, redmagic_vid,
134 backbone_labs_vid};
52}; 135};
53 136
54} // namespace InputCommon 137} // namespace InputCommon
diff --git a/src/input_common/main.cpp b/src/input_common/main.cpp
index f8749ebbf..62a7ae40f 100644
--- a/src/input_common/main.cpp
+++ b/src/input_common/main.cpp
@@ -4,7 +4,6 @@
4#include <memory> 4#include <memory>
5#include "common/input.h" 5#include "common/input.h"
6#include "common/param_package.h" 6#include "common/param_package.h"
7#include "input_common/drivers/android.h"
8#include "input_common/drivers/camera.h" 7#include "input_common/drivers/camera.h"
9#include "input_common/drivers/keyboard.h" 8#include "input_common/drivers/keyboard.h"
10#include "input_common/drivers/mouse.h" 9#include "input_common/drivers/mouse.h"
@@ -28,6 +27,10 @@
28#include "input_common/drivers/sdl_driver.h" 27#include "input_common/drivers/sdl_driver.h"
29#endif 28#endif
30 29
30#ifdef ANDROID
31#include "input_common/drivers/android.h"
32#endif
33
31namespace InputCommon { 34namespace InputCommon {
32 35
33/// Dummy engine to get periodic updates 36/// Dummy engine to get periodic updates
@@ -79,7 +82,9 @@ struct InputSubsystem::Impl {
79 RegisterEngine("cemuhookudp", udp_client); 82 RegisterEngine("cemuhookudp", udp_client);
80 RegisterEngine("tas", tas_input); 83 RegisterEngine("tas", tas_input);
81 RegisterEngine("camera", camera); 84 RegisterEngine("camera", camera);
85#ifdef ANDROID
82 RegisterEngine("android", android); 86 RegisterEngine("android", android);
87#endif
83 RegisterEngine("virtual_amiibo", virtual_amiibo); 88 RegisterEngine("virtual_amiibo", virtual_amiibo);
84 RegisterEngine("virtual_gamepad", virtual_gamepad); 89 RegisterEngine("virtual_gamepad", virtual_gamepad);
85#ifdef HAVE_SDL2 90#ifdef HAVE_SDL2
@@ -111,7 +116,9 @@ struct InputSubsystem::Impl {
111 UnregisterEngine(udp_client); 116 UnregisterEngine(udp_client);
112 UnregisterEngine(tas_input); 117 UnregisterEngine(tas_input);
113 UnregisterEngine(camera); 118 UnregisterEngine(camera);
119#ifdef ANDROID
114 UnregisterEngine(android); 120 UnregisterEngine(android);
121#endif
115 UnregisterEngine(virtual_amiibo); 122 UnregisterEngine(virtual_amiibo);
116 UnregisterEngine(virtual_gamepad); 123 UnregisterEngine(virtual_gamepad);
117#ifdef HAVE_SDL2 124#ifdef HAVE_SDL2
@@ -128,12 +135,16 @@ struct InputSubsystem::Impl {
128 Common::ParamPackage{{"display", "Any"}, {"engine", "any"}}, 135 Common::ParamPackage{{"display", "Any"}, {"engine", "any"}},
129 }; 136 };
130 137
138#ifndef ANDROID
131 auto keyboard_devices = keyboard->GetInputDevices(); 139 auto keyboard_devices = keyboard->GetInputDevices();
132 devices.insert(devices.end(), keyboard_devices.begin(), keyboard_devices.end()); 140 devices.insert(devices.end(), keyboard_devices.begin(), keyboard_devices.end());
133 auto mouse_devices = mouse->GetInputDevices(); 141 auto mouse_devices = mouse->GetInputDevices();
134 devices.insert(devices.end(), mouse_devices.begin(), mouse_devices.end()); 142 devices.insert(devices.end(), mouse_devices.begin(), mouse_devices.end());
143#endif
144#ifdef ANDROID
135 auto android_devices = android->GetInputDevices(); 145 auto android_devices = android->GetInputDevices();
136 devices.insert(devices.end(), android_devices.begin(), android_devices.end()); 146 devices.insert(devices.end(), android_devices.begin(), android_devices.end());
147#endif
137#ifdef HAVE_LIBUSB 148#ifdef HAVE_LIBUSB
138 auto gcadapter_devices = gcadapter->GetInputDevices(); 149 auto gcadapter_devices = gcadapter->GetInputDevices();
139 devices.insert(devices.end(), gcadapter_devices.begin(), gcadapter_devices.end()); 150 devices.insert(devices.end(), gcadapter_devices.begin(), gcadapter_devices.end());
@@ -162,9 +173,11 @@ struct InputSubsystem::Impl {
162 if (engine == mouse->GetEngineName()) { 173 if (engine == mouse->GetEngineName()) {
163 return mouse; 174 return mouse;
164 } 175 }
176#ifdef ANDROID
165 if (engine == android->GetEngineName()) { 177 if (engine == android->GetEngineName()) {
166 return android; 178 return android;
167 } 179 }
180#endif
168#ifdef HAVE_LIBUSB 181#ifdef HAVE_LIBUSB
169 if (engine == gcadapter->GetEngineName()) { 182 if (engine == gcadapter->GetEngineName()) {
170 return gcadapter; 183 return gcadapter;
@@ -245,9 +258,11 @@ struct InputSubsystem::Impl {
245 if (engine == mouse->GetEngineName()) { 258 if (engine == mouse->GetEngineName()) {
246 return true; 259 return true;
247 } 260 }
261#ifdef ANDROID
248 if (engine == android->GetEngineName()) { 262 if (engine == android->GetEngineName()) {
249 return true; 263 return true;
250 } 264 }
265#endif
251#ifdef HAVE_LIBUSB 266#ifdef HAVE_LIBUSB
252 if (engine == gcadapter->GetEngineName()) { 267 if (engine == gcadapter->GetEngineName()) {
253 return true; 268 return true;
@@ -276,7 +291,9 @@ struct InputSubsystem::Impl {
276 void BeginConfiguration() { 291 void BeginConfiguration() {
277 keyboard->BeginConfiguration(); 292 keyboard->BeginConfiguration();
278 mouse->BeginConfiguration(); 293 mouse->BeginConfiguration();
294#ifdef ANDROID
279 android->BeginConfiguration(); 295 android->BeginConfiguration();
296#endif
280#ifdef HAVE_LIBUSB 297#ifdef HAVE_LIBUSB
281 gcadapter->BeginConfiguration(); 298 gcadapter->BeginConfiguration();
282#endif 299#endif
@@ -290,7 +307,9 @@ struct InputSubsystem::Impl {
290 void EndConfiguration() { 307 void EndConfiguration() {
291 keyboard->EndConfiguration(); 308 keyboard->EndConfiguration();
292 mouse->EndConfiguration(); 309 mouse->EndConfiguration();
310#ifdef ANDROID
293 android->EndConfiguration(); 311 android->EndConfiguration();
312#endif
294#ifdef HAVE_LIBUSB 313#ifdef HAVE_LIBUSB
295 gcadapter->EndConfiguration(); 314 gcadapter->EndConfiguration();
296#endif 315#endif
@@ -321,7 +340,6 @@ struct InputSubsystem::Impl {
321 std::shared_ptr<TasInput::Tas> tas_input; 340 std::shared_ptr<TasInput::Tas> tas_input;
322 std::shared_ptr<CemuhookUDP::UDPClient> udp_client; 341 std::shared_ptr<CemuhookUDP::UDPClient> udp_client;
323 std::shared_ptr<Camera> camera; 342 std::shared_ptr<Camera> camera;
324 std::shared_ptr<Android> android;
325 std::shared_ptr<VirtualAmiibo> virtual_amiibo; 343 std::shared_ptr<VirtualAmiibo> virtual_amiibo;
326 std::shared_ptr<VirtualGamepad> virtual_gamepad; 344 std::shared_ptr<VirtualGamepad> virtual_gamepad;
327 345
@@ -333,6 +351,10 @@ struct InputSubsystem::Impl {
333 std::shared_ptr<SDLDriver> sdl; 351 std::shared_ptr<SDLDriver> sdl;
334 std::shared_ptr<Joycons> joycon; 352 std::shared_ptr<Joycons> joycon;
335#endif 353#endif
354
355#ifdef ANDROID
356 std::shared_ptr<Android> android;
357#endif
336}; 358};
337 359
338InputSubsystem::InputSubsystem() : impl{std::make_unique<Impl>()} {} 360InputSubsystem::InputSubsystem() : impl{std::make_unique<Impl>()} {}
@@ -387,6 +409,7 @@ const Camera* InputSubsystem::GetCamera() const {
387 return impl->camera.get(); 409 return impl->camera.get();
388} 410}
389 411
412#ifdef ANDROID
390Android* InputSubsystem::GetAndroid() { 413Android* InputSubsystem::GetAndroid() {
391 return impl->android.get(); 414 return impl->android.get();
392} 415}
@@ -394,6 +417,7 @@ Android* InputSubsystem::GetAndroid() {
394const Android* InputSubsystem::GetAndroid() const { 417const Android* InputSubsystem::GetAndroid() const {
395 return impl->android.get(); 418 return impl->android.get();
396} 419}
420#endif
397 421
398VirtualAmiibo* InputSubsystem::GetVirtualAmiibo() { 422VirtualAmiibo* InputSubsystem::GetVirtualAmiibo() {
399 return impl->virtual_amiibo.get(); 423 return impl->virtual_amiibo.get();