aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpineappleEA <pineaea@gmail.com>2024-02-18 07:19:28 +0100
committerpineappleEA <pineaea@gmail.com>2024-02-18 07:19:28 +0100
commitd2d37b01f7f421d3e6667eee15c220ab11bce5a3 (patch)
treeefedbb07409a24a206796b369a1b876c2a905738
parentebd0305be19f4156952811ce1fbe20ecc137eb6f (diff)
early-access version 4146EA-4146
-rwxr-xr-xREADME.md2
-rwxr-xr-xsrc/android/app/src/main/AndroidManifest.xml1
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt224
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt2
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt39
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt26
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt10
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt11
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt39
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt25
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt14
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/LicenseAdapter.kt4
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt6
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/NativeInput.kt416
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuInputDevice.kt93
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuVibrator.kt76
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/AnalogDirection.kt11
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/ButtonName.kt19
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/InputType.kt13
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeAnalog.kt14
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeButton.kt38
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeTrigger.kt10
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NpadStyleIndex.kt30
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/PlayerInput.kt83
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt15
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/AnalogInputSetting.kt31
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ButtonInputSetting.kt29
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt9
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.kt7
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputProfileSetting.kt32
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputSetting.kt134
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/IntSingleChoiceSetting.kt38
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ModifierInputSetting.kt31
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt11
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt233
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt14
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt15
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt11
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt10
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt9
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputDialogFragment.kt300
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileAdapter.kt68
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileDialogFragment.kt148
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/NewInputProfileDialogFragment.kt79
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt58
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt247
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt278
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt85
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt794
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsSearchFragment.kt183
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsViewModel.kt112
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt24
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/HeaderViewHolder.kt2
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputProfileViewHolder.kt34
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputViewHolder.kt60
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt26
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt81
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt23
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt29
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt24
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt91
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/CoreErrorDialogFragment.kt47
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt18
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt13
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt229
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt12
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt3
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt46
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt14
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt16
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProgressDialogFragment.kt102
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt52
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt46
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt99
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt7
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt25
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt15
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt71
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt64
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt456
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/utils/LifecycleUtils.kt38
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt15
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NfcReader.kt6
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ParamPackage.kt141
-rwxr-xr-xsrc/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ViewUtils.kt33
-rwxr-xr-xsrc/android/app/src/main/jni/CMakeLists.txt1
-rwxr-xr-xsrc/android/app/src/main/jni/android_config.cpp141
-rwxr-xr-xsrc/android/app/src/main/jni/android_config.h7
-rwxr-xr-xsrc/android/app/src/main/jni/emu_window/emu_window.cpp49
-rwxr-xr-xsrc/android/app/src/main/jni/emu_window/emu_window.h15
-rwxr-xr-xsrc/android/app/src/main/jni/native.cpp167
-rwxr-xr-xsrc/android/app/src/main/jni/native.h5
-rwxr-xr-xsrc/android/app/src/main/jni/native_config.cpp117
-rwxr-xr-xsrc/android/app/src/main/jni/native_input.cpp631
-rwxr-xr-xsrc/android/app/src/main/res/drawable/button_anim.xml142
-rwxr-xr-xsrc/android/app/src/main/res/drawable/ic_controller_disconnected.xml9
-rwxr-xr-xsrc/android/app/src/main/res/drawable/ic_more_vert.xml9
-rwxr-xr-xsrc/android/app/src/main/res/drawable/ic_new_label.xml9
-rwxr-xr-xsrc/android/app/src/main/res/drawable/ic_overlay.xml21
-rwxr-xr-xsrc/android/app/src/main/res/drawable/ic_share.xml9
-rwxr-xr-xsrc/android/app/src/main/res/drawable/stick_one_direction_anim.xml118
-rwxr-xr-xsrc/android/app/src/main/res/drawable/stick_two_direction_anim.xml173
-rwxr-xr-xsrc/android/app/src/main/res/layout-ldrtl/list_item_setting_input.xml63
-rwxr-xr-xsrc/android/app/src/main/res/layout/card_driver_option.xml9
-rwxr-xr-xsrc/android/app/src/main/res/layout/card_folder.xml3
-rwxr-xr-xsrc/android/app/src/main/res/layout/card_game.xml3
-rwxr-xr-xsrc/android/app/src/main/res/layout/card_simple_outlined.xml3
-rwxr-xr-xsrc/android/app/src/main/res/layout/dialog_input_profiles.xml6
-rwxr-xr-xsrc/android/app/src/main/res/layout/dialog_mapping.xml26
-rwxr-xr-xsrc/android/app/src/main/res/layout/fragment_game_properties.xml3
-rwxr-xr-xsrc/android/app/src/main/res/layout/list_item_input_profile.xml74
-rwxr-xr-xsrc/android/app/src/main/res/layout/list_item_setting_input.xml63
-rwxr-xr-xsrc/android/app/src/main/res/menu/menu_in_game.xml7
-rwxr-xr-xsrc/android/app/src/main/res/menu/menu_input_options.xml34
-rwxr-xr-xsrc/android/app/src/main/res/navigation/settings_navigation.xml2
-rwxr-xr-xsrc/android/app/src/main/res/values-w600dp/dimens.xml2
-rwxr-xr-xsrc/android/app/src/main/res/values/dimens.xml2
-rwxr-xr-xsrc/android/app/src/main/res/values/strings.xml93
-rwxr-xr-xsrc/common/android/id_cache.cpp163
-rwxr-xr-xsrc/common/android/id_cache.h24
-rwxr-xr-xsrc/common/settings_input.h4
-rwxr-xr-xsrc/core/memory/cheat_engine.cpp4
-rwxr-xr-xsrc/frontend_common/config.cpp1
-rwxr-xr-xsrc/frontend_common/content_manager.h5
-rwxr-xr-xsrc/hid_core/frontend/emulated_controller.cpp10
-rwxr-xr-xsrc/hid_core/frontend/emulated_controller.h3
-rwxr-xr-xsrc/input_common/CMakeLists.txt10
-rwxr-xr-xsrc/input_common/drivers/android.cpp324
-rwxr-xr-xsrc/input_common/drivers/android.h123
-rwxr-xr-xsrc/input_common/main.cpp28
-rwxr-xr-xsrc/yuzu/configuration/qt_config.cpp1
-rwxr-xr-xsrc/yuzu/main.cpp143
-rwxr-xr-xsrc/yuzu/main.h1
-rwxr-xr-xsrc/yuzu/main.ui18
-rwxr-xr-xsrc/yuzu_cmd/sdl_config.cpp1
135 files changed, 7044 insertions, 1984 deletions
diff --git a/README.md b/README.md
index 3104ceb70..d587ac6c8 100755
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
1yuzu emulator early access 1yuzu emulator early access
2============= 2=============
3 3
4This is the source code for early-access 4145. 4This is the source code for early-access 4146.
5 5
6## Legal Notice 6## Legal Notice
7 7
diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml
index 7890b30ca..b037fc055 100755
--- 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..02a20dacf 100755
--- 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
@@ -3,24 +3,21 @@
3 3
4package org.yuzu.yuzu_emu 4package org.yuzu.yuzu_emu
5 5
6import android.app.Dialog
7import android.content.DialogInterface 6import android.content.DialogInterface
8import android.net.Uri 7import android.net.Uri
9import android.os.Bundle
10import android.text.Html 8import android.text.Html
11import android.text.method.LinkMovementMethod 9import android.text.method.LinkMovementMethod
12import android.view.Surface 10import android.view.Surface
13import android.view.View 11import android.view.View
14import android.widget.TextView 12import android.widget.TextView
15import androidx.annotation.Keep 13import androidx.annotation.Keep
16import androidx.fragment.app.DialogFragment
17import com.google.android.material.dialog.MaterialAlertDialogBuilder 14import com.google.android.material.dialog.MaterialAlertDialogBuilder
18import java.lang.ref.WeakReference 15import java.lang.ref.WeakReference
19import org.yuzu.yuzu_emu.activities.EmulationActivity 16import org.yuzu.yuzu_emu.activities.EmulationActivity
17import org.yuzu.yuzu_emu.fragments.CoreErrorDialogFragment
20import org.yuzu.yuzu_emu.utils.DocumentsTree 18import org.yuzu.yuzu_emu.utils.DocumentsTree
21import org.yuzu.yuzu_emu.utils.FileUtil 19import org.yuzu.yuzu_emu.utils.FileUtil
22import org.yuzu.yuzu_emu.utils.Log 20import org.yuzu.yuzu_emu.utils.Log
23import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable
24import org.yuzu.yuzu_emu.model.InstallResult 21import org.yuzu.yuzu_emu.model.InstallResult
25import org.yuzu.yuzu_emu.model.Patch 22import org.yuzu.yuzu_emu.model.Patch
26import org.yuzu.yuzu_emu.model.GameVerificationResult 23import org.yuzu.yuzu_emu.model.GameVerificationResult
@@ -30,34 +27,6 @@ import org.yuzu.yuzu_emu.model.GameVerificationResult
30 * with the native side of the Yuzu code. 27 * with the native side of the Yuzu code.
31 */ 28 */
32object NativeLibrary { 29object 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 30 @JvmField
62 var sEmulationActivity = WeakReference<EmulationActivity?>(null) 31 var sEmulationActivity = WeakReference<EmulationActivity?>(null)
63 32
@@ -127,112 +96,6 @@ object NativeLibrary {
127 FileUtil.getFilename(Uri.parse(path)) 96 FileUtil.getFilename(Uri.parse(path))
128 } 97 }
129 98
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) 99 external fun setAppDirectory(directory: String)
237 100
238 /** 101 /**
@@ -318,46 +181,13 @@ object NativeLibrary {
318 ErrorUnknown 181 ErrorUnknown
319 } 182 }
320 183
321 private var coreErrorAlertResult = false 184 var coreErrorAlertResult = false
322 private val coreErrorAlertLock = Object() 185 val coreErrorAlertLock = Object()
323
324 class CoreErrorDialogFragment : DialogFragment() {
325 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
326 val title = requireArguments().serializable<String>("title")
327 val message = requireArguments().serializable<String>("message")
328
329 return MaterialAlertDialogBuilder(requireActivity())
330 .setTitle(title)
331 .setMessage(message)
332 .setPositiveButton(R.string.continue_button, null)
333 .setNegativeButton(R.string.abort_button) { _: DialogInterface?, _: Int ->
334 coreErrorAlertResult = false
335 synchronized(coreErrorAlertLock) { coreErrorAlertLock.notify() }
336 }
337 .create()
338 }
339
340 override fun onDismiss(dialog: DialogInterface) {
341 coreErrorAlertResult = true
342 synchronized(coreErrorAlertLock) { coreErrorAlertLock.notify() }
343 }
344
345 companion object {
346 fun newInstance(title: String?, message: String?): CoreErrorDialogFragment {
347 val frag = CoreErrorDialogFragment()
348 val args = Bundle()
349 args.putString("title", title)
350 args.putString("message", message)
351 frag.arguments = args
352 return frag
353 }
354 }
355 }
356 186
357 private fun onCoreErrorImpl(title: String, message: String) { 187 private fun onCoreErrorImpl(title: String, message: String) {
358 val emulationActivity = sEmulationActivity.get() 188 val emulationActivity = sEmulationActivity.get()
359 if (emulationActivity == null) { 189 if (emulationActivity == null) {
360 error("[NativeLibrary] EmulationActivity not present") 190 Log.error("[NativeLibrary] EmulationActivity not present")
361 return 191 return
362 } 192 }
363 193
@@ -373,7 +203,7 @@ object NativeLibrary {
373 fun onCoreError(error: CoreError?, details: String): Boolean { 203 fun onCoreError(error: CoreError?, details: String): Boolean {
374 val emulationActivity = sEmulationActivity.get() 204 val emulationActivity = sEmulationActivity.get()
375 if (emulationActivity == null) { 205 if (emulationActivity == null) {
376 error("[NativeLibrary] EmulationActivity not present") 206 Log.error("[NativeLibrary] EmulationActivity not present")
377 return false 207 return false
378 } 208 }
379 209
@@ -404,7 +234,7 @@ object NativeLibrary {
404 } 234 }
405 235
406 // Show the AlertDialog on the main thread. 236 // Show the AlertDialog on the main thread.
407 emulationActivity.runOnUiThread(Runnable { onCoreErrorImpl(title, message) }) 237 emulationActivity.runOnUiThread { onCoreErrorImpl(title, message) }
408 238
409 // Wait for the lock to notify that it is complete. 239 // Wait for the lock to notify that it is complete.
410 synchronized(coreErrorAlertLock) { coreErrorAlertLock.wait() } 240 synchronized(coreErrorAlertLock) { coreErrorAlertLock.wait() }
@@ -629,46 +459,4 @@ object NativeLibrary {
629 * Checks if all necessary keys are present for decryption 459 * Checks if all necessary keys are present for decryption
630 */ 460 */
631 external fun areKeysPresent(): Boolean 461 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} 462}
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 100755
--- 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 100755
--- 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/adapters/DriverAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt
index f218c76ef..50663ad91 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt
@@ -3,15 +3,15 @@
3 3
4package org.yuzu.yuzu_emu.adapters 4package org.yuzu.yuzu_emu.adapters
5 5
6import android.text.TextUtils
7import android.view.LayoutInflater 6import android.view.LayoutInflater
8import android.view.View
9import android.view.ViewGroup 7import android.view.ViewGroup
10import org.yuzu.yuzu_emu.R 8import org.yuzu.yuzu_emu.R
11import org.yuzu.yuzu_emu.databinding.CardDriverOptionBinding 9import org.yuzu.yuzu_emu.databinding.CardDriverOptionBinding
12import org.yuzu.yuzu_emu.features.settings.model.StringSetting 10import org.yuzu.yuzu_emu.features.settings.model.StringSetting
13import org.yuzu.yuzu_emu.model.Driver 11import org.yuzu.yuzu_emu.model.Driver
14import org.yuzu.yuzu_emu.model.DriverViewModel 12import org.yuzu.yuzu_emu.model.DriverViewModel
13import org.yuzu.yuzu_emu.utils.ViewUtils.marquee
14import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
15import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder 15import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
16 16
17class DriverAdapter(private val driverViewModel: DriverViewModel) : 17class DriverAdapter(private val driverViewModel: DriverViewModel) :
@@ -44,25 +44,15 @@ class DriverAdapter(private val driverViewModel: DriverViewModel) :
44 } 44 }
45 45
46 // Delay marquee by 3s 46 // Delay marquee by 3s
47 title.postDelayed( 47 title.marquee()
48 { 48 version.marquee()
49 title.isSelected = true 49 description.marquee()
50 title.ellipsize = TextUtils.TruncateAt.MARQUEE
51 version.isSelected = true
52 version.ellipsize = TextUtils.TruncateAt.MARQUEE
53 description.isSelected = true
54 description.ellipsize = TextUtils.TruncateAt.MARQUEE
55 },
56 3000
57 )
58 title.text = model.title 50 title.text = model.title
59 version.text = model.version 51 version.text = model.version
60 description.text = model.description 52 description.text = model.description
61 if (model.title != binding.root.context.getString(R.string.system_gpu_driver)) { 53 buttonDelete.setVisible(
62 buttonDelete.visibility = View.VISIBLE 54 model.title != binding.root.context.getString(R.string.system_gpu_driver)
63 } else { 55 )
64 buttonDelete.visibility = View.GONE
65 }
66 } 56 }
67 } 57 }
68 } 58 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt
index 3d8f0bda8..5cbd15d2a 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt
@@ -4,7 +4,6 @@
4package org.yuzu.yuzu_emu.adapters 4package org.yuzu.yuzu_emu.adapters
5 5
6import android.net.Uri 6import android.net.Uri
7import android.text.TextUtils
8import android.view.LayoutInflater 7import android.view.LayoutInflater
9import android.view.ViewGroup 8import android.view.ViewGroup
10import androidx.fragment.app.FragmentActivity 9import androidx.fragment.app.FragmentActivity
@@ -12,6 +11,7 @@ import org.yuzu.yuzu_emu.databinding.CardFolderBinding
12import org.yuzu.yuzu_emu.fragments.GameFolderPropertiesDialogFragment 11import org.yuzu.yuzu_emu.fragments.GameFolderPropertiesDialogFragment
13import org.yuzu.yuzu_emu.model.GameDir 12import org.yuzu.yuzu_emu.model.GameDir
14import org.yuzu.yuzu_emu.model.GamesViewModel 13import org.yuzu.yuzu_emu.model.GamesViewModel
14import org.yuzu.yuzu_emu.utils.ViewUtils.marquee
15import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder 15import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
16 16
17class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesViewModel) : 17class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesViewModel) :
@@ -29,13 +29,7 @@ class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesVie
29 override fun bind(model: GameDir) { 29 override fun bind(model: GameDir) {
30 binding.apply { 30 binding.apply {
31 path.text = Uri.parse(model.uriString).path 31 path.text = Uri.parse(model.uriString).path
32 path.postDelayed( 32 path.marquee()
33 {
34 path.isSelected = true
35 path.ellipsize = TextUtils.TruncateAt.MARQUEE
36 },
37 3000
38 )
39 33
40 buttonEdit.setOnClickListener { 34 buttonEdit.setOnClickListener {
41 GameFolderPropertiesDialogFragment.newInstance(model) 35 GameFolderPropertiesDialogFragment.newInstance(model)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
index 85c8249e6..b1f247ac3 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
@@ -4,7 +4,6 @@
4package org.yuzu.yuzu_emu.adapters 4package org.yuzu.yuzu_emu.adapters
5 5
6import android.net.Uri 6import android.net.Uri
7import android.text.TextUtils
8import android.view.LayoutInflater 7import android.view.LayoutInflater
9import android.view.ViewGroup 8import android.view.ViewGroup
10import android.widget.ImageView 9import android.widget.ImageView
@@ -27,6 +26,7 @@ import org.yuzu.yuzu_emu.databinding.CardGameBinding
27import org.yuzu.yuzu_emu.model.Game 26import org.yuzu.yuzu_emu.model.Game
28import org.yuzu.yuzu_emu.model.GamesViewModel 27import org.yuzu.yuzu_emu.model.GamesViewModel
29import org.yuzu.yuzu_emu.utils.GameIconUtils 28import org.yuzu.yuzu_emu.utils.GameIconUtils
29import org.yuzu.yuzu_emu.utils.ViewUtils.marquee
30import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder 30import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
31 31
32class GameAdapter(private val activity: AppCompatActivity) : 32class GameAdapter(private val activity: AppCompatActivity) :
@@ -44,14 +44,7 @@ class GameAdapter(private val activity: AppCompatActivity) :
44 44
45 binding.textGameTitle.text = model.title.replace("[\\t\\n\\r]+".toRegex(), " ") 45 binding.textGameTitle.text = model.title.replace("[\\t\\n\\r]+".toRegex(), " ")
46 46
47 binding.textGameTitle.postDelayed( 47 binding.textGameTitle.marquee()
48 {
49 binding.textGameTitle.ellipsize = TextUtils.TruncateAt.MARQUEE
50 binding.textGameTitle.isSelected = true
51 },
52 3000
53 )
54
55 binding.cardGame.setOnClickListener { onClick(model) } 48 binding.cardGame.setOnClickListener { onClick(model) }
56 binding.cardGame.setOnLongClickListener { onLongClick(model) } 49 binding.cardGame.setOnLongClickListener { onLongClick(model) }
57 } 50 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt
index 0046d5314..7366e2c77 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt
@@ -3,21 +3,18 @@
3 3
4package org.yuzu.yuzu_emu.adapters 4package org.yuzu.yuzu_emu.adapters
5 5
6import android.text.TextUtils
7import android.view.LayoutInflater 6import android.view.LayoutInflater
8import android.view.View
9import android.view.ViewGroup 7import android.view.ViewGroup
10import androidx.core.content.res.ResourcesCompat 8import androidx.core.content.res.ResourcesCompat
11import androidx.lifecycle.Lifecycle
12import androidx.lifecycle.LifecycleOwner 9import androidx.lifecycle.LifecycleOwner
13import androidx.lifecycle.lifecycleScope
14import androidx.lifecycle.repeatOnLifecycle
15import kotlinx.coroutines.launch
16import org.yuzu.yuzu_emu.databinding.CardInstallableIconBinding 10import org.yuzu.yuzu_emu.databinding.CardInstallableIconBinding
17import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding 11import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding
18import org.yuzu.yuzu_emu.model.GameProperty 12import org.yuzu.yuzu_emu.model.GameProperty
19import org.yuzu.yuzu_emu.model.InstallableProperty 13import org.yuzu.yuzu_emu.model.InstallableProperty
20import org.yuzu.yuzu_emu.model.SubmenuProperty 14import org.yuzu.yuzu_emu.model.SubmenuProperty
15import org.yuzu.yuzu_emu.utils.ViewUtils.marquee
16import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
17import org.yuzu.yuzu_emu.utils.collect
21import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder 18import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
22 19
23class GamePropertiesAdapter( 20class GamePropertiesAdapter(
@@ -76,23 +73,15 @@ class GamePropertiesAdapter(
76 ) 73 )
77 ) 74 )
78 75
79 binding.details.postDelayed({ 76 binding.details.marquee()
80 binding.details.isSelected = true
81 binding.details.ellipsize = TextUtils.TruncateAt.MARQUEE
82 }, 3000)
83
84 if (submenuProperty.details != null) { 77 if (submenuProperty.details != null) {
85 binding.details.visibility = View.VISIBLE 78 binding.details.setVisible(true)
86 binding.details.text = submenuProperty.details.invoke() 79 binding.details.text = submenuProperty.details.invoke()
87 } else if (submenuProperty.detailsFlow != null) { 80 } else if (submenuProperty.detailsFlow != null) {
88 binding.details.visibility = View.VISIBLE 81 binding.details.setVisible(true)
89 viewLifecycle.lifecycleScope.launch { 82 submenuProperty.detailsFlow.collect(viewLifecycle) { binding.details.text = it }
90 viewLifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
91 submenuProperty.detailsFlow.collect { binding.details.text = it }
92 }
93 }
94 } else { 83 } else {
95 binding.details.visibility = View.GONE 84 binding.details.setVisible(false)
96 } 85 }
97 } 86 }
98 } 87 }
@@ -112,14 +101,10 @@ class GamePropertiesAdapter(
112 ) 101 )
113 ) 102 )
114 103
115 if (installableProperty.install != null) { 104 binding.buttonInstall.setVisible(installableProperty.install != null)
116 binding.buttonInstall.visibility = View.VISIBLE 105 binding.buttonInstall.setOnClickListener { installableProperty.install?.invoke() }
117 binding.buttonInstall.setOnClickListener { installableProperty.install.invoke() } 106 binding.buttonExport.setVisible(installableProperty.export != null)
118 } 107 binding.buttonExport.setOnClickListener { installableProperty.export?.invoke() }
119 if (installableProperty.export != null) {
120 binding.buttonExport.visibility = View.VISIBLE
121 binding.buttonExport.setOnClickListener { installableProperty.export.invoke() }
122 }
123 } 108 }
124 } 109 }
125 110
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt
index b512845d5..0bd196673 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt
@@ -3,22 +3,19 @@
3 3
4package org.yuzu.yuzu_emu.adapters 4package org.yuzu.yuzu_emu.adapters
5 5
6import android.text.TextUtils
7import android.view.LayoutInflater 6import android.view.LayoutInflater
8import android.view.View
9import android.view.ViewGroup 7import android.view.ViewGroup
10import androidx.appcompat.app.AppCompatActivity 8import androidx.appcompat.app.AppCompatActivity
11import androidx.core.content.ContextCompat 9import androidx.core.content.ContextCompat
12import androidx.core.content.res.ResourcesCompat 10import androidx.core.content.res.ResourcesCompat
13import androidx.lifecycle.Lifecycle
14import androidx.lifecycle.LifecycleOwner 11import androidx.lifecycle.LifecycleOwner
15import androidx.lifecycle.lifecycleScope
16import androidx.lifecycle.repeatOnLifecycle
17import kotlinx.coroutines.launch
18import org.yuzu.yuzu_emu.R 12import org.yuzu.yuzu_emu.R
19import org.yuzu.yuzu_emu.databinding.CardHomeOptionBinding 13import org.yuzu.yuzu_emu.databinding.CardHomeOptionBinding
20import org.yuzu.yuzu_emu.fragments.MessageDialogFragment 14import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
21import org.yuzu.yuzu_emu.model.HomeSetting 15import org.yuzu.yuzu_emu.model.HomeSetting
16import org.yuzu.yuzu_emu.utils.ViewUtils.marquee
17import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
18import org.yuzu.yuzu_emu.utils.collect
22import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder 19import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
23 20
24class HomeSettingAdapter( 21class HomeSettingAdapter(
@@ -59,18 +56,8 @@ class HomeSettingAdapter(
59 binding.optionIcon.alpha = 0.5f 56 binding.optionIcon.alpha = 0.5f
60 } 57 }
61 58
62 viewLifecycle.lifecycleScope.launch { 59 model.details.collect(viewLifecycle) { updateOptionDetails(it) }
63 viewLifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) { 60 binding.optionDetail.marquee()
64 model.details.collect { updateOptionDetails(it) }
65 }
66 }
67 binding.optionDetail.postDelayed(
68 {
69 binding.optionDetail.ellipsize = TextUtils.TruncateAt.MARQUEE
70 binding.optionDetail.isSelected = true
71 },
72 3000
73 )
74 61
75 binding.root.setOnClickListener { onClick(model) } 62 binding.root.setOnClickListener { onClick(model) }
76 } 63 }
@@ -90,7 +77,7 @@ class HomeSettingAdapter(
90 private fun updateOptionDetails(detailString: String) { 77 private fun updateOptionDetails(detailString: String) {
91 if (detailString.isNotEmpty()) { 78 if (detailString.isNotEmpty()) {
92 binding.optionDetail.text = detailString 79 binding.optionDetail.text = detailString
93 binding.optionDetail.visibility = View.VISIBLE 80 binding.optionDetail.setVisible(true)
94 } 81 }
95 } 82 }
96 } 83 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt
index 4218c4e52..1ba75fa2f 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt
@@ -4,10 +4,10 @@
4package org.yuzu.yuzu_emu.adapters 4package org.yuzu.yuzu_emu.adapters
5 5
6import android.view.LayoutInflater 6import android.view.LayoutInflater
7import android.view.View
8import android.view.ViewGroup 7import android.view.ViewGroup
9import org.yuzu.yuzu_emu.databinding.CardInstallableBinding 8import org.yuzu.yuzu_emu.databinding.CardInstallableBinding
10import org.yuzu.yuzu_emu.model.Installable 9import org.yuzu.yuzu_emu.model.Installable
10import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
11import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder 11import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
12 12
13class InstallableAdapter(installables: List<Installable>) : 13class InstallableAdapter(installables: List<Installable>) :
@@ -26,14 +26,10 @@ class InstallableAdapter(installables: List<Installable>) :
26 binding.title.setText(model.titleId) 26 binding.title.setText(model.titleId)
27 binding.description.setText(model.descriptionId) 27 binding.description.setText(model.descriptionId)
28 28
29 if (model.install != null) { 29 binding.buttonInstall.setVisible(model.install != null)
30 binding.buttonInstall.visibility = View.VISIBLE 30 binding.buttonInstall.setOnClickListener { model.install?.invoke() }
31 binding.buttonInstall.setOnClickListener { model.install.invoke() } 31 binding.buttonExport.setVisible(model.export != null)
32 } 32 binding.buttonExport.setOnClickListener { model.export?.invoke() }
33 if (model.export != null) {
34 binding.buttonExport.visibility = View.VISIBLE
35 binding.buttonExport.setOnClickListener { model.export.invoke() }
36 }
37 } 33 }
38 } 34 }
39} 35}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/LicenseAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/LicenseAdapter.kt
index 38bb1f96f..1379968f9 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/LicenseAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/LicenseAdapter.kt
@@ -4,12 +4,12 @@
4package org.yuzu.yuzu_emu.adapters 4package org.yuzu.yuzu_emu.adapters
5 5
6import android.view.LayoutInflater 6import android.view.LayoutInflater
7import android.view.View
8import android.view.ViewGroup 7import android.view.ViewGroup
9import androidx.appcompat.app.AppCompatActivity 8import androidx.appcompat.app.AppCompatActivity
10import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding 9import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
11import org.yuzu.yuzu_emu.fragments.LicenseBottomSheetDialogFragment 10import org.yuzu.yuzu_emu.fragments.LicenseBottomSheetDialogFragment
12import org.yuzu.yuzu_emu.model.License 11import org.yuzu.yuzu_emu.model.License
12import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
13import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder 13import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
14 14
15class LicenseAdapter(private val activity: AppCompatActivity, licenses: List<License>) : 15class LicenseAdapter(private val activity: AppCompatActivity, licenses: List<License>) :
@@ -25,7 +25,7 @@ class LicenseAdapter(private val activity: AppCompatActivity, licenses: List<Lic
25 binding.apply { 25 binding.apply {
26 textSettingName.text = root.context.getString(model.titleId) 26 textSettingName.text = root.context.getString(model.titleId)
27 textSettingDescription.text = root.context.getString(model.descriptionId) 27 textSettingDescription.text = root.context.getString(model.descriptionId)
28 textSettingValue.visibility = View.GONE 28 textSettingValue.setVisible(false)
29 29
30 root.setOnClickListener { onClick(model) } 30 root.setOnClickListener { onClick(model) }
31 } 31 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt
index 02118e1a8..a5f610b31 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt
@@ -5,7 +5,6 @@ package org.yuzu.yuzu_emu.adapters
5 5
6import android.text.Html 6import android.text.Html
7import android.view.LayoutInflater 7import android.view.LayoutInflater
8import android.view.View
9import android.view.ViewGroup 8import android.view.ViewGroup
10import androidx.appcompat.app.AppCompatActivity 9import androidx.appcompat.app.AppCompatActivity
11import androidx.core.content.res.ResourcesCompat 10import androidx.core.content.res.ResourcesCompat
@@ -17,6 +16,7 @@ import org.yuzu.yuzu_emu.model.SetupCallback
17import org.yuzu.yuzu_emu.model.SetupPage 16import org.yuzu.yuzu_emu.model.SetupPage
18import org.yuzu.yuzu_emu.model.StepState 17import org.yuzu.yuzu_emu.model.StepState
19import org.yuzu.yuzu_emu.utils.ViewUtils 18import org.yuzu.yuzu_emu.utils.ViewUtils
19import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
20import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder 20import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
21 21
22class SetupAdapter(val activity: AppCompatActivity, pages: List<SetupPage>) : 22class SetupAdapter(val activity: AppCompatActivity, pages: List<SetupPage>) :
@@ -30,8 +30,8 @@ class SetupAdapter(val activity: AppCompatActivity, pages: List<SetupPage>) :
30 AbstractViewHolder<SetupPage>(binding), SetupCallback { 30 AbstractViewHolder<SetupPage>(binding), SetupCallback {
31 override fun bind(model: SetupPage) { 31 override fun bind(model: SetupPage) {
32 if (model.stepCompleted.invoke() == StepState.COMPLETE) { 32 if (model.stepCompleted.invoke() == StepState.COMPLETE) {
33 binding.buttonAction.visibility = View.INVISIBLE 33 binding.buttonAction.setVisible(visible = false, gone = false)
34 binding.textConfirmation.visibility = View.VISIBLE 34 binding.textConfirmation.setVisible(true)
35 } 35 }
36 36
37 binding.icon.setImageDrawable( 37 binding.icon.setImageDrawable(
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 100755
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 100755
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 100755
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 100755
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 100755
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 100755
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 100755
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 100755
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 100755
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 100755
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 100755
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 100755
--- 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 100755
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 100755
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/DateTimeSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt
index 1d81f5f2b..58febff1d 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt
@@ -3,13 +3,16 @@
3 3
4package org.yuzu.yuzu_emu.features.settings.model.view 4package org.yuzu.yuzu_emu.features.settings.model.view
5 5
6import androidx.annotation.StringRes
6import org.yuzu.yuzu_emu.features.settings.model.AbstractLongSetting 7import org.yuzu.yuzu_emu.features.settings.model.AbstractLongSetting
7 8
8class DateTimeSetting( 9class DateTimeSetting(
9 private val longSetting: AbstractLongSetting, 10 private val longSetting: AbstractLongSetting,
10 titleId: Int, 11 @StringRes titleId: Int = 0,
11 descriptionId: Int 12 titleString: String = "",
12) : SettingsItem(longSetting, titleId, descriptionId) { 13 @StringRes descriptionId: Int = 0,
14 descriptionString: String = ""
15) : SettingsItem(longSetting, titleId, titleString, descriptionId, descriptionString) {
13 override val type = TYPE_DATETIME_SETTING 16 override val type = TYPE_DATETIME_SETTING
14 17
15 fun getValue(needsGlobal: Boolean = false): Long = longSetting.getLong(needsGlobal) 18 fun getValue(needsGlobal: Boolean = false): Long = longSetting.getLong(needsGlobal)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.kt
index d31ce1c31..8a6a51d5c 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.kt
@@ -3,8 +3,11 @@
3 3
4package org.yuzu.yuzu_emu.features.settings.model.view 4package org.yuzu.yuzu_emu.features.settings.model.view
5 5
6import androidx.annotation.StringRes
7
6class HeaderSetting( 8class HeaderSetting(
7 titleId: Int 9 @StringRes titleId: Int = 0,
8) : SettingsItem(emptySetting, titleId, 0) { 10 titleString: String = ""
11) : SettingsItem(emptySetting, titleId, titleString, 0, "") {
9 override val type = TYPE_HEADER 12 override val type = TYPE_HEADER
10} 13}
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 100755
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 100755
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 100755
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 100755
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 425160024..06f607424 100755
--- 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
@@ -4,13 +4,16 @@
4package org.yuzu.yuzu_emu.features.settings.model.view 4package org.yuzu.yuzu_emu.features.settings.model.view
5 5
6import androidx.annotation.DrawableRes 6import androidx.annotation.DrawableRes
7import androidx.annotation.StringRes
7 8
8class RunnableSetting( 9class RunnableSetting(
9 titleId: Int, 10 @StringRes titleId: Int = 0,
10 descriptionId: Int, 11 titleString: String = "",
11 val isRuntimeRunnable: Boolean, 12 @StringRes descriptionId: Int = 0,
13 descriptionString: String = "",
14 val isRunnable: Boolean,
12 @DrawableRes val iconId: Int = 0, 15 @DrawableRes val iconId: Int = 0,
13 val runnable: () -> Unit 16 val runnable: () -> Unit
14) : SettingsItem(emptySetting, titleId, descriptionId) { 17) : SettingsItem(emptySetting, titleId, titleString, descriptionId, descriptionString) {
15 override val type = TYPE_RUNNABLE 18 override val type = TYPE_RUNNABLE
16} 19}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
index 21ca97bc1..8f724835e 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
@@ -3,8 +3,12 @@
3 3
4package org.yuzu.yuzu_emu.features.settings.model.view 4package org.yuzu.yuzu_emu.features.settings.model.view
5 5
6import androidx.annotation.StringRes
6import org.yuzu.yuzu_emu.NativeLibrary 7import org.yuzu.yuzu_emu.NativeLibrary
7import org.yuzu.yuzu_emu.R 8import org.yuzu.yuzu_emu.R
9import org.yuzu.yuzu_emu.YuzuApplication
10import org.yuzu.yuzu_emu.features.input.NativeInput
11import org.yuzu.yuzu_emu.features.input.model.NpadStyleIndex
8import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting 12import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
9import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting 13import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
10import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting 14import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
@@ -23,13 +27,34 @@ import org.yuzu.yuzu_emu.utils.NativeConfig
23 */ 27 */
24abstract class SettingsItem( 28abstract class SettingsItem(
25 val setting: AbstractSetting, 29 val setting: AbstractSetting,
26 val nameId: Int, 30 @StringRes val titleId: Int,
27 val descriptionId: Int 31 val titleString: String,
32 @StringRes val descriptionId: Int,
33 val descriptionString: String
28) { 34) {
29 abstract val type: Int 35 abstract val type: Int
30 36
37 val title: String by lazy {
38 if (titleId != 0) {
39 return@lazy YuzuApplication.appContext.getString(titleId)
40 }
41 return@lazy titleString
42 }
43
44 val description: String by lazy {
45 if (descriptionId != 0) {
46 return@lazy YuzuApplication.appContext.getString(descriptionId)
47 }
48 return@lazy descriptionString
49 }
50
31 val isEditable: Boolean 51 val isEditable: Boolean
32 get() { 52 get() {
53 // Can't change docked mode toggle when using handheld mode
54 if (setting.key == BooleanSetting.USE_DOCKED_MODE.key) {
55 return NativeInput.getStyleIndex(0) != NpadStyleIndex.Handheld
56 }
57
33 // Can't edit settings that aren't saveable in per-game config even if they are switchable 58 // Can't edit settings that aren't saveable in per-game config even if they are switchable
34 if (NativeConfig.isPerGameConfigLoaded() && !setting.isSaveable) { 59 if (NativeConfig.isPerGameConfigLoaded() && !setting.isSaveable) {
35 return false 60 return false
@@ -59,6 +84,9 @@ abstract class SettingsItem(
59 const val TYPE_STRING_SINGLE_CHOICE = 5 84 const val TYPE_STRING_SINGLE_CHOICE = 5
60 const val TYPE_DATETIME_SETTING = 6 85 const val TYPE_DATETIME_SETTING = 6
61 const val TYPE_RUNNABLE = 7 86 const val TYPE_RUNNABLE = 7
87 const val TYPE_INPUT = 8
88 const val TYPE_INT_SINGLE_CHOICE = 9
89 const val TYPE_INPUT_PROFILE = 10
62 90
63 const val FASTMEM_COMBINED = "fastmem_combined" 91 const val FASTMEM_COMBINED = "fastmem_combined"
64 92
@@ -80,237 +108,242 @@ abstract class SettingsItem(
80 put( 108 put(
81 SwitchSetting( 109 SwitchSetting(
82 BooleanSetting.RENDERER_USE_SPEED_LIMIT, 110 BooleanSetting.RENDERER_USE_SPEED_LIMIT,
83 R.string.frame_limit_enable, 111 titleId = R.string.frame_limit_enable,
84 R.string.frame_limit_enable_description 112 descriptionId = R.string.frame_limit_enable_description
85 ) 113 )
86 ) 114 )
87 put( 115 put(
88 SliderSetting( 116 SliderSetting(
89 ShortSetting.RENDERER_SPEED_LIMIT, 117 ShortSetting.RENDERER_SPEED_LIMIT,
90 R.string.frame_limit_slider, 118 titleId = R.string.frame_limit_slider,
91 R.string.frame_limit_slider_description, 119 descriptionId = R.string.frame_limit_slider_description,
92 1, 120 min = 1,
93 400, 121 max = 400,
94 "%" 122 units = "%"
95 ) 123 )
96 ) 124 )
97 put( 125 put(
98 SingleChoiceSetting( 126 SingleChoiceSetting(
99 IntSetting.CPU_BACKEND, 127 IntSetting.CPU_BACKEND,
100 R.string.cpu_backend, 128 titleId = R.string.cpu_backend,
101 0, 129 choicesId = R.array.cpuBackendArm64Names,
102 R.array.cpuBackendArm64Names, 130 valuesId = R.array.cpuBackendArm64Values
103 R.array.cpuBackendArm64Values
104 ) 131 )
105 ) 132 )
106 put( 133 put(
107 SingleChoiceSetting( 134 SingleChoiceSetting(
108 IntSetting.CPU_ACCURACY, 135 IntSetting.CPU_ACCURACY,
109 R.string.cpu_accuracy, 136 titleId = R.string.cpu_accuracy,
110 0, 137 choicesId = R.array.cpuAccuracyNames,
111 R.array.cpuAccuracyNames, 138 valuesId = R.array.cpuAccuracyValues
112 R.array.cpuAccuracyValues
113 ) 139 )
114 ) 140 )
115 put( 141 put(
116 SwitchSetting( 142 SwitchSetting(
117 BooleanSetting.PICTURE_IN_PICTURE, 143 BooleanSetting.PICTURE_IN_PICTURE,
118 R.string.picture_in_picture, 144 titleId = R.string.picture_in_picture,
119 R.string.picture_in_picture_description 145 descriptionId = R.string.picture_in_picture_description
120 ) 146 )
121 ) 147 )
148
149 val dockedModeSetting = object : AbstractBooleanSetting {
150 override val key = BooleanSetting.USE_DOCKED_MODE.key
151
152 override fun getBoolean(needsGlobal: Boolean): Boolean {
153 if (NativeInput.getStyleIndex(0) == NpadStyleIndex.Handheld) {
154 return false
155 }
156 return BooleanSetting.USE_DOCKED_MODE.getBoolean(needsGlobal)
157 }
158
159 override fun setBoolean(value: Boolean) =
160 BooleanSetting.USE_DOCKED_MODE.setBoolean(value)
161
162 override val defaultValue = BooleanSetting.USE_DOCKED_MODE.defaultValue
163
164 override fun getValueAsString(needsGlobal: Boolean): String =
165 BooleanSetting.USE_DOCKED_MODE.getValueAsString(needsGlobal)
166
167 override fun reset() = BooleanSetting.USE_DOCKED_MODE.reset()
168 }
122 put( 169 put(
123 SwitchSetting( 170 SwitchSetting(
124 BooleanSetting.USE_DOCKED_MODE, 171 dockedModeSetting,
125 R.string.use_docked_mode, 172 titleId = R.string.use_docked_mode,
126 R.string.use_docked_mode_description 173 descriptionId = R.string.use_docked_mode_description
127 ) 174 )
128 ) 175 )
176
129 put( 177 put(
130 SingleChoiceSetting( 178 SingleChoiceSetting(
131 IntSetting.REGION_INDEX, 179 IntSetting.REGION_INDEX,
132 R.string.emulated_region, 180 titleId = R.string.emulated_region,
133 0, 181 choicesId = R.array.regionNames,
134 R.array.regionNames, 182 valuesId = R.array.regionValues
135 R.array.regionValues
136 ) 183 )
137 ) 184 )
138 put( 185 put(
139 SingleChoiceSetting( 186 SingleChoiceSetting(
140 IntSetting.LANGUAGE_INDEX, 187 IntSetting.LANGUAGE_INDEX,
141 R.string.emulated_language, 188 titleId = R.string.emulated_language,
142 0, 189 choicesId = R.array.languageNames,
143 R.array.languageNames, 190 valuesId = R.array.languageValues
144 R.array.languageValues
145 ) 191 )
146 ) 192 )
147 put( 193 put(
148 SwitchSetting( 194 SwitchSetting(
149 BooleanSetting.USE_CUSTOM_RTC, 195 BooleanSetting.USE_CUSTOM_RTC,
150 R.string.use_custom_rtc, 196 titleId = R.string.use_custom_rtc,
151 R.string.use_custom_rtc_description 197 descriptionId = R.string.use_custom_rtc_description
152 ) 198 )
153 ) 199 )
154 put(DateTimeSetting(LongSetting.CUSTOM_RTC, R.string.set_custom_rtc, 0)) 200 put(DateTimeSetting(LongSetting.CUSTOM_RTC, titleId = R.string.set_custom_rtc))
155 put( 201 put(
156 SingleChoiceSetting( 202 SingleChoiceSetting(
157 IntSetting.RENDERER_ACCURACY, 203 IntSetting.RENDERER_ACCURACY,
158 R.string.renderer_accuracy, 204 titleId = R.string.renderer_accuracy,
159 0, 205 choicesId = R.array.rendererAccuracyNames,
160 R.array.rendererAccuracyNames, 206 valuesId = R.array.rendererAccuracyValues
161 R.array.rendererAccuracyValues
162 ) 207 )
163 ) 208 )
164 put( 209 put(
165 SingleChoiceSetting( 210 SingleChoiceSetting(
166 IntSetting.RENDERER_RESOLUTION, 211 IntSetting.RENDERER_RESOLUTION,
167 R.string.renderer_resolution, 212 titleId = R.string.renderer_resolution,
168 0, 213 choicesId = R.array.rendererResolutionNames,
169 R.array.rendererResolutionNames, 214 valuesId = R.array.rendererResolutionValues
170 R.array.rendererResolutionValues
171 ) 215 )
172 ) 216 )
173 put( 217 put(
174 SingleChoiceSetting( 218 SingleChoiceSetting(
175 IntSetting.RENDERER_VSYNC, 219 IntSetting.RENDERER_VSYNC,
176 R.string.renderer_vsync, 220 titleId = R.string.renderer_vsync,
177 0, 221 choicesId = R.array.rendererVSyncNames,
178 R.array.rendererVSyncNames, 222 valuesId = R.array.rendererVSyncValues
179 R.array.rendererVSyncValues
180 ) 223 )
181 ) 224 )
182 put( 225 put(
183 SingleChoiceSetting( 226 SingleChoiceSetting(
184 IntSetting.RENDERER_SCALING_FILTER, 227 IntSetting.RENDERER_SCALING_FILTER,
185 R.string.renderer_scaling_filter, 228 titleId = R.string.renderer_scaling_filter,
186 0, 229 choicesId = R.array.rendererScalingFilterNames,
187 R.array.rendererScalingFilterNames, 230 valuesId = R.array.rendererScalingFilterValues
188 R.array.rendererScalingFilterValues
189 ) 231 )
190 ) 232 )
191 put( 233 put(
192 SliderSetting( 234 SliderSetting(
193 IntSetting.FSR_SHARPENING_SLIDER, 235 IntSetting.FSR_SHARPENING_SLIDER,
194 R.string.fsr_sharpness, 236 titleId = R.string.fsr_sharpness,
195 R.string.fsr_sharpness_description, 237 descriptionId = R.string.fsr_sharpness_description,
196 0, 238 units = "%"
197 100,
198 "%"
199 ) 239 )
200 ) 240 )
201 put( 241 put(
202 SingleChoiceSetting( 242 SingleChoiceSetting(
203 IntSetting.RENDERER_ANTI_ALIASING, 243 IntSetting.RENDERER_ANTI_ALIASING,
204 R.string.renderer_anti_aliasing, 244 titleId = R.string.renderer_anti_aliasing,
205 0, 245 choicesId = R.array.rendererAntiAliasingNames,
206 R.array.rendererAntiAliasingNames, 246 valuesId = R.array.rendererAntiAliasingValues
207 R.array.rendererAntiAliasingValues
208 ) 247 )
209 ) 248 )
210 put( 249 put(
211 SingleChoiceSetting( 250 SingleChoiceSetting(
212 IntSetting.RENDERER_SCREEN_LAYOUT, 251 IntSetting.RENDERER_SCREEN_LAYOUT,
213 R.string.renderer_screen_layout, 252 titleId = R.string.renderer_screen_layout,
214 0, 253 choicesId = R.array.rendererScreenLayoutNames,
215 R.array.rendererScreenLayoutNames, 254 valuesId = R.array.rendererScreenLayoutValues
216 R.array.rendererScreenLayoutValues
217 ) 255 )
218 ) 256 )
219 put( 257 put(
220 SingleChoiceSetting( 258 SingleChoiceSetting(
221 IntSetting.RENDERER_ASPECT_RATIO, 259 IntSetting.RENDERER_ASPECT_RATIO,
222 R.string.renderer_aspect_ratio, 260 titleId = R.string.renderer_aspect_ratio,
223 0, 261 choicesId = R.array.rendererAspectRatioNames,
224 R.array.rendererAspectRatioNames, 262 valuesId = R.array.rendererAspectRatioValues
225 R.array.rendererAspectRatioValues
226 ) 263 )
227 ) 264 )
228 put( 265 put(
229 SingleChoiceSetting( 266 SingleChoiceSetting(
230 IntSetting.VERTICAL_ALIGNMENT, 267 IntSetting.VERTICAL_ALIGNMENT,
231 R.string.vertical_alignment, 268 titleId = R.string.vertical_alignment,
232 0, 269 descriptionId = 0,
233 R.array.verticalAlignmentEntries, 270 choicesId = R.array.verticalAlignmentEntries,
234 R.array.verticalAlignmentValues 271 valuesId = R.array.verticalAlignmentValues
235 ) 272 )
236 ) 273 )
237 put( 274 put(
238 SwitchSetting( 275 SwitchSetting(
239 BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE, 276 BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE,
240 R.string.use_disk_shader_cache, 277 titleId = R.string.use_disk_shader_cache,
241 R.string.use_disk_shader_cache_description 278 descriptionId = R.string.use_disk_shader_cache_description
242 ) 279 )
243 ) 280 )
244 put( 281 put(
245 SwitchSetting( 282 SwitchSetting(
246 BooleanSetting.RENDERER_FORCE_MAX_CLOCK, 283 BooleanSetting.RENDERER_FORCE_MAX_CLOCK,
247 R.string.renderer_force_max_clock, 284 titleId = R.string.renderer_force_max_clock,
248 R.string.renderer_force_max_clock_description 285 descriptionId = R.string.renderer_force_max_clock_description
249 ) 286 )
250 ) 287 )
251 put( 288 put(
252 SwitchSetting( 289 SwitchSetting(
253 BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS, 290 BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS,
254 R.string.renderer_asynchronous_shaders, 291 titleId = R.string.renderer_asynchronous_shaders,
255 R.string.renderer_asynchronous_shaders_description 292 descriptionId = R.string.renderer_asynchronous_shaders_description
256 ) 293 )
257 ) 294 )
258 put( 295 put(
259 SwitchSetting( 296 SwitchSetting(
260 BooleanSetting.RENDERER_REACTIVE_FLUSHING, 297 BooleanSetting.RENDERER_REACTIVE_FLUSHING,
261 R.string.renderer_reactive_flushing, 298 titleId = R.string.renderer_reactive_flushing,
262 R.string.renderer_reactive_flushing_description 299 descriptionId = R.string.renderer_reactive_flushing_description
263 ) 300 )
264 ) 301 )
265 put( 302 put(
266 SingleChoiceSetting( 303 SingleChoiceSetting(
267 IntSetting.MAX_ANISOTROPY, 304 IntSetting.MAX_ANISOTROPY,
268 R.string.anisotropic_filtering, 305 titleId = R.string.anisotropic_filtering,
269 R.string.anisotropic_filtering_description, 306 descriptionId = R.string.anisotropic_filtering_description,
270 R.array.anisoEntries, 307 choicesId = R.array.anisoEntries,
271 R.array.anisoValues 308 valuesId = R.array.anisoValues
272 ) 309 )
273 ) 310 )
274 put( 311 put(
275 SingleChoiceSetting( 312 SingleChoiceSetting(
276 IntSetting.AUDIO_OUTPUT_ENGINE, 313 IntSetting.AUDIO_OUTPUT_ENGINE,
277 R.string.audio_output_engine, 314 titleId = R.string.audio_output_engine,
278 0, 315 choicesId = R.array.outputEngineEntries,
279 R.array.outputEngineEntries, 316 valuesId = R.array.outputEngineValues
280 R.array.outputEngineValues
281 ) 317 )
282 ) 318 )
283 put( 319 put(
284 SliderSetting( 320 SliderSetting(
285 ByteSetting.AUDIO_VOLUME, 321 ByteSetting.AUDIO_VOLUME,
286 R.string.audio_volume, 322 titleId = R.string.audio_volume,
287 R.string.audio_volume_description, 323 descriptionId = R.string.audio_volume_description,
288 0, 324 units = "%"
289 100,
290 "%"
291 ) 325 )
292 ) 326 )
293 put( 327 put(
294 SingleChoiceSetting( 328 SingleChoiceSetting(
295 IntSetting.RENDERER_BACKEND, 329 IntSetting.RENDERER_BACKEND,
296 R.string.renderer_api, 330 titleId = R.string.renderer_api,
297 0, 331 choicesId = R.array.rendererApiNames,
298 R.array.rendererApiNames, 332 valuesId = R.array.rendererApiValues
299 R.array.rendererApiValues
300 ) 333 )
301 ) 334 )
302 put( 335 put(
303 SwitchSetting( 336 SwitchSetting(
304 BooleanSetting.RENDERER_DEBUG, 337 BooleanSetting.RENDERER_DEBUG,
305 R.string.renderer_debug, 338 titleId = R.string.renderer_debug,
306 R.string.renderer_debug_description 339 descriptionId = R.string.renderer_debug_description
307 ) 340 )
308 ) 341 )
309 put( 342 put(
310 SwitchSetting( 343 SwitchSetting(
311 BooleanSetting.CPU_DEBUG_MODE, 344 BooleanSetting.CPU_DEBUG_MODE,
312 R.string.cpu_debug_mode, 345 titleId = R.string.cpu_debug_mode,
313 R.string.cpu_debug_mode_description 346 descriptionId = R.string.cpu_debug_mode_description
314 ) 347 )
315 ) 348 )
316 349
@@ -346,7 +379,7 @@ abstract class SettingsItem(
346 379
347 override fun reset() = setBoolean(defaultValue) 380 override fun reset() = setBoolean(defaultValue)
348 } 381 }
349 put(SwitchSetting(fastmem, R.string.fastmem, 0)) 382 put(SwitchSetting(fastmem, R.string.fastmem))
350 } 383 }
351 } 384 }
352} 385}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt
index 97a5a9e59..ea5e099ed 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt
@@ -3,16 +3,20 @@
3 3
4package org.yuzu.yuzu_emu.features.settings.model.view 4package org.yuzu.yuzu_emu.features.settings.model.view
5 5
6import androidx.annotation.ArrayRes
7import androidx.annotation.StringRes
6import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting 8import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
7import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting 9import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
8 10
9class SingleChoiceSetting( 11class SingleChoiceSetting(
10 setting: AbstractSetting, 12 setting: AbstractSetting,
11 titleId: Int, 13 @StringRes titleId: Int = 0,
12 descriptionId: Int, 14 titleString: String = "",
13 val choicesId: Int, 15 @StringRes descriptionId: Int = 0,
14 val valuesId: Int 16 descriptionString: String = "",
15) : SettingsItem(setting, titleId, descriptionId) { 17 @ArrayRes val choicesId: Int,
18 @ArrayRes val valuesId: Int
19) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) {
16 override val type = TYPE_SINGLE_CHOICE 20 override val type = TYPE_SINGLE_CHOICE
17 21
18 fun getSelectedValue(needsGlobal: Boolean = false) = 22 fun getSelectedValue(needsGlobal: Boolean = false) =
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt
index b9b709bf7..6a5cdf48b 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt
@@ -3,6 +3,7 @@
3 3
4package org.yuzu.yuzu_emu.features.settings.model.view 4package org.yuzu.yuzu_emu.features.settings.model.view
5 5
6import androidx.annotation.StringRes
6import org.yuzu.yuzu_emu.features.settings.model.AbstractByteSetting 7import org.yuzu.yuzu_emu.features.settings.model.AbstractByteSetting
7import org.yuzu.yuzu_emu.features.settings.model.AbstractFloatSetting 8import org.yuzu.yuzu_emu.features.settings.model.AbstractFloatSetting
8import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting 9import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
@@ -12,12 +13,14 @@ import kotlin.math.roundToInt
12 13
13class SliderSetting( 14class SliderSetting(
14 setting: AbstractSetting, 15 setting: AbstractSetting,
15 titleId: Int, 16 @StringRes titleId: Int = 0,
16 descriptionId: Int, 17 titleString: String = "",
17 val min: Int, 18 @StringRes descriptionId: Int = 0,
18 val max: Int, 19 descriptionString: String = "",
19 val units: String 20 val min: Int = 0,
20) : SettingsItem(setting, titleId, descriptionId) { 21 val max: Int = 100,
22 val units: String = ""
23) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) {
21 override val type = TYPE_SLIDER 24 override val type = TYPE_SLIDER
22 25
23 fun getSelectedValue(needsGlobal: Boolean = false) = 26 fun getSelectedValue(needsGlobal: Boolean = false) =
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt
index ba7920f50..5260ff4dc 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt
@@ -3,15 +3,18 @@
3 3
4package org.yuzu.yuzu_emu.features.settings.model.view 4package org.yuzu.yuzu_emu.features.settings.model.view
5 5
6import androidx.annotation.StringRes
6import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting 7import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting
7 8
8class StringSingleChoiceSetting( 9class StringSingleChoiceSetting(
9 private val stringSetting: AbstractStringSetting, 10 private val stringSetting: AbstractStringSetting,
10 titleId: Int, 11 @StringRes titleId: Int = 0,
11 descriptionId: Int, 12 titleString: String = "",
13 @StringRes descriptionId: Int = 0,
14 descriptionString: String = "",
12 val choices: Array<String>, 15 val choices: Array<String>,
13 val values: Array<String> 16 val values: Array<String>
14) : SettingsItem(stringSetting, titleId, descriptionId) { 17) : SettingsItem(stringSetting, titleId, titleString, descriptionId, descriptionString) {
15 override val type = TYPE_STRING_SINGLE_CHOICE 18 override val type = TYPE_STRING_SINGLE_CHOICE
16 19
17 fun getValueAt(index: Int): String = 20 fun getValueAt(index: Int): String =
@@ -20,7 +23,7 @@ class StringSingleChoiceSetting(
20 fun getSelectedValue(needsGlobal: Boolean = false) = stringSetting.getString(needsGlobal) 23 fun getSelectedValue(needsGlobal: Boolean = false) = stringSetting.getString(needsGlobal)
21 fun setSelectedValue(value: String) = stringSetting.setString(value) 24 fun setSelectedValue(value: String) = stringSetting.setString(value)
22 25
23 val selectValueIndex: Int 26 val selectedValueIndex: Int
24 get() { 27 get() {
25 for (i in values.indices) { 28 for (i in values.indices) {
26 if (values[i] == getSelectedValue()) { 29 if (values[i] == getSelectedValue()) {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt
index 94953b18a..c722393dd 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt
@@ -8,10 +8,12 @@ import androidx.annotation.StringRes
8import org.yuzu.yuzu_emu.features.settings.model.Settings 8import org.yuzu.yuzu_emu.features.settings.model.Settings
9 9
10class SubmenuSetting( 10class SubmenuSetting(
11 @StringRes titleId: Int, 11 @StringRes titleId: Int = 0,
12 @StringRes descriptionId: Int, 12 titleString: String = "",
13 @DrawableRes val iconId: Int, 13 @StringRes descriptionId: Int = 0,
14 descriptionString: String = "",
15 @DrawableRes val iconId: Int = 0,
14 val menuKey: Settings.MenuTag 16 val menuKey: Settings.MenuTag
15) : SettingsItem(emptySetting, titleId, descriptionId) { 17) : SettingsItem(emptySetting, titleId, titleString, descriptionId, descriptionString) {
16 override val type = TYPE_SUBMENU 18 override val type = TYPE_SUBMENU
17} 19}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt
index 44d47dd69..4984bf52e 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt
@@ -3,15 +3,18 @@
3 3
4package org.yuzu.yuzu_emu.features.settings.model.view 4package org.yuzu.yuzu_emu.features.settings.model.view
5 5
6import androidx.annotation.StringRes
6import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting 7import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
7import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting 8import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
8import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting 9import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
9 10
10class SwitchSetting( 11class SwitchSetting(
11 setting: AbstractSetting, 12 setting: AbstractSetting,
12 titleId: Int, 13 @StringRes titleId: Int = 0,
13 descriptionId: Int 14 titleString: String = "",
14) : SettingsItem(setting, titleId, descriptionId) { 15 @StringRes descriptionId: Int = 0,
16 descriptionString: String = ""
17) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) {
15 override val type = TYPE_SWITCH 18 override val type = TYPE_SWITCH
16 19
17 fun getIsChecked(needsGlobal: Boolean = false): Boolean { 20 fun getIsChecked(needsGlobal: Boolean = false): Boolean {
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 100755
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 100755
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 100755
index 000000000..1bae593ae
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileDialogFragment.kt
@@ -0,0 +1,148 @@
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.recyclerview.widget.LinearLayoutManager
15import com.google.android.material.dialog.MaterialAlertDialogBuilder
16import org.yuzu.yuzu_emu.R
17import org.yuzu.yuzu_emu.databinding.DialogInputProfilesBinding
18import org.yuzu.yuzu_emu.features.settings.model.view.InputProfileSetting
19import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
20import org.yuzu.yuzu_emu.utils.collect
21
22class InputProfileDialogFragment : DialogFragment() {
23 private var position = 0
24
25 private val settingsViewModel: SettingsViewModel by activityViewModels()
26
27 private lateinit var binding: DialogInputProfilesBinding
28
29 private lateinit var setting: InputProfileSetting
30
31 override fun onCreate(savedInstanceState: Bundle?) {
32 super.onCreate(savedInstanceState)
33 position = requireArguments().getInt(POSITION)
34 }
35
36 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
37 binding = DialogInputProfilesBinding.inflate(layoutInflater)
38
39 setting = settingsViewModel.clickedItem as InputProfileSetting
40 val options = mutableListOf<ProfileItem>().apply {
41 add(
42 NewProfileItem(
43 createNewProfile = {
44 NewInputProfileDialogFragment.newInstance(
45 settingsViewModel,
46 setting,
47 position
48 ).show(parentFragmentManager, NewInputProfileDialogFragment.TAG)
49 dismiss()
50 }
51 )
52 )
53
54 val onActionDismiss = {
55 settingsViewModel.setReloadListAndNotifyDataset(true)
56 dismiss()
57 }
58 setting.getProfileNames().forEach {
59 add(
60 ExistingProfileItem(
61 it,
62 deleteProfile = {
63 settingsViewModel.setShouldShowDeleteProfileDialog(it)
64 },
65 saveProfile = {
66 if (!setting.saveProfile(it)) {
67 Toast.makeText(
68 requireContext(),
69 R.string.failed_to_save_profile,
70 Toast.LENGTH_SHORT
71 ).show()
72 }
73 onActionDismiss.invoke()
74 },
75 loadProfile = {
76 if (!setting.loadProfile(it)) {
77 Toast.makeText(
78 requireContext(),
79 R.string.failed_to_load_profile,
80 Toast.LENGTH_SHORT
81 ).show()
82 }
83 onActionDismiss.invoke()
84 }
85 )
86 )
87 }
88 }
89 binding.listProfiles.apply {
90 layoutManager = LinearLayoutManager(requireContext())
91 adapter = InputProfileAdapter(options)
92 }
93
94 return MaterialAlertDialogBuilder(requireContext())
95 .setView(binding.root)
96 .create()
97 }
98
99 override fun onCreateView(
100 inflater: LayoutInflater,
101 container: ViewGroup?,
102 savedInstanceState: Bundle?
103 ): View {
104 return binding.root
105 }
106
107 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
108 super.onViewCreated(view, savedInstanceState)
109
110 settingsViewModel.shouldShowDeleteProfileDialog.collect(viewLifecycleOwner) {
111 if (it.isNotEmpty()) {
112 MessageDialogFragment.newInstance(
113 activity = requireActivity(),
114 titleId = R.string.delete_input_profile,
115 descriptionId = R.string.delete_input_profile_description,
116 positiveAction = {
117 setting.deleteProfile(it)
118 settingsViewModel.setReloadListAndNotifyDataset(true)
119 },
120 negativeAction = {},
121 negativeButtonTitleId = android.R.string.cancel
122 ).show(parentFragmentManager, MessageDialogFragment.TAG)
123 settingsViewModel.setShouldShowDeleteProfileDialog("")
124 dismiss()
125 }
126 }
127 }
128
129 companion object {
130 const val TAG = "InputProfileDialogFragment"
131
132 const val POSITION = "Position"
133
134 fun newInstance(
135 settingsViewModel: SettingsViewModel,
136 profileSetting: InputProfileSetting,
137 position: Int
138 ): InputProfileDialogFragment {
139 settingsViewModel.clickedItem = profileSetting
140
141 val args = Bundle()
142 args.putInt(POSITION, position)
143 val fragment = InputProfileDialogFragment()
144 fragment.arguments = args
145 return fragment
146 }
147 }
148}
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 100755
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..455b3b5ff 100755
--- 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
@@ -13,21 +13,16 @@ import androidx.appcompat.app.AppCompatActivity
13import androidx.core.view.ViewCompat 13import androidx.core.view.ViewCompat
14import androidx.core.view.WindowCompat 14import androidx.core.view.WindowCompat
15import androidx.core.view.WindowInsetsCompat 15import androidx.core.view.WindowInsetsCompat
16import androidx.lifecycle.Lifecycle
17import androidx.lifecycle.lifecycleScope
18import androidx.lifecycle.repeatOnLifecycle
19import androidx.navigation.fragment.NavHostFragment 16import androidx.navigation.fragment.NavHostFragment
20import androidx.navigation.navArgs 17import androidx.navigation.navArgs
21import com.google.android.material.color.MaterialColors 18import com.google.android.material.color.MaterialColors
22import kotlinx.coroutines.flow.collectLatest
23import kotlinx.coroutines.launch
24import org.yuzu.yuzu_emu.NativeLibrary 19import org.yuzu.yuzu_emu.NativeLibrary
25import java.io.IOException 20import java.io.IOException
26import org.yuzu.yuzu_emu.R 21import org.yuzu.yuzu_emu.R
27import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding 22import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding
23import org.yuzu.yuzu_emu.features.input.NativeInput
28import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile 24import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
29import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment 25import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment
30import org.yuzu.yuzu_emu.model.SettingsViewModel
31import org.yuzu.yuzu_emu.utils.* 26import org.yuzu.yuzu_emu.utils.*
32 27
33class SettingsActivity : AppCompatActivity() { 28class SettingsActivity : AppCompatActivity() {
@@ -70,39 +65,23 @@ class SettingsActivity : AppCompatActivity() {
70 ) 65 )
71 } 66 }
72 67
73 lifecycleScope.apply { 68 settingsViewModel.shouldRecreate.collect(
74 launch { 69 this,
75 repeatOnLifecycle(Lifecycle.State.CREATED) { 70 resetState = { settingsViewModel.setShouldRecreate(false) }
76 settingsViewModel.shouldRecreate.collectLatest { 71 ) { if (it) recreate() }
77 if (it) { 72 settingsViewModel.shouldNavigateBack.collect(
78 settingsViewModel.setShouldRecreate(false) 73 this,
79 recreate() 74 resetState = { settingsViewModel.setShouldNavigateBack(false) }
80 } 75 ) { if (it) navigateBack() }
81 } 76 settingsViewModel.shouldShowResetSettingsDialog.collect(
82 } 77 this,
83 } 78 resetState = { settingsViewModel.setShouldShowResetSettingsDialog(false) }
84 launch { 79 ) {
85 repeatOnLifecycle(Lifecycle.State.CREATED) { 80 if (it) {
86 settingsViewModel.shouldNavigateBack.collectLatest { 81 ResetSettingsDialogFragment().show(
87 if (it) { 82 supportFragmentManager,
88 settingsViewModel.setShouldNavigateBack(false) 83 ResetSettingsDialogFragment.TAG
89 navigateBack() 84 )
90 }
91 }
92 }
93 }
94 launch {
95 repeatOnLifecycle(Lifecycle.State.CREATED) {
96 settingsViewModel.shouldShowResetSettingsDialog.collectLatest {
97 if (it) {
98 settingsViewModel.setShouldShowResetSettingsDialog(false)
99 ResetSettingsDialogFragment().show(
100 supportFragmentManager,
101 ResetSettingsDialogFragment.TAG
102 )
103 }
104 }
105 }
106 } 85 }
107 } 86 }
108 87
@@ -137,6 +116,7 @@ class SettingsActivity : AppCompatActivity() {
137 super.onStop() 116 super.onStop()
138 Log.info("[SettingsActivity] Settings activity stopping. Saving settings to INI...") 117 Log.info("[SettingsActivity] Settings activity stopping. Saving settings to INI...")
139 if (isFinishing) { 118 if (isFinishing) {
119 NativeInput.reloadInputDevices()
140 NativeLibrary.applySettings() 120 NativeLibrary.applySettings()
141 if (args.game == null) { 121 if (args.game == null) {
142 NativeConfig.saveGlobalConfig() 122 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 100755
--- 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/features/settings/ui/SettingsDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt
new file mode 100755
index 000000000..a81ff6b1a
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt
@@ -0,0 +1,278 @@
1// SPDX-FileCopyrightText: 2023 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.content.DialogInterface
8import android.os.Bundle
9import android.view.LayoutInflater
10import android.view.View
11import android.view.ViewGroup
12import androidx.fragment.app.DialogFragment
13import androidx.fragment.app.activityViewModels
14import com.google.android.material.dialog.MaterialAlertDialogBuilder
15import com.google.android.material.slider.Slider
16import org.yuzu.yuzu_emu.R
17import org.yuzu.yuzu_emu.databinding.DialogSliderBinding
18import org.yuzu.yuzu_emu.features.input.NativeInput
19import org.yuzu.yuzu_emu.features.input.model.AnalogDirection
20import org.yuzu.yuzu_emu.features.settings.model.view.AnalogInputSetting
21import org.yuzu.yuzu_emu.features.settings.model.view.ButtonInputSetting
22import org.yuzu.yuzu_emu.features.settings.model.view.IntSingleChoiceSetting
23import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
24import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting
25import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting
26import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting
27import org.yuzu.yuzu_emu.utils.ParamPackage
28import org.yuzu.yuzu_emu.utils.collect
29
30class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener {
31 private var type = 0
32 private var position = 0
33
34 private var defaultCancelListener =
35 DialogInterface.OnClickListener { _: DialogInterface?, _: Int -> closeDialog() }
36
37 private val settingsViewModel: SettingsViewModel by activityViewModels()
38
39 private lateinit var sliderBinding: DialogSliderBinding
40
41 override fun onCreate(savedInstanceState: Bundle?) {
42 super.onCreate(savedInstanceState)
43 type = requireArguments().getInt(TYPE)
44 position = requireArguments().getInt(POSITION)
45
46 if (settingsViewModel.clickedItem == null) dismiss()
47 }
48
49 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
50 return when (type) {
51 TYPE_RESET_SETTING -> {
52 MaterialAlertDialogBuilder(requireContext())
53 .setMessage(R.string.reset_setting_confirmation)
54 .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
55 when (val item = settingsViewModel.clickedItem) {
56 is AnalogInputSetting -> {
57 val stickParam = NativeInput.getStickParam(
58 item.playerIndex,
59 item.nativeAnalog
60 )
61 if (stickParam.get("engine", "") == "analog_from_button") {
62 when (item.analogDirection) {
63 AnalogDirection.Up -> stickParam.erase("up")
64 AnalogDirection.Down -> stickParam.erase("down")
65 AnalogDirection.Left -> stickParam.erase("left")
66 AnalogDirection.Right -> stickParam.erase("right")
67 }
68 NativeInput.setStickParam(
69 item.playerIndex,
70 item.nativeAnalog,
71 stickParam
72 )
73 settingsViewModel.setAdapterItemChanged(position)
74 } else {
75 NativeInput.setStickParam(
76 item.playerIndex,
77 item.nativeAnalog,
78 ParamPackage()
79 )
80 settingsViewModel.setDatasetChanged(true)
81 }
82 }
83
84 is ButtonInputSetting -> {
85 NativeInput.setButtonParam(
86 item.playerIndex,
87 item.nativeButton,
88 ParamPackage()
89 )
90 settingsViewModel.setAdapterItemChanged(position)
91 }
92
93 else -> {
94 settingsViewModel.clickedItem!!.setting.reset()
95 settingsViewModel.setAdapterItemChanged(position)
96 }
97 }
98 }
99 .setNegativeButton(android.R.string.cancel, null)
100 .create()
101 }
102
103 SettingsItem.TYPE_SINGLE_CHOICE -> {
104 val item = settingsViewModel.clickedItem as SingleChoiceSetting
105 val value = getSelectionForSingleChoiceValue(item)
106 MaterialAlertDialogBuilder(requireContext())
107 .setTitle(item.title)
108 .setSingleChoiceItems(item.choicesId, value, this)
109 .create()
110 }
111
112 SettingsItem.TYPE_SLIDER -> {
113 sliderBinding = DialogSliderBinding.inflate(layoutInflater)
114 val item = settingsViewModel.clickedItem as SliderSetting
115
116 settingsViewModel.setSliderTextValue(item.getSelectedValue().toFloat(), item.units)
117 sliderBinding.slider.apply {
118 valueFrom = item.min.toFloat()
119 valueTo = item.max.toFloat()
120 value = settingsViewModel.sliderProgress.value.toFloat()
121 addOnChangeListener { _: Slider, value: Float, _: Boolean ->
122 settingsViewModel.setSliderTextValue(value, item.units)
123 }
124 }
125
126 MaterialAlertDialogBuilder(requireContext())
127 .setTitle(item.title)
128 .setView(sliderBinding.root)
129 .setPositiveButton(android.R.string.ok, this)
130 .setNegativeButton(android.R.string.cancel, defaultCancelListener)
131 .create()
132 }
133
134 SettingsItem.TYPE_STRING_SINGLE_CHOICE -> {
135 val item = settingsViewModel.clickedItem as StringSingleChoiceSetting
136 MaterialAlertDialogBuilder(requireContext())
137 .setTitle(item.title)
138 .setSingleChoiceItems(item.choices, item.selectedValueIndex, this)
139 .create()
140 }
141
142 SettingsItem.TYPE_INT_SINGLE_CHOICE -> {
143 val item = settingsViewModel.clickedItem as IntSingleChoiceSetting
144 MaterialAlertDialogBuilder(requireContext())
145 .setTitle(item.title)
146 .setSingleChoiceItems(item.choices, item.selectedValueIndex, this)
147 .create()
148 }
149
150 else -> super.onCreateDialog(savedInstanceState)
151 }
152 }
153
154 override fun onCreateView(
155 inflater: LayoutInflater,
156 container: ViewGroup?,
157 savedInstanceState: Bundle?
158 ): View? {
159 return when (type) {
160 SettingsItem.TYPE_SLIDER -> sliderBinding.root
161 else -> super.onCreateView(inflater, container, savedInstanceState)
162 }
163 }
164
165 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
166 super.onViewCreated(view, savedInstanceState)
167 when (type) {
168 SettingsItem.TYPE_SLIDER -> {
169 settingsViewModel.sliderTextValue.collect(viewLifecycleOwner) {
170 sliderBinding.textValue.text = it
171 }
172 settingsViewModel.sliderProgress.collect(viewLifecycleOwner) {
173 sliderBinding.slider.value = it.toFloat()
174 }
175 }
176 }
177 }
178
179 override fun onClick(dialog: DialogInterface, which: Int) {
180 when (settingsViewModel.clickedItem) {
181 is SingleChoiceSetting -> {
182 val scSetting = settingsViewModel.clickedItem as SingleChoiceSetting
183 val value = getValueForSingleChoiceSelection(scSetting, which)
184 scSetting.setSelectedValue(value)
185 }
186
187 is StringSingleChoiceSetting -> {
188 val scSetting = settingsViewModel.clickedItem as StringSingleChoiceSetting
189 val value = scSetting.getValueAt(which)
190 scSetting.setSelectedValue(value)
191 }
192
193 is IntSingleChoiceSetting -> {
194 val scSetting = settingsViewModel.clickedItem as IntSingleChoiceSetting
195 val value = scSetting.getValueAt(which)
196 scSetting.setSelectedValue(value)
197 }
198
199 is SliderSetting -> {
200 val sliderSetting = settingsViewModel.clickedItem as SliderSetting
201 sliderSetting.setSelectedValue(settingsViewModel.sliderProgress.value)
202 }
203 }
204 closeDialog()
205 }
206
207 private fun closeDialog() {
208 settingsViewModel.setAdapterItemChanged(position)
209 settingsViewModel.clickedItem = null
210 settingsViewModel.setSliderProgress(-1f)
211 dismiss()
212 }
213
214 private fun getValueForSingleChoiceSelection(item: SingleChoiceSetting, which: Int): Int {
215 val valuesId = item.valuesId
216 return if (valuesId > 0) {
217 val valuesArray = requireContext().resources.getIntArray(valuesId)
218 valuesArray[which]
219 } else {
220 which
221 }
222 }
223
224 private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int {
225 val value = item.getSelectedValue()
226 val valuesId = item.valuesId
227 if (valuesId > 0) {
228 val valuesArray = requireContext().resources.getIntArray(valuesId)
229 for (index in valuesArray.indices) {
230 val current = valuesArray[index]
231 if (current == value) {
232 return index
233 }
234 }
235 } else {
236 return value
237 }
238 return -1
239 }
240
241 companion object {
242 const val TAG = "SettingsDialogFragment"
243
244 const val TYPE_RESET_SETTING = -1
245
246 const val TITLE = "Title"
247 const val TYPE = "Type"
248 const val POSITION = "Position"
249
250 fun newInstance(
251 settingsViewModel: SettingsViewModel,
252 clickedItem: SettingsItem,
253 type: Int,
254 position: Int
255 ): SettingsDialogFragment {
256 when (type) {
257 SettingsItem.TYPE_HEADER,
258 SettingsItem.TYPE_SWITCH,
259 SettingsItem.TYPE_SUBMENU,
260 SettingsItem.TYPE_DATETIME_SETTING,
261 SettingsItem.TYPE_RUNNABLE ->
262 throw IllegalArgumentException("[SettingsDialogFragment] Incompatible type!")
263
264 SettingsItem.TYPE_SLIDER -> settingsViewModel.setSliderProgress(
265 (clickedItem as SliderSetting).getSelectedValue().toFloat()
266 )
267 }
268 settingsViewModel.clickedItem = clickedItem
269
270 val args = Bundle()
271 args.putInt(TYPE, type)
272 args.putInt(POSITION, position)
273 val fragment = SettingsDialogFragment()
274 fragment.arguments = args
275 return fragment
276 }
277 }
278}
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..ec16f16c4 100755
--- 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
@@ -13,20 +13,17 @@ import androidx.core.view.WindowInsetsCompat
13import androidx.core.view.updatePadding 13import androidx.core.view.updatePadding
14import androidx.fragment.app.Fragment 14import androidx.fragment.app.Fragment
15import androidx.fragment.app.activityViewModels 15import androidx.fragment.app.activityViewModels
16import androidx.lifecycle.Lifecycle
17import androidx.lifecycle.lifecycleScope
18import androidx.lifecycle.repeatOnLifecycle
19import androidx.navigation.findNavController 16import androidx.navigation.findNavController
20import androidx.navigation.fragment.navArgs 17import androidx.navigation.fragment.navArgs
21import androidx.recyclerview.widget.LinearLayoutManager 18import androidx.recyclerview.widget.LinearLayoutManager
22import com.google.android.material.transition.MaterialSharedAxis 19import com.google.android.material.transition.MaterialSharedAxis
23import kotlinx.coroutines.flow.collectLatest
24import kotlinx.coroutines.launch
25import org.yuzu.yuzu_emu.R 20import org.yuzu.yuzu_emu.R
26import org.yuzu.yuzu_emu.databinding.FragmentSettingsBinding 21import org.yuzu.yuzu_emu.databinding.FragmentSettingsBinding
22import org.yuzu.yuzu_emu.features.input.NativeInput
27import org.yuzu.yuzu_emu.features.settings.model.Settings 23import org.yuzu.yuzu_emu.features.settings.model.Settings
28import org.yuzu.yuzu_emu.model.SettingsViewModel 24import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
29import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins 25import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
26import org.yuzu.yuzu_emu.utils.collect
30 27
31class SettingsFragment : Fragment() { 28class SettingsFragment : Fragment() {
32 private lateinit var presenter: SettingsFragmentPresenter 29 private lateinit var presenter: SettingsFragmentPresenter
@@ -45,6 +42,12 @@ class SettingsFragment : Fragment() {
45 returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) 42 returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
46 reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) 43 reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
47 exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) 44 exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
45
46 val playerIndex = getPlayerIndex()
47 if (playerIndex != -1) {
48 NativeInput.loadInputProfiles()
49 NativeInput.reloadInputDevices()
50 }
48 } 51 }
49 52
50 override fun onCreateView( 53 override fun onCreateView(
@@ -56,9 +59,9 @@ class SettingsFragment : Fragment() {
56 return binding.root 59 return binding.root
57 } 60 }
58 61
59 // This is using the correct scope, lint is just acting up 62 @SuppressLint("NotifyDataSetChanged")
60 @SuppressLint("UnsafeRepeatOnLifecycleDetector")
61 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 63 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
64 super.onViewCreated(view, savedInstanceState)
62 settingsAdapter = SettingsAdapter(this, requireContext()) 65 settingsAdapter = SettingsAdapter(this, requireContext())
63 presenter = SettingsFragmentPresenter( 66 presenter = SettingsFragmentPresenter(
64 settingsViewModel, 67 settingsViewModel,
@@ -71,7 +74,17 @@ class SettingsFragment : Fragment() {
71 ) { 74 ) {
72 args.game!!.title 75 args.game!!.title
73 } else { 76 } else {
74 getString(args.menuTag.titleId) 77 when (args.menuTag) {
78 Settings.MenuTag.SECTION_INPUT_PLAYER_ONE -> Settings.getPlayerString(1)
79 Settings.MenuTag.SECTION_INPUT_PLAYER_TWO -> Settings.getPlayerString(2)
80 Settings.MenuTag.SECTION_INPUT_PLAYER_THREE -> Settings.getPlayerString(3)
81 Settings.MenuTag.SECTION_INPUT_PLAYER_FOUR -> Settings.getPlayerString(4)
82 Settings.MenuTag.SECTION_INPUT_PLAYER_FIVE -> Settings.getPlayerString(5)
83 Settings.MenuTag.SECTION_INPUT_PLAYER_SIX -> Settings.getPlayerString(6)
84 Settings.MenuTag.SECTION_INPUT_PLAYER_SEVEN -> Settings.getPlayerString(7)
85 Settings.MenuTag.SECTION_INPUT_PLAYER_EIGHT -> Settings.getPlayerString(8)
86 else -> getString(args.menuTag.titleId)
87 }
75 } 88 }
76 binding.listSettings.apply { 89 binding.listSettings.apply {
77 adapter = settingsAdapter 90 adapter = settingsAdapter
@@ -82,16 +95,37 @@ class SettingsFragment : Fragment() {
82 settingsViewModel.setShouldNavigateBack(true) 95 settingsViewModel.setShouldNavigateBack(true)
83 } 96 }
84 97
85 viewLifecycleOwner.lifecycleScope.apply { 98 settingsViewModel.shouldReloadSettingsList.collect(
86 launch { 99 viewLifecycleOwner,
87 repeatOnLifecycle(Lifecycle.State.CREATED) { 100 resetState = { settingsViewModel.setShouldReloadSettingsList(false) }
88 settingsViewModel.shouldReloadSettingsList.collectLatest { 101 ) { if (it) presenter.loadSettingsList() }
89 if (it) { 102 settingsViewModel.adapterItemChanged.collect(
90 settingsViewModel.setShouldReloadSettingsList(false) 103 viewLifecycleOwner,
91 presenter.loadSettingsList() 104 resetState = { settingsViewModel.setAdapterItemChanged(-1) }
92 } 105 ) { if (it != -1) settingsAdapter?.notifyItemChanged(it) }
93 } 106 settingsViewModel.datasetChanged.collect(
94 } 107 viewLifecycleOwner,
108 resetState = { settingsViewModel.setDatasetChanged(false) }
109 ) { if (it) settingsAdapter?.notifyDataSetChanged() }
110 settingsViewModel.reloadListAndNotifyDataset.collect(
111 viewLifecycleOwner,
112 resetState = { settingsViewModel.setReloadListAndNotifyDataset(false) }
113 ) { if (it) presenter.loadSettingsList(true) }
114 settingsViewModel.shouldShowResetInputDialog.collect(
115 viewLifecycleOwner,
116 resetState = { settingsViewModel.setShouldShowResetInputDialog(false) }
117 ) {
118 if (it) {
119 MessageDialogFragment.newInstance(
120 activity = requireActivity(),
121 titleId = R.string.reset_mapping,
122 descriptionId = R.string.reset_mapping_description,
123 positiveAction = {
124 NativeInput.resetControllerMappings(getPlayerIndex())
125 settingsViewModel.setReloadListAndNotifyDataset(true)
126 },
127 negativeAction = {}
128 ).show(parentFragmentManager, MessageDialogFragment.TAG)
95 } 129 }
96 } 130 }
97 131
@@ -115,6 +149,19 @@ class SettingsFragment : Fragment() {
115 setInsets() 149 setInsets()
116 } 150 }
117 151
152 private fun getPlayerIndex(): Int =
153 when (args.menuTag) {
154 Settings.MenuTag.SECTION_INPUT_PLAYER_ONE -> 0
155 Settings.MenuTag.SECTION_INPUT_PLAYER_TWO -> 1
156 Settings.MenuTag.SECTION_INPUT_PLAYER_THREE -> 2
157 Settings.MenuTag.SECTION_INPUT_PLAYER_FOUR -> 3
158 Settings.MenuTag.SECTION_INPUT_PLAYER_FIVE -> 4
159 Settings.MenuTag.SECTION_INPUT_PLAYER_SIX -> 5
160 Settings.MenuTag.SECTION_INPUT_PLAYER_SEVEN -> 6
161 Settings.MenuTag.SECTION_INPUT_PLAYER_EIGHT -> 7
162 else -> -1
163 }
164
118 private fun setInsets() { 165 private fun setInsets() {
119 ViewCompat.setOnApplyWindowInsetsListener( 166 ViewCompat.setOnApplyWindowInsetsListener(
120 binding.root 167 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 db1a58147..e491c29a2 100755
--- 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,73 +62,90 @@ 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>) {
84 sl.apply { 110 sl.apply {
85 add( 111 add(
86 SubmenuSetting( 112 SubmenuSetting(
87 R.string.preferences_system, 113 titleId = R.string.preferences_system,
88 R.string.preferences_system_description, 114 descriptionId = R.string.preferences_system_description,
89 R.drawable.ic_system_settings, 115 iconId = R.drawable.ic_system_settings,
90 Settings.MenuTag.SECTION_SYSTEM 116 menuKey = MenuTag.SECTION_SYSTEM
91 ) 117 )
92 ) 118 )
93 add( 119 add(
94 SubmenuSetting( 120 SubmenuSetting(
95 R.string.preferences_graphics, 121 titleId = R.string.preferences_graphics,
96 R.string.preferences_graphics_description, 122 descriptionId = R.string.preferences_graphics_description,
97 R.drawable.ic_graphics, 123 iconId = R.drawable.ic_graphics,
98 Settings.MenuTag.SECTION_RENDERER 124 menuKey = MenuTag.SECTION_RENDERER
99 ) 125 )
100 ) 126 )
101 add( 127 add(
102 SubmenuSetting( 128 SubmenuSetting(
103 R.string.preferences_audio, 129 titleId = R.string.preferences_audio,
104 R.string.preferences_audio_description, 130 descriptionId = R.string.preferences_audio_description,
105 R.drawable.ic_audio, 131 iconId = R.drawable.ic_audio,
106 Settings.MenuTag.SECTION_AUDIO 132 menuKey = MenuTag.SECTION_AUDIO
107 ) 133 )
108 ) 134 )
109 add( 135 add(
110 SubmenuSetting( 136 SubmenuSetting(
111 R.string.preferences_debug, 137 titleId = R.string.preferences_debug,
112 R.string.preferences_debug_description, 138 descriptionId = R.string.preferences_debug_description,
113 R.drawable.ic_code, 139 iconId = R.drawable.ic_code,
114 Settings.MenuTag.SECTION_DEBUG 140 menuKey = MenuTag.SECTION_DEBUG
115 ) 141 )
116 ) 142 )
117 add( 143 add(
118 RunnableSetting( 144 RunnableSetting(
119 R.string.reset_to_default, 145 titleId = R.string.reset_to_default,
120 R.string.reset_to_default_description, 146 descriptionId = R.string.reset_to_default_description,
121 false, 147 isRunnable = !NativeLibrary.isRunning(),
122 R.drawable.ic_restore 148 iconId = R.drawable.ic_restore
123 ) { settingsViewModel.setShouldShowResetSettingsDialog(true) } 149 ) { settingsViewModel.setShouldShowResetSettingsDialog(true) }
124 ) 150 )
125 } 151 }
@@ -164,6 +190,671 @@ class SettingsFragmentPresenter(
164 } 190 }
165 } 191 }
166 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
167 private fun addThemeSettings(sl: ArrayList<SettingsItem>) { 858 private fun addThemeSettings(sl: ArrayList<SettingsItem>) {
168 sl.apply { 859 sl.apply {
169 val theme: AbstractIntSetting = object : AbstractIntSetting { 860 val theme: AbstractIntSetting = object : AbstractIntSetting {
@@ -186,20 +877,18 @@ class SettingsFragmentPresenter(
186 add( 877 add(
187 SingleChoiceSetting( 878 SingleChoiceSetting(
188 theme, 879 theme,
189 R.string.change_app_theme, 880 titleId = R.string.change_app_theme,
190 0, 881 choicesId = R.array.themeEntriesA12,
191 R.array.themeEntriesA12, 882 valuesId = R.array.themeValuesA12
192 R.array.themeValuesA12
193 ) 883 )
194 ) 884 )
195 } else { 885 } else {
196 add( 886 add(
197 SingleChoiceSetting( 887 SingleChoiceSetting(
198 theme, 888 theme,
199 R.string.change_app_theme, 889 titleId = R.string.change_app_theme,
200 0, 890 choicesId = R.array.themeEntries,
201 R.array.themeEntries, 891 valuesId = R.array.themeValues
202 R.array.themeValues
203 ) 892 )
204 ) 893 )
205 } 894 }
@@ -228,10 +917,9 @@ class SettingsFragmentPresenter(
228 add( 917 add(
229 SingleChoiceSetting( 918 SingleChoiceSetting(
230 themeMode, 919 themeMode,
231 R.string.change_theme_mode, 920 titleId = R.string.change_theme_mode,
232 0, 921 choicesId = R.array.themeModeEntries,
233 R.array.themeModeEntries, 922 valuesId = R.array.themeModeValues
234 R.array.themeModeValues
235 ) 923 )
236 ) 924 )
237 925
@@ -262,8 +950,8 @@ class SettingsFragmentPresenter(
262 add( 950 add(
263 SwitchSetting( 951 SwitchSetting(
264 blackBackgrounds, 952 blackBackgrounds,
265 R.string.use_black_backgrounds, 953 titleId = R.string.use_black_backgrounds,
266 R.string.use_black_backgrounds_description 954 descriptionId = R.string.use_black_backgrounds_description
267 ) 955 )
268 ) 956 )
269 } 957 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsSearchFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsSearchFragment.kt
new file mode 100755
index 000000000..ed60cf34f
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsSearchFragment.kt
@@ -0,0 +1,183 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.ui
5
6import android.content.Context
7import android.os.Bundle
8import android.view.LayoutInflater
9import android.view.View
10import android.view.ViewGroup
11import android.view.inputmethod.InputMethodManager
12import androidx.core.view.ViewCompat
13import androidx.core.view.WindowInsetsCompat
14import androidx.core.view.updatePadding
15import androidx.core.widget.doOnTextChanged
16import androidx.fragment.app.Fragment
17import androidx.fragment.app.activityViewModels
18import androidx.recyclerview.widget.LinearLayoutManager
19import com.google.android.material.divider.MaterialDividerItemDecoration
20import com.google.android.material.transition.MaterialSharedAxis
21import info.debatty.java.stringsimilarity.Cosine
22import org.yuzu.yuzu_emu.R
23import org.yuzu.yuzu_emu.databinding.FragmentSettingsSearchBinding
24import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
25import org.yuzu.yuzu_emu.utils.NativeConfig
26import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
27import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
28import org.yuzu.yuzu_emu.utils.collect
29
30class SettingsSearchFragment : Fragment() {
31 private var _binding: FragmentSettingsSearchBinding? = null
32 private val binding get() = _binding!!
33
34 private var settingsAdapter: SettingsAdapter? = null
35
36 private val settingsViewModel: SettingsViewModel by activityViewModels()
37
38 override fun onCreate(savedInstanceState: Bundle?) {
39 super.onCreate(savedInstanceState)
40 enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
41 returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
42 reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
43 exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
44 }
45
46 override fun onCreateView(
47 inflater: LayoutInflater,
48 container: ViewGroup?,
49 savedInstanceState: Bundle?
50 ): View {
51 _binding = FragmentSettingsSearchBinding.inflate(layoutInflater)
52 return binding.root
53 }
54
55 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
56 super.onViewCreated(view, savedInstanceState)
57
58 if (savedInstanceState != null) {
59 binding.searchText.setText(savedInstanceState.getString(SEARCH_TEXT))
60 }
61
62 settingsAdapter = SettingsAdapter(this, requireContext())
63
64 val dividerDecoration = MaterialDividerItemDecoration(
65 requireContext(),
66 LinearLayoutManager.VERTICAL
67 )
68 dividerDecoration.isLastItemDecorated = false
69 binding.settingsList.apply {
70 adapter = settingsAdapter
71 layoutManager = LinearLayoutManager(requireContext())
72 addItemDecoration(dividerDecoration)
73 }
74
75 focusSearch()
76
77 binding.backButton.setOnClickListener { settingsViewModel.setShouldNavigateBack(true) }
78 binding.searchBackground.setOnClickListener { focusSearch() }
79 binding.clearButton.setOnClickListener { binding.searchText.setText("") }
80 binding.searchText.doOnTextChanged { _, _, _, _ ->
81 search()
82 binding.settingsList.smoothScrollToPosition(0)
83 }
84 settingsViewModel.shouldReloadSettingsList.collect(viewLifecycleOwner) {
85 if (it) {
86 settingsViewModel.setShouldReloadSettingsList(false)
87 search()
88 }
89 }
90
91 search()
92
93 setInsets()
94 }
95
96 override fun onSaveInstanceState(outState: Bundle) {
97 super.onSaveInstanceState(outState)
98 outState.putString(SEARCH_TEXT, binding.searchText.text.toString())
99 }
100
101 private fun search() {
102 val searchTerm = binding.searchText.text.toString().lowercase()
103 binding.clearButton.setVisible(visible = searchTerm.isNotEmpty(), gone = false)
104 if (searchTerm.isEmpty()) {
105 binding.noResultsView.setVisible(visible = false, gone = false)
106 settingsAdapter?.submitList(emptyList())
107 return
108 }
109
110 val baseList = SettingsItem.settingsItems
111 val similarityAlgorithm = if (searchTerm.length > 2) Cosine() else Cosine(1)
112 val sortedList: List<SettingsItem> = baseList.mapNotNull { item ->
113 val title = item.value.title.lowercase()
114 val similarity = similarityAlgorithm.similarity(searchTerm, title)
115 if (similarity > 0.08) {
116 Pair(similarity, item)
117 } else {
118 null
119 }
120 }.sortedByDescending { it.first }.mapNotNull {
121 val item = it.second.value
122 val pairedSettingKey = item.setting.pairedSettingKey
123 val optionalSetting: SettingsItem? = if (pairedSettingKey.isNotEmpty()) {
124 val pairedSettingValue = NativeConfig.getBoolean(pairedSettingKey, false)
125 if (pairedSettingValue) it.second.value else null
126 } else {
127 it.second.value
128 }
129 optionalSetting
130 }
131 settingsAdapter?.submitList(sortedList)
132 binding.noResultsView.setVisible(visible = sortedList.isEmpty(), gone = false)
133 }
134
135 private fun focusSearch() {
136 binding.searchText.requestFocus()
137 val imm = requireActivity()
138 .getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
139 imm?.showSoftInput(binding.searchText, InputMethodManager.SHOW_IMPLICIT)
140 }
141
142 private fun setInsets() =
143 ViewCompat.setOnApplyWindowInsetsListener(
144 binding.root
145 ) { _: View, windowInsets: WindowInsetsCompat ->
146 val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_med)
147 val sideMargin = resources.getDimensionPixelSize(R.dimen.spacing_medlarge)
148 val topMargin = resources.getDimensionPixelSize(R.dimen.spacing_chip)
149
150 val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
151 val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
152
153 val leftInsets = barInsets.left + cutoutInsets.left
154 val rightInsets = barInsets.right + cutoutInsets.right
155
156 binding.settingsList.updatePadding(bottom = barInsets.bottom + extraListSpacing)
157 binding.frameSearch.updatePadding(
158 left = leftInsets + sideMargin,
159 top = barInsets.top + topMargin,
160 right = rightInsets + sideMargin
161 )
162 binding.noResultsView.updatePadding(
163 left = leftInsets,
164 right = rightInsets,
165 bottom = barInsets.bottom
166 )
167
168 binding.settingsList.updateMargins(
169 left = leftInsets + sideMargin,
170 right = rightInsets + sideMargin
171 )
172 binding.divider.updateMargins(
173 left = leftInsets + sideMargin,
174 right = rightInsets + sideMargin
175 )
176
177 windowInsets
178 }
179
180 companion object {
181 const val SEARCH_TEXT = "SearchText"
182 }
183}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsViewModel.kt
new file mode 100755
index 000000000..fbdca04e9
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsViewModel.kt
@@ -0,0 +1,112 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.ui
5
6import androidx.lifecycle.ViewModel
7import kotlinx.coroutines.flow.MutableStateFlow
8import kotlinx.coroutines.flow.StateFlow
9import kotlinx.coroutines.flow.asStateFlow
10import org.yuzu.yuzu_emu.R
11import org.yuzu.yuzu_emu.YuzuApplication
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
16
17class SettingsViewModel : ViewModel() {
18 var game: Game? = null
19
20 var clickedItem: SettingsItem? = null
21
22 var currentDevice = 0
23
24 val shouldRecreate: StateFlow<Boolean> get() = _shouldRecreate
25 private val _shouldRecreate = MutableStateFlow(false)
26
27 val shouldNavigateBack: StateFlow<Boolean> get() = _shouldNavigateBack
28 private val _shouldNavigateBack = MutableStateFlow(false)
29
30 val shouldShowResetSettingsDialog: StateFlow<Boolean> get() = _shouldShowResetSettingsDialog
31 private val _shouldShowResetSettingsDialog = MutableStateFlow(false)
32
33 val shouldReloadSettingsList: StateFlow<Boolean> get() = _shouldReloadSettingsList
34 private val _shouldReloadSettingsList = MutableStateFlow(false)
35
36 val sliderProgress: StateFlow<Int> get() = _sliderProgress
37 private val _sliderProgress = MutableStateFlow(-1)
38
39 val sliderTextValue: StateFlow<String> get() = _sliderTextValue
40 private val _sliderTextValue = MutableStateFlow("")
41
42 val adapterItemChanged: StateFlow<Int> get() = _adapterItemChanged
43 private val _adapterItemChanged = MutableStateFlow(-1)
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
57 fun setShouldRecreate(value: Boolean) {
58 _shouldRecreate.value = value
59 }
60
61 fun setShouldNavigateBack(value: Boolean) {
62 _shouldNavigateBack.value = value
63 }
64
65 fun setShouldShowResetSettingsDialog(value: Boolean) {
66 _shouldShowResetSettingsDialog.value = value
67 }
68
69 fun setShouldReloadSettingsList(value: Boolean) {
70 _shouldReloadSettingsList.value = value
71 }
72
73 fun setSliderTextValue(value: Float, units: String) {
74 _sliderProgress.value = value.toInt()
75 _sliderTextValue.value = String.format(
76 YuzuApplication.appContext.getString(R.string.value_with_units),
77 value.toInt().toString(),
78 units
79 )
80 }
81
82 fun setSliderProgress(value: Float) {
83 _sliderProgress.value = value.toInt()
84 }
85
86 fun setAdapterItemChanged(value: Int) {
87 _adapterItemChanged.value = value
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 }
112}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt
index 5ad0899dd..367db7fd2 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt
@@ -14,6 +14,7 @@ import org.yuzu.yuzu_emu.features.settings.model.view.DateTimeSetting
14import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem 14import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
15import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter 15import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
16import org.yuzu.yuzu_emu.utils.NativeConfig 16import org.yuzu.yuzu_emu.utils.NativeConfig
17import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
17 18
18class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : 19class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
19 SettingViewHolder(binding.root, adapter) { 20 SettingViewHolder(binding.root, adapter) {
@@ -21,28 +22,19 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
21 22
22 override fun bind(item: SettingsItem) { 23 override fun bind(item: SettingsItem) {
23 setting = item as DateTimeSetting 24 setting = item as DateTimeSetting
24 binding.textSettingName.setText(item.nameId) 25 binding.textSettingName.text = item.title
25 if (item.descriptionId != 0) { 26 binding.textSettingDescription.setVisible(item.description.isNotEmpty())
26 binding.textSettingDescription.setText(item.descriptionId) 27 binding.textSettingDescription.text = item.description
27 binding.textSettingDescription.visibility = View.VISIBLE 28 binding.textSettingValue.setVisible(true)
28 } else {
29 binding.textSettingDescription.visibility = View.GONE
30 }
31
32 binding.textSettingValue.visibility = View.VISIBLE
33 val epochTime = setting.getValue() 29 val epochTime = setting.getValue()
34 val instant = Instant.ofEpochMilli(epochTime * 1000) 30 val instant = Instant.ofEpochMilli(epochTime * 1000)
35 val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC")) 31 val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC"))
36 val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) 32 val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
37 binding.textSettingValue.text = dateFormatter.format(zonedTime) 33 binding.textSettingValue.text = dateFormatter.format(zonedTime)
38 34
39 binding.buttonClear.visibility = if (setting.setting.global || 35 binding.buttonClear.setVisible(
40 !NativeConfig.isPerGameConfigLoaded() 36 !setting.setting.global || NativeConfig.isPerGameConfigLoaded()
41 ) { 37 )
42 View.GONE
43 } else {
44 View.VISIBLE
45 }
46 binding.buttonClear.setOnClickListener { 38 binding.buttonClear.setOnClickListener {
47 adapter.onClearClick(setting, bindingAdapterPosition) 39 adapter.onClearClick(setting, bindingAdapterPosition)
48 } 40 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/HeaderViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/HeaderViewHolder.kt
index f5bcf705c..0815c36e2 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/HeaderViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/HeaderViewHolder.kt
@@ -16,7 +16,7 @@ class HeaderViewHolder(val binding: ListItemSettingsHeaderBinding, adapter: Sett
16 } 16 }
17 17
18 override fun bind(item: SettingsItem) { 18 override fun bind(item: SettingsItem) {
19 binding.textHeaderName.setText(item.nameId) 19 binding.textHeaderName.text = item.title
20 } 20 }
21 21
22 override fun onClick(clicked: View) { 22 override fun onClick(clicked: View) {
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 100755
index 000000000..86af3a226
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputProfileViewHolder.kt
@@ -0,0 +1,34 @@
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
12import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
13
14class InputProfileViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
15 SettingViewHolder(binding.root, adapter) {
16 private lateinit var setting: InputProfileSetting
17
18 override fun bind(item: SettingsItem) {
19 setting = item as InputProfileSetting
20 binding.textSettingName.text = setting.title
21 binding.textSettingValue.text =
22 setting.getCurrentProfile().ifEmpty { binding.root.context.getString(R.string.not_set) }
23
24 binding.textSettingDescription.setVisible(false)
25 binding.buttonClear.setVisible(false)
26 binding.icon.setVisible(false)
27 binding.buttonClear.setVisible(false)
28 }
29
30 override fun onClick(clicked: View) =
31 adapter.onInputProfileClick(setting, bindingAdapterPosition)
32
33 override fun onLongClick(clicked: View): Boolean = false
34}
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 100755
index 000000000..9d9047804
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputViewHolder.kt
@@ -0,0 +1,60 @@
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
15import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
16
17class InputViewHolder(val binding: ListItemSettingInputBinding, adapter: SettingsAdapter) :
18 SettingViewHolder(binding.root, adapter) {
19 private lateinit var setting: InputSetting
20
21 override fun bind(item: SettingsItem) {
22 setting = item as InputSetting
23 binding.textSettingName.text = setting.title
24 binding.textSettingValue.text = setting.getSelectedValue()
25
26 when (item) {
27 is AnalogInputSetting -> {
28 val param = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
29 binding.buttonOptions.setVisible(
30 param.get("engine", "") == "analog_from_button" ||
31 param.has("axis_x") || param.has("axis_y")
32 )
33 }
34
35 is ButtonInputSetting -> {
36 val param = NativeInput.getButtonParam(item.playerIndex, item.nativeButton)
37 binding.buttonOptions.setVisible(
38 param.has("code") || param.has("button") || param.has("hat") ||
39 param.has("axis")
40 )
41 }
42
43 is ModifierInputSetting -> {
44 val params = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
45 binding.buttonOptions.setVisible(params.has("modifier"))
46 }
47 }
48
49 binding.buttonOptions.setOnClickListener(null)
50 binding.buttonOptions.setOnClickListener {
51 adapter.onInputOptionsClick(binding.buttonOptions, setting, bindingAdapterPosition)
52 }
53 }
54
55 override fun onClick(clicked: View) =
56 adapter.onInputClick(setting, bindingAdapterPosition)
57
58 override fun onLongClick(clicked: View): Boolean =
59 adapter.onLongClick(setting, bindingAdapterPosition)
60}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt
index 507184238..fc2ffb515 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt
@@ -5,11 +5,11 @@ package org.yuzu.yuzu_emu.features.settings.ui.viewholder
5 5
6import android.view.View 6import android.view.View
7import androidx.core.content.res.ResourcesCompat 7import androidx.core.content.res.ResourcesCompat
8import org.yuzu.yuzu_emu.NativeLibrary
9import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding 8import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
10import org.yuzu.yuzu_emu.features.settings.model.view.RunnableSetting 9import org.yuzu.yuzu_emu.features.settings.model.view.RunnableSetting
11import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem 10import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
12import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter 11import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
12import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
13 13
14class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : 14class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
15 SettingViewHolder(binding.root, adapter) { 15 SettingViewHolder(binding.root, adapter) {
@@ -17,34 +17,28 @@ class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
17 17
18 override fun bind(item: SettingsItem) { 18 override fun bind(item: SettingsItem) {
19 setting = item as RunnableSetting 19 setting = item as RunnableSetting
20 if (item.iconId != 0) { 20 binding.icon.setVisible(setting.iconId != 0)
21 binding.icon.visibility = View.VISIBLE 21 if (setting.iconId != 0) {
22 binding.icon.setImageDrawable( 22 binding.icon.setImageDrawable(
23 ResourcesCompat.getDrawable( 23 ResourcesCompat.getDrawable(
24 binding.icon.resources, 24 binding.icon.resources,
25 item.iconId, 25 setting.iconId,
26 binding.icon.context.theme 26 binding.icon.context.theme
27 ) 27 )
28 ) 28 )
29 } else {
30 binding.icon.visibility = View.GONE
31 } 29 }
32 30
33 binding.textSettingName.setText(item.nameId) 31 binding.textSettingName.text = setting.title
34 if (item.descriptionId != 0) { 32 binding.textSettingDescription.setVisible(setting.description.isNotEmpty())
35 binding.textSettingDescription.setText(item.descriptionId) 33 binding.textSettingDescription.text = item.description
36 binding.textSettingDescription.visibility = View.VISIBLE 34 binding.textSettingValue.setVisible(false)
37 } else { 35 binding.buttonClear.setVisible(false)
38 binding.textSettingDescription.visibility = View.GONE
39 }
40 binding.textSettingValue.visibility = View.GONE
41 binding.buttonClear.visibility = View.GONE
42 36
43 setStyle(setting.isEditable, binding) 37 setStyle(setting.isEditable, binding)
44 } 38 }
45 39
46 override fun onClick(clicked: View) { 40 override fun onClick(clicked: View) {
47 if (!setting.isRuntimeRunnable && !NativeLibrary.isRunning()) { 41 if (setting.isRunnable) {
48 setting.runnable.invoke() 42 setting.runnable.invoke()
49 } 43 }
50 } 44 }
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 02dab3785..e2fe0b072 100755
--- 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
@@ -5,11 +5,13 @@ package org.yuzu.yuzu_emu.features.settings.ui.viewholder
5 5
6import android.view.View 6import android.view.View
7import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding 7import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
8import org.yuzu.yuzu_emu.features.settings.model.view.IntSingleChoiceSetting
8import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem 9import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
9import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting 10import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting
10import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting 11import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting
11import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter 12import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
12import org.yuzu.yuzu_emu.utils.NativeConfig 13import org.yuzu.yuzu_emu.utils.NativeConfig
14import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
13 15
14class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : 16class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
15 SettingViewHolder(binding.root, adapter) { 17 SettingViewHolder(binding.root, adapter) {
@@ -17,40 +19,38 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
17 19
18 override fun bind(item: SettingsItem) { 20 override fun bind(item: SettingsItem) {
19 setting = item 21 setting = item
20 binding.textSettingName.setText(item.nameId) 22 binding.textSettingName.text = setting.title
21 if (item.descriptionId != 0) { 23 binding.textSettingDescription.setVisible(item.description.isNotEmpty())
22 binding.textSettingDescription.setText(item.descriptionId) 24 binding.textSettingDescription.text = item.description
23 binding.textSettingDescription.visibility = View.VISIBLE
24 } else {
25 binding.textSettingDescription.visibility = View.GONE
26 }
27 25
28 binding.textSettingValue.visibility = View.VISIBLE 26 binding.textSettingValue.setVisible(true)
29 if (item is SingleChoiceSetting) { 27 when (item) {
30 val resMgr = binding.textSettingValue.context.resources 28 is SingleChoiceSetting -> {
31 val values = resMgr.getIntArray(item.valuesId) 29 val resMgr = binding.textSettingValue.context.resources
32 for (i in values.indices) { 30 val values = resMgr.getIntArray(item.valuesId)
33 if (values[i] == item.getSelectedValue()) { 31 for (i in values.indices) {
34 binding.textSettingValue.text = resMgr.getStringArray(item.choicesId)[i] 32 if (values[i] == item.getSelectedValue()) {
35 break 33 binding.textSettingValue.text = resMgr.getStringArray(item.choicesId)[i]
34 break
35 }
36 } 36 }
37 } 37 }
38 } else if (item is StringSingleChoiceSetting) { 38
39 for (i in item.values.indices) { 39 is StringSingleChoiceSetting -> {
40 if (item.values[i] == item.getSelectedValue()) { 40 binding.textSettingValue.text = item.getSelectedValue()
41 binding.textSettingValue.text = item.choices[i]
42 break
43 }
44 } 41 }
45 }
46 42
47 binding.buttonClear.visibility = if (setting.setting.global || 43 is IntSingleChoiceSetting -> {
48 !NativeConfig.isPerGameConfigLoaded() 44 binding.textSettingValue.text = item.getChoiceAt(item.getSelectedValue())
49 ) { 45 }
50 View.GONE 46 }
51 } else { 47 if (binding.textSettingValue.text.isEmpty()) {
52 View.VISIBLE 48 binding.textSettingValue.setVisible(false)
53 } 49 }
50
51 binding.buttonClear.setVisible(
52 !setting.setting.global || NativeConfig.isPerGameConfigLoaded()
53 )
54 binding.buttonClear.setOnClickListener { 54 binding.buttonClear.setOnClickListener {
55 adapter.onClearClick(setting, bindingAdapterPosition) 55 adapter.onClearClick(setting, bindingAdapterPosition)
56 } 56 }
@@ -63,16 +63,25 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
63 return 63 return
64 } 64 }
65 65
66 if (setting is SingleChoiceSetting) { 66 when (setting) {
67 adapter.onSingleChoiceClick( 67 is SingleChoiceSetting -> adapter.onSingleChoiceClick(
68 (setting as SingleChoiceSetting), 68 setting as SingleChoiceSetting,
69 bindingAdapterPosition
70 )
71 } else if (setting is StringSingleChoiceSetting) {
72 adapter.onStringSingleChoiceClick(
73 (setting as StringSingleChoiceSetting),
74 bindingAdapterPosition 69 bindingAdapterPosition
75 ) 70 )
71
72 is StringSingleChoiceSetting -> {
73 adapter.onStringSingleChoiceClick(
74 setting as StringSingleChoiceSetting,
75 bindingAdapterPosition
76 )
77 }
78
79 is IntSingleChoiceSetting -> {
80 adapter.onIntSingleChoiceClick(
81 setting as IntSingleChoiceSetting,
82 bindingAdapterPosition
83 )
84 }
76 } 85 }
77 } 86 }
78 87
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt
index 596c18012..a37b59b44 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt
@@ -10,6 +10,7 @@ import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
10import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting 10import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting
11import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter 11import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
12import org.yuzu.yuzu_emu.utils.NativeConfig 12import org.yuzu.yuzu_emu.utils.NativeConfig
13import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
13 14
14class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : 15class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
15 SettingViewHolder(binding.root, adapter) { 16 SettingViewHolder(binding.root, adapter) {
@@ -17,27 +18,19 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda
17 18
18 override fun bind(item: SettingsItem) { 19 override fun bind(item: SettingsItem) {
19 setting = item as SliderSetting 20 setting = item as SliderSetting
20 binding.textSettingName.setText(item.nameId) 21 binding.textSettingName.text = setting.title
21 if (item.descriptionId != 0) { 22 binding.textSettingDescription.setVisible(item.description.isNotEmpty())
22 binding.textSettingDescription.setText(item.descriptionId) 23 binding.textSettingDescription.text = setting.description
23 binding.textSettingDescription.visibility = View.VISIBLE 24 binding.textSettingValue.setVisible(true)
24 } else {
25 binding.textSettingDescription.visibility = View.GONE
26 }
27 binding.textSettingValue.visibility = View.VISIBLE
28 binding.textSettingValue.text = String.format( 25 binding.textSettingValue.text = String.format(
29 binding.textSettingValue.context.getString(R.string.value_with_units), 26 binding.textSettingValue.context.getString(R.string.value_with_units),
30 setting.getSelectedValue(), 27 setting.getSelectedValue(),
31 setting.units 28 setting.units
32 ) 29 )
33 30
34 binding.buttonClear.visibility = if (setting.setting.global || 31 binding.buttonClear.setVisible(
35 !NativeConfig.isPerGameConfigLoaded() 32 !setting.setting.global || NativeConfig.isPerGameConfigLoaded()
36 ) { 33 )
37 View.GONE
38 } else {
39 View.VISIBLE
40 }
41 binding.buttonClear.setOnClickListener { 34 binding.buttonClear.setOnClickListener {
42 adapter.onClearClick(setting, bindingAdapterPosition) 35 adapter.onClearClick(setting, bindingAdapterPosition)
43 } 36 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt
index 20d35a17d..f7a9c08c3 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt
@@ -9,39 +9,34 @@ import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
9import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem 9import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
10import org.yuzu.yuzu_emu.features.settings.model.view.SubmenuSetting 10import org.yuzu.yuzu_emu.features.settings.model.view.SubmenuSetting
11import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter 11import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
12import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
12 13
13class SubmenuViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : 14class SubmenuViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
14 SettingViewHolder(binding.root, adapter) { 15 SettingViewHolder(binding.root, adapter) {
15 private lateinit var item: SubmenuSetting 16 private lateinit var setting: SubmenuSetting
16 17
17 override fun bind(item: SettingsItem) { 18 override fun bind(item: SettingsItem) {
18 this.item = item as SubmenuSetting 19 setting = item as SubmenuSetting
19 if (item.iconId != 0) { 20 binding.icon.setVisible(setting.iconId != 0)
20 binding.icon.visibility = View.VISIBLE 21 if (setting.iconId != 0) {
21 binding.icon.setImageDrawable( 22 binding.icon.setImageDrawable(
22 ResourcesCompat.getDrawable( 23 ResourcesCompat.getDrawable(
23 binding.icon.resources, 24 binding.icon.resources,
24 item.iconId, 25 setting.iconId,
25 binding.icon.context.theme 26 binding.icon.context.theme
26 ) 27 )
27 ) 28 )
28 } else {
29 binding.icon.visibility = View.GONE
30 } 29 }
31 30
32 binding.textSettingName.setText(item.nameId) 31 binding.textSettingName.text = setting.title
33 if (item.descriptionId != 0) { 32 binding.textSettingDescription.setVisible(setting.description.isNotEmpty())
34 binding.textSettingDescription.setText(item.descriptionId) 33 binding.textSettingDescription.text = setting.description
35 binding.textSettingDescription.visibility = View.VISIBLE 34 binding.textSettingValue.setVisible(false)
36 } else { 35 binding.buttonClear.setVisible(false)
37 binding.textSettingDescription.visibility = View.GONE
38 }
39 binding.textSettingValue.visibility = View.GONE
40 binding.buttonClear.visibility = View.GONE
41 } 36 }
42 37
43 override fun onClick(clicked: View) { 38 override fun onClick(clicked: View) {
44 adapter.onSubmenuClick(item) 39 adapter.onSubmenuClick(setting)
45 } 40 }
46 41
47 override fun onLongClick(clicked: View): Boolean { 42 override fun onLongClick(clicked: View): Boolean {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt
index d26bf9374..53f7b301f 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt
@@ -10,6 +10,7 @@ import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
10import org.yuzu.yuzu_emu.features.settings.model.view.SwitchSetting 10import org.yuzu.yuzu_emu.features.settings.model.view.SwitchSetting
11import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter 11import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
12import org.yuzu.yuzu_emu.utils.NativeConfig 12import org.yuzu.yuzu_emu.utils.NativeConfig
13import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
13 14
14class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter: SettingsAdapter) : 15class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter: SettingsAdapter) :
15 SettingViewHolder(binding.root, adapter) { 16 SettingViewHolder(binding.root, adapter) {
@@ -18,28 +19,19 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter
18 19
19 override fun bind(item: SettingsItem) { 20 override fun bind(item: SettingsItem) {
20 setting = item as SwitchSetting 21 setting = item as SwitchSetting
21 binding.textSettingName.setText(item.nameId) 22 binding.textSettingName.text = setting.title
22 if (item.descriptionId != 0) { 23 binding.textSettingDescription.setVisible(setting.description.isNotEmpty())
23 binding.textSettingDescription.setText(item.descriptionId) 24 binding.textSettingDescription.text = setting.description
24 binding.textSettingDescription.visibility = View.VISIBLE
25 } else {
26 binding.textSettingDescription.text = ""
27 binding.textSettingDescription.visibility = View.GONE
28 }
29 25
30 binding.switchWidget.setOnCheckedChangeListener(null) 26 binding.switchWidget.setOnCheckedChangeListener(null)
31 binding.switchWidget.isChecked = setting.getIsChecked(setting.needsRuntimeGlobal) 27 binding.switchWidget.isChecked = setting.getIsChecked(setting.needsRuntimeGlobal)
32 binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean -> 28 binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean ->
33 adapter.onBooleanClick(item, binding.switchWidget.isChecked, bindingAdapterPosition) 29 adapter.onBooleanClick(setting, binding.switchWidget.isChecked, bindingAdapterPosition)
34 } 30 }
35 31
36 binding.buttonClear.visibility = if (setting.setting.global || 32 binding.buttonClear.setVisible(
37 !NativeConfig.isPerGameConfigLoaded() 33 !setting.setting.global || NativeConfig.isPerGameConfigLoaded()
38 ) { 34 )
39 View.GONE
40 } else {
41 View.VISIBLE
42 }
43 binding.buttonClear.setOnClickListener { 35 binding.buttonClear.setOnClickListener {
44 adapter.onClearClick(setting, bindingAdapterPosition) 36 adapter.onClearClick(setting, bindingAdapterPosition)
45 } 37 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt
index 872553ac4..110aa2960 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt
@@ -3,7 +3,6 @@
3 3
4package org.yuzu.yuzu_emu.fragments 4package org.yuzu.yuzu_emu.fragments
5 5
6import android.annotation.SuppressLint
7import android.content.Intent 6import android.content.Intent
8import android.os.Bundle 7import android.os.Bundle
9import android.view.LayoutInflater 8import android.view.LayoutInflater
@@ -16,9 +15,6 @@ import androidx.core.view.updatePadding
16import androidx.documentfile.provider.DocumentFile 15import androidx.documentfile.provider.DocumentFile
17import androidx.fragment.app.Fragment 16import androidx.fragment.app.Fragment
18import androidx.fragment.app.activityViewModels 17import androidx.fragment.app.activityViewModels
19import androidx.lifecycle.Lifecycle
20import androidx.lifecycle.lifecycleScope
21import androidx.lifecycle.repeatOnLifecycle
22import androidx.navigation.findNavController 18import androidx.navigation.findNavController
23import androidx.navigation.fragment.navArgs 19import androidx.navigation.fragment.navArgs
24import androidx.recyclerview.widget.LinearLayoutManager 20import androidx.recyclerview.widget.LinearLayoutManager
@@ -32,6 +28,7 @@ import org.yuzu.yuzu_emu.model.HomeViewModel
32import org.yuzu.yuzu_emu.utils.AddonUtil 28import org.yuzu.yuzu_emu.utils.AddonUtil
33import org.yuzu.yuzu_emu.utils.FileUtil.copyFilesTo 29import org.yuzu.yuzu_emu.utils.FileUtil.copyFilesTo
34import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins 30import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
31import org.yuzu.yuzu_emu.utils.collect
35import java.io.File 32import java.io.File
36 33
37class AddonsFragment : Fragment() { 34class AddonsFragment : Fragment() {
@@ -60,8 +57,6 @@ class AddonsFragment : Fragment() {
60 return binding.root 57 return binding.root
61 } 58 }
62 59
63 // This is using the correct scope, lint is just acting up
64 @SuppressLint("UnsafeRepeatOnLifecycleDetector")
65 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 60 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
66 super.onViewCreated(view, savedInstanceState) 61 super.onViewCreated(view, savedInstanceState)
67 homeViewModel.setNavigationVisibility(visible = false, animated = false) 62 homeViewModel.setNavigationVisibility(visible = false, animated = false)
@@ -78,57 +73,41 @@ class AddonsFragment : Fragment() {
78 adapter = AddonAdapter(addonViewModel) 73 adapter = AddonAdapter(addonViewModel)
79 } 74 }
80 75
81 viewLifecycleOwner.lifecycleScope.apply { 76 addonViewModel.addonList.collect(viewLifecycleOwner) {
82 launch { 77 (binding.listAddons.adapter as AddonAdapter).submitList(it)
83 repeatOnLifecycle(Lifecycle.State.STARTED) { 78 }
84 addonViewModel.addonList.collect { 79 addonViewModel.showModInstallPicker.collect(
85 (binding.listAddons.adapter as AddonAdapter).submitList(it) 80 viewLifecycleOwner,
86 } 81 resetState = { addonViewModel.showModInstallPicker(false) }
87 } 82 ) { if (it) installAddon.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) }
88 } 83 addonViewModel.showModNoticeDialog.collect(
89 launch { 84 viewLifecycleOwner,
90 repeatOnLifecycle(Lifecycle.State.STARTED) { 85 resetState = { addonViewModel.showModNoticeDialog(false) }
91 addonViewModel.showModInstallPicker.collect { 86 ) {
92 if (it) { 87 if (it) {
93 installAddon.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) 88 MessageDialogFragment.newInstance(
94 addonViewModel.showModInstallPicker(false) 89 requireActivity(),
95 } 90 titleId = R.string.addon_notice,
96 } 91 descriptionId = R.string.addon_notice_description,
97 } 92 dismissible = false,
98 } 93 positiveAction = { addonViewModel.showModInstallPicker(true) },
99 launch { 94 negativeAction = {},
100 repeatOnLifecycle(Lifecycle.State.STARTED) { 95 negativeButtonTitleId = R.string.close
101 addonViewModel.showModNoticeDialog.collect { 96 ).show(parentFragmentManager, MessageDialogFragment.TAG)
102 if (it) {
103 MessageDialogFragment.newInstance(
104 requireActivity(),
105 titleId = R.string.addon_notice,
106 descriptionId = R.string.addon_notice_description,
107 dismissible = false,
108 positiveAction = { addonViewModel.showModInstallPicker(true) },
109 negativeAction = {},
110 negativeButtonTitleId = R.string.close
111 ).show(parentFragmentManager, MessageDialogFragment.TAG)
112 addonViewModel.showModNoticeDialog(false)
113 }
114 }
115 }
116 } 97 }
117 launch { 98 }
118 repeatOnLifecycle(Lifecycle.State.STARTED) { 99 addonViewModel.addonToDelete.collect(
119 addonViewModel.addonToDelete.collect { 100 viewLifecycleOwner,
120 if (it != null) { 101 resetState = { addonViewModel.setAddonToDelete(null) }
121 MessageDialogFragment.newInstance( 102 ) {
122 requireActivity(), 103 if (it != null) {
123 titleId = R.string.confirm_uninstall, 104 MessageDialogFragment.newInstance(
124 descriptionId = R.string.confirm_uninstall_description, 105 requireActivity(),
125 positiveAction = { addonViewModel.onDeleteAddon(it) }, 106 titleId = R.string.confirm_uninstall,
126 negativeAction = {} 107 descriptionId = R.string.confirm_uninstall_description,
127 ).show(parentFragmentManager, MessageDialogFragment.TAG) 108 positiveAction = { addonViewModel.onDeleteAddon(it) },
128 addonViewModel.setAddonToDelete(null) 109 negativeAction = {}
129 } 110 ).show(parentFragmentManager, MessageDialogFragment.TAG)
130 }
131 }
132 } 111 }
133 } 112 }
134 113
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/CoreErrorDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/CoreErrorDialogFragment.kt
new file mode 100755
index 000000000..299f8da71
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/CoreErrorDialogFragment.kt
@@ -0,0 +1,47 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.fragments
5
6import android.app.Dialog
7import android.content.DialogInterface
8import android.os.Bundle
9import androidx.fragment.app.DialogFragment
10import com.google.android.material.dialog.MaterialAlertDialogBuilder
11import org.yuzu.yuzu_emu.NativeLibrary
12import org.yuzu.yuzu_emu.R
13
14class CoreErrorDialogFragment : DialogFragment() {
15 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
16 MaterialAlertDialogBuilder(requireActivity())
17 .setTitle(requireArguments().getString(TITLE))
18 .setMessage(requireArguments().getString(MESSAGE))
19 .setPositiveButton(R.string.continue_button, null)
20 .setNegativeButton(R.string.abort_button) { _: DialogInterface?, _: Int ->
21 NativeLibrary.coreErrorAlertResult = false
22 synchronized(NativeLibrary.coreErrorAlertLock) {
23 NativeLibrary.coreErrorAlertLock.notify()
24 }
25 }
26 .create()
27
28 override fun onDismiss(dialog: DialogInterface) {
29 super.onDismiss(dialog)
30 NativeLibrary.coreErrorAlertResult = true
31 synchronized(NativeLibrary.coreErrorAlertLock) { NativeLibrary.coreErrorAlertLock.notify() }
32 }
33
34 companion object {
35 const val TITLE = "Title"
36 const val MESSAGE = "Message"
37
38 fun newInstance(title: String, message: String): CoreErrorDialogFragment {
39 val frag = CoreErrorDialogFragment()
40 val args = Bundle()
41 args.putString(TITLE, title)
42 args.putString(MESSAGE, message)
43 frag.arguments = args
44 return frag
45 }
46 }
47}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt
index 41cff46c1..8b23a1021 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt
@@ -3,7 +3,6 @@
3 3
4package org.yuzu.yuzu_emu.fragments 4package org.yuzu.yuzu_emu.fragments
5 5
6import android.annotation.SuppressLint
7import android.os.Bundle 6import android.os.Bundle
8import android.view.LayoutInflater 7import android.view.LayoutInflater
9import android.view.View 8import android.view.View
@@ -14,9 +13,6 @@ import androidx.core.view.WindowInsetsCompat
14import androidx.core.view.updatePadding 13import androidx.core.view.updatePadding
15import androidx.fragment.app.Fragment 14import androidx.fragment.app.Fragment
16import androidx.fragment.app.activityViewModels 15import androidx.fragment.app.activityViewModels
17import androidx.lifecycle.Lifecycle
18import androidx.lifecycle.lifecycleScope
19import androidx.lifecycle.repeatOnLifecycle
20import androidx.navigation.findNavController 16import androidx.navigation.findNavController
21import androidx.navigation.fragment.navArgs 17import androidx.navigation.fragment.navArgs
22import androidx.recyclerview.widget.GridLayoutManager 18import androidx.recyclerview.widget.GridLayoutManager
@@ -35,6 +31,7 @@ import org.yuzu.yuzu_emu.utils.FileUtil
35import org.yuzu.yuzu_emu.utils.GpuDriverHelper 31import org.yuzu.yuzu_emu.utils.GpuDriverHelper
36import org.yuzu.yuzu_emu.utils.NativeConfig 32import org.yuzu.yuzu_emu.utils.NativeConfig
37import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins 33import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
34import org.yuzu.yuzu_emu.utils.collect
38import java.io.File 35import java.io.File
39import java.io.IOException 36import java.io.IOException
40 37
@@ -63,8 +60,6 @@ class DriverManagerFragment : Fragment() {
63 return binding.root 60 return binding.root
64 } 61 }
65 62
66 // This is using the correct scope, lint is just acting up
67 @SuppressLint("UnsafeRepeatOnLifecycleDetector")
68 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 63 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
69 super.onViewCreated(view, savedInstanceState) 64 super.onViewCreated(view, savedInstanceState)
70 homeViewModel.setNavigationVisibility(visible = false, animated = true) 65 homeViewModel.setNavigationVisibility(visible = false, animated = true)
@@ -89,15 +84,8 @@ class DriverManagerFragment : Fragment() {
89 } 84 }
90 } 85 }
91 86
92 viewLifecycleOwner.lifecycleScope.apply { 87 driverViewModel.showClearButton.collect(viewLifecycleOwner) {
93 launch { 88 binding.toolbarDrivers.menu.findItem(R.id.menu_driver_use_global).isVisible = it
94 repeatOnLifecycle(Lifecycle.State.STARTED) {
95 driverViewModel.showClearButton.collect {
96 binding.toolbarDrivers.menu
97 .findItem(R.id.menu_driver_use_global).isVisible = it
98 }
99 }
100 }
101 } 89 }
102 } 90 }
103 91
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt
index 6a47b29f0..bad56e434 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt
@@ -10,14 +10,11 @@ import android.view.View
10import android.view.ViewGroup 10import android.view.ViewGroup
11import androidx.fragment.app.DialogFragment 11import androidx.fragment.app.DialogFragment
12import androidx.fragment.app.activityViewModels 12import androidx.fragment.app.activityViewModels
13import androidx.lifecycle.Lifecycle
14import androidx.lifecycle.lifecycleScope
15import androidx.lifecycle.repeatOnLifecycle
16import com.google.android.material.dialog.MaterialAlertDialogBuilder 13import com.google.android.material.dialog.MaterialAlertDialogBuilder
17import kotlinx.coroutines.launch
18import org.yuzu.yuzu_emu.R 14import org.yuzu.yuzu_emu.R
19import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding 15import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
20import org.yuzu.yuzu_emu.model.DriverViewModel 16import org.yuzu.yuzu_emu.model.DriverViewModel
17import org.yuzu.yuzu_emu.utils.collect
21 18
22class DriversLoadingDialogFragment : DialogFragment() { 19class DriversLoadingDialogFragment : DialogFragment() {
23 private val driverViewModel: DriverViewModel by activityViewModels() 20 private val driverViewModel: DriverViewModel by activityViewModels()
@@ -44,13 +41,7 @@ class DriversLoadingDialogFragment : DialogFragment() {
44 41
45 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 42 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
46 super.onViewCreated(view, savedInstanceState) 43 super.onViewCreated(view, savedInstanceState)
47 viewLifecycleOwner.lifecycleScope.apply { 44 driverViewModel.isInteractionAllowed.collect(viewLifecycleOwner) { if (it) dismiss() }
48 launch {
49 repeatOnLifecycle(Lifecycle.State.RESUMED) {
50 driverViewModel.isInteractionAllowed.collect { if (it) dismiss() }
51 }
52 }
53 }
54 } 45 }
55 46
56 companion object { 47 companion object {
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..c3b2b11f8 100755
--- 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
@@ -32,9 +32,6 @@ import androidx.drawerlayout.widget.DrawerLayout
32import androidx.drawerlayout.widget.DrawerLayout.DrawerListener 32import androidx.drawerlayout.widget.DrawerLayout.DrawerListener
33import androidx.fragment.app.Fragment 33import androidx.fragment.app.Fragment
34import androidx.fragment.app.activityViewModels 34import androidx.fragment.app.activityViewModels
35import androidx.lifecycle.Lifecycle
36import androidx.lifecycle.lifecycleScope
37import androidx.lifecycle.repeatOnLifecycle
38import androidx.navigation.findNavController 35import androidx.navigation.findNavController
39import androidx.navigation.fragment.navArgs 36import androidx.navigation.fragment.navArgs
40import androidx.window.layout.FoldingFeature 37import androidx.window.layout.FoldingFeature
@@ -42,9 +39,6 @@ import androidx.window.layout.WindowInfoTracker
42import androidx.window.layout.WindowLayoutInfo 39import androidx.window.layout.WindowLayoutInfo
43import com.google.android.material.dialog.MaterialAlertDialogBuilder 40import com.google.android.material.dialog.MaterialAlertDialogBuilder
44import com.google.android.material.slider.Slider 41import com.google.android.material.slider.Slider
45import kotlinx.coroutines.Dispatchers
46import kotlinx.coroutines.flow.collectLatest
47import kotlinx.coroutines.launch
48import org.yuzu.yuzu_emu.HomeNavigationDirections 42import org.yuzu.yuzu_emu.HomeNavigationDirections
49import org.yuzu.yuzu_emu.NativeLibrary 43import org.yuzu.yuzu_emu.NativeLibrary
50import org.yuzu.yuzu_emu.R 44import org.yuzu.yuzu_emu.R
@@ -63,6 +57,7 @@ import org.yuzu.yuzu_emu.model.EmulationViewModel
63import org.yuzu.yuzu_emu.overlay.model.OverlayControl 57import org.yuzu.yuzu_emu.overlay.model.OverlayControl
64import org.yuzu.yuzu_emu.overlay.model.OverlayLayout 58import org.yuzu.yuzu_emu.overlay.model.OverlayLayout
65import org.yuzu.yuzu_emu.utils.* 59import org.yuzu.yuzu_emu.utils.*
60import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
66import java.lang.NullPointerException 61import java.lang.NullPointerException
67 62
68class EmulationFragment : Fragment(), SurfaceHolder.Callback { 63class EmulationFragment : Fragment(), SurfaceHolder.Callback {
@@ -90,14 +85,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
90 if (context is EmulationActivity) { 85 if (context is EmulationActivity) {
91 emulationActivity = context 86 emulationActivity = context
92 NativeLibrary.setEmulationActivity(context) 87 NativeLibrary.setEmulationActivity(context)
93
94 lifecycleScope.launch(Dispatchers.Main) {
95 lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
96 WindowInfoTracker.getOrCreate(context)
97 .windowLayoutInfo(context)
98 .collect { updateFoldableLayout(context, it) }
99 }
100 }
101 } else { 88 } else {
102 throw IllegalStateException("EmulationFragment must have EmulationActivity parent") 89 throw IllegalStateException("EmulationFragment must have EmulationActivity parent")
103 } 90 }
@@ -168,8 +155,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
168 return binding.root 155 return binding.root
169 } 156 }
170 157
171 // This is using the correct scope, lint is just acting up
172 @SuppressLint("UnsafeRepeatOnLifecycleDetector")
173 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 158 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
174 super.onViewCreated(view, savedInstanceState) 159 super.onViewCreated(view, savedInstanceState)
175 if (requireActivity().isFinishing) { 160 if (requireActivity().isFinishing) {
@@ -277,6 +262,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
277 true 262 true
278 } 263 }
279 264
265 R.id.menu_controls -> {
266 val action = HomeNavigationDirections.actionGlobalSettingsActivity(
267 null,
268 Settings.MenuTag.SECTION_INPUT
269 )
270 binding.root.findNavController().navigate(action)
271 true
272 }
273
280 R.id.menu_overlay_controls -> { 274 R.id.menu_overlay_controls -> {
281 showOverlayOptions() 275 showOverlayOptions()
282 true 276 true
@@ -341,129 +335,86 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
341 binding.loadingTitle.isSelected = true 335 binding.loadingTitle.isSelected = true
342 binding.loadingText.isSelected = true 336 binding.loadingText.isSelected = true
343 337
344 viewLifecycleOwner.lifecycleScope.apply { 338 WindowInfoTracker.getOrCreate(requireContext())
345 launch { 339 .windowLayoutInfo(requireActivity()).collect(viewLifecycleOwner) {
346 repeatOnLifecycle(Lifecycle.State.STARTED) { 340 updateFoldableLayout(requireActivity() as EmulationActivity, it)
347 WindowInfoTracker.getOrCreate(requireContext())
348 .windowLayoutInfo(requireActivity())
349 .collect {
350 updateFoldableLayout(requireActivity() as EmulationActivity, it)
351 }
352 }
353 } 341 }
354 launch { 342 emulationViewModel.shaderProgress.collect(viewLifecycleOwner) {
355 repeatOnLifecycle(Lifecycle.State.CREATED) { 343 if (it > 0 && it != emulationViewModel.totalShaders.value) {
356 emulationViewModel.shaderProgress.collectLatest { 344 binding.loadingProgressIndicator.isIndeterminate = false
357 if (it > 0 && it != emulationViewModel.totalShaders.value) {
358 binding.loadingProgressIndicator.isIndeterminate = false
359
360 if (it < binding.loadingProgressIndicator.max) {
361 binding.loadingProgressIndicator.progress = it
362 }
363 }
364 345
365 if (it == emulationViewModel.totalShaders.value) { 346 if (it < binding.loadingProgressIndicator.max) {
366 binding.loadingText.setText(R.string.loading) 347 binding.loadingProgressIndicator.progress = it
367 binding.loadingProgressIndicator.isIndeterminate = true
368 }
369 }
370 } 348 }
371 } 349 }
372 launch { 350
373 repeatOnLifecycle(Lifecycle.State.CREATED) { 351 if (it == emulationViewModel.totalShaders.value) {
374 emulationViewModel.totalShaders.collectLatest { 352 binding.loadingText.setText(R.string.loading)
375 binding.loadingProgressIndicator.max = it 353 binding.loadingProgressIndicator.isIndeterminate = true
376 }
377 }
378 }
379 launch {
380 repeatOnLifecycle(Lifecycle.State.CREATED) {
381 emulationViewModel.shaderMessage.collectLatest {
382 if (it.isNotEmpty()) {
383 binding.loadingText.text = it
384 }
385 }
386 }
387 } 354 }
388 launch { 355 }
389 repeatOnLifecycle(Lifecycle.State.RESUMED) { 356 emulationViewModel.totalShaders.collect(viewLifecycleOwner) {
390 driverViewModel.isInteractionAllowed.collect { 357 binding.loadingProgressIndicator.max = it
391 if (it) { 358 }
392 startEmulation() 359 emulationViewModel.shaderMessage.collect(viewLifecycleOwner) {
393 } 360 if (it.isNotEmpty()) {
394 } 361 binding.loadingText.text = it
395 }
396 } 362 }
397 launch { 363 }
398 repeatOnLifecycle(Lifecycle.State.CREATED) { 364
399 emulationViewModel.emulationStarted.collectLatest { 365 emulationViewModel.emulationStarted.collect(viewLifecycleOwner) {
400 if (it) { 366 if (it) {
401 binding.drawerLayout.setDrawerLockMode(IntSetting.LOCK_DRAWER.getInt()) 367 binding.drawerLayout.setDrawerLockMode(IntSetting.LOCK_DRAWER.getInt())
402 ViewUtils.showView(binding.surfaceInputOverlay) 368 ViewUtils.showView(binding.surfaceInputOverlay)
403 ViewUtils.hideView(binding.loadingIndicator) 369 ViewUtils.hideView(binding.loadingIndicator)
404 370
405 emulationState.updateSurface() 371 emulationState.updateSurface()
406 372
407 // Setup overlays 373 // Setup overlays
408 updateShowFpsOverlay() 374 updateShowFpsOverlay()
409 updateThermalOverlay() 375 updateThermalOverlay()
410 }
411 }
412 }
413 } 376 }
414 launch { 377 }
415 repeatOnLifecycle(Lifecycle.State.CREATED) { 378 emulationViewModel.isEmulationStopping.collect(viewLifecycleOwner) {
416 emulationViewModel.isEmulationStopping.collectLatest { 379 if (it) {
417 if (it) { 380 binding.loadingText.setText(R.string.shutting_down)
418 binding.loadingText.setText(R.string.shutting_down) 381 ViewUtils.showView(binding.loadingIndicator)
419 ViewUtils.showView(binding.loadingIndicator) 382 ViewUtils.hideView(binding.inputContainer)
420 ViewUtils.hideView(binding.inputContainer) 383 ViewUtils.hideView(binding.showFpsText)
421 ViewUtils.hideView(binding.showFpsText)
422 }
423 }
424 }
425 } 384 }
426 launch { 385 }
427 repeatOnLifecycle(Lifecycle.State.CREATED) { 386 emulationViewModel.drawerOpen.collect(viewLifecycleOwner) {
428 emulationViewModel.drawerOpen.collect { 387 if (it) {
429 if (it) { 388 binding.drawerLayout.open()
430 binding.drawerLayout.open() 389 binding.inGameMenu.requestFocus()
431 binding.inGameMenu.requestFocus() 390 } else {
432 } else { 391 binding.drawerLayout.close()
433 binding.drawerLayout.close()
434 }
435 }
436 }
437 } 392 }
438 launch { 393 }
439 repeatOnLifecycle(Lifecycle.State.CREATED) { 394 emulationViewModel.programChanged.collect(viewLifecycleOwner) {
440 emulationViewModel.programChanged.collect { 395 if (it != 0) {
441 if (it != 0) { 396 emulationViewModel.setEmulationStarted(false)
442 emulationViewModel.setEmulationStarted(false) 397 binding.drawerLayout.close()
443 binding.drawerLayout.close() 398 binding.drawerLayout
444 binding.drawerLayout 399 .setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
445 .setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) 400 ViewUtils.hideView(binding.surfaceInputOverlay)
446 ViewUtils.hideView(binding.surfaceInputOverlay) 401 ViewUtils.showView(binding.loadingIndicator)
447 ViewUtils.showView(binding.loadingIndicator)
448 }
449 }
450 }
451 } 402 }
452 launch { 403 }
453 repeatOnLifecycle(Lifecycle.State.CREATED) { 404 emulationViewModel.emulationStopped.collect(viewLifecycleOwner) {
454 emulationViewModel.emulationStopped.collect { 405 if (it && emulationViewModel.programChanged.value != -1) {
455 if (it && emulationViewModel.programChanged.value != -1) { 406 if (perfStatsUpdater != null) {
456 if (perfStatsUpdater != null) { 407 perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!)
457 perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!)
458 }
459 emulationState.changeProgram(emulationViewModel.programChanged.value)
460 emulationViewModel.setProgramChanged(-1)
461 emulationViewModel.setEmulationStopped(false)
462 }
463 }
464 } 408 }
409 emulationState.changeProgram(emulationViewModel.programChanged.value)
410 emulationViewModel.setProgramChanged(-1)
411 emulationViewModel.setEmulationStopped(false)
465 } 412 }
466 } 413 }
414
415 driverViewModel.isInteractionAllowed.collect(viewLifecycleOwner) {
416 if (it) startEmulation()
417 }
467 } 418 }
468 419
469 private fun startEmulation(programIndex: Int = 0) { 420 private fun startEmulation(programIndex: Int = 0) {
@@ -491,14 +442,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
491 binding.drawerLayout.close() 442 binding.drawerLayout.close()
492 } 443 }
493 if (showInputOverlay) { 444 if (showInputOverlay) {
494 binding.surfaceInputOverlay.visibility = View.INVISIBLE 445 binding.surfaceInputOverlay.setVisible(visible = false, gone = false)
495 } 446 }
496 } else { 447 } else {
497 if (showInputOverlay && emulationViewModel.emulationStarted.value) { 448 binding.surfaceInputOverlay.setVisible(
498 binding.surfaceInputOverlay.visibility = View.VISIBLE 449 showInputOverlay && emulationViewModel.emulationStarted.value
499 } else { 450 )
500 binding.surfaceInputOverlay.visibility = View.INVISIBLE
501 }
502 if (!isInFoldableLayout) { 451 if (!isInFoldableLayout) {
503 if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { 452 if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
504 binding.surfaceInputOverlay.layout = OverlayLayout.Portrait 453 binding.surfaceInputOverlay.layout = OverlayLayout.Portrait
@@ -535,7 +484,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
535 } 484 }
536 485
537 private fun updateShowFpsOverlay() { 486 private fun updateShowFpsOverlay() {
538 if (BooleanSetting.SHOW_PERFORMANCE_OVERLAY.getBoolean()) { 487 val showOverlay = BooleanSetting.SHOW_PERFORMANCE_OVERLAY.getBoolean()
488 binding.showFpsText.setVisible(showOverlay)
489 if (showOverlay) {
539 val SYSTEM_FPS = 0 490 val SYSTEM_FPS = 0
540 val FPS = 1 491 val FPS = 1
541 val FRAMETIME = 2 492 val FRAMETIME = 2
@@ -555,17 +506,17 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
555 } 506 }
556 } 507 }
557 perfStatsUpdateHandler.post(perfStatsUpdater!!) 508 perfStatsUpdateHandler.post(perfStatsUpdater!!)
558 binding.showFpsText.visibility = View.VISIBLE
559 } else { 509 } else {
560 if (perfStatsUpdater != null) { 510 if (perfStatsUpdater != null) {
561 perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!) 511 perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!)
562 } 512 }
563 binding.showFpsText.visibility = View.GONE
564 } 513 }
565 } 514 }
566 515
567 private fun updateThermalOverlay() { 516 private fun updateThermalOverlay() {
568 if (BooleanSetting.SHOW_THERMAL_OVERLAY.getBoolean()) { 517 val showOverlay = BooleanSetting.SHOW_THERMAL_OVERLAY.getBoolean()
518 binding.showThermalsText.setVisible(showOverlay)
519 if (showOverlay) {
569 thermalStatsUpdater = { 520 thermalStatsUpdater = {
570 if (emulationViewModel.emulationStarted.value && 521 if (emulationViewModel.emulationStarted.value &&
571 !emulationViewModel.isEmulationStopping.value 522 !emulationViewModel.isEmulationStopping.value
@@ -587,12 +538,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
587 } 538 }
588 } 539 }
589 thermalStatsUpdateHandler.post(thermalStatsUpdater!!) 540 thermalStatsUpdateHandler.post(thermalStatsUpdater!!)
590 binding.showThermalsText.visibility = View.VISIBLE
591 } else { 541 } else {
592 if (thermalStatsUpdater != null) { 542 if (thermalStatsUpdater != null) {
593 thermalStatsUpdateHandler.removeCallbacks(thermalStatsUpdater!!) 543 thermalStatsUpdateHandler.removeCallbacks(thermalStatsUpdater!!)
594 } 544 }
595 binding.showThermalsText.visibility = View.GONE
596 } 545 }
597 } 546 }
598 547
@@ -861,12 +810,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
861 } 810 }
862 } 811 }
863 } 812 }
864 binding.doneControlConfig.visibility = View.VISIBLE 813 binding.doneControlConfig.setVisible(false)
865 binding.surfaceInputOverlay.setIsInEditMode(true) 814 binding.surfaceInputOverlay.setIsInEditMode(true)
866 } 815 }
867 816
868 private fun stopConfiguringControls() { 817 private fun stopConfiguringControls() {
869 binding.doneControlConfig.visibility = View.GONE 818 binding.doneControlConfig.setVisible(false)
870 binding.surfaceInputOverlay.setIsInEditMode(false) 819 binding.surfaceInputOverlay.setIsInEditMode(false)
871 // Unlock the orientation if it was locked for editing 820 // Unlock the orientation if it was locked for editing
872 if (IntSetting.RENDERER_SCREEN_LAYOUT.getInt() == EmulationOrientation.Unspecified.int) { 821 if (IntSetting.RENDERER_SCREEN_LAYOUT.getInt() == EmulationOrientation.Unspecified.int) {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt
index 5c558b1a5..3a6f7a38c 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt
@@ -13,9 +13,6 @@ import androidx.core.view.WindowInsetsCompat
13import androidx.core.view.updatePadding 13import androidx.core.view.updatePadding
14import androidx.fragment.app.Fragment 14import androidx.fragment.app.Fragment
15import androidx.fragment.app.activityViewModels 15import androidx.fragment.app.activityViewModels
16import androidx.lifecycle.Lifecycle
17import androidx.lifecycle.lifecycleScope
18import androidx.lifecycle.repeatOnLifecycle
19import androidx.navigation.findNavController 16import androidx.navigation.findNavController
20import androidx.recyclerview.widget.GridLayoutManager 17import androidx.recyclerview.widget.GridLayoutManager
21import com.google.android.material.transition.MaterialSharedAxis 18import com.google.android.material.transition.MaterialSharedAxis
@@ -27,6 +24,7 @@ import org.yuzu.yuzu_emu.model.GamesViewModel
27import org.yuzu.yuzu_emu.model.HomeViewModel 24import org.yuzu.yuzu_emu.model.HomeViewModel
28import org.yuzu.yuzu_emu.ui.main.MainActivity 25import org.yuzu.yuzu_emu.ui.main.MainActivity
29import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins 26import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
27import org.yuzu.yuzu_emu.utils.collect
30 28
31class GameFoldersFragment : Fragment() { 29class GameFoldersFragment : Fragment() {
32 private var _binding: FragmentFoldersBinding? = null 30 private var _binding: FragmentFoldersBinding? = null
@@ -70,12 +68,8 @@ class GameFoldersFragment : Fragment() {
70 adapter = FolderAdapter(requireActivity(), gamesViewModel) 68 adapter = FolderAdapter(requireActivity(), gamesViewModel)
71 } 69 }
72 70
73 viewLifecycleOwner.lifecycleScope.launch { 71 gamesViewModel.folders.collect(viewLifecycleOwner) {
74 repeatOnLifecycle(Lifecycle.State.CREATED) { 72 (binding.listFolders.adapter as FolderAdapter).submitList(it)
75 gamesViewModel.folders.collect {
76 (binding.listFolders.adapter as FolderAdapter).submitList(it)
77 }
78 }
79 } 73 }
80 74
81 val mainActivity = requireActivity() as MainActivity 75 val mainActivity = requireActivity() as MainActivity
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt
index dbd56e84f..97a8954bb 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt
@@ -27,6 +27,7 @@ import org.yuzu.yuzu_emu.databinding.FragmentGameInfoBinding
27import org.yuzu.yuzu_emu.model.GameVerificationResult 27import org.yuzu.yuzu_emu.model.GameVerificationResult
28import org.yuzu.yuzu_emu.model.HomeViewModel 28import org.yuzu.yuzu_emu.model.HomeViewModel
29import org.yuzu.yuzu_emu.utils.GameMetadata 29import org.yuzu.yuzu_emu.utils.GameMetadata
30import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
30import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins 31import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
31 32
32class GameInfoFragment : Fragment() { 33class GameInfoFragment : Fragment() {
@@ -85,7 +86,7 @@ class GameInfoFragment : Fragment() {
85 copyToClipboard(getString(R.string.developer), args.game.developer) 86 copyToClipboard(getString(R.string.developer), args.game.developer)
86 } 87 }
87 } else { 88 } else {
88 developer.visibility = View.GONE 89 developer.setVisible(false)
89 } 90 }
90 91
91 version.setHint(R.string.version) 92 version.setHint(R.string.version)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
index 3ea5e16ca..c06842c59 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
@@ -3,11 +3,9 @@
3 3
4package org.yuzu.yuzu_emu.fragments 4package org.yuzu.yuzu_emu.fragments
5 5
6import android.annotation.SuppressLint
7import android.content.pm.ShortcutInfo 6import android.content.pm.ShortcutInfo
8import android.content.pm.ShortcutManager 7import android.content.pm.ShortcutManager
9import android.os.Bundle 8import android.os.Bundle
10import android.text.TextUtils
11import android.view.LayoutInflater 9import android.view.LayoutInflater
12import android.view.View 10import android.view.View
13import android.view.ViewGroup 11import android.view.ViewGroup
@@ -18,9 +16,7 @@ import androidx.core.view.WindowInsetsCompat
18import androidx.core.view.updatePadding 16import androidx.core.view.updatePadding
19import androidx.fragment.app.Fragment 17import androidx.fragment.app.Fragment
20import androidx.fragment.app.activityViewModels 18import androidx.fragment.app.activityViewModels
21import androidx.lifecycle.Lifecycle
22import androidx.lifecycle.lifecycleScope 19import androidx.lifecycle.lifecycleScope
23import androidx.lifecycle.repeatOnLifecycle
24import androidx.navigation.findNavController 20import androidx.navigation.findNavController
25import androidx.navigation.fragment.navArgs 21import androidx.navigation.fragment.navArgs
26import androidx.recyclerview.widget.GridLayoutManager 22import androidx.recyclerview.widget.GridLayoutManager
@@ -46,7 +42,9 @@ import org.yuzu.yuzu_emu.utils.FileUtil
46import org.yuzu.yuzu_emu.utils.GameIconUtils 42import org.yuzu.yuzu_emu.utils.GameIconUtils
47import org.yuzu.yuzu_emu.utils.GpuDriverHelper 43import org.yuzu.yuzu_emu.utils.GpuDriverHelper
48import org.yuzu.yuzu_emu.utils.MemoryUtil 44import org.yuzu.yuzu_emu.utils.MemoryUtil
45import org.yuzu.yuzu_emu.utils.ViewUtils.marquee
49import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins 46import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
47import org.yuzu.yuzu_emu.utils.collect
50import java.io.BufferedOutputStream 48import java.io.BufferedOutputStream
51import java.io.File 49import java.io.File
52 50
@@ -76,8 +74,6 @@ class GamePropertiesFragment : Fragment() {
76 return binding.root 74 return binding.root
77 } 75 }
78 76
79 // This is using the correct scope, lint is just acting up
80 @SuppressLint("UnsafeRepeatOnLifecycleDetector")
81 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 77 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
82 super.onViewCreated(view, savedInstanceState) 78 super.onViewCreated(view, savedInstanceState)
83 homeViewModel.setNavigationVisibility(visible = false, animated = true) 79 homeViewModel.setNavigationVisibility(visible = false, animated = true)
@@ -107,13 +103,7 @@ class GamePropertiesFragment : Fragment() {
107 103
108 GameIconUtils.loadGameIcon(args.game, binding.imageGameScreen) 104 GameIconUtils.loadGameIcon(args.game, binding.imageGameScreen)
109 binding.title.text = args.game.title 105 binding.title.text = args.game.title
110 binding.title.postDelayed( 106 binding.title.marquee()
111 {
112 binding.title.ellipsize = TextUtils.TruncateAt.MARQUEE
113 binding.title.isSelected = true
114 },
115 3000
116 )
117 107
118 binding.buttonStart.setOnClickListener { 108 binding.buttonStart.setOnClickListener {
119 LaunchGameDialogFragment.newInstance(args.game) 109 LaunchGameDialogFragment.newInstance(args.game)
@@ -122,28 +112,14 @@ class GamePropertiesFragment : Fragment() {
122 112
123 reloadList() 113 reloadList()
124 114
125 viewLifecycleOwner.lifecycleScope.apply { 115 homeViewModel.openImportSaves.collect(
126 launch { 116 viewLifecycleOwner,
127 repeatOnLifecycle(Lifecycle.State.STARTED) { 117 resetState = { homeViewModel.setOpenImportSaves(false) }
128 homeViewModel.openImportSaves.collect { 118 ) { if (it) importSaves.launch(arrayOf("application/zip")) }
129 if (it) { 119 homeViewModel.reloadPropertiesList.collect(
130 importSaves.launch(arrayOf("application/zip")) 120 viewLifecycleOwner,
131 homeViewModel.setOpenImportSaves(false) 121 resetState = { homeViewModel.reloadPropertiesList(false) }
132 } 122 ) { if (it) reloadList() }
133 }
134 }
135 }
136 launch {
137 repeatOnLifecycle(Lifecycle.State.STARTED) {
138 homeViewModel.reloadPropertiesList.collect {
139 if (it) {
140 reloadList()
141 homeViewModel.reloadPropertiesList(false)
142 }
143 }
144 }
145 }
146 }
147 123
148 setInsets() 124 setInsets()
149 } 125 }
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 100755
--- 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/fragments/InstallableFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
index 63112dc6f..d218da1c8 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
@@ -14,9 +14,6 @@ import androidx.core.view.WindowInsetsCompat
14import androidx.core.view.updatePadding 14import androidx.core.view.updatePadding
15import androidx.fragment.app.Fragment 15import androidx.fragment.app.Fragment
16import androidx.fragment.app.activityViewModels 16import androidx.fragment.app.activityViewModels
17import androidx.lifecycle.Lifecycle
18import androidx.lifecycle.lifecycleScope
19import androidx.lifecycle.repeatOnLifecycle
20import androidx.navigation.findNavController 17import androidx.navigation.findNavController
21import androidx.recyclerview.widget.GridLayoutManager 18import androidx.recyclerview.widget.GridLayoutManager
22import com.google.android.material.transition.MaterialSharedAxis 19import com.google.android.material.transition.MaterialSharedAxis
@@ -35,6 +32,7 @@ import org.yuzu.yuzu_emu.ui.main.MainActivity
35import org.yuzu.yuzu_emu.utils.DirectoryInitialization 32import org.yuzu.yuzu_emu.utils.DirectoryInitialization
36import org.yuzu.yuzu_emu.utils.FileUtil 33import org.yuzu.yuzu_emu.utils.FileUtil
37import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins 34import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
35import org.yuzu.yuzu_emu.utils.collect
38import java.io.BufferedOutputStream 36import java.io.BufferedOutputStream
39import java.io.File 37import java.io.File
40import java.math.BigInteger 38import java.math.BigInteger
@@ -75,14 +73,10 @@ class InstallableFragment : Fragment() {
75 binding.root.findNavController().popBackStack() 73 binding.root.findNavController().popBackStack()
76 } 74 }
77 75
78 viewLifecycleOwner.lifecycleScope.launch { 76 homeViewModel.openImportSaves.collect(viewLifecycleOwner) {
79 repeatOnLifecycle(Lifecycle.State.CREATED) { 77 if (it) {
80 homeViewModel.openImportSaves.collect { 78 importSaves.launch(arrayOf("application/zip"))
81 if (it) { 79 homeViewModel.setOpenImportSaves(false)
82 importSaves.launch(arrayOf("application/zip"))
83 homeViewModel.setOpenImportSaves(false)
84 }
85 }
86 } 80 }
87 } 81 }
88 82
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProgressDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProgressDialogFragment.kt
index d201cb80c..ee3bb0386 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProgressDialogFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProgressDialogFragment.kt
@@ -13,15 +13,13 @@ import androidx.appcompat.app.AlertDialog
13import androidx.fragment.app.DialogFragment 13import androidx.fragment.app.DialogFragment
14import androidx.fragment.app.FragmentActivity 14import androidx.fragment.app.FragmentActivity
15import androidx.fragment.app.activityViewModels 15import androidx.fragment.app.activityViewModels
16import androidx.lifecycle.Lifecycle
17import androidx.lifecycle.ViewModelProvider 16import androidx.lifecycle.ViewModelProvider
18import androidx.lifecycle.lifecycleScope
19import androidx.lifecycle.repeatOnLifecycle
20import com.google.android.material.dialog.MaterialAlertDialogBuilder 17import com.google.android.material.dialog.MaterialAlertDialogBuilder
21import kotlinx.coroutines.launch
22import org.yuzu.yuzu_emu.R 18import org.yuzu.yuzu_emu.R
23import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding 19import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
24import org.yuzu.yuzu_emu.model.TaskViewModel 20import org.yuzu.yuzu_emu.model.TaskViewModel
21import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
22import org.yuzu.yuzu_emu.utils.collect
25 23
26class ProgressDialogFragment : DialogFragment() { 24class ProgressDialogFragment : DialogFragment() {
27 private val taskViewModel: TaskViewModel by activityViewModels() 25 private val taskViewModel: TaskViewModel by activityViewModels()
@@ -64,72 +62,50 @@ class ProgressDialogFragment : DialogFragment() {
64 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 62 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
65 super.onViewCreated(view, savedInstanceState) 63 super.onViewCreated(view, savedInstanceState)
66 binding.message.isSelected = true 64 binding.message.isSelected = true
67 viewLifecycleOwner.lifecycleScope.apply { 65 taskViewModel.isComplete.collect(viewLifecycleOwner) {
68 launch { 66 if (it) {
69 repeatOnLifecycle(Lifecycle.State.CREATED) { 67 dismiss()
70 taskViewModel.isComplete.collect { 68 when (val result = taskViewModel.result.value) {
71 if (it) { 69 is String -> Toast.makeText(
72 dismiss() 70 requireContext(),
73 when (val result = taskViewModel.result.value) { 71 result,
74 is String -> Toast.makeText( 72 Toast.LENGTH_LONG
75 requireContext(), 73 ).show()
76 result, 74
77 Toast.LENGTH_LONG 75 is MessageDialogFragment -> result.show(
78 ).show() 76 requireActivity().supportFragmentManager,
79 77 MessageDialogFragment.TAG
80 is MessageDialogFragment -> result.show( 78 )
81 requireActivity().supportFragmentManager, 79
82 MessageDialogFragment.TAG 80 else -> {
83 ) 81 // Do nothing
84
85 else -> {
86 // Do nothing
87 }
88 }
89 taskViewModel.clear()
90 }
91 } 82 }
92 } 83 }
84 taskViewModel.clear()
93 } 85 }
94 launch { 86 }
95 repeatOnLifecycle(Lifecycle.State.CREATED) { 87 taskViewModel.cancelled.collect(viewLifecycleOwner) {
96 taskViewModel.cancelled.collect { 88 if (it) {
97 if (it) { 89 dialog?.setTitle(R.string.cancelling)
98 dialog?.setTitle(R.string.cancelling)
99 }
100 }
101 }
102 }
103 launch {
104 repeatOnLifecycle(Lifecycle.State.CREATED) {
105 taskViewModel.progress.collect {
106 if (it != 0.0) {
107 binding.progressBar.apply {
108 isIndeterminate = false
109 progress = (
110 (it / taskViewModel.maxProgress.value) *
111 PROGRESS_BAR_RESOLUTION
112 ).toInt()
113 min = 0
114 max = PROGRESS_BAR_RESOLUTION
115 }
116 }
117 }
118 }
119 } 90 }
120 launch { 91 }
121 repeatOnLifecycle(Lifecycle.State.CREATED) { 92 taskViewModel.progress.collect(viewLifecycleOwner) {
122 taskViewModel.message.collect { 93 if (it != 0.0) {
123 if (it.isEmpty()) { 94 binding.progressBar.apply {
124 binding.message.visibility = View.GONE 95 isIndeterminate = false
125 } else { 96 progress = (
126 binding.message.visibility = View.VISIBLE 97 (it / taskViewModel.maxProgress.value) *
127 binding.message.text = it 98 PROGRESS_BAR_RESOLUTION
128 } 99 ).toInt()
129 } 100 min = 0
101 max = PROGRESS_BAR_RESOLUTION
130 } 102 }
131 } 103 }
132 } 104 }
105 taskViewModel.message.collect(viewLifecycleOwner) {
106 binding.message.setVisible(it.isNotEmpty())
107 binding.message.text = it
108 }
133 } 109 }
134 110
135 // By default, the ProgressDialog will immediately dismiss itself upon a button being pressed. 111 // By default, the ProgressDialog will immediately dismiss itself upon a button being pressed.
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt
index 20b10b1a0..662ae9760 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt
@@ -3,7 +3,6 @@
3 3
4package org.yuzu.yuzu_emu.fragments 4package org.yuzu.yuzu_emu.fragments
5 5
6import android.annotation.SuppressLint
7import android.content.Context 6import android.content.Context
8import android.content.SharedPreferences 7import android.content.SharedPreferences
9import android.os.Bundle 8import android.os.Bundle
@@ -18,14 +17,9 @@ import androidx.core.view.updatePadding
18import androidx.core.widget.doOnTextChanged 17import androidx.core.widget.doOnTextChanged
19import androidx.fragment.app.Fragment 18import androidx.fragment.app.Fragment
20import androidx.fragment.app.activityViewModels 19import androidx.fragment.app.activityViewModels
21import androidx.lifecycle.Lifecycle
22import androidx.lifecycle.lifecycleScope
23import androidx.lifecycle.repeatOnLifecycle
24import androidx.preference.PreferenceManager 20import androidx.preference.PreferenceManager
25import info.debatty.java.stringsimilarity.Jaccard 21import info.debatty.java.stringsimilarity.Jaccard
26import info.debatty.java.stringsimilarity.JaroWinkler 22import info.debatty.java.stringsimilarity.JaroWinkler
27import kotlinx.coroutines.flow.collectLatest
28import kotlinx.coroutines.launch
29import java.util.Locale 23import java.util.Locale
30import org.yuzu.yuzu_emu.R 24import org.yuzu.yuzu_emu.R
31import org.yuzu.yuzu_emu.YuzuApplication 25import org.yuzu.yuzu_emu.YuzuApplication
@@ -35,6 +29,8 @@ import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager
35import org.yuzu.yuzu_emu.model.Game 29import org.yuzu.yuzu_emu.model.Game
36import org.yuzu.yuzu_emu.model.GamesViewModel 30import org.yuzu.yuzu_emu.model.GamesViewModel
37import org.yuzu.yuzu_emu.model.HomeViewModel 31import org.yuzu.yuzu_emu.model.HomeViewModel
32import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
33import org.yuzu.yuzu_emu.utils.collect
38 34
39class SearchFragment : Fragment() { 35class SearchFragment : Fragment() {
40 private var _binding: FragmentSearchBinding? = null 36 private var _binding: FragmentSearchBinding? = null
@@ -58,8 +54,6 @@ class SearchFragment : Fragment() {
58 return binding.root 54 return binding.root
59 } 55 }
60 56
61 // This is using the correct scope, lint is just acting up
62 @SuppressLint("UnsafeRepeatOnLifecycleDetector")
63 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 57 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
64 super.onViewCreated(view, savedInstanceState) 58 super.onViewCreated(view, savedInstanceState)
65 homeViewModel.setNavigationVisibility(visible = true, animated = true) 59 homeViewModel.setNavigationVisibility(visible = true, animated = true)
@@ -81,42 +75,18 @@ class SearchFragment : Fragment() {
81 binding.chipGroup.setOnCheckedStateChangeListener { _, _ -> filterAndSearch() } 75 binding.chipGroup.setOnCheckedStateChangeListener { _, _ -> filterAndSearch() }
82 76
83 binding.searchText.doOnTextChanged { text: CharSequence?, _: Int, _: Int, _: Int -> 77 binding.searchText.doOnTextChanged { text: CharSequence?, _: Int, _: Int, _: Int ->
84 if (text.toString().isNotEmpty()) { 78 binding.clearButton.setVisible(text.toString().isNotEmpty())
85 binding.clearButton.visibility = View.VISIBLE
86 } else {
87 binding.clearButton.visibility = View.INVISIBLE
88 }
89 filterAndSearch() 79 filterAndSearch()
90 } 80 }
91 81
92 viewLifecycleOwner.lifecycleScope.apply { 82 gamesViewModel.searchFocused.collect(
93 launch { 83 viewLifecycleOwner,
94 repeatOnLifecycle(Lifecycle.State.CREATED) { 84 resetState = { gamesViewModel.setSearchFocused(false) }
95 gamesViewModel.searchFocused.collect { 85 ) { if (it) focusSearch() }
96 if (it) { 86 gamesViewModel.games.collect(viewLifecycleOwner) { filterAndSearch() }
97 focusSearch() 87 gamesViewModel.searchedGames.collect(viewLifecycleOwner) {
98 gamesViewModel.setSearchFocused(false) 88 (binding.gridGamesSearch.adapter as GameAdapter).submitList(it)
99 } 89 binding.noResultsView.setVisible(it.isNotEmpty())
100 }
101 }
102 }
103 launch {
104 repeatOnLifecycle(Lifecycle.State.CREATED) {
105 gamesViewModel.games.collectLatest { filterAndSearch() }
106 }
107 }
108 launch {
109 repeatOnLifecycle(Lifecycle.State.CREATED) {
110 gamesViewModel.searchedGames.collect {
111 (binding.gridGamesSearch.adapter as GameAdapter).submitList(it)
112 if (it.isEmpty()) {
113 binding.noResultsView.visibility = View.VISIBLE
114 } else {
115 binding.noResultsView.visibility = View.GONE
116 }
117 }
118 }
119 }
120 } 90 }
121 91
122 binding.clearButton.setOnClickListener { binding.searchText.setText("") } 92 binding.clearButton.setOnClickListener { binding.searchText.setText("") }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt
index ebf41a639..4f7548e98 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt
@@ -4,7 +4,6 @@
4package org.yuzu.yuzu_emu.fragments 4package org.yuzu.yuzu_emu.fragments
5 5
6import android.Manifest 6import android.Manifest
7import android.annotation.SuppressLint
8import android.content.Intent 7import android.content.Intent
9import android.os.Build 8import android.os.Build
10import android.os.Bundle 9import android.os.Bundle
@@ -23,9 +22,6 @@ import androidx.core.view.isVisible
23import androidx.core.view.updatePadding 22import androidx.core.view.updatePadding
24import androidx.fragment.app.Fragment 23import androidx.fragment.app.Fragment
25import androidx.fragment.app.activityViewModels 24import androidx.fragment.app.activityViewModels
26import androidx.lifecycle.Lifecycle
27import androidx.lifecycle.lifecycleScope
28import androidx.lifecycle.repeatOnLifecycle
29import androidx.navigation.findNavController 25import androidx.navigation.findNavController
30import androidx.preference.PreferenceManager 26import androidx.preference.PreferenceManager
31import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback 27import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
@@ -46,6 +42,8 @@ import org.yuzu.yuzu_emu.ui.main.MainActivity
46import org.yuzu.yuzu_emu.utils.DirectoryInitialization 42import org.yuzu.yuzu_emu.utils.DirectoryInitialization
47import org.yuzu.yuzu_emu.utils.NativeConfig 43import org.yuzu.yuzu_emu.utils.NativeConfig
48import org.yuzu.yuzu_emu.utils.ViewUtils 44import org.yuzu.yuzu_emu.utils.ViewUtils
45import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
46import org.yuzu.yuzu_emu.utils.collect
49 47
50class SetupFragment : Fragment() { 48class SetupFragment : Fragment() {
51 private var _binding: FragmentSetupBinding? = null 49 private var _binding: FragmentSetupBinding? = null
@@ -77,8 +75,6 @@ class SetupFragment : Fragment() {
77 return binding.root 75 return binding.root
78 } 76 }
79 77
80 // This is using the correct scope, lint is just acting up
81 @SuppressLint("UnsafeRepeatOnLifecycleDetector")
82 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 78 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
83 mainActivity = requireActivity() as MainActivity 79 mainActivity = requireActivity() as MainActivity
84 80
@@ -210,28 +206,14 @@ class SetupFragment : Fragment() {
210 ) 206 )
211 } 207 }
212 208
213 viewLifecycleOwner.lifecycleScope.apply { 209 homeViewModel.shouldPageForward.collect(
214 launch { 210 viewLifecycleOwner,
215 repeatOnLifecycle(Lifecycle.State.CREATED) { 211 resetState = { homeViewModel.setShouldPageForward(false) }
216 homeViewModel.shouldPageForward.collect { 212 ) { if (it) pageForward() }
217 if (it) { 213 homeViewModel.gamesDirSelected.collect(
218 pageForward() 214 viewLifecycleOwner,
219 homeViewModel.setShouldPageForward(false) 215 resetState = { homeViewModel.setGamesDirSelected(false) }
220 } 216 ) { if (it) gamesDirCallback.onStepCompleted() }
221 }
222 }
223 }
224 launch {
225 repeatOnLifecycle(Lifecycle.State.CREATED) {
226 homeViewModel.gamesDirSelected.collect {
227 if (it) {
228 gamesDirCallback.onStepCompleted()
229 homeViewModel.setGamesDirSelected(false)
230 }
231 }
232 }
233 }
234 }
235 217
236 binding.viewPager2.apply { 218 binding.viewPager2.apply {
237 adapter = SetupAdapter(requireActivity() as AppCompatActivity, pages) 219 adapter = SetupAdapter(requireActivity() as AppCompatActivity, pages)
@@ -292,12 +274,8 @@ class SetupFragment : Fragment() {
292 val backIsVisible = savedInstanceState.getBoolean(KEY_BACK_VISIBILITY) 274 val backIsVisible = savedInstanceState.getBoolean(KEY_BACK_VISIBILITY)
293 hasBeenWarned = savedInstanceState.getBooleanArray(KEY_HAS_BEEN_WARNED)!! 275 hasBeenWarned = savedInstanceState.getBooleanArray(KEY_HAS_BEEN_WARNED)!!
294 276
295 if (nextIsVisible) { 277 binding.buttonNext.setVisible(nextIsVisible)
296 binding.buttonNext.visibility = View.VISIBLE 278 binding.buttonBack.setVisible(backIsVisible)
297 }
298 if (backIsVisible) {
299 binding.buttonBack.visibility = View.VISIBLE
300 }
301 } else { 279 } else {
302 hasBeenWarned = BooleanArray(pages.size) 280 hasBeenWarned = BooleanArray(pages.size)
303 } 281 }
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 100755
--- 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 100755
--- 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 100755
--- 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 100755
--- 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/ui/GamesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt
index 23ca49b53..fadb20e39 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt
@@ -3,7 +3,6 @@
3 3
4package org.yuzu.yuzu_emu.ui 4package org.yuzu.yuzu_emu.ui
5 5
6import android.annotation.SuppressLint
7import android.os.Bundle 6import android.os.Bundle
8import android.view.LayoutInflater 7import android.view.LayoutInflater
9import android.view.View 8import android.view.View
@@ -14,19 +13,16 @@ import androidx.core.view.WindowInsetsCompat
14import androidx.core.view.updatePadding 13import androidx.core.view.updatePadding
15import androidx.fragment.app.Fragment 14import androidx.fragment.app.Fragment
16import androidx.fragment.app.activityViewModels 15import androidx.fragment.app.activityViewModels
17import androidx.lifecycle.Lifecycle
18import androidx.lifecycle.lifecycleScope
19import androidx.lifecycle.repeatOnLifecycle
20import com.google.android.material.color.MaterialColors 16import com.google.android.material.color.MaterialColors
21import kotlinx.coroutines.flow.collectLatest
22import kotlinx.coroutines.launch
23import org.yuzu.yuzu_emu.R 17import org.yuzu.yuzu_emu.R
24import org.yuzu.yuzu_emu.adapters.GameAdapter 18import org.yuzu.yuzu_emu.adapters.GameAdapter
25import org.yuzu.yuzu_emu.databinding.FragmentGamesBinding 19import org.yuzu.yuzu_emu.databinding.FragmentGamesBinding
26import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager 20import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager
27import org.yuzu.yuzu_emu.model.GamesViewModel 21import org.yuzu.yuzu_emu.model.GamesViewModel
28import org.yuzu.yuzu_emu.model.HomeViewModel 22import org.yuzu.yuzu_emu.model.HomeViewModel
23import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
29import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins 24import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
25import org.yuzu.yuzu_emu.utils.collect
30 26
31class GamesFragment : Fragment() { 27class GamesFragment : Fragment() {
32 private var _binding: FragmentGamesBinding? = null 28 private var _binding: FragmentGamesBinding? = null
@@ -44,8 +40,6 @@ class GamesFragment : Fragment() {
44 return binding.root 40 return binding.root
45 } 41 }
46 42
47 // This is using the correct scope, lint is just acting up
48 @SuppressLint("UnsafeRepeatOnLifecycleDetector")
49 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 43 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
50 super.onViewCreated(view, savedInstanceState) 44 super.onViewCreated(view, savedInstanceState)
51 homeViewModel.setNavigationVisibility(visible = true, animated = true) 45 homeViewModel.setNavigationVisibility(visible = true, animated = true)
@@ -88,49 +82,28 @@ class GamesFragment : Fragment() {
88 } 82 }
89 } 83 }
90 84
91 viewLifecycleOwner.lifecycleScope.apply { 85 gamesViewModel.isReloading.collect(viewLifecycleOwner) {
92 launch { 86 binding.swipeRefresh.isRefreshing = it
93 repeatOnLifecycle(Lifecycle.State.RESUMED) { 87 binding.noticeText.setVisible(
94 gamesViewModel.isReloading.collect { 88 visible = gamesViewModel.games.value.isEmpty() && !it,
95 binding.swipeRefresh.isRefreshing = it 89 gone = false
96 if (gamesViewModel.games.value.isEmpty() && !it) { 90 )
97 binding.noticeText.visibility = View.VISIBLE 91 }
98 } else { 92 gamesViewModel.games.collect(viewLifecycleOwner) {
99 binding.noticeText.visibility = View.INVISIBLE 93 (binding.gridGames.adapter as GameAdapter).submitList(it)
100 } 94 }
101 } 95 gamesViewModel.shouldSwapData.collect(
102 } 96 viewLifecycleOwner,
103 } 97 resetState = { gamesViewModel.setShouldSwapData(false) }
104 launch { 98 ) {
105 repeatOnLifecycle(Lifecycle.State.RESUMED) { 99 if (it) {
106 gamesViewModel.games.collectLatest { 100 (binding.gridGames.adapter as GameAdapter).submitList(gamesViewModel.games.value)
107 (binding.gridGames.adapter as GameAdapter).submitList(it)
108 }
109 }
110 }
111 launch {
112 repeatOnLifecycle(Lifecycle.State.RESUMED) {
113 gamesViewModel.shouldSwapData.collect {
114 if (it) {
115 (binding.gridGames.adapter as GameAdapter).submitList(
116 gamesViewModel.games.value
117 )
118 gamesViewModel.setShouldSwapData(false)
119 }
120 }
121 }
122 }
123 launch {
124 repeatOnLifecycle(Lifecycle.State.RESUMED) {
125 gamesViewModel.shouldScrollToTop.collect {
126 if (it) {
127 scrollToTop()
128 gamesViewModel.setShouldScrollToTop(false)
129 }
130 }
131 }
132 } 101 }
133 } 102 }
103 gamesViewModel.shouldScrollToTop.collect(
104 viewLifecycleOwner,
105 resetState = { gamesViewModel.setShouldScrollToTop(false) }
106 ) { if (it) scrollToTop() }
134 107
135 setInsets() 108 setInsets()
136 } 109 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
index 4df4ac4c6..d16f8a931 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
@@ -19,9 +19,6 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
19import androidx.core.view.ViewCompat 19import androidx.core.view.ViewCompat
20import androidx.core.view.WindowCompat 20import androidx.core.view.WindowCompat
21import androidx.core.view.WindowInsetsCompat 21import androidx.core.view.WindowInsetsCompat
22import androidx.lifecycle.Lifecycle
23import androidx.lifecycle.lifecycleScope
24import androidx.lifecycle.repeatOnLifecycle
25import androidx.navigation.NavController 22import androidx.navigation.NavController
26import androidx.navigation.fragment.NavHostFragment 23import androidx.navigation.fragment.NavHostFragment
27import androidx.navigation.ui.setupWithNavController 24import androidx.navigation.ui.setupWithNavController
@@ -30,7 +27,6 @@ import com.google.android.material.color.MaterialColors
30import com.google.android.material.navigation.NavigationBarView 27import com.google.android.material.navigation.NavigationBarView
31import java.io.File 28import java.io.File
32import java.io.FilenameFilter 29import java.io.FilenameFilter
33import kotlinx.coroutines.launch
34import org.yuzu.yuzu_emu.HomeNavigationDirections 30import org.yuzu.yuzu_emu.HomeNavigationDirections
35import org.yuzu.yuzu_emu.NativeLibrary 31import org.yuzu.yuzu_emu.NativeLibrary
36import org.yuzu.yuzu_emu.R 32import org.yuzu.yuzu_emu.R
@@ -47,6 +43,7 @@ import org.yuzu.yuzu_emu.model.InstallResult
47import org.yuzu.yuzu_emu.model.TaskState 43import org.yuzu.yuzu_emu.model.TaskState
48import org.yuzu.yuzu_emu.model.TaskViewModel 44import org.yuzu.yuzu_emu.model.TaskViewModel
49import org.yuzu.yuzu_emu.utils.* 45import org.yuzu.yuzu_emu.utils.*
46import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
50import java.io.BufferedInputStream 47import java.io.BufferedInputStream
51import java.io.BufferedOutputStream 48import java.io.BufferedOutputStream
52import java.util.zip.ZipEntry 49import java.util.zip.ZipEntry
@@ -139,42 +136,23 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
139 136
140 // Prevents navigation from being drawn for a short time on recreation if set to hidden 137 // Prevents navigation from being drawn for a short time on recreation if set to hidden
141 if (!homeViewModel.navigationVisible.value.first) { 138 if (!homeViewModel.navigationVisible.value.first) {
142 binding.navigationView.visibility = View.INVISIBLE 139 binding.navigationView.setVisible(visible = false, gone = false)
143 binding.statusBarShade.visibility = View.INVISIBLE 140 binding.statusBarShade.setVisible(visible = false, gone = false)
144 } 141 }
145 142
146 lifecycleScope.apply { 143 homeViewModel.navigationVisible.collect(this) { showNavigation(it.first, it.second) }
147 launch { 144 homeViewModel.statusBarShadeVisible.collect(this) { showStatusBarShade(it) }
148 repeatOnLifecycle(Lifecycle.State.CREATED) { 145 homeViewModel.contentToInstall.collect(
149 homeViewModel.navigationVisible.collect { showNavigation(it.first, it.second) } 146 this,
150 } 147 resetState = { homeViewModel.setContentToInstall(null) }
151 } 148 ) {
152 launch { 149 if (it != null) {
153 repeatOnLifecycle(Lifecycle.State.CREATED) { 150 installContent(it)
154 homeViewModel.statusBarShadeVisible.collect { showStatusBarShade(it) }
155 }
156 }
157 launch {
158 repeatOnLifecycle(Lifecycle.State.CREATED) {
159 homeViewModel.contentToInstall.collect {
160 if (it != null) {
161 installContent(it)
162 homeViewModel.setContentToInstall(null)
163 }
164 }
165 }
166 }
167 launch {
168 repeatOnLifecycle(Lifecycle.State.CREATED) {
169 homeViewModel.checkKeys.collect {
170 if (it) {
171 checkKeys()
172 homeViewModel.setCheckKeys(false)
173 }
174 }
175 }
176 } 151 }
177 } 152 }
153 homeViewModel.checkKeys.collect(this, resetState = { homeViewModel.setCheckKeys(false) }) {
154 if (it) checkKeys()
155 }
178 156
179 setInsets() 157 setInsets()
180 } 158 }
@@ -214,18 +192,14 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
214 192
215 private fun showNavigation(visible: Boolean, animated: Boolean) { 193 private fun showNavigation(visible: Boolean, animated: Boolean) {
216 if (!animated) { 194 if (!animated) {
217 if (visible) { 195 binding.navigationView.setVisible(visible)
218 binding.navigationView.visibility = View.VISIBLE
219 } else {
220 binding.navigationView.visibility = View.INVISIBLE
221 }
222 return 196 return
223 } 197 }
224 198
225 val smallLayout = resources.getBoolean(R.bool.small_layout) 199 val smallLayout = resources.getBoolean(R.bool.small_layout)
226 binding.navigationView.animate().apply { 200 binding.navigationView.animate().apply {
227 if (visible) { 201 if (visible) {
228 binding.navigationView.visibility = View.VISIBLE 202 binding.navigationView.setVisible(true)
229 duration = 300 203 duration = 300
230 interpolator = PathInterpolator(0.05f, 0.7f, 0.1f, 1f) 204 interpolator = PathInterpolator(0.05f, 0.7f, 0.1f, 1f)
231 205
@@ -264,7 +238,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
264 } 238 }
265 }.withEndAction { 239 }.withEndAction {
266 if (!visible) { 240 if (!visible) {
267 binding.navigationView.visibility = View.INVISIBLE 241 binding.navigationView.setVisible(visible = false, gone = false)
268 } 242 }
269 }.start() 243 }.start()
270 } 244 }
@@ -272,7 +246,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
272 private fun showStatusBarShade(visible: Boolean) { 246 private fun showStatusBarShade(visible: Boolean) {
273 binding.statusBarShade.animate().apply { 247 binding.statusBarShade.animate().apply {
274 if (visible) { 248 if (visible) {
275 binding.statusBarShade.visibility = View.VISIBLE 249 binding.statusBarShade.setVisible(true)
276 binding.statusBarShade.translationY = binding.statusBarShade.height.toFloat() * -2 250 binding.statusBarShade.translationY = binding.statusBarShade.height.toFloat() * -2
277 duration = 300 251 duration = 300
278 translationY(0f) 252 translationY(0f)
@@ -284,7 +258,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
284 } 258 }
285 }.withEndAction { 259 }.withEndAction {
286 if (!visible) { 260 if (!visible) {
287 binding.statusBarShade.visibility = View.INVISIBLE 261 binding.statusBarShade.setVisible(visible = false, gone = false)
288 } 262 }
289 }.start() 263 }.start()
290 } 264 }
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 100755
--- 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/LifecycleUtils.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/LifecycleUtils.kt
new file mode 100755
index 000000000..d5c19c681
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/LifecycleUtils.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.utils
5
6import androidx.lifecycle.Lifecycle
7import androidx.lifecycle.LifecycleOwner
8import androidx.lifecycle.lifecycleScope
9import androidx.lifecycle.repeatOnLifecycle
10import kotlinx.coroutines.flow.Flow
11import kotlinx.coroutines.flow.MutableStateFlow
12import kotlinx.coroutines.launch
13
14/**
15 * Collects this [Flow] with a given [LifecycleOwner].
16 * @param scope [LifecycleOwner] that this [Flow] will be collected with.
17 * @param repeatState When to repeat collection on this [Flow].
18 * @param resetState Optional lambda to reset state of an underlying [MutableStateFlow] after
19 * [stateCollector] has been run.
20 * @param stateCollector Lambda that receives new state.
21 */
22inline fun <reified T> Flow<T>.collect(
23 scope: LifecycleOwner,
24 repeatState: Lifecycle.State = Lifecycle.State.CREATED,
25 crossinline resetState: () -> Unit = {},
26 crossinline stateCollector: (state: T) -> Unit
27) {
28 scope.apply {
29 lifecycleScope.launch {
30 repeatOnLifecycle(repeatState) {
31 this@collect.collect {
32 stateCollector(it)
33 resetState()
34 }
35 }
36 }
37 }
38}
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 100755
--- 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 100755
--- 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 100755
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/java/org/yuzu/yuzu_emu/utils/ViewUtils.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ViewUtils.kt
index ffbfa9337..244091aec 100755
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ViewUtils.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ViewUtils.kt
@@ -3,8 +3,10 @@
3 3
4package org.yuzu.yuzu_emu.utils 4package org.yuzu.yuzu_emu.utils
5 5
6import android.text.TextUtils
6import android.view.View 7import android.view.View
7import android.view.ViewGroup 8import android.view.ViewGroup
9import android.widget.TextView
8 10
9object ViewUtils { 11object ViewUtils {
10 fun showView(view: View, length: Long = 300) { 12 fun showView(view: View, length: Long = 300) {
@@ -57,4 +59,35 @@ object ViewUtils {
57 } 59 }
58 this.layoutParams = layoutParams 60 this.layoutParams = layoutParams
59 } 61 }
62
63 /**
64 * Shows or hides a view.
65 * @param visible Whether a view will be made View.VISIBLE or View.INVISIBLE/GONE.
66 * @param gone Optional parameter for hiding a view. Uses View.GONE if true and View.INVISIBLE otherwise.
67 */
68 fun View.setVisible(visible: Boolean, gone: Boolean = true) {
69 visibility = if (visible) {
70 View.VISIBLE
71 } else {
72 if (gone) {
73 View.GONE
74 } else {
75 View.INVISIBLE
76 }
77 }
78 }
79
80 /**
81 * Starts a marquee on some text.
82 * @param delay Optional parameter for changing the start delay. 3 seconds of delay by default.
83 */
84 fun TextView.marquee(delay: Long = 3000) {
85 ellipsize = null
86 marqueeRepeatLimit = -1
87 isSingleLine = true
88 postDelayed({
89 ellipsize = TextUtils.TruncateAt.MARQUEE
90 isSelected = true
91 }, delay)
92 }
60} 93}
diff --git a/src/android/app/src/main/jni/CMakeLists.txt b/src/android/app/src/main/jni/CMakeLists.txt
index 20b319c12..ec8ae5c57 100755
--- 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 100755
--- 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 100755
--- 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 100755
--- 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 100755
--- 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 100755
--- 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 100755
--- 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 100755
--- 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 100755
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 100755
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 100755
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 100755
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 100755
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 100755
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 100755
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 100755
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 100755
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 100755
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/card_driver_option.xml b/src/android/app/src/main/res/layout/card_driver_option.xml
index bda524f0f..09e26990b 100755
--- a/src/android/app/src/main/res/layout/card_driver_option.xml
+++ b/src/android/app/src/main/res/layout/card_driver_option.xml
@@ -39,10 +39,7 @@
39 style="@style/TextAppearance.Material3.TitleMedium" 39 style="@style/TextAppearance.Material3.TitleMedium"
40 android:layout_width="match_parent" 40 android:layout_width="match_parent"
41 android:layout_height="wrap_content" 41 android:layout_height="wrap_content"
42 android:ellipsize="none"
43 android:marqueeRepeatLimit="marquee_forever"
44 android:requiresFadingEdge="horizontal" 42 android:requiresFadingEdge="horizontal"
45 android:singleLine="true"
46 android:textAlignment="viewStart" 43 android:textAlignment="viewStart"
47 tools:text="@string/select_gpu_driver_default" /> 44 tools:text="@string/select_gpu_driver_default" />
48 45
@@ -52,10 +49,7 @@
52 android:layout_width="match_parent" 49 android:layout_width="match_parent"
53 android:layout_height="wrap_content" 50 android:layout_height="wrap_content"
54 android:layout_marginTop="6dp" 51 android:layout_marginTop="6dp"
55 android:ellipsize="none"
56 android:marqueeRepeatLimit="marquee_forever"
57 android:requiresFadingEdge="horizontal" 52 android:requiresFadingEdge="horizontal"
58 android:singleLine="true"
59 android:textAlignment="viewStart" 53 android:textAlignment="viewStart"
60 tools:text="@string/install_gpu_driver_description" /> 54 tools:text="@string/install_gpu_driver_description" />
61 55
@@ -65,10 +59,7 @@
65 android:layout_width="match_parent" 59 android:layout_width="match_parent"
66 android:layout_height="wrap_content" 60 android:layout_height="wrap_content"
67 android:layout_marginTop="6dp" 61 android:layout_marginTop="6dp"
68 android:ellipsize="none"
69 android:marqueeRepeatLimit="marquee_forever"
70 android:requiresFadingEdge="horizontal" 62 android:requiresFadingEdge="horizontal"
71 android:singleLine="true"
72 android:textAlignment="viewStart" 63 android:textAlignment="viewStart"
73 tools:text="@string/install_gpu_driver_description" /> 64 tools:text="@string/install_gpu_driver_description" />
74 65
diff --git a/src/android/app/src/main/res/layout/card_folder.xml b/src/android/app/src/main/res/layout/card_folder.xml
index ed4a7ca8f..e3a5f1a86 100755
--- a/src/android/app/src/main/res/layout/card_folder.xml
+++ b/src/android/app/src/main/res/layout/card_folder.xml
@@ -21,10 +21,7 @@
21 android:layout_width="0dp" 21 android:layout_width="0dp"
22 android:layout_height="wrap_content" 22 android:layout_height="wrap_content"
23 android:layout_gravity="center_vertical|start" 23 android:layout_gravity="center_vertical|start"
24 android:ellipsize="none"
25 android:marqueeRepeatLimit="marquee_forever"
26 android:requiresFadingEdge="horizontal" 24 android:requiresFadingEdge="horizontal"
27 android:singleLine="true"
28 android:textAlignment="viewStart" 25 android:textAlignment="viewStart"
29 app:layout_constraintBottom_toBottomOf="parent" 26 app:layout_constraintBottom_toBottomOf="parent"
30 app:layout_constraintEnd_toStartOf="@+id/button_layout" 27 app:layout_constraintEnd_toStartOf="@+id/button_layout"
diff --git a/src/android/app/src/main/res/layout/card_game.xml b/src/android/app/src/main/res/layout/card_game.xml
index 6340171ec..411b50315 100755
--- a/src/android/app/src/main/res/layout/card_game.xml
+++ b/src/android/app/src/main/res/layout/card_game.xml
@@ -40,10 +40,7 @@
40 android:layout_width="0dp" 40 android:layout_width="0dp"
41 android:layout_height="wrap_content" 41 android:layout_height="wrap_content"
42 android:layout_marginTop="8dp" 42 android:layout_marginTop="8dp"
43 android:ellipsize="none"
44 android:marqueeRepeatLimit="marquee_forever"
45 android:requiresFadingEdge="horizontal" 43 android:requiresFadingEdge="horizontal"
46 android:singleLine="true"
47 android:textAlignment="center" 44 android:textAlignment="center"
48 android:textSize="14sp" 45 android:textSize="14sp"
49 app:layout_constraintEnd_toEndOf="@+id/image_game_screen" 46 app:layout_constraintEnd_toEndOf="@+id/image_game_screen"
diff --git a/src/android/app/src/main/res/layout/card_simple_outlined.xml b/src/android/app/src/main/res/layout/card_simple_outlined.xml
index b73930e7e..e29df6a2d 100755
--- a/src/android/app/src/main/res/layout/card_simple_outlined.xml
+++ b/src/android/app/src/main/res/layout/card_simple_outlined.xml
@@ -59,9 +59,6 @@
59 android:textAlignment="viewStart" 59 android:textAlignment="viewStart"
60 android:textSize="14sp" 60 android:textSize="14sp"
61 android:textStyle="bold" 61 android:textStyle="bold"
62 android:singleLine="true"
63 android:marqueeRepeatLimit="marquee_forever"
64 android:ellipsize="none"
65 android:requiresFadingEdge="horizontal" 62 android:requiresFadingEdge="horizontal"
66 android:layout_marginTop="6dp" 63 android:layout_marginTop="6dp"
67 android:visibility="gone" 64 android:visibility="gone"
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 100755
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 100755
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/fragment_game_properties.xml b/src/android/app/src/main/res/layout/fragment_game_properties.xml
index 436ebd79d..5e3f3cf28 100755
--- a/src/android/app/src/main/res/layout/fragment_game_properties.xml
+++ b/src/android/app/src/main/res/layout/fragment_game_properties.xml
@@ -76,10 +76,7 @@
76 android:layout_marginTop="12dp" 76 android:layout_marginTop="12dp"
77 android:layout_marginBottom="12dp" 77 android:layout_marginBottom="12dp"
78 android:layout_marginHorizontal="16dp" 78 android:layout_marginHorizontal="16dp"
79 android:ellipsize="none"
80 android:marqueeRepeatLimit="marquee_forever"
81 android:requiresFadingEdge="horizontal" 79 android:requiresFadingEdge="horizontal"
82 android:singleLine="true"
83 android:textAlignment="center" 80 android:textAlignment="center"
84 tools:text="deko_basic" /> 81 tools:text="deko_basic" />
85 82
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 100755
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 100755
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 100755
--- 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 100755
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 100755
--- 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 100755
--- 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 100755
--- 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 100755
--- 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 100755
--- 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 100755
--- 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 fccd14810..086503d8e 100755
--- 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/core/memory/cheat_engine.cpp b/src/core/memory/cheat_engine.cpp
index 66135e7c3..690ef3548 100755
--- a/src/core/memory/cheat_engine.cpp
+++ b/src/core/memory/cheat_engine.cpp
@@ -117,9 +117,9 @@ bool StandardVmCallbacks::IsAddressInRange(VAddr in) const {
117 (in < metadata.heap_extents.base || 117 (in < metadata.heap_extents.base ||
118 in >= metadata.heap_extents.base + metadata.heap_extents.size) && 118 in >= metadata.heap_extents.base + metadata.heap_extents.size) &&
119 (in < metadata.alias_extents.base || 119 (in < metadata.alias_extents.base ||
120 in >= metadata.heap_extents.base + metadata.alias_extents.size) && 120 in >= metadata.alias_extents.base + metadata.alias_extents.size) &&
121 (in < metadata.aslr_extents.base || 121 (in < metadata.aslr_extents.base ||
122 in >= metadata.heap_extents.base + metadata.aslr_extents.size)) { 122 in >= metadata.aslr_extents.base + metadata.aslr_extents.size)) {
123 LOG_DEBUG(CheatEngine, 123 LOG_DEBUG(CheatEngine,
124 "Cheat attempting to access memory at invalid address={:016X}, if this " 124 "Cheat attempting to access memory at invalid address={:016X}, if this "
125 "persists, " 125 "persists, "
diff --git a/src/frontend_common/config.cpp b/src/frontend_common/config.cpp
index 2bebfeef9..95f8c8c36 100755
--- a/src/frontend_common/config.cpp
+++ b/src/frontend_common/config.cpp
@@ -138,6 +138,7 @@ void Config::ReadPlayerValues(const std::size_t player_index) {
138 if (profile_name.empty()) { 138 if (profile_name.empty()) {
139 // Use the global input config 139 // Use the global input config
140 player = Settings::values.players.GetValue(true)[player_index]; 140 player = Settings::values.players.GetValue(true)[player_index];
141 player.profile_name = "";
141 return; 142 return;
142 } 143 }
143 player.profile_name = profile_name; 144 player.profile_name = profile_name;
diff --git a/src/frontend_common/content_manager.h b/src/frontend_common/content_manager.h
index f3efe3465..c4e97a47b 100755
--- a/src/frontend_common/content_manager.h
+++ b/src/frontend_common/content_manager.h
@@ -251,11 +251,12 @@ inline InstallResult InstallNCA(FileSys::VfsFilesystem& vfs, const std::string&
251 * \param callback Callback to report the progress of the installation. The first size_t 251 * \param callback Callback to report the progress of the installation. The first size_t
252 * parameter is the total size of the installed contents and the second is the current progress. If 252 * parameter is the total size of the installed contents and the second is the current progress. If
253 * you return true to the callback, it will cancel the installation as soon as possible. 253 * you return true to the callback, it will cancel the installation as soon as possible.
254 * \param firmware_only Set to true to only scan system nand NCAs (firmware), post firmware install.
254 * \return A list of entries that failed to install. Returns an empty vector if successful. 255 * \return A list of entries that failed to install. Returns an empty vector if successful.
255 */ 256 */
256inline std::vector<std::string> VerifyInstalledContents( 257inline std::vector<std::string> VerifyInstalledContents(
257 Core::System& system, FileSys::ManualContentProvider& provider, 258 Core::System& system, FileSys::ManualContentProvider& provider,
258 const std::function<bool(size_t, size_t)>& callback) { 259 const std::function<bool(size_t, size_t)>& callback, bool firmware_only = false) {
259 // Get content registries. 260 // Get content registries.
260 auto bis_contents = system.GetFileSystemController().GetSystemNANDContents(); 261 auto bis_contents = system.GetFileSystemController().GetSystemNANDContents();
261 auto user_contents = system.GetFileSystemController().GetUserNANDContents(); 262 auto user_contents = system.GetFileSystemController().GetUserNANDContents();
@@ -264,7 +265,7 @@ inline std::vector<std::string> VerifyInstalledContents(
264 if (bis_contents) { 265 if (bis_contents) {
265 content_providers.push_back(bis_contents); 266 content_providers.push_back(bis_contents);
266 } 267 }
267 if (user_contents) { 268 if (user_contents && !firmware_only) {
268 content_providers.push_back(user_contents); 269 content_providers.push_back(user_contents);
269 } 270 }
270 271
diff --git a/src/hid_core/frontend/emulated_controller.cpp b/src/hid_core/frontend/emulated_controller.cpp
index 819460eb5..3fa06d188 100755
--- a/src/hid_core/frontend/emulated_controller.cpp
+++ b/src/hid_core/frontend/emulated_controller.cpp
@@ -176,16 +176,19 @@ void EmulatedController::LoadDevices() {
176 camera_params[1] = Common::ParamPackage{"engine:camera,camera:1"}; 176 camera_params[1] = Common::ParamPackage{"engine:camera,camera:1"};
177 ring_params[1] = Common::ParamPackage{"engine:joycon,axis_x:100,axis_y:101"}; 177 ring_params[1] = Common::ParamPackage{"engine:joycon,axis_x:100,axis_y:101"};
178 nfc_params[0] = Common::ParamPackage{"engine:virtual_amiibo,nfc:1"}; 178 nfc_params[0] = Common::ParamPackage{"engine:virtual_amiibo,nfc:1"};
179 android_params = Common::ParamPackage{"engine:android,port:100"};
179 } 180 }
180 181
181 output_params[LeftIndex] = left_joycon; 182 output_params[LeftIndex] = left_joycon;
182 output_params[RightIndex] = right_joycon; 183 output_params[RightIndex] = right_joycon;
183 output_params[2] = camera_params[1]; 184 output_params[2] = camera_params[1];
184 output_params[3] = nfc_params[0]; 185 output_params[3] = nfc_params[0];
186 output_params[4] = android_params;
185 output_params[LeftIndex].Set("output", true); 187 output_params[LeftIndex].Set("output", true);
186 output_params[RightIndex].Set("output", true); 188 output_params[RightIndex].Set("output", true);
187 output_params[2].Set("output", true); 189 output_params[2].Set("output", true);
188 output_params[3].Set("output", true); 190 output_params[3].Set("output", true);
191 output_params[4].Set("output", true);
189 192
190 LoadTASParams(); 193 LoadTASParams();
191 LoadVirtualGamepadParams(); 194 LoadVirtualGamepadParams();
@@ -578,6 +581,9 @@ void EmulatedController::DisableConfiguration() {
578 581
579 // Get Joycon colors before turning on the controller 582 // Get Joycon colors before turning on the controller
580 for (const auto& color_device : color_devices) { 583 for (const auto& color_device : color_devices) {
584 if (color_device == nullptr) {
585 continue;
586 }
581 color_device->ForceUpdate(); 587 color_device->ForceUpdate();
582 } 588 }
583 589
@@ -1277,6 +1283,10 @@ bool EmulatedController::SetVibration(DeviceIndex device_index, const VibrationV
1277 .high_frequency = vibration.high_frequency, 1283 .high_frequency = vibration.high_frequency,
1278 .type = type, 1284 .type = type,
1279 }; 1285 };
1286
1287 // Send vibrations to Android's input overlay
1288 output_devices[4]->SetVibration(status);
1289
1280 return output_devices[index]->SetVibration(status) == Common::Input::DriverResult::Success; 1290 return output_devices[index]->SetVibration(status) == Common::Input::DriverResult::Success;
1281} 1291}
1282 1292
diff --git a/src/hid_core/frontend/emulated_controller.h b/src/hid_core/frontend/emulated_controller.h
index 701b38300..ab3c6fcd3 100755
--- a/src/hid_core/frontend/emulated_controller.h
+++ b/src/hid_core/frontend/emulated_controller.h
@@ -21,7 +21,7 @@
21 21
22namespace Core::HID { 22namespace Core::HID {
23const std::size_t max_emulated_controllers = 2; 23const std::size_t max_emulated_controllers = 2;
24const std::size_t output_devices_size = 4; 24const std::size_t output_devices_size = 5;
25struct ControllerMotionInfo { 25struct ControllerMotionInfo {
26 Common::Input::MotionStatus raw_status{}; 26 Common::Input::MotionStatus raw_status{};
27 MotionInput emulated{}; 27 MotionInput emulated{};
@@ -597,6 +597,7 @@ private:
597 CameraParams camera_params; 597 CameraParams camera_params;
598 RingAnalogParams ring_params; 598 RingAnalogParams ring_params;
599 NfcParams nfc_params; 599 NfcParams nfc_params;
600 Common::ParamPackage android_params;
600 OutputParams output_params; 601 OutputParams output_params;
601 602
602 ButtonDevices button_devices; 603 ButtonDevices button_devices;
diff --git a/src/input_common/CMakeLists.txt b/src/input_common/CMakeLists.txt
index 0bc65262b..3c0e6f98d 100755
--- 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 100755
--- 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 100755
--- 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 a40fdccc5..96aac78fe 100755
--- 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();
diff --git a/src/yuzu/configuration/qt_config.cpp b/src/yuzu/configuration/qt_config.cpp
index 1051031f2..37951b9c8 100755
--- a/src/yuzu/configuration/qt_config.cpp
+++ b/src/yuzu/configuration/qt_config.cpp
@@ -90,6 +90,7 @@ void QtConfig::ReadQtPlayerValues(const std::size_t player_index) {
90 if (profile_name.empty()) { 90 if (profile_name.empty()) {
91 // Use the global input config 91 // Use the global input config
92 player = Settings::values.players.GetValue(true)[player_index]; 92 player = Settings::values.players.GetValue(true)[player_index];
93 player.profile_name = "";
93 return; 94 return;
94 } 95 }
95 } 96 }
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp
index 97814836f..5fcd9f9f3 100755
--- a/src/yuzu/main.cpp
+++ b/src/yuzu/main.cpp
@@ -1603,6 +1603,7 @@ void GMainWindow::ConnectMenuEvents() {
1603 // Help 1603 // Help
1604 connect_menu(ui->action_Open_yuzu_Folder, &GMainWindow::OnOpenYuzuFolder); 1604 connect_menu(ui->action_Open_yuzu_Folder, &GMainWindow::OnOpenYuzuFolder);
1605 connect_menu(ui->action_Verify_installed_contents, &GMainWindow::OnVerifyInstalledContents); 1605 connect_menu(ui->action_Verify_installed_contents, &GMainWindow::OnVerifyInstalledContents);
1606 connect_menu(ui->action_Install_Firmware, &GMainWindow::OnInstallFirmware);
1606 connect_menu(ui->action_About, &GMainWindow::OnAbout); 1607 connect_menu(ui->action_About, &GMainWindow::OnAbout);
1607} 1608}
1608 1609
@@ -1631,6 +1632,8 @@ void GMainWindow::UpdateMenuState() {
1631 action->setEnabled(emulation_running); 1632 action->setEnabled(emulation_running);
1632 } 1633 }
1633 1634
1635 ui->action_Install_Firmware->setEnabled(!emulation_running);
1636
1634 for (QAction* action : applet_actions) { 1637 for (QAction* action : applet_actions) {
1635 action->setEnabled(is_firmware_available && !emulation_running); 1638 action->setEnabled(is_firmware_available && !emulation_running);
1636 } 1639 }
@@ -4150,6 +4153,146 @@ void GMainWindow::OnVerifyInstalledContents() {
4150 } 4153 }
4151} 4154}
4152 4155
4156void GMainWindow::OnInstallFirmware() {
4157 // Don't do this while emulation is running, that'd probably be a bad idea.
4158 if (emu_thread != nullptr && emu_thread->IsRunning()) {
4159 return;
4160 }
4161
4162 // Check for installed keys, error out, suggest restart?
4163 if (!ContentManager::AreKeysPresent()) {
4164 QMessageBox::information(
4165 this, tr("Keys not installed"),
4166 tr("Install decryption keys and restart yuzu before attempting to install firmware."));
4167 return;
4168 }
4169
4170 QString firmware_source_location =
4171 QFileDialog::getExistingDirectory(this, tr("Select Dumped Firmware Source Location"),
4172 QString::fromStdString(""), QFileDialog::ShowDirsOnly);
4173 if (firmware_source_location.isEmpty()) {
4174 return;
4175 }
4176
4177 QProgressDialog progress(tr("Installing Firmware..."), tr("Cancel"), 0, 100, this);
4178 progress.setWindowModality(Qt::WindowModal);
4179 progress.setMinimumDuration(100);
4180 progress.setAutoClose(false);
4181 progress.setAutoReset(false);
4182 progress.show();
4183
4184 // Declare progress callback.
4185 auto QtProgressCallback = [&](size_t total_size, size_t processed_size) {
4186 progress.setValue(static_cast<int>((processed_size * 100) / total_size));
4187 return progress.wasCanceled();
4188 };
4189
4190 LOG_INFO(Frontend, "Installing firmware from {}", firmware_source_location.toStdString());
4191
4192 // Check for a reasonable number of .nca files (don't hardcode them, just see if there's some in
4193 // there.)
4194 std::filesystem::path firmware_source_path = firmware_source_location.toStdString();
4195 if (!Common::FS::IsDir(firmware_source_path)) {
4196 progress.close();
4197 return;
4198 }
4199
4200 std::vector<std::filesystem::path> out;
4201 const Common::FS::DirEntryCallable callback =
4202 [&out](const std::filesystem::directory_entry& entry) {
4203 if (entry.path().has_extension() && entry.path().extension() == ".nca")
4204 out.emplace_back(entry.path());
4205
4206 return true;
4207 };
4208
4209 QtProgressCallback(100, 10);
4210
4211 Common::FS::IterateDirEntries(firmware_source_path, callback, Common::FS::DirEntryFilter::File);
4212 if (out.size() <= 0) {
4213 progress.close();
4214 QMessageBox::warning(this, tr("Firmware install failed"),
4215 tr("Unable to locate potential firmware NCA files"));
4216 return;
4217 }
4218
4219 // Locate and erase the content of nand/system/Content/registered/*.nca, if any.
4220 auto sysnand_content_vdir = system->GetFileSystemController().GetSystemNANDContentDirectory();
4221 if (!sysnand_content_vdir->CleanSubdirectoryRecursive("registered")) {
4222 progress.close();
4223 QMessageBox::critical(this, tr("Firmware install failed"),
4224 tr("Failed to delete one or more firmware file."));
4225 return;
4226 }
4227
4228 LOG_INFO(Frontend,
4229 "Cleaned nand/system/Content/registered folder in preparation for new firmware.");
4230
4231 QtProgressCallback(100, 20);
4232
4233 auto firmware_vdir = sysnand_content_vdir->GetDirectoryRelative("registered");
4234
4235 bool success = true;
4236 bool cancelled = false;
4237 int i = 0;
4238 for (const auto& firmware_src_path : out) {
4239 i++;
4240 auto firmware_src_vfile =
4241 vfs->OpenFile(firmware_src_path.generic_string(), FileSys::OpenMode::Read);
4242 auto firmware_dst_vfile =
4243 firmware_vdir->CreateFileRelative(firmware_src_path.filename().string());
4244
4245 if (!VfsRawCopy(firmware_src_vfile, firmware_dst_vfile)) {
4246 LOG_ERROR(Frontend, "Failed to copy firmware file {} to {} in registered folder!",
4247 firmware_src_path.generic_string(), firmware_src_path.filename().string());
4248 success = false;
4249 }
4250
4251 if (QtProgressCallback(100, 20 + (int)(((float)(i) / (float)out.size()) * 70.0))) {
4252 success = false;
4253 cancelled = true;
4254 break;
4255 }
4256 }
4257
4258 if (!success && !cancelled) {
4259 progress.close();
4260 QMessageBox::critical(this, tr("Firmware install failed"),
4261 tr("One or more firmware files failed to copy into NAND."));
4262 return;
4263 } else if (cancelled) {
4264 progress.close();
4265 QMessageBox::warning(this, tr("Firmware install failed"),
4266 tr("Firmware installation cancelled, firmware may be in bad state, "
4267 "restart yuzu or re-install firmware."));
4268 return;
4269 }
4270
4271 // Re-scan VFS for the newly placed firmware files.
4272 system->GetFileSystemController().CreateFactories(*vfs);
4273
4274 auto VerifyFirmwareCallback = [&](size_t total_size, size_t processed_size) {
4275 progress.setValue(90 + static_cast<int>((processed_size * 10) / total_size));
4276 return progress.wasCanceled();
4277 };
4278
4279 auto result =
4280 ContentManager::VerifyInstalledContents(*system, *provider, VerifyFirmwareCallback, true);
4281
4282 if (result.size() > 0) {
4283 const auto failed_names =
4284 QString::fromStdString(fmt::format("{}", fmt::join(result, "\n")));
4285 progress.close();
4286 QMessageBox::critical(
4287 this, tr("Firmware integrity verification failed!"),
4288 tr("Verification failed for the following files:\n\n%1").arg(failed_names));
4289 return;
4290 }
4291
4292 progress.close();
4293 OnCheckFirmwareDecryption();
4294}
4295
4153void GMainWindow::OnAbout() { 4296void GMainWindow::OnAbout() {
4154 AboutDialog aboutDialog(this); 4297 AboutDialog aboutDialog(this);
4155 aboutDialog.exec(); 4298 aboutDialog.exec();
diff --git a/src/yuzu/main.h b/src/yuzu/main.h
index f1376e3a3..8510f0035 100755
--- a/src/yuzu/main.h
+++ b/src/yuzu/main.h
@@ -380,6 +380,7 @@ private slots:
380 void OnLoadAmiibo(); 380 void OnLoadAmiibo();
381 void OnOpenYuzuFolder(); 381 void OnOpenYuzuFolder();
382 void OnVerifyInstalledContents(); 382 void OnVerifyInstalledContents();
383 void OnInstallFirmware();
383 void OnAbout(); 384 void OnAbout();
384 void OnToggleFilterBar(); 385 void OnToggleFilterBar();
385 void OnToggleStatusBar(); 386 void OnToggleStatusBar();
diff --git a/src/yuzu/main.ui b/src/yuzu/main.ui
index 7a5d1e10d..656c3e8ef 100755
--- a/src/yuzu/main.ui
+++ b/src/yuzu/main.ui
@@ -25,7 +25,16 @@
25 </property> 25 </property>
26 <widget class="QWidget" name="centralwidget"> 26 <widget class="QWidget" name="centralwidget">
27 <layout class="QHBoxLayout" name="horizontalLayout"> 27 <layout class="QHBoxLayout" name="horizontalLayout">
28 <property name="margin" stdset="0"> 28 <property name="leftMargin">
29 <number>0</number>
30 </property>
31 <property name="topMargin">
32 <number>0</number>
33 </property>
34 <property name="rightMargin">
35 <number>0</number>
36 </property>
37 <property name="bottomMargin">
29 <number>0</number> 38 <number>0</number>
30 </property> 39 </property>
31 </layout> 40 </layout>
@@ -156,8 +165,8 @@
156 <addaction name="separator"/> 165 <addaction name="separator"/>
157 <addaction name="action_Configure_Tas"/> 166 <addaction name="action_Configure_Tas"/>
158 </widget> 167 </widget>
159 <addaction name="action_Rederive"/>
160 <addaction name="action_Verify_installed_contents"/> 168 <addaction name="action_Verify_installed_contents"/>
169 <addaction name="action_Install_Firmware"/>
161 <addaction name="separator"/> 170 <addaction name="separator"/>
162 <addaction name="menu_cabinet_applet"/> 171 <addaction name="menu_cabinet_applet"/>
163 <addaction name="action_Load_Album"/> 172 <addaction name="action_Load_Album"/>
@@ -455,6 +464,11 @@
455 <string>Open &amp;Controller Menu</string> 464 <string>Open &amp;Controller Menu</string>
456 </property> 465 </property>
457 </action> 466 </action>
467 <action name="action_Install_Firmware">
468 <property name="text">
469 <string>Install Firmware</string>
470 </property>
471 </action>
458 </widget> 472 </widget>
459 <resources> 473 <resources>
460 <include location="yuzu.qrc"/> 474 <include location="yuzu.qrc"/>
diff --git a/src/yuzu_cmd/sdl_config.cpp b/src/yuzu_cmd/sdl_config.cpp
index 995114510..6e0f254b6 100755
--- a/src/yuzu_cmd/sdl_config.cpp
+++ b/src/yuzu_cmd/sdl_config.cpp
@@ -103,6 +103,7 @@ void SdlConfig::ReadSdlPlayerValues(const std::size_t player_index) {
103 if (profile_name.empty()) { 103 if (profile_name.empty()) {
104 // Use the global input config 104 // Use the global input config
105 player = Settings::values.players.GetValue(true)[player_index]; 105 player = Settings::values.players.GetValue(true)[player_index];
106 player.profile_name = "";
106 return; 107 return;
107 } 108 }
108 } 109 }