diff options
145 files changed, 6688 insertions, 2052 deletions
diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml index 7890b30ca..b037fc055 100644 --- a/src/android/app/src/main/AndroidManifest.xml +++ b/src/android/app/src/main/AndroidManifest.xml @@ -14,6 +14,7 @@ SPDX-License-Identifier: GPL-3.0-or-later      <uses-permission android:name="android.permission.INTERNET" />      <uses-permission android:name="android.permission.NFC" />      <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> +    <uses-permission android:name="android.permission.VIBRATE" />      <application          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 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt @@ -3,24 +3,21 @@  package org.yuzu.yuzu_emu -import android.app.Dialog  import android.content.DialogInterface  import android.net.Uri -import android.os.Bundle  import android.text.Html  import android.text.method.LinkMovementMethod  import android.view.Surface  import android.view.View  import android.widget.TextView  import androidx.annotation.Keep -import androidx.fragment.app.DialogFragment  import com.google.android.material.dialog.MaterialAlertDialogBuilder  import java.lang.ref.WeakReference  import org.yuzu.yuzu_emu.activities.EmulationActivity +import org.yuzu.yuzu_emu.fragments.CoreErrorDialogFragment  import org.yuzu.yuzu_emu.utils.DocumentsTree  import org.yuzu.yuzu_emu.utils.FileUtil  import org.yuzu.yuzu_emu.utils.Log -import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable  import org.yuzu.yuzu_emu.model.InstallResult  import org.yuzu.yuzu_emu.model.Patch  import org.yuzu.yuzu_emu.model.GameVerificationResult @@ -30,34 +27,6 @@ import org.yuzu.yuzu_emu.model.GameVerificationResult   * with the native side of the Yuzu code.   */  object NativeLibrary { -    /** -     * Default controller id for each device -     */ -    const val Player1Device = 0 -    const val Player2Device = 1 -    const val Player3Device = 2 -    const val Player4Device = 3 -    const val Player5Device = 4 -    const val Player6Device = 5 -    const val Player7Device = 6 -    const val Player8Device = 7 -    const val ConsoleDevice = 8 - -    /** -     * Controller type for each device -     */ -    const val ProController = 3 -    const val Handheld = 4 -    const val JoyconDual = 5 -    const val JoyconLeft = 6 -    const val JoyconRight = 7 -    const val GameCube = 8 -    const val Pokeball = 9 -    const val NES = 10 -    const val SNES = 11 -    const val N64 = 12 -    const val SegaGenesis = 13 -      @JvmField      var sEmulationActivity = WeakReference<EmulationActivity?>(null) @@ -127,112 +96,6 @@ object NativeLibrary {              FileUtil.getFilename(Uri.parse(path))          } -    /** -     * Returns true if pro controller isn't available and handheld is -     */ -    external fun isHandheldOnly(): Boolean - -    /** -     * Changes controller type for a specific device. -     * -     * @param Device The input descriptor of the gamepad. -     * @param Type The NpadStyleIndex of the gamepad. -     */ -    external fun setDeviceType(Device: Int, Type: Int): Boolean - -    /** -     * Handles event when a gamepad is connected. -     * -     * @param Device The input descriptor of the gamepad. -     */ -    external fun onGamePadConnectEvent(Device: Int): Boolean - -    /** -     * Handles event when a gamepad is disconnected. -     * -     * @param Device The input descriptor of the gamepad. -     */ -    external fun onGamePadDisconnectEvent(Device: Int): Boolean - -    /** -     * Handles button press events for a gamepad. -     * -     * @param Device The input descriptor of the gamepad. -     * @param Button Key code identifying which button was pressed. -     * @param Action Mask identifying which action is happening (button pressed down, or button released). -     * @return If we handled the button press. -     */ -    external fun onGamePadButtonEvent(Device: Int, Button: Int, Action: Int): Boolean - -    /** -     * Handles joystick movement events. -     * -     * @param Device The device ID of the gamepad. -     * @param Axis   The axis ID -     * @param x_axis The value of the x-axis represented by the given ID. -     * @param y_axis The value of the y-axis represented by the given ID. -     */ -    external fun onGamePadJoystickEvent( -        Device: Int, -        Axis: Int, -        x_axis: Float, -        y_axis: Float -    ): Boolean - -    /** -     * Handles motion events. -     * -     * @param delta_timestamp         The finger id corresponding to this event -     * @param gyro_x,gyro_y,gyro_z    The value of the accelerometer sensor. -     * @param accel_x,accel_y,accel_z The value of the y-axis -     */ -    external fun onGamePadMotionEvent( -        Device: Int, -        delta_timestamp: Long, -        gyro_x: Float, -        gyro_y: Float, -        gyro_z: Float, -        accel_x: Float, -        accel_y: Float, -        accel_z: Float -    ): Boolean - -    /** -     * Signals and load a nfc tag -     * -     * @param data         Byte array containing all the data from a nfc tag -     */ -    external fun onReadNfcTag(data: ByteArray?): Boolean - -    /** -     * Removes current loaded nfc tag -     */ -    external fun onRemoveNfcTag(): Boolean - -    /** -     * Handles touch press events. -     * -     * @param finger_id The finger id corresponding to this event -     * @param x_axis    The value of the x-axis. -     * @param y_axis    The value of the y-axis. -     */ -    external fun onTouchPressed(finger_id: Int, x_axis: Float, y_axis: Float) - -    /** -     * Handles touch movement. -     * -     * @param x_axis The value of the instantaneous x-axis. -     * @param y_axis The value of the instantaneous y-axis. -     */ -    external fun onTouchMoved(finger_id: Int, x_axis: Float, y_axis: Float) - -    /** -     * Handles touch release events. -     * -     * @param finger_id The finger id corresponding to this event -     */ -    external fun onTouchReleased(finger_id: Int) -      external fun setAppDirectory(directory: String)      /** @@ -318,46 +181,13 @@ object NativeLibrary {          ErrorUnknown      } -    private var coreErrorAlertResult = false -    private val coreErrorAlertLock = Object() - -    class CoreErrorDialogFragment : DialogFragment() { -        override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { -            val title = requireArguments().serializable<String>("title") -            val message = requireArguments().serializable<String>("message") - -            return MaterialAlertDialogBuilder(requireActivity()) -                .setTitle(title) -                .setMessage(message) -                .setPositiveButton(R.string.continue_button, null) -                .setNegativeButton(R.string.abort_button) { _: DialogInterface?, _: Int -> -                    coreErrorAlertResult = false -                    synchronized(coreErrorAlertLock) { coreErrorAlertLock.notify() } -                } -                .create() -        } - -        override fun onDismiss(dialog: DialogInterface) { -            coreErrorAlertResult = true -            synchronized(coreErrorAlertLock) { coreErrorAlertLock.notify() } -        } - -        companion object { -            fun newInstance(title: String?, message: String?): CoreErrorDialogFragment { -                val frag = CoreErrorDialogFragment() -                val args = Bundle() -                args.putString("title", title) -                args.putString("message", message) -                frag.arguments = args -                return frag -            } -        } -    } +    var coreErrorAlertResult = false +    val coreErrorAlertLock = Object()      private fun onCoreErrorImpl(title: String, message: String) {          val emulationActivity = sEmulationActivity.get()          if (emulationActivity == null) { -            error("[NativeLibrary] EmulationActivity not present") +            Log.error("[NativeLibrary] EmulationActivity not present")              return          } @@ -373,7 +203,7 @@ object NativeLibrary {      fun onCoreError(error: CoreError?, details: String): Boolean {          val emulationActivity = sEmulationActivity.get()          if (emulationActivity == null) { -            error("[NativeLibrary] EmulationActivity not present") +            Log.error("[NativeLibrary] EmulationActivity not present")              return false          } @@ -404,7 +234,7 @@ object NativeLibrary {          }          // Show the AlertDialog on the main thread. -        emulationActivity.runOnUiThread(Runnable { onCoreErrorImpl(title, message) }) +        emulationActivity.runOnUiThread { onCoreErrorImpl(title, message) }          // Wait for the lock to notify that it is complete.          synchronized(coreErrorAlertLock) { coreErrorAlertLock.wait() } @@ -629,46 +459,4 @@ object NativeLibrary {       * Checks if all necessary keys are present for decryption       */      external fun areKeysPresent(): Boolean - -    /** -     * Button type for use in onTouchEvent -     */ -    object ButtonType { -        const val BUTTON_A = 0 -        const val BUTTON_B = 1 -        const val BUTTON_X = 2 -        const val BUTTON_Y = 3 -        const val STICK_L = 4 -        const val STICK_R = 5 -        const val TRIGGER_L = 6 -        const val TRIGGER_R = 7 -        const val TRIGGER_ZL = 8 -        const val TRIGGER_ZR = 9 -        const val BUTTON_PLUS = 10 -        const val BUTTON_MINUS = 11 -        const val DPAD_LEFT = 12 -        const val DPAD_UP = 13 -        const val DPAD_RIGHT = 14 -        const val DPAD_DOWN = 15 -        const val BUTTON_SL = 16 -        const val BUTTON_SR = 17 -        const val BUTTON_HOME = 18 -        const val BUTTON_CAPTURE = 19 -    } - -    /** -     * Stick type for use in onTouchEvent -     */ -    object StickType { -        const val STICK_L = 0 -        const val STICK_R = 1 -    } - -    /** -     * Button states -     */ -    object ButtonState { -        const val RELEASED = 0 -        const val PRESSED = 1 -    }  } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt index 76778c10a..72943f33e 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt @@ -7,6 +7,7 @@ import android.app.Application  import android.app.NotificationChannel  import android.app.NotificationManager  import android.content.Context +import org.yuzu.yuzu_emu.features.input.NativeInput  import java.io.File  import org.yuzu.yuzu_emu.utils.DirectoryInitialization  import org.yuzu.yuzu_emu.utils.DocumentsTree @@ -37,6 +38,7 @@ class YuzuApplication : Application() {          documentsTree = DocumentsTree()          DirectoryInitialization.start()          GpuDriverHelper.initializeDriverParameters() +        NativeInput.reloadInputDevices()          NativeLibrary.logDeviceInfo()          Log.logDeviceInfo() 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..c962558a7 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt @@ -39,6 +39,7 @@ import org.yuzu.yuzu_emu.NativeLibrary  import org.yuzu.yuzu_emu.R  import org.yuzu.yuzu_emu.YuzuApplication  import org.yuzu.yuzu_emu.databinding.ActivityEmulationBinding +import org.yuzu.yuzu_emu.features.input.NativeInput  import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting  import org.yuzu.yuzu_emu.features.settings.model.IntSetting  import org.yuzu.yuzu_emu.features.settings.model.Settings @@ -47,7 +48,9 @@ import org.yuzu.yuzu_emu.model.Game  import org.yuzu.yuzu_emu.utils.InputHandler  import org.yuzu.yuzu_emu.utils.Log  import org.yuzu.yuzu_emu.utils.MemoryUtil +import org.yuzu.yuzu_emu.utils.NativeConfig  import org.yuzu.yuzu_emu.utils.NfcReader +import org.yuzu.yuzu_emu.utils.ParamPackage  import org.yuzu.yuzu_emu.utils.ThemeHelper  import java.text.NumberFormat  import kotlin.math.roundToInt @@ -63,8 +66,6 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {      private var motionTimestamp: Long = 0      private var flipMotionOrientation: Boolean = false -    private var controllerIds = InputHandler.getGameControllerIds() -      private val actionPause = "ACTION_EMULATOR_PAUSE"      private val actionPlay = "ACTION_EMULATOR_PLAY"      private val actionMute = "ACTION_EMULATOR_MUTE" @@ -78,6 +79,33 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {          super.onCreate(savedInstanceState) +        InputHandler.updateControllerData() +        val players = NativeConfig.getInputSettings(true) +        var hasConfiguredControllers = false +        players.forEach { +            if (it.hasMapping()) { +                hasConfiguredControllers = true +            } +        } +        if (!hasConfiguredControllers && InputHandler.androidControllers.isNotEmpty()) { +            var params: ParamPackage? = null +            for (controller in InputHandler.registeredControllers) { +                if (controller.get("port", -1) == 0) { +                    params = controller +                    break +                } +            } + +            if (params != null) { +                NativeInput.updateMappingsWithDefault( +                    0, +                    params, +                    params.get("display", getString(R.string.unknown)) +                ) +                NativeConfig.saveGlobalConfig() +            } +        } +          binding = ActivityEmulationBinding.inflate(layoutInflater)          setContentView(binding.root) @@ -95,8 +123,6 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {          nfcReader = NfcReader(this)          nfcReader.initialize() -        InputHandler.initialize() -          val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)          if (!preferences.getBoolean(Settings.PREF_MEMORY_WARNING_SHOWN, false)) {              if (MemoryUtil.isLessThan(MemoryUtil.REQUIRED_MEMORY, MemoryUtil.totalMemory)) { @@ -147,7 +173,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {          super.onResume()          nfcReader.startScanning()          startMotionSensorListener() -        InputHandler.updateControllerIds() +        InputHandler.updateControllerData()          buildPictureInPictureParams()      } @@ -172,6 +198,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {          super.onNewIntent(intent)          setIntent(intent)          nfcReader.onNewIntent(intent) +        InputHandler.updateControllerData()      }      override fun dispatchKeyEvent(event: KeyEvent): Boolean { @@ -244,8 +271,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {          }          val deltaTimestamp = (event.timestamp - motionTimestamp) / 1000          motionTimestamp = event.timestamp -        NativeLibrary.onGamePadMotionEvent( -            NativeLibrary.Player1Device, +        NativeInput.onDeviceMotionEvent( +            NativeInput.Player1Device,              deltaTimestamp,              gyro[0],              gyro[1], @@ -254,8 +281,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {              accel[1],              accel[2]          ) -        NativeLibrary.onGamePadMotionEvent( -            NativeLibrary.ConsoleDevice, +        NativeInput.onDeviceMotionEvent( +            NativeInput.ConsoleDevice,              deltaTimestamp,              gyro[0],              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 100644 --- 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 @@  package org.yuzu.yuzu_emu.adapters -import android.text.TextUtils  import android.view.LayoutInflater -import android.view.View  import android.view.ViewGroup  import org.yuzu.yuzu_emu.R  import org.yuzu.yuzu_emu.databinding.CardDriverOptionBinding  import org.yuzu.yuzu_emu.features.settings.model.StringSetting  import org.yuzu.yuzu_emu.model.Driver  import org.yuzu.yuzu_emu.model.DriverViewModel +import org.yuzu.yuzu_emu.utils.ViewUtils.marquee +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible  import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder  class DriverAdapter(private val driverViewModel: DriverViewModel) : @@ -44,25 +44,15 @@ class DriverAdapter(private val driverViewModel: DriverViewModel) :                  }                  // Delay marquee by 3s -                title.postDelayed( -                    { -                        title.isSelected = true -                        title.ellipsize = TextUtils.TruncateAt.MARQUEE -                        version.isSelected = true -                        version.ellipsize = TextUtils.TruncateAt.MARQUEE -                        description.isSelected = true -                        description.ellipsize = TextUtils.TruncateAt.MARQUEE -                    }, -                    3000 -                ) +                title.marquee() +                version.marquee() +                description.marquee()                  title.text = model.title                  version.text = model.version                  description.text = model.description -                if (model.title != binding.root.context.getString(R.string.system_gpu_driver)) { -                    buttonDelete.visibility = View.VISIBLE -                } else { -                    buttonDelete.visibility = View.GONE -                } +                buttonDelete.setVisible( +                    model.title != binding.root.context.getString(R.string.system_gpu_driver) +                )              }          }      } 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 100644 --- 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 @@  package org.yuzu.yuzu_emu.adapters  import android.net.Uri -import android.text.TextUtils  import android.view.LayoutInflater  import android.view.ViewGroup  import androidx.fragment.app.FragmentActivity @@ -12,6 +11,7 @@ import org.yuzu.yuzu_emu.databinding.CardFolderBinding  import org.yuzu.yuzu_emu.fragments.GameFolderPropertiesDialogFragment  import org.yuzu.yuzu_emu.model.GameDir  import org.yuzu.yuzu_emu.model.GamesViewModel +import org.yuzu.yuzu_emu.utils.ViewUtils.marquee  import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder  class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesViewModel) : @@ -29,13 +29,7 @@ class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesVie          override fun bind(model: GameDir) {              binding.apply {                  path.text = Uri.parse(model.uriString).path -                path.postDelayed( -                    { -                        path.isSelected = true -                        path.ellipsize = TextUtils.TruncateAt.MARQUEE -                    }, -                    3000 -                ) +                path.marquee()                  buttonEdit.setOnClickListener {                      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 100644 --- 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 @@  package org.yuzu.yuzu_emu.adapters  import android.net.Uri -import android.text.TextUtils  import android.view.LayoutInflater  import android.view.ViewGroup  import android.widget.ImageView @@ -27,6 +26,7 @@ import org.yuzu.yuzu_emu.databinding.CardGameBinding  import org.yuzu.yuzu_emu.model.Game  import org.yuzu.yuzu_emu.model.GamesViewModel  import org.yuzu.yuzu_emu.utils.GameIconUtils +import org.yuzu.yuzu_emu.utils.ViewUtils.marquee  import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder  class GameAdapter(private val activity: AppCompatActivity) : @@ -44,14 +44,7 @@ class GameAdapter(private val activity: AppCompatActivity) :              binding.textGameTitle.text = model.title.replace("[\\t\\n\\r]+".toRegex(), " ") -            binding.textGameTitle.postDelayed( -                { -                    binding.textGameTitle.ellipsize = TextUtils.TruncateAt.MARQUEE -                    binding.textGameTitle.isSelected = true -                }, -                3000 -            ) - +            binding.textGameTitle.marquee()              binding.cardGame.setOnClickListener { onClick(model) }              binding.cardGame.setOnLongClickListener { onLongClick(model) }          } 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 100644 --- 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 @@  package org.yuzu.yuzu_emu.adapters -import android.text.TextUtils  import android.view.LayoutInflater -import android.view.View  import android.view.ViewGroup  import androidx.core.content.res.ResourcesCompat -import androidx.lifecycle.Lifecycle  import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import kotlinx.coroutines.launch  import org.yuzu.yuzu_emu.databinding.CardInstallableIconBinding  import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding  import org.yuzu.yuzu_emu.model.GameProperty  import org.yuzu.yuzu_emu.model.InstallableProperty  import org.yuzu.yuzu_emu.model.SubmenuProperty +import org.yuzu.yuzu_emu.utils.ViewUtils.marquee +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible +import org.yuzu.yuzu_emu.utils.collect  import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder  class GamePropertiesAdapter( @@ -76,23 +73,15 @@ class GamePropertiesAdapter(                  )              ) -            binding.details.postDelayed({ -                binding.details.isSelected = true -                binding.details.ellipsize = TextUtils.TruncateAt.MARQUEE -            }, 3000) - +            binding.details.marquee()              if (submenuProperty.details != null) { -                binding.details.visibility = View.VISIBLE +                binding.details.setVisible(true)                  binding.details.text = submenuProperty.details.invoke()              } else if (submenuProperty.detailsFlow != null) { -                binding.details.visibility = View.VISIBLE -                viewLifecycle.lifecycleScope.launch { -                    viewLifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { -                        submenuProperty.detailsFlow.collect { binding.details.text = it } -                    } -                } +                binding.details.setVisible(true) +                submenuProperty.detailsFlow.collect(viewLifecycle) { binding.details.text = it }              } else { -                binding.details.visibility = View.GONE +                binding.details.setVisible(false)              }          }      } @@ -112,14 +101,10 @@ class GamePropertiesAdapter(                  )              ) -            if (installableProperty.install != null) { -                binding.buttonInstall.visibility = View.VISIBLE -                binding.buttonInstall.setOnClickListener { installableProperty.install.invoke() } -            } -            if (installableProperty.export != null) { -                binding.buttonExport.visibility = View.VISIBLE -                binding.buttonExport.setOnClickListener { installableProperty.export.invoke() } -            } +            binding.buttonInstall.setVisible(installableProperty.install != null) +            binding.buttonInstall.setOnClickListener { installableProperty.install?.invoke() } +            binding.buttonExport.setVisible(installableProperty.export != null) +            binding.buttonExport.setOnClickListener { installableProperty.export?.invoke() }          }      } 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 100644 --- 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 @@  package org.yuzu.yuzu_emu.adapters -import android.text.TextUtils  import android.view.LayoutInflater -import android.view.View  import android.view.ViewGroup  import androidx.appcompat.app.AppCompatActivity  import androidx.core.content.ContextCompat  import androidx.core.content.res.ResourcesCompat -import androidx.lifecycle.Lifecycle  import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import kotlinx.coroutines.launch  import org.yuzu.yuzu_emu.R  import org.yuzu.yuzu_emu.databinding.CardHomeOptionBinding  import org.yuzu.yuzu_emu.fragments.MessageDialogFragment  import org.yuzu.yuzu_emu.model.HomeSetting +import org.yuzu.yuzu_emu.utils.ViewUtils.marquee +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible +import org.yuzu.yuzu_emu.utils.collect  import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder  class HomeSettingAdapter( @@ -59,18 +56,8 @@ class HomeSettingAdapter(                  binding.optionIcon.alpha = 0.5f              } -            viewLifecycle.lifecycleScope.launch { -                viewLifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) { -                    model.details.collect { updateOptionDetails(it) } -                } -            } -            binding.optionDetail.postDelayed( -                { -                    binding.optionDetail.ellipsize = TextUtils.TruncateAt.MARQUEE -                    binding.optionDetail.isSelected = true -                }, -                3000 -            ) +            model.details.collect(viewLifecycle) { updateOptionDetails(it) } +            binding.optionDetail.marquee()              binding.root.setOnClickListener { onClick(model) }          } @@ -90,7 +77,7 @@ class HomeSettingAdapter(          private fun updateOptionDetails(detailString: String) {              if (detailString.isNotEmpty()) {                  binding.optionDetail.text = detailString -                binding.optionDetail.visibility = View.VISIBLE +                binding.optionDetail.setVisible(true)              }          }      } 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 100644 --- 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 @@  package org.yuzu.yuzu_emu.adapters  import android.view.LayoutInflater -import android.view.View  import android.view.ViewGroup  import org.yuzu.yuzu_emu.databinding.CardInstallableBinding  import org.yuzu.yuzu_emu.model.Installable +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible  import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder  class InstallableAdapter(installables: List<Installable>) : @@ -26,14 +26,10 @@ class InstallableAdapter(installables: List<Installable>) :              binding.title.setText(model.titleId)              binding.description.setText(model.descriptionId) -            if (model.install != null) { -                binding.buttonInstall.visibility = View.VISIBLE -                binding.buttonInstall.setOnClickListener { model.install.invoke() } -            } -            if (model.export != null) { -                binding.buttonExport.visibility = View.VISIBLE -                binding.buttonExport.setOnClickListener { model.export.invoke() } -            } +            binding.buttonInstall.setVisible(model.install != null) +            binding.buttonInstall.setOnClickListener { model.install?.invoke() } +            binding.buttonExport.setVisible(model.export != null) +            binding.buttonExport.setOnClickListener { model.export?.invoke() }          }      }  } 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 100644 --- 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 @@  package org.yuzu.yuzu_emu.adapters  import android.view.LayoutInflater -import android.view.View  import android.view.ViewGroup  import androidx.appcompat.app.AppCompatActivity  import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding  import org.yuzu.yuzu_emu.fragments.LicenseBottomSheetDialogFragment  import org.yuzu.yuzu_emu.model.License +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible  import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder  class LicenseAdapter(private val activity: AppCompatActivity, licenses: List<License>) : @@ -25,7 +25,7 @@ class LicenseAdapter(private val activity: AppCompatActivity, licenses: List<Lic              binding.apply {                  textSettingName.text = root.context.getString(model.titleId)                  textSettingDescription.text = root.context.getString(model.descriptionId) -                textSettingValue.visibility = View.GONE +                textSettingValue.setVisible(false)                  root.setOnClickListener { onClick(model) }              } 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 100644 --- 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  import android.text.Html  import android.view.LayoutInflater -import android.view.View  import android.view.ViewGroup  import androidx.appcompat.app.AppCompatActivity  import androidx.core.content.res.ResourcesCompat @@ -17,6 +16,7 @@ import org.yuzu.yuzu_emu.model.SetupCallback  import org.yuzu.yuzu_emu.model.SetupPage  import org.yuzu.yuzu_emu.model.StepState  import org.yuzu.yuzu_emu.utils.ViewUtils +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible  import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder  class SetupAdapter(val activity: AppCompatActivity, pages: List<SetupPage>) : @@ -30,8 +30,8 @@ class SetupAdapter(val activity: AppCompatActivity, pages: List<SetupPage>) :          AbstractViewHolder<SetupPage>(binding), SetupCallback {          override fun bind(model: SetupPage) {              if (model.stepCompleted.invoke() == StepState.COMPLETE) { -                binding.buttonAction.visibility = View.INVISIBLE -                binding.textConfirmation.visibility = View.VISIBLE +                binding.buttonAction.setVisible(visible = false, gone = false) +                binding.textConfirmation.setVisible(true)              }              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 100644 index 000000000..15d776311 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/NativeInput.kt @@ -0,0 +1,416 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.input + +import org.yuzu.yuzu_emu.features.input.model.NativeButton +import org.yuzu.yuzu_emu.features.input.model.NativeAnalog +import org.yuzu.yuzu_emu.features.input.model.InputType +import org.yuzu.yuzu_emu.features.input.model.ButtonName +import org.yuzu.yuzu_emu.features.input.model.NpadStyleIndex +import org.yuzu.yuzu_emu.utils.NativeConfig +import org.yuzu.yuzu_emu.utils.ParamPackage +import android.view.InputDevice + +object NativeInput { +    /** +     * Default controller id for each device +     */ +    const val Player1Device = 0 +    const val Player2Device = 1 +    const val Player3Device = 2 +    const val Player4Device = 3 +    const val Player5Device = 4 +    const val Player6Device = 5 +    const val Player7Device = 6 +    const val Player8Device = 7 +    const val ConsoleDevice = 8 + +    /** +     * Button states +     */ +    object ButtonState { +        const val RELEASED = 0 +        const val PRESSED = 1 +    } + +    /** +     * Returns true if pro controller isn't available and handheld is. +     * Intended to check where the input overlay should direct its inputs. +     */ +    external fun isHandheldOnly(): Boolean + +    /** +     * Handles button press events for a gamepad. +     * @param guid 32 character hexadecimal string consisting of the controller's PID+VID. +     * @param port Port determined by controller connection order. +     * @param buttonId The Android Keycode corresponding to this event. +     * @param action Mask identifying which action is happening (button pressed down, or button released). +     */ +    external fun onGamePadButtonEvent( +        guid: String, +        port: Int, +        buttonId: Int, +        action: Int +    ) + +    /** +     * Handles axis movement events. +     * @param guid 32 character hexadecimal string consisting of the controller's PID+VID. +     * @param port Port determined by controller connection order. +     * @param axis The axis ID. +     * @param value Value along the given axis. +     */ +    external fun onGamePadAxisEvent(guid: String, port: Int, axis: Int, value: Float) + +    /** +     * Handles motion events. +     * @param guid 32 character hexadecimal string consisting of the controller's PID+VID. +     * @param port Port determined by controller connection order. +     * @param deltaTimestamp The finger id corresponding to this event. +     * @param xGyro The value of the x-axis for the gyroscope. +     * @param yGyro The value of the y-axis for the gyroscope. +     * @param zGyro The value of the z-axis for the gyroscope. +     * @param xAccel The value of the x-axis for the accelerometer. +     * @param yAccel The value of the y-axis for the accelerometer. +     * @param zAccel The value of the z-axis for the accelerometer. +     */ +    external fun onGamePadMotionEvent( +        guid: String, +        port: Int, +        deltaTimestamp: Long, +        xGyro: Float, +        yGyro: Float, +        zGyro: Float, +        xAccel: Float, +        yAccel: Float, +        zAccel: Float +    ) + +    /** +     * Signals and load a nfc tag +     * @param data Byte array containing all the data from a nfc tag. +     */ +    external fun onReadNfcTag(data: ByteArray?) + +    /** +     * Removes current loaded nfc tag. +     */ +    external fun onRemoveNfcTag() + +    /** +     * Handles touch press events. +     * @param fingerId The finger id corresponding to this event. +     * @param xAxis The value of the x-axis on the touchscreen. +     * @param yAxis The value of the y-axis on the touchscreen. +     */ +    external fun onTouchPressed(fingerId: Int, xAxis: Float, yAxis: Float) + +    /** +     * Handles touch movement. +     * @param fingerId The finger id corresponding to this event. +     * @param xAxis The value of the x-axis on the touchscreen. +     * @param yAxis The value of the y-axis on the touchscreen. +     */ +    external fun onTouchMoved(fingerId: Int, xAxis: Float, yAxis: Float) + +    /** +     * Handles touch release events. +     * @param fingerId The finger id corresponding to this event +     */ +    external fun onTouchReleased(fingerId: Int) + +    /** +     * Sends a button input to the global virtual controllers. +     * @param port Port determined by controller connection order. +     * @param button The [NativeButton] corresponding to this event. +     * @param action Mask identifying which action is happening (button pressed down, or button released). +     */ +    fun onOverlayButtonEvent(port: Int, button: NativeButton, action: Int) = +        onOverlayButtonEventImpl(port, button.int, action) + +    private external fun onOverlayButtonEventImpl(port: Int, buttonId: Int, action: Int) + +    /** +     * Sends a joystick input to the global virtual controllers. +     * @param port Port determined by controller connection order. +     * @param stick The [NativeAnalog] corresponding to this event. +     * @param xAxis Value along the X axis. +     * @param yAxis Value along the Y axis. +     */ +    fun onOverlayJoystickEvent(port: Int, stick: NativeAnalog, xAxis: Float, yAxis: Float) = +        onOverlayJoystickEventImpl(port, stick.int, xAxis, yAxis) + +    private external fun onOverlayJoystickEventImpl( +        port: Int, +        stickId: Int, +        xAxis: Float, +        yAxis: Float +    ) + +    /** +     * Handles motion events for the global virtual controllers. +     * @param port Port determined by controller connection order +     * @param deltaTimestamp The finger id corresponding to this event. +     * @param xGyro The value of the x-axis for the gyroscope. +     * @param yGyro The value of the y-axis for the gyroscope. +     * @param zGyro The value of the z-axis for the gyroscope. +     * @param xAccel The value of the x-axis for the accelerometer. +     * @param yAccel The value of the y-axis for the accelerometer. +     * @param zAccel The value of the z-axis for the accelerometer. +     */ +    external fun onDeviceMotionEvent( +        port: Int, +        deltaTimestamp: Long, +        xGyro: Float, +        yGyro: Float, +        zGyro: Float, +        xAccel: Float, +        yAccel: Float, +        zAccel: Float +    ) + +    /** +     * Reloads all input devices from the currently loaded Settings::values.players into HID Core +     */ +    external fun reloadInputDevices() + +    /** +     * Registers a controller to be used with mapping +     * @param device An [InputDevice] or the input overlay wrapped with [YuzuInputDevice] +     */ +    external fun registerController(device: YuzuInputDevice) + +    /** +     * Gets the names of input devices that have been registered with the input subsystem via [registerController] +     */ +    external fun getInputDevices(): Array<String> + +    /** +     * Reads all input profiles from disk. Must be called before creating a profile picker. +     */ +    external fun loadInputProfiles() + +    /** +     * Gets the names of each available input profile. +     */ +    external fun getInputProfileNames(): Array<String> + +    /** +     * Checks if the user-provided name for an input profile is valid. +     * @param name User-provided name for an input profile. +     * @return Whether [name] is valid or not. +     */ +    external fun isProfileNameValid(name: String): Boolean + +    /** +     * Creates a new input profile. +     * @param name The new profile's name. +     * @param playerIndex Index of the player that's currently being edited. Used to write the profile +     * name to this player's config. +     * @return Whether creating the profile was successful or not. +     */ +    external fun createProfile(name: String, playerIndex: Int): Boolean + +    /** +     * Deletes an input profile. +     * @param name Name of the profile to delete. +     * @param playerIndex Index of the player that's currently being edited. Used to remove the profile +     * name from this player's config if they have it loaded. +     * @return Whether deleting this profile was successful or not. +     */ +    external fun deleteProfile(name: String, playerIndex: Int): Boolean + +    /** +     * Loads an input profile. +     * @param name Name of the input profile to load. +     * @param playerIndex Index of the player that will have this profile loaded. +     * @return Whether loading this profile was successful or not. +     */ +    external fun loadProfile(name: String, playerIndex: Int): Boolean + +    /** +     * Saves an input profile. +     * @param name Name of the profile to save. +     * @param playerIndex Index of the player that's currently being edited. Used to write the profile +     * name to this player's config. +     * @return Whether saving the profile was successful or not. +     */ +    external fun saveProfile(name: String, playerIndex: Int): Boolean + +    /** +     * Intended to be used immediately before a call to [NativeConfig.saveControlPlayerValues] +     * Must be used while per-game config is loaded. +     */ +    external fun loadPerGameConfiguration( +        playerIndex: Int, +        selectedIndex: Int, +        selectedProfileName: String +    ) + +    /** +     * Tells the input subsystem to start listening for inputs to map. +     * @param type Type of input to map as shown by the int property in each [InputType]. +     */ +    external fun beginMapping(type: Int) + +    /** +     * Gets an input's [ParamPackage] as a serialized string. Used for input verification before mapping. +     * Must be run after [beginMapping] and before [stopMapping]. +     */ +    external fun getNextInput(): String + +    /** +     * Tells the input subsystem to stop listening for inputs to map. +     */ +    external fun stopMapping() + +    /** +     * Updates a controller's mappings with auto-mapping params. +     * @param playerIndex Index of the player to auto-map. +     * @param deviceParams [ParamPackage] representing the device to auto-map as received +     * from [getInputDevices]. +     * @param displayName Name of the device to auto-map as received from the "display" param in [deviceParams]. +     * Intended to be a way to provide a default name for a controller if the "display" param is empty. +     */ +    fun updateMappingsWithDefault( +        playerIndex: Int, +        deviceParams: ParamPackage, +        displayName: String +    ) = updateMappingsWithDefaultImpl(playerIndex, deviceParams.serialize(), displayName) + +    private external fun updateMappingsWithDefaultImpl( +        playerIndex: Int, +        deviceParams: String, +        displayName: String +    ) + +    /** +     * Gets the params for a specific button. +     * @param playerIndex Index of the player to get params from. +     * @param button The [NativeButton] to get params for. +     * @return A [ParamPackage] representing a player's specific button. +     */ +    fun getButtonParam(playerIndex: Int, button: NativeButton): ParamPackage = +        ParamPackage(getButtonParamImpl(playerIndex, button.int)) + +    private external fun getButtonParamImpl(playerIndex: Int, buttonId: Int): String + +    /** +     * Sets the params for a specific button. +     * @param playerIndex Index of the player to set params for. +     * @param button The [NativeButton] to set params for. +     * @param param A [ParamPackage] to set. +     */ +    fun setButtonParam(playerIndex: Int, button: NativeButton, param: ParamPackage) = +        setButtonParamImpl(playerIndex, button.int, param.serialize()) + +    private external fun setButtonParamImpl(playerIndex: Int, buttonId: Int, param: String) + +    /** +     * Gets the params for a specific stick. +     * @param playerIndex Index of the player to get params from. +     * @param stick The [NativeAnalog] to get params for. +     * @return A [ParamPackage] representing a player's specific stick. +     */ +    fun getStickParam(playerIndex: Int, stick: NativeAnalog): ParamPackage = +        ParamPackage(getStickParamImpl(playerIndex, stick.int)) + +    private external fun getStickParamImpl(playerIndex: Int, stickId: Int): String + +    /** +     * Sets the params for a specific stick. +     * @param playerIndex Index of the player to set params for. +     * @param stick The [NativeAnalog] to set params for. +     * @param param A [ParamPackage] to set. +     */ +    fun setStickParam(playerIndex: Int, stick: NativeAnalog, param: ParamPackage) = +        setStickParamImpl(playerIndex, stick.int, param.serialize()) + +    private external fun setStickParamImpl(playerIndex: Int, stickId: Int, param: String) + +    /** +     * Gets the int representation of a [ButtonName]. Tells you what to show as the mapped input for +     * a button/analog/other. +     * @param param A [ParamPackage] that represents a specific button's params. +     * @return The [ButtonName] for [param]. +     */ +    fun getButtonName(param: ParamPackage): ButtonName = +        ButtonName.from(getButtonNameImpl(param.serialize())) + +    private external fun getButtonNameImpl(param: String): Int + +    /** +     * Gets each supported [NpadStyleIndex] for a given player. +     * @param playerIndex Index of the player to get supported indexes for. +     * @return List of each supported [NpadStyleIndex]. +     */ +    fun getSupportedStyleTags(playerIndex: Int): List<NpadStyleIndex> = +        getSupportedStyleTagsImpl(playerIndex).map { NpadStyleIndex.from(it) } + +    private external fun getSupportedStyleTagsImpl(playerIndex: Int): IntArray + +    /** +     * Gets the [NpadStyleIndex] for a given player. +     * @param playerIndex Index of the player to get an [NpadStyleIndex] from. +     * @return The [NpadStyleIndex] for a given player. +     */ +    fun getStyleIndex(playerIndex: Int): NpadStyleIndex = +        NpadStyleIndex.from(getStyleIndexImpl(playerIndex)) + +    private external fun getStyleIndexImpl(playerIndex: Int): Int + +    /** +     * Sets the [NpadStyleIndex] for a given player. +     * @param playerIndex Index of the player to change. +     * @param style The new style to set. +     */ +    fun setStyleIndex(playerIndex: Int, style: NpadStyleIndex) = +        setStyleIndexImpl(playerIndex, style.int) + +    private external fun setStyleIndexImpl(playerIndex: Int, styleIndex: Int) + +    /** +     * Checks if a device is a controller. +     * @param params [ParamPackage] for an input device retrieved from [getInputDevices] +     * @return Whether the device is a controller or not. +     */ +    fun isController(params: ParamPackage): Boolean = isControllerImpl(params.serialize()) + +    private external fun isControllerImpl(params: String): Boolean + +    /** +     * Checks if a controller is connected +     * @param playerIndex Index of the player to check. +     * @return Whether the player is connected or not. +     */ +    external fun getIsConnected(playerIndex: Int): Boolean + +    /** +     * Connects/disconnects a controller and ensures that connection order stays in-tact. +     * @param playerIndex Index of the player to connect/disconnect. +     * @param connected Whether to connect or disconnect this controller. +     */ +    fun connectControllers(playerIndex: Int, connected: Boolean = true) { +        val connectedControllers = mutableListOf<Boolean>().apply { +            if (connected) { +                for (i in 0 until 8) { +                    add(i <= playerIndex) +                } +            } else { +                for (i in 0 until 8) { +                    add(i < playerIndex) +                } +            } +        } +        connectControllersImpl(connectedControllers.toBooleanArray()) +    } + +    private external fun connectControllersImpl(connected: BooleanArray) + +    /** +     * Resets all of the button and analog mappings for a player. +     * @param playerIndex Index of the player that will have its mappings reset. +     */ +    external fun resetControllerMappings(playerIndex: Int) +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuInputDevice.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuInputDevice.kt new file mode 100644 index 000000000..15cc38c7f --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuInputDevice.kt @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.input + +import android.view.InputDevice +import androidx.annotation.Keep +import org.yuzu.yuzu_emu.YuzuApplication +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.utils.InputHandler.getGUID + +@Keep +interface YuzuInputDevice { +    fun getName(): String + +    fun getGUID(): String + +    fun getPort(): Int + +    fun getSupportsVibration(): Boolean + +    fun vibrate(intensity: Float) + +    fun getAxes(): Array<Int> = arrayOf() +    fun hasKeys(keys: IntArray): BooleanArray = BooleanArray(0) +} + +class YuzuPhysicalDevice( +    private val device: InputDevice, +    private val port: Int, +    useSystemVibrator: Boolean +) : YuzuInputDevice { +    private val vibrator = if (useSystemVibrator) { +        YuzuVibrator.getSystemVibrator() +    } else { +        YuzuVibrator.getControllerVibrator(device) +    } + +    override fun getName(): String { +        return device.name +    } + +    override fun getGUID(): String { +        return device.getGUID() +    } + +    override fun getPort(): Int { +        return port +    } + +    override fun getSupportsVibration(): Boolean { +        return vibrator.supportsVibration() +    } + +    override fun vibrate(intensity: Float) { +        vibrator.vibrate(intensity) +    } + +    override fun getAxes(): Array<Int> = device.motionRanges.map { it.axis }.toTypedArray() +    override fun hasKeys(keys: IntArray): BooleanArray = device.hasKeys(*keys) +} + +class YuzuInputOverlayDevice( +    private val vibration: Boolean, +    private val port: Int +) : YuzuInputDevice { +    private val vibrator = YuzuVibrator.getSystemVibrator() + +    override fun getName(): String { +        return YuzuApplication.appContext.getString(R.string.input_overlay) +    } + +    override fun getGUID(): String { +        return "00000000000000000000000000000000" +    } + +    override fun getPort(): Int { +        return port +    } + +    override fun getSupportsVibration(): Boolean { +        if (vibration) { +            return vibrator.supportsVibration() +        } +        return false +    } + +    override fun vibrate(intensity: Float) { +        if (vibration) { +            vibrator.vibrate(intensity) +        } +    } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuVibrator.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuVibrator.kt new file mode 100644 index 000000000..aac49ecae --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuVibrator.kt @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.input + +import android.content.Context +import android.os.Build +import android.os.CombinedVibration +import android.os.VibrationEffect +import android.os.Vibrator +import android.os.VibratorManager +import android.view.InputDevice +import androidx.annotation.Keep +import androidx.annotation.RequiresApi +import org.yuzu.yuzu_emu.YuzuApplication + +@Keep +@Suppress("DEPRECATION") +interface YuzuVibrator { +    fun supportsVibration(): Boolean + +    fun vibrate(intensity: Float) + +    companion object { +        fun getControllerVibrator(device: InputDevice): YuzuVibrator = +            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { +                YuzuVibratorManager(device.vibratorManager) +            } else { +                YuzuVibratorManagerCompat(device.vibrator) +            } + +        fun getSystemVibrator(): YuzuVibrator = +            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { +                val vibratorManager = YuzuApplication.appContext +                    .getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager +                YuzuVibratorManager(vibratorManager) +            } else { +                val vibrator = YuzuApplication.appContext +                    .getSystemService(Context.VIBRATOR_SERVICE) as Vibrator +                YuzuVibratorManagerCompat(vibrator) +            } + +        fun getVibrationEffect(intensity: Float): VibrationEffect? { +            if (intensity > 0f) { +                return VibrationEffect.createOneShot( +                    50, +                    (255.0 * intensity).toInt().coerceIn(1, 255) +                ) +            } +            return null +        } +    } +} + +@RequiresApi(Build.VERSION_CODES.S) +class YuzuVibratorManager(private val vibratorManager: VibratorManager) : YuzuVibrator { +    override fun supportsVibration(): Boolean { +        return vibratorManager.vibratorIds.isNotEmpty() +    } + +    override fun vibrate(intensity: Float) { +        val vibration = YuzuVibrator.getVibrationEffect(intensity) ?: return +        vibratorManager.vibrate(CombinedVibration.createParallel(vibration)) +    } +} + +class YuzuVibratorManagerCompat(private val vibrator: Vibrator) : YuzuVibrator { +    override fun supportsVibration(): Boolean { +        return vibrator.hasVibrator() +    } + +    override fun vibrate(intensity: Float) { +        val vibration = YuzuVibrator.getVibrationEffect(intensity) ?: return +        vibrator.vibrate(vibration) +    } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/AnalogDirection.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/AnalogDirection.kt new file mode 100644 index 000000000..0a5fab2ae --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/AnalogDirection.kt @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.input.model + +enum class AnalogDirection(val int: Int, val param: String) { +    Up(0, "up"), +    Down(1, "down"), +    Left(2, "left"), +    Right(3, "right") +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/ButtonName.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/ButtonName.kt new file mode 100644 index 000000000..b8846ecad --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/ButtonName.kt @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.input.model + +// Loosely matches the enum in common/input.h +enum class ButtonName(val int: Int) { +    Invalid(1), + +    // This will display the engine name instead of the button name +    Engine(2), + +    // This will display the button by value instead of the button name +    Value(3); + +    companion object { +        fun from(int: Int): ButtonName = entries.firstOrNull { it.int == int } ?: Invalid +    } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/InputType.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/InputType.kt new file mode 100644 index 000000000..f725231cb --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/InputType.kt @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.input.model + +// Must match the corresponding enum in input_common/main.h +enum class InputType(val int: Int) { +    None(0), +    Button(1), +    Stick(2), +    Motion(3), +    Touch(4) +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeAnalog.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeAnalog.kt new file mode 100644 index 000000000..c3b7a785d --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeAnalog.kt @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.input.model + +// Must match enum in src/common/settings_input.h +enum class NativeAnalog(val int: Int) { +    LStick(0), +    RStick(1); + +    companion object { +        fun from(int: Int): NativeAnalog = entries.firstOrNull { it.int == int } ?: LStick +    } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeButton.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeButton.kt new file mode 100644 index 000000000..c5ccd7115 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeButton.kt @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.input.model + +// Must match enum in src/common/settings_input.h +enum class NativeButton(val int: Int) { +    A(0), +    B(1), +    X(2), +    Y(3), +    LStick(4), +    RStick(5), +    L(6), +    R(7), +    ZL(8), +    ZR(9), +    Plus(10), +    Minus(11), + +    DLeft(12), +    DUp(13), +    DRight(14), +    DDown(15), + +    SLLeft(16), +    SRLeft(17), + +    Home(18), +    Capture(19), + +    SLRight(20), +    SRRight(21); + +    companion object { +        fun from(int: Int): NativeButton = entries.firstOrNull { it.int == int } ?: A +    } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeTrigger.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeTrigger.kt new file mode 100644 index 000000000..625f352b4 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeTrigger.kt @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.input.model + +// Must match enum in src/common/settings_input.h +enum class NativeTrigger(val int: Int) { +    LTrigger(0), +    RTrigger(1) +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NpadStyleIndex.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NpadStyleIndex.kt new file mode 100644 index 000000000..e2a3d7aff --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NpadStyleIndex.kt @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.input.model + +import androidx.annotation.StringRes +import org.yuzu.yuzu_emu.R + +// Must match enum in src/core/hid/hid_types.h +enum class NpadStyleIndex(val int: Int, @StringRes val nameId: Int = 0) { +    None(0), +    Fullkey(3, R.string.pro_controller), +    Handheld(4, R.string.handheld), +    HandheldNES(4), +    JoyconDual(5, R.string.dual_joycons), +    JoyconLeft(6, R.string.left_joycon), +    JoyconRight(7, R.string.right_joycon), +    GameCube(8, R.string.gamecube_controller), +    Pokeball(9), +    NES(10), +    SNES(12), +    N64(13), +    SegaGenesis(14), +    SystemExt(32), +    System(33); + +    companion object { +        fun from(int: Int): NpadStyleIndex = entries.firstOrNull { it.int == int } ?: None +    } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/PlayerInput.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/PlayerInput.kt new file mode 100644 index 000000000..d35de80c4 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/PlayerInput.kt @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.input.model + +import androidx.annotation.Keep + +@Keep +data class PlayerInput( +    var connected: Boolean, +    var buttons: Array<String>, +    var analogs: Array<String>, +    var motions: Array<String>, + +    var vibrationEnabled: Boolean, +    var vibrationStrength: Int, + +    var bodyColorLeft: Long, +    var bodyColorRight: Long, +    var buttonColorLeft: Long, +    var buttonColorRight: Long, +    var profileName: String, + +    var useSystemVibrator: Boolean +) { +    // It's recommended to use the generated equals() and hashCode() methods +    // when using arrays in a data class +    override fun equals(other: Any?): Boolean { +        if (this === other) return true +        if (javaClass != other?.javaClass) return false + +        other as PlayerInput + +        if (connected != other.connected) return false +        if (!buttons.contentEquals(other.buttons)) return false +        if (!analogs.contentEquals(other.analogs)) return false +        if (!motions.contentEquals(other.motions)) return false +        if (vibrationEnabled != other.vibrationEnabled) return false +        if (vibrationStrength != other.vibrationStrength) return false +        if (bodyColorLeft != other.bodyColorLeft) return false +        if (bodyColorRight != other.bodyColorRight) return false +        if (buttonColorLeft != other.buttonColorLeft) return false +        if (buttonColorRight != other.buttonColorRight) return false +        if (profileName != other.profileName) return false +        return useSystemVibrator == other.useSystemVibrator +    } + +    override fun hashCode(): Int { +        var result = connected.hashCode() +        result = 31 * result + buttons.contentHashCode() +        result = 31 * result + analogs.contentHashCode() +        result = 31 * result + motions.contentHashCode() +        result = 31 * result + vibrationEnabled.hashCode() +        result = 31 * result + vibrationStrength +        result = 31 * result + bodyColorLeft.hashCode() +        result = 31 * result + bodyColorRight.hashCode() +        result = 31 * result + buttonColorLeft.hashCode() +        result = 31 * result + buttonColorRight.hashCode() +        result = 31 * result + profileName.hashCode() +        result = 31 * result + useSystemVibrator.hashCode() +        return result +    } + +    fun hasMapping(): Boolean { +        var hasMapping = false +        buttons.forEach { +            if (it != "[empty]") { +                hasMapping = true +            } +        } +        analogs.forEach { +            if (it != "[empty]") { +                hasMapping = true +            } +        } +        motions.forEach { +            if (it != "[empty]") { +                hasMapping = true +            } +        } +        return hasMapping +    } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt index 862c6c483..4f6b93bd2 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt @@ -4,17 +4,30 @@  package org.yuzu.yuzu_emu.features.settings.model  import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.YuzuApplication  object Settings { -    enum class MenuTag(val titleId: Int) { +    enum class MenuTag(val titleId: Int = 0) {          SECTION_ROOT(R.string.advanced_settings),          SECTION_SYSTEM(R.string.preferences_system),          SECTION_RENDERER(R.string.preferences_graphics),          SECTION_AUDIO(R.string.preferences_audio), +        SECTION_INPUT(R.string.preferences_controls), +        SECTION_INPUT_PLAYER_ONE, +        SECTION_INPUT_PLAYER_TWO, +        SECTION_INPUT_PLAYER_THREE, +        SECTION_INPUT_PLAYER_FOUR, +        SECTION_INPUT_PLAYER_FIVE, +        SECTION_INPUT_PLAYER_SIX, +        SECTION_INPUT_PLAYER_SEVEN, +        SECTION_INPUT_PLAYER_EIGHT,          SECTION_THEME(R.string.preferences_theme),          SECTION_DEBUG(R.string.preferences_debug);      } +    fun getPlayerString(player: Int): String = +        YuzuApplication.appContext.getString(R.string.preferences_player, player) +      const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch"      const val PREF_MEMORY_WARNING_SHOWN = "MemoryWarningShown" diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/AnalogInputSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/AnalogInputSetting.kt new file mode 100644 index 000000000..a2996725e --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/AnalogInputSetting.kt @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.settings.model.view + +import androidx.annotation.StringRes +import org.yuzu.yuzu_emu.features.input.NativeInput +import org.yuzu.yuzu_emu.features.input.model.AnalogDirection +import org.yuzu.yuzu_emu.features.input.model.InputType +import org.yuzu.yuzu_emu.features.input.model.NativeAnalog +import org.yuzu.yuzu_emu.utils.ParamPackage + +class AnalogInputSetting( +    override val playerIndex: Int, +    val nativeAnalog: NativeAnalog, +    val analogDirection: AnalogDirection, +    @StringRes titleId: Int = 0, +    titleString: String = "" +) : InputSetting(titleId, titleString) { +    override val type = TYPE_INPUT +    override val inputType = InputType.Stick + +    override fun getSelectedValue(): String { +        val params = NativeInput.getStickParam(playerIndex, nativeAnalog) +        val analog = analogToText(params, analogDirection.param) +        return getDisplayString(params, analog) +    } + +    override fun setSelectedValue(param: ParamPackage) = +        NativeInput.setStickParam(playerIndex, nativeAnalog, param) +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ButtonInputSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ButtonInputSetting.kt new file mode 100644 index 000000000..786d09a7a --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ButtonInputSetting.kt @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.settings.model.view + +import androidx.annotation.StringRes +import org.yuzu.yuzu_emu.utils.ParamPackage +import org.yuzu.yuzu_emu.features.input.NativeInput +import org.yuzu.yuzu_emu.features.input.model.InputType +import org.yuzu.yuzu_emu.features.input.model.NativeButton + +class ButtonInputSetting( +    override val playerIndex: Int, +    val nativeButton: NativeButton, +    @StringRes titleId: Int = 0, +    titleString: String = "" +) : InputSetting(titleId, titleString) { +    override val type = TYPE_INPUT +    override val inputType = InputType.Button + +    override fun getSelectedValue(): String { +        val params = NativeInput.getButtonParam(playerIndex, nativeButton) +        val button = buttonToText(params) +        return getDisplayString(params, button) +    } + +    override fun setSelectedValue(param: ParamPackage) = +        NativeInput.setButtonParam(playerIndex, nativeButton, param) +} 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 100644 --- 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 @@  package org.yuzu.yuzu_emu.features.settings.model.view +import androidx.annotation.StringRes  import org.yuzu.yuzu_emu.features.settings.model.AbstractLongSetting  class DateTimeSetting(      private val longSetting: AbstractLongSetting, -    titleId: Int, -    descriptionId: Int -) : SettingsItem(longSetting, titleId, descriptionId) { +    @StringRes titleId: Int = 0, +    titleString: String = "", +    @StringRes descriptionId: Int = 0, +    descriptionString: String = "" +) : SettingsItem(longSetting, titleId, titleString, descriptionId, descriptionString) {      override val type = TYPE_DATETIME_SETTING      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 100644 --- 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 @@  package org.yuzu.yuzu_emu.features.settings.model.view +import androidx.annotation.StringRes +  class HeaderSetting( -    titleId: Int -) : SettingsItem(emptySetting, titleId, 0) { +    @StringRes titleId: Int = 0, +    titleString: String = "" +) : SettingsItem(emptySetting, titleId, titleString, 0, "") {      override val type = TYPE_HEADER  } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputProfileSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputProfileSetting.kt new file mode 100644 index 000000000..c46de08c5 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputProfileSetting.kt @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.settings.model.view + +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.features.input.NativeInput +import org.yuzu.yuzu_emu.utils.NativeConfig + +class InputProfileSetting(private val playerIndex: Int) : +    SettingsItem(emptySetting, R.string.profile, "", 0, "") { +    override val type = TYPE_INPUT_PROFILE + +    fun getCurrentProfile(): String = +        NativeConfig.getInputSettings(true)[playerIndex].profileName + +    fun getProfileNames(): Array<String> = NativeInput.getInputProfileNames() + +    fun isProfileNameValid(name: String): Boolean = NativeInput.isProfileNameValid(name) + +    fun createProfile(name: String): Boolean = NativeInput.createProfile(name, playerIndex) + +    fun deleteProfile(name: String): Boolean = NativeInput.deleteProfile(name, playerIndex) + +    fun loadProfile(name: String): Boolean { +        val result = NativeInput.loadProfile(name, playerIndex) +        NativeInput.reloadInputDevices() +        return result +    } + +    fun saveProfile(name: String): Boolean = NativeInput.saveProfile(name, playerIndex) +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputSetting.kt new file mode 100644 index 000000000..2d118bff3 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputSetting.kt @@ -0,0 +1,134 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.settings.model.view + +import androidx.annotation.StringRes +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.YuzuApplication +import org.yuzu.yuzu_emu.features.input.NativeInput +import org.yuzu.yuzu_emu.features.input.model.ButtonName +import org.yuzu.yuzu_emu.features.input.model.InputType +import org.yuzu.yuzu_emu.utils.ParamPackage + +sealed class InputSetting( +    @StringRes titleId: Int, +    titleString: String +) : SettingsItem(emptySetting, titleId, titleString, 0, "") { +    override val type = TYPE_INPUT +    abstract val inputType: InputType +    abstract val playerIndex: Int + +    protected val context get() = YuzuApplication.appContext + +    abstract fun getSelectedValue(): String + +    abstract fun setSelectedValue(param: ParamPackage) + +    protected fun getDisplayString(params: ParamPackage, control: String): String { +        val deviceName = params.get("display", "") +        deviceName.ifEmpty { +            return context.getString(R.string.not_set) +        } +        return "$deviceName: $control" +    } + +    private fun getDirectionName(direction: String): String = +        when (direction) { +            "up" -> context.getString(R.string.up) +            "down" -> context.getString(R.string.down) +            "left" -> context.getString(R.string.left) +            "right" -> context.getString(R.string.right) +            else -> direction +        } + +    protected fun buttonToText(param: ParamPackage): String { +        if (!param.has("engine")) { +            return context.getString(R.string.not_set) +        } + +        val toggle = if (param.get("toggle", false)) "~" else "" +        val inverted = if (param.get("inverted", false)) "!" else "" +        val invert = if (param.get("invert", "+") == "-") "-" else "" +        val turbo = if (param.get("turbo", false)) "$" else "" +        val commonButtonName = NativeInput.getButtonName(param) + +        if (commonButtonName == ButtonName.Invalid) { +            return context.getString(R.string.invalid) +        } + +        if (commonButtonName == ButtonName.Engine) { +            return param.get("engine", "") +        } + +        if (commonButtonName == ButtonName.Value) { +            if (param.has("hat")) { +                val hat = getDirectionName(param.get("direction", "")) +                return context.getString(R.string.qualified_hat, turbo, toggle, inverted, hat) +            } +            if (param.has("axis")) { +                val axis = param.get("axis", "") +                return context.getString( +                    R.string.qualified_button_stick_axis, +                    toggle, +                    inverted, +                    invert, +                    axis +                ) +            } +            if (param.has("button")) { +                val button = param.get("button", "") +                return context.getString(R.string.qualified_button, turbo, toggle, inverted, button) +            } +        } + +        return context.getString(R.string.unknown) +    } + +    protected fun analogToText(param: ParamPackage, direction: String): String { +        if (!param.has("engine")) { +            return context.getString(R.string.not_set) +        } + +        if (param.get("engine", "") == "analog_from_button") { +            return buttonToText(ParamPackage(param.get(direction, ""))) +        } + +        if (!param.has("axis_x") || !param.has("axis_y")) { +            return context.getString(R.string.unknown) +        } + +        val xAxis = param.get("axis_x", "") +        val yAxis = param.get("axis_y", "") +        val xInvert = param.get("invert_x", "+") == "-" +        val yInvert = param.get("invert_y", "+") == "-" + +        if (direction == "modifier") { +            return context.getString(R.string.unused) +        } + +        when (direction) { +            "up" -> { +                val yInvertString = if (yInvert) "+" else "-" +                return context.getString(R.string.qualified_axis, yAxis, yInvertString) +            } + +            "down" -> { +                val yInvertString = if (yInvert) "-" else "+" +                return context.getString(R.string.qualified_axis, yAxis, yInvertString) +            } + +            "left" -> { +                val xInvertString = if (xInvert) "+" else "-" +                return context.getString(R.string.qualified_axis, xAxis, xInvertString) +            } + +            "right" -> { +                val xInvertString = if (xInvert) "-" else "+" +                return context.getString(R.string.qualified_axis, xAxis, xInvertString) +            } +        } + +        return context.getString(R.string.unknown) +    } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/IntSingleChoiceSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/IntSingleChoiceSetting.kt new file mode 100644 index 000000000..e024c793a --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/IntSingleChoiceSetting.kt @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.settings.model.view + +import androidx.annotation.StringRes +import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting + +class IntSingleChoiceSetting( +    private val intSetting: AbstractIntSetting, +    @StringRes titleId: Int = 0, +    titleString: String = "", +    @StringRes descriptionId: Int = 0, +    descriptionString: String = "", +    val choices: Array<String>, +    val values: Array<Int> +) : SettingsItem(intSetting, titleId, titleString, descriptionId, descriptionString) { +    override val type = TYPE_INT_SINGLE_CHOICE + +    fun getValueAt(index: Int): Int = +        if (values.indices.contains(index)) values[index] else -1 + +    fun getChoiceAt(index: Int): String = +        if (choices.indices.contains(index)) choices[index] else "" + +    fun getSelectedValue(needsGlobal: Boolean = false) = intSetting.getInt(needsGlobal) +    fun setSelectedValue(value: Int) = intSetting.setInt(value) + +    val selectedValueIndex: Int +        get() { +            for (i in values.indices) { +                if (values[i] == getSelectedValue()) { +                    return i +                } +            } +            return -1 +        } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ModifierInputSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ModifierInputSetting.kt new file mode 100644 index 000000000..a1db3cc87 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ModifierInputSetting.kt @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.settings.model.view + +import androidx.annotation.StringRes +import org.yuzu.yuzu_emu.features.input.NativeInput +import org.yuzu.yuzu_emu.features.input.model.InputType +import org.yuzu.yuzu_emu.features.input.model.NativeAnalog +import org.yuzu.yuzu_emu.utils.ParamPackage + +class ModifierInputSetting( +    override val playerIndex: Int, +    val nativeAnalog: NativeAnalog, +    @StringRes titleId: Int = 0, +    titleString: String = "" +) : InputSetting(titleId, titleString) { +    override val inputType = InputType.Button + +    override fun getSelectedValue(): String { +        val analogParam = NativeInput.getStickParam(playerIndex, nativeAnalog) +        val modifierParam = ParamPackage(analogParam.get("modifier", "")) +        return buttonToText(modifierParam) +    } + +    override fun setSelectedValue(param: ParamPackage) { +        val newParam = NativeInput.getStickParam(playerIndex, nativeAnalog) +        newParam.set("modifier", param.serialize()) +        NativeInput.setStickParam(playerIndex, nativeAnalog, newParam) +    } +} 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 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt @@ -4,13 +4,16 @@  package org.yuzu.yuzu_emu.features.settings.model.view  import androidx.annotation.DrawableRes +import androidx.annotation.StringRes  class RunnableSetting( -    titleId: Int, -    descriptionId: Int, -    val isRuntimeRunnable: Boolean, +    @StringRes titleId: Int = 0, +    titleString: String = "", +    @StringRes descriptionId: Int = 0, +    descriptionString: String = "", +    val isRunnable: Boolean,      @DrawableRes val iconId: Int = 0,      val runnable: () -> Unit -) : SettingsItem(emptySetting, titleId, descriptionId) { +) : SettingsItem(emptySetting, titleId, titleString, descriptionId, descriptionString) {      override val type = TYPE_RUNNABLE  } 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 100644 --- 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 @@  package org.yuzu.yuzu_emu.features.settings.model.view +import androidx.annotation.StringRes  import org.yuzu.yuzu_emu.NativeLibrary  import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.YuzuApplication +import org.yuzu.yuzu_emu.features.input.NativeInput +import org.yuzu.yuzu_emu.features.input.model.NpadStyleIndex  import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting  import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting  import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting @@ -23,13 +27,34 @@ import org.yuzu.yuzu_emu.utils.NativeConfig   */  abstract class SettingsItem(      val setting: AbstractSetting, -    val nameId: Int, -    val descriptionId: Int +    @StringRes val titleId: Int, +    val titleString: String, +    @StringRes val descriptionId: Int, +    val descriptionString: String  ) {      abstract val type: Int +    val title: String by lazy { +        if (titleId != 0) { +            return@lazy YuzuApplication.appContext.getString(titleId) +        } +        return@lazy titleString +    } + +    val description: String by lazy { +        if (descriptionId != 0) { +            return@lazy YuzuApplication.appContext.getString(descriptionId) +        } +        return@lazy descriptionString +    } +      val isEditable: Boolean          get() { +            // Can't change docked mode toggle when using handheld mode +            if (setting.key == BooleanSetting.USE_DOCKED_MODE.key) { +                return NativeInput.getStyleIndex(0) != NpadStyleIndex.Handheld +            } +              // Can't edit settings that aren't saveable in per-game config even if they are switchable              if (NativeConfig.isPerGameConfigLoaded() && !setting.isSaveable) {                  return false @@ -59,6 +84,9 @@ abstract class SettingsItem(          const val TYPE_STRING_SINGLE_CHOICE = 5          const val TYPE_DATETIME_SETTING = 6          const val TYPE_RUNNABLE = 7 +        const val TYPE_INPUT = 8 +        const val TYPE_INT_SINGLE_CHOICE = 9 +        const val TYPE_INPUT_PROFILE = 10          const val FASTMEM_COMBINED = "fastmem_combined" @@ -80,237 +108,242 @@ abstract class SettingsItem(              put(                  SwitchSetting(                      BooleanSetting.RENDERER_USE_SPEED_LIMIT, -                    R.string.frame_limit_enable, -                    R.string.frame_limit_enable_description +                    titleId = R.string.frame_limit_enable, +                    descriptionId = R.string.frame_limit_enable_description                  )              )              put(                  SliderSetting(                      ShortSetting.RENDERER_SPEED_LIMIT, -                    R.string.frame_limit_slider, -                    R.string.frame_limit_slider_description, -                    1, -                    400, -                    "%" +                    titleId = R.string.frame_limit_slider, +                    descriptionId = R.string.frame_limit_slider_description, +                    min = 1, +                    max = 400, +                    units = "%"                  )              )              put(                  SingleChoiceSetting(                      IntSetting.CPU_BACKEND, -                    R.string.cpu_backend, -                    0, -                    R.array.cpuBackendArm64Names, -                    R.array.cpuBackendArm64Values +                    titleId = R.string.cpu_backend, +                    choicesId = R.array.cpuBackendArm64Names, +                    valuesId = R.array.cpuBackendArm64Values                  )              )              put(                  SingleChoiceSetting(                      IntSetting.CPU_ACCURACY, -                    R.string.cpu_accuracy, -                    0, -                    R.array.cpuAccuracyNames, -                    R.array.cpuAccuracyValues +                    titleId = R.string.cpu_accuracy, +                    choicesId = R.array.cpuAccuracyNames, +                    valuesId = R.array.cpuAccuracyValues                  )              )              put(                  SwitchSetting(                      BooleanSetting.PICTURE_IN_PICTURE, -                    R.string.picture_in_picture, -                    R.string.picture_in_picture_description +                    titleId = R.string.picture_in_picture, +                    descriptionId = R.string.picture_in_picture_description                  )              ) + +            val dockedModeSetting = object : AbstractBooleanSetting { +                override val key = BooleanSetting.USE_DOCKED_MODE.key + +                override fun getBoolean(needsGlobal: Boolean): Boolean { +                    if (NativeInput.getStyleIndex(0) == NpadStyleIndex.Handheld) { +                        return false +                    } +                    return BooleanSetting.USE_DOCKED_MODE.getBoolean(needsGlobal) +                } + +                override fun setBoolean(value: Boolean) = +                    BooleanSetting.USE_DOCKED_MODE.setBoolean(value) + +                override val defaultValue = BooleanSetting.USE_DOCKED_MODE.defaultValue + +                override fun getValueAsString(needsGlobal: Boolean): String = +                    BooleanSetting.USE_DOCKED_MODE.getValueAsString(needsGlobal) + +                override fun reset() = BooleanSetting.USE_DOCKED_MODE.reset() +            }              put(                  SwitchSetting( -                    BooleanSetting.USE_DOCKED_MODE, -                    R.string.use_docked_mode, -                    R.string.use_docked_mode_description +                    dockedModeSetting, +                    titleId = R.string.use_docked_mode, +                    descriptionId = R.string.use_docked_mode_description                  )              ) +              put(                  SingleChoiceSetting(                      IntSetting.REGION_INDEX, -                    R.string.emulated_region, -                    0, -                    R.array.regionNames, -                    R.array.regionValues +                    titleId = R.string.emulated_region, +                    choicesId = R.array.regionNames, +                    valuesId = R.array.regionValues                  )              )              put(                  SingleChoiceSetting(                      IntSetting.LANGUAGE_INDEX, -                    R.string.emulated_language, -                    0, -                    R.array.languageNames, -                    R.array.languageValues +                    titleId = R.string.emulated_language, +                    choicesId = R.array.languageNames, +                    valuesId = R.array.languageValues                  )              )              put(                  SwitchSetting(                      BooleanSetting.USE_CUSTOM_RTC, -                    R.string.use_custom_rtc, -                    R.string.use_custom_rtc_description +                    titleId = R.string.use_custom_rtc, +                    descriptionId = R.string.use_custom_rtc_description                  )              ) -            put(DateTimeSetting(LongSetting.CUSTOM_RTC, R.string.set_custom_rtc, 0)) +            put(DateTimeSetting(LongSetting.CUSTOM_RTC, titleId = R.string.set_custom_rtc))              put(                  SingleChoiceSetting(                      IntSetting.RENDERER_ACCURACY, -                    R.string.renderer_accuracy, -                    0, -                    R.array.rendererAccuracyNames, -                    R.array.rendererAccuracyValues +                    titleId = R.string.renderer_accuracy, +                    choicesId = R.array.rendererAccuracyNames, +                    valuesId = R.array.rendererAccuracyValues                  )              )              put(                  SingleChoiceSetting(                      IntSetting.RENDERER_RESOLUTION, -                    R.string.renderer_resolution, -                    0, -                    R.array.rendererResolutionNames, -                    R.array.rendererResolutionValues +                    titleId = R.string.renderer_resolution, +                    choicesId = R.array.rendererResolutionNames, +                    valuesId = R.array.rendererResolutionValues                  )              )              put(                  SingleChoiceSetting(                      IntSetting.RENDERER_VSYNC, -                    R.string.renderer_vsync, -                    0, -                    R.array.rendererVSyncNames, -                    R.array.rendererVSyncValues +                    titleId = R.string.renderer_vsync, +                    choicesId = R.array.rendererVSyncNames, +                    valuesId = R.array.rendererVSyncValues                  )              )              put(                  SingleChoiceSetting(                      IntSetting.RENDERER_SCALING_FILTER, -                    R.string.renderer_scaling_filter, -                    0, -                    R.array.rendererScalingFilterNames, -                    R.array.rendererScalingFilterValues +                    titleId = R.string.renderer_scaling_filter, +                    choicesId = R.array.rendererScalingFilterNames, +                    valuesId = R.array.rendererScalingFilterValues                  )              )              put(                  SliderSetting(                      IntSetting.FSR_SHARPENING_SLIDER, -                    R.string.fsr_sharpness, -                    R.string.fsr_sharpness_description, -                    0, -                    100, -                    "%" +                    titleId = R.string.fsr_sharpness, +                    descriptionId = R.string.fsr_sharpness_description, +                    units = "%"                  )              )              put(                  SingleChoiceSetting(                      IntSetting.RENDERER_ANTI_ALIASING, -                    R.string.renderer_anti_aliasing, -                    0, -                    R.array.rendererAntiAliasingNames, -                    R.array.rendererAntiAliasingValues +                    titleId = R.string.renderer_anti_aliasing, +                    choicesId = R.array.rendererAntiAliasingNames, +                    valuesId = R.array.rendererAntiAliasingValues                  )              )              put(                  SingleChoiceSetting(                      IntSetting.RENDERER_SCREEN_LAYOUT, -                    R.string.renderer_screen_layout, -                    0, -                    R.array.rendererScreenLayoutNames, -                    R.array.rendererScreenLayoutValues +                    titleId = R.string.renderer_screen_layout, +                    choicesId = R.array.rendererScreenLayoutNames, +                    valuesId = R.array.rendererScreenLayoutValues                  )              )              put(                  SingleChoiceSetting(                      IntSetting.RENDERER_ASPECT_RATIO, -                    R.string.renderer_aspect_ratio, -                    0, -                    R.array.rendererAspectRatioNames, -                    R.array.rendererAspectRatioValues +                    titleId = R.string.renderer_aspect_ratio, +                    choicesId = R.array.rendererAspectRatioNames, +                    valuesId = R.array.rendererAspectRatioValues                  )              )              put(                  SingleChoiceSetting(                      IntSetting.VERTICAL_ALIGNMENT, -                    R.string.vertical_alignment, -                    0, -                    R.array.verticalAlignmentEntries, -                    R.array.verticalAlignmentValues +                    titleId = R.string.vertical_alignment, +                    descriptionId = 0, +                    choicesId = R.array.verticalAlignmentEntries, +                    valuesId = R.array.verticalAlignmentValues                  )              )              put(                  SwitchSetting(                      BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE, -                    R.string.use_disk_shader_cache, -                    R.string.use_disk_shader_cache_description +                    titleId = R.string.use_disk_shader_cache, +                    descriptionId = R.string.use_disk_shader_cache_description                  )              )              put(                  SwitchSetting(                      BooleanSetting.RENDERER_FORCE_MAX_CLOCK, -                    R.string.renderer_force_max_clock, -                    R.string.renderer_force_max_clock_description +                    titleId = R.string.renderer_force_max_clock, +                    descriptionId = R.string.renderer_force_max_clock_description                  )              )              put(                  SwitchSetting(                      BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS, -                    R.string.renderer_asynchronous_shaders, -                    R.string.renderer_asynchronous_shaders_description +                    titleId = R.string.renderer_asynchronous_shaders, +                    descriptionId = R.string.renderer_asynchronous_shaders_description                  )              )              put(                  SwitchSetting(                      BooleanSetting.RENDERER_REACTIVE_FLUSHING, -                    R.string.renderer_reactive_flushing, -                    R.string.renderer_reactive_flushing_description +                    titleId = R.string.renderer_reactive_flushing, +                    descriptionId = R.string.renderer_reactive_flushing_description                  )              )              put(                  SingleChoiceSetting(                      IntSetting.MAX_ANISOTROPY, -                    R.string.anisotropic_filtering, -                    R.string.anisotropic_filtering_description, -                    R.array.anisoEntries, -                    R.array.anisoValues +                    titleId = R.string.anisotropic_filtering, +                    descriptionId = R.string.anisotropic_filtering_description, +                    choicesId = R.array.anisoEntries, +                    valuesId = R.array.anisoValues                  )              )              put(                  SingleChoiceSetting(                      IntSetting.AUDIO_OUTPUT_ENGINE, -                    R.string.audio_output_engine, -                    0, -                    R.array.outputEngineEntries, -                    R.array.outputEngineValues +                    titleId = R.string.audio_output_engine, +                    choicesId = R.array.outputEngineEntries, +                    valuesId = R.array.outputEngineValues                  )              )              put(                  SliderSetting(                      ByteSetting.AUDIO_VOLUME, -                    R.string.audio_volume, -                    R.string.audio_volume_description, -                    0, -                    100, -                    "%" +                    titleId = R.string.audio_volume, +                    descriptionId = R.string.audio_volume_description, +                    units = "%"                  )              )              put(                  SingleChoiceSetting(                      IntSetting.RENDERER_BACKEND, -                    R.string.renderer_api, -                    0, -                    R.array.rendererApiNames, -                    R.array.rendererApiValues +                    titleId = R.string.renderer_api, +                    choicesId = R.array.rendererApiNames, +                    valuesId = R.array.rendererApiValues                  )              )              put(                  SwitchSetting(                      BooleanSetting.RENDERER_DEBUG, -                    R.string.renderer_debug, -                    R.string.renderer_debug_description +                    titleId = R.string.renderer_debug, +                    descriptionId = R.string.renderer_debug_description                  )              )              put(                  SwitchSetting(                      BooleanSetting.CPU_DEBUG_MODE, -                    R.string.cpu_debug_mode, -                    R.string.cpu_debug_mode_description +                    titleId = R.string.cpu_debug_mode, +                    descriptionId = R.string.cpu_debug_mode_description                  )              ) @@ -346,7 +379,7 @@ abstract class SettingsItem(                  override fun reset() = setBoolean(defaultValue)              } -            put(SwitchSetting(fastmem, R.string.fastmem, 0)) +            put(SwitchSetting(fastmem, R.string.fastmem))          }      }  } 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 100644 --- 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 @@  package org.yuzu.yuzu_emu.features.settings.model.view +import androidx.annotation.ArrayRes +import androidx.annotation.StringRes  import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting  import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting  class SingleChoiceSetting(      setting: AbstractSetting, -    titleId: Int, -    descriptionId: Int, -    val choicesId: Int, -    val valuesId: Int -) : SettingsItem(setting, titleId, descriptionId) { +    @StringRes titleId: Int = 0, +    titleString: String = "", +    @StringRes descriptionId: Int = 0, +    descriptionString: String = "", +    @ArrayRes val choicesId: Int, +    @ArrayRes val valuesId: Int +) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) {      override val type = TYPE_SINGLE_CHOICE      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 100644 --- 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 @@  package org.yuzu.yuzu_emu.features.settings.model.view +import androidx.annotation.StringRes  import org.yuzu.yuzu_emu.features.settings.model.AbstractByteSetting  import org.yuzu.yuzu_emu.features.settings.model.AbstractFloatSetting  import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting @@ -12,12 +13,14 @@ import kotlin.math.roundToInt  class SliderSetting(      setting: AbstractSetting, -    titleId: Int, -    descriptionId: Int, -    val min: Int, -    val max: Int, -    val units: String -) : SettingsItem(setting, titleId, descriptionId) { +    @StringRes titleId: Int = 0, +    titleString: String = "", +    @StringRes descriptionId: Int = 0, +    descriptionString: String = "", +    val min: Int = 0, +    val max: Int = 100, +    val units: String = "" +) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) {      override val type = TYPE_SLIDER      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 100644 --- 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 @@  package org.yuzu.yuzu_emu.features.settings.model.view +import androidx.annotation.StringRes  import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting  class StringSingleChoiceSetting(      private val stringSetting: AbstractStringSetting, -    titleId: Int, -    descriptionId: Int, +    @StringRes titleId: Int = 0, +    titleString: String = "", +    @StringRes descriptionId: Int = 0, +    descriptionString: String = "",      val choices: Array<String>,      val values: Array<String> -) : SettingsItem(stringSetting, titleId, descriptionId) { +) : SettingsItem(stringSetting, titleId, titleString, descriptionId, descriptionString) {      override val type = TYPE_STRING_SINGLE_CHOICE      fun getValueAt(index: Int): String = @@ -20,7 +23,7 @@ class StringSingleChoiceSetting(      fun getSelectedValue(needsGlobal: Boolean = false) = stringSetting.getString(needsGlobal)      fun setSelectedValue(value: String) = stringSetting.setString(value) -    val selectValueIndex: Int +    val selectedValueIndex: Int          get() {              for (i in values.indices) {                  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 100644 --- 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  import org.yuzu.yuzu_emu.features.settings.model.Settings  class SubmenuSetting( -    @StringRes titleId: Int, -    @StringRes descriptionId: Int, -    @DrawableRes val iconId: Int, +    @StringRes titleId: Int = 0, +    titleString: String = "", +    @StringRes descriptionId: Int = 0, +    descriptionString: String = "", +    @DrawableRes val iconId: Int = 0,      val menuKey: Settings.MenuTag -) : SettingsItem(emptySetting, titleId, descriptionId) { +) : SettingsItem(emptySetting, titleId, titleString, descriptionId, descriptionString) {      override val type = TYPE_SUBMENU  } 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 100644 --- 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 @@  package org.yuzu.yuzu_emu.features.settings.model.view +import androidx.annotation.StringRes  import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting  import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting  import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting  class SwitchSetting(      setting: AbstractSetting, -    titleId: Int, -    descriptionId: Int -) : SettingsItem(setting, titleId, descriptionId) { +    @StringRes titleId: Int = 0, +    titleString: String = "", +    @StringRes descriptionId: Int = 0, +    descriptionString: String = "" +) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) {      override val type = TYPE_SWITCH      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 100644 index 000000000..16a1d0504 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputDialogFragment.kt @@ -0,0 +1,300 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.settings.ui + +import android.app.Dialog +import android.graphics.drawable.Animatable2 +import android.graphics.drawable.AnimatedVectorDrawable +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.view.InputDevice +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.activityViewModels +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.databinding.DialogMappingBinding +import org.yuzu.yuzu_emu.features.input.NativeInput +import org.yuzu.yuzu_emu.features.input.model.NativeAnalog +import org.yuzu.yuzu_emu.features.input.model.NativeButton +import org.yuzu.yuzu_emu.features.settings.model.view.AnalogInputSetting +import org.yuzu.yuzu_emu.features.settings.model.view.ButtonInputSetting +import org.yuzu.yuzu_emu.features.settings.model.view.InputSetting +import org.yuzu.yuzu_emu.features.settings.model.view.ModifierInputSetting +import org.yuzu.yuzu_emu.utils.InputHandler +import org.yuzu.yuzu_emu.utils.ParamPackage + +class InputDialogFragment : DialogFragment() { +    private var inputAccepted = false + +    private var position: Int = 0 + +    private lateinit var inputSetting: InputSetting + +    private lateinit var binding: DialogMappingBinding + +    private val settingsViewModel: SettingsViewModel by activityViewModels() + +    override fun onCreate(savedInstanceState: Bundle?) { +        super.onCreate(savedInstanceState) +        if (settingsViewModel.clickedItem == null) dismiss() + +        position = requireArguments().getInt(POSITION) + +        InputHandler.updateControllerData() +    } + +    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { +        inputSetting = settingsViewModel.clickedItem as InputSetting +        binding = DialogMappingBinding.inflate(layoutInflater) + +        val builder = MaterialAlertDialogBuilder(requireContext()) +            .setPositiveButton(android.R.string.cancel) { _, _ -> +                NativeInput.stopMapping() +                dismiss() +            } +            .setView(binding.root) + +        val playButtonMapAnimation = { twoDirections: Boolean -> +            val stickAnimation: AnimatedVectorDrawable +            val buttonAnimation: AnimatedVectorDrawable +            binding.imageStickAnimation.apply { +                val anim = if (twoDirections) { +                    R.drawable.stick_two_direction_anim +                } else { +                    R.drawable.stick_one_direction_anim +                } +                setBackgroundResource(anim) +                stickAnimation = background as AnimatedVectorDrawable +            } +            binding.imageButtonAnimation.apply { +                setBackgroundResource(R.drawable.button_anim) +                buttonAnimation = background as AnimatedVectorDrawable +            } +            stickAnimation.registerAnimationCallback(object : Animatable2.AnimationCallback() { +                override fun onAnimationEnd(drawable: Drawable?) { +                    buttonAnimation.start() +                } +            }) +            buttonAnimation.registerAnimationCallback(object : Animatable2.AnimationCallback() { +                override fun onAnimationEnd(drawable: Drawable?) { +                    stickAnimation.start() +                } +            }) +            stickAnimation.start() +        } + +        when (val setting = inputSetting) { +            is AnalogInputSetting -> { +                when (setting.nativeAnalog) { +                    NativeAnalog.LStick -> builder.setTitle( +                        getString(R.string.map_control, getString(R.string.left_stick)) +                    ) + +                    NativeAnalog.RStick -> builder.setTitle( +                        getString(R.string.map_control, getString(R.string.right_stick)) +                    ) +                } + +                builder.setMessage(R.string.stick_map_description) + +                playButtonMapAnimation.invoke(true) +            } + +            is ModifierInputSetting -> { +                builder.setTitle(getString(R.string.map_control, setting.title)) +                    .setMessage(R.string.button_map_description) +                playButtonMapAnimation.invoke(false) +            } + +            is ButtonInputSetting -> { +                if (setting.nativeButton == NativeButton.DUp || +                    setting.nativeButton == NativeButton.DDown || +                    setting.nativeButton == NativeButton.DLeft || +                    setting.nativeButton == NativeButton.DRight +                ) { +                    builder.setTitle(getString(R.string.map_dpad_direction, setting.title)) +                } else { +                    builder.setTitle(getString(R.string.map_control, setting.title)) +                } +                builder.setMessage(R.string.button_map_description) +                playButtonMapAnimation.invoke(false) +            } +        } + +        return builder.create() +    } + +    override fun onCreateView( +        inflater: LayoutInflater, +        container: ViewGroup?, +        savedInstanceState: Bundle? +    ): View { +        return binding.root +    } + +    override fun onViewCreated(view: View, savedInstanceState: Bundle?) { +        super.onViewCreated(view, savedInstanceState) +        view.requestFocus() +        view.setOnFocusChangeListener { v, hasFocus -> if (!hasFocus) v.requestFocus() } +        dialog?.setOnKeyListener { _, _, keyEvent -> onKeyEvent(keyEvent) } +        binding.root.setOnGenericMotionListener { _, motionEvent -> onMotionEvent(motionEvent) } +        NativeInput.beginMapping(inputSetting.inputType.int) +    } + +    private fun onKeyEvent(event: KeyEvent): Boolean { +        if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK && +            event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD +        ) { +            return false +        } + +        val action = when (event.action) { +            KeyEvent.ACTION_DOWN -> NativeInput.ButtonState.PRESSED +            KeyEvent.ACTION_UP -> NativeInput.ButtonState.RELEASED +            else -> return false +        } +        val controllerData = +            InputHandler.androidControllers[event.device.controllerNumber] ?: return false +        NativeInput.onGamePadButtonEvent( +            controllerData.getGUID(), +            controllerData.getPort(), +            event.keyCode, +            action +        ) +        onInputReceived(event.device) +        return true +    } + +    private fun onMotionEvent(event: MotionEvent): Boolean { +        if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK && +            event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD +        ) { +            return false +        } + +        // Temp workaround for DPads that give both axis and button input. The input system can't +        // take in a specific axis direction for a binding so you lose half of the directions for a DPad. + +        val controllerData = +            InputHandler.androidControllers[event.device.controllerNumber] ?: return false +        event.device.motionRanges.forEach { +            NativeInput.onGamePadAxisEvent( +                controllerData.getGUID(), +                controllerData.getPort(), +                it.axis, +                event.getAxisValue(it.axis) +            ) +            onInputReceived(event.device) +        } +        return true +    } + +    private fun onInputReceived(device: InputDevice) { +        val params = ParamPackage(NativeInput.getNextInput()) +        if (params.has("engine") && isInputAcceptable(params) && !inputAccepted) { +            inputAccepted = true +            setResult(params, device) +        } +    } + +    private fun setResult(params: ParamPackage, device: InputDevice) { +        NativeInput.stopMapping() +        params.set("display", "${device.name} ${params.get("port", 0)}") +        when (val item = settingsViewModel.clickedItem as InputSetting) { +            is ModifierInputSetting, +            is ButtonInputSetting -> { +                // Invert DPad up and left bindings by default +                val tempSetting = inputSetting as? ButtonInputSetting +                if (tempSetting != null) { +                    if (tempSetting.nativeButton == NativeButton.DUp || +                        tempSetting.nativeButton == NativeButton.DLeft && +                        params.has("axis") +                    ) { +                        params.set("invert", "-") +                    } +                } + +                item.setSelectedValue(params) +                settingsViewModel.setAdapterItemChanged(position) +            } + +            is AnalogInputSetting -> { +                var analogParam = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog) +                analogParam = adjustAnalogParam(params, analogParam, item.analogDirection.param) + +                // Invert Y-Axis by default +                analogParam.set("invert_y", "-") + +                item.setSelectedValue(analogParam) +                settingsViewModel.setReloadListAndNotifyDataset(true) +            } +        } +        dismiss() +    } + +    private fun adjustAnalogParam( +        inputParam: ParamPackage, +        analogParam: ParamPackage, +        buttonName: String +    ): ParamPackage { +        // The poller returned a complete axis, so set all the buttons +        if (inputParam.has("axis_x") && inputParam.has("axis_y")) { +            return inputParam +        } + +        // Check if the current configuration has either no engine or an axis binding. +        // Clears out the old binding and adds one with analog_from_button. +        if (!analogParam.has("engine") || analogParam.has("axis_x") || analogParam.has("axis_y")) { +            analogParam.clear() +            analogParam.set("engine", "analog_from_button") +        } +        analogParam.set(buttonName, inputParam.serialize()) +        return analogParam +    } + +    private fun isInputAcceptable(params: ParamPackage): Boolean { +        if (InputHandler.registeredControllers.size == 1) { +            return true +        } + +        if (params.has("motion")) { +            return true +        } + +        val currentDevice = settingsViewModel.getCurrentDeviceParams(params) +        if (currentDevice.get("engine", "any") == "any") { +            return true +        } + +        val guidMatch = params.get("guid", "") == currentDevice.get("guid", "") || +            params.get("guid", "") == currentDevice.get("guid2", "") +        return params.get("engine", "") == currentDevice.get("engine", "") && +            guidMatch && +            params.get("port", 0) == currentDevice.get("port", 0) +    } + +    companion object { +        const val TAG = "InputDialogFragment" + +        const val POSITION = "Position" + +        fun newInstance( +            inputMappingViewModel: SettingsViewModel, +            setting: InputSetting, +            position: Int +        ): InputDialogFragment { +            inputMappingViewModel.clickedItem = setting +            val args = Bundle() +            args.putInt(POSITION, position) +            val fragment = InputDialogFragment() +            fragment.arguments = args +            return fragment +        } +    } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileAdapter.kt new file mode 100644 index 000000000..5656e9d8d --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileAdapter.kt @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.settings.ui + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import org.yuzu.yuzu_emu.YuzuApplication +import org.yuzu.yuzu_emu.adapters.AbstractListAdapter +import org.yuzu.yuzu_emu.databinding.ListItemInputProfileBinding +import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder +import org.yuzu.yuzu_emu.R + +class InputProfileAdapter(options: List<ProfileItem>) : +    AbstractListAdapter<ProfileItem, AbstractViewHolder<ProfileItem>>(options) { +    override fun onCreateViewHolder( +        parent: ViewGroup, +        viewType: Int +    ): AbstractViewHolder<ProfileItem> { +        ListItemInputProfileBinding.inflate(LayoutInflater.from(parent.context), parent, false) +            .also { return InputProfileViewHolder(it) } +    } + +    inner class InputProfileViewHolder(val binding: ListItemInputProfileBinding) : +        AbstractViewHolder<ProfileItem>(binding) { +        override fun bind(model: ProfileItem) { +            when (model) { +                is ExistingProfileItem -> { +                    binding.title.text = model.name +                    binding.buttonNew.visibility = View.GONE +                    binding.buttonDelete.visibility = View.VISIBLE +                    binding.buttonDelete.setOnClickListener { model.deleteProfile.invoke() } +                    binding.buttonSave.visibility = View.VISIBLE +                    binding.buttonSave.setOnClickListener { model.saveProfile.invoke() } +                    binding.buttonLoad.visibility = View.VISIBLE +                    binding.buttonLoad.setOnClickListener { model.loadProfile.invoke() } +                } + +                is NewProfileItem -> { +                    binding.title.text = model.name +                    binding.buttonNew.visibility = View.VISIBLE +                    binding.buttonNew.setOnClickListener { model.createNewProfile.invoke() } +                    binding.buttonSave.visibility = View.GONE +                    binding.buttonDelete.visibility = View.GONE +                    binding.buttonLoad.visibility = View.GONE +                } +            } +        } +    } +} + +sealed interface ProfileItem { +    val name: String +} + +data class NewProfileItem( +    val createNewProfile: () -> Unit +) : ProfileItem { +    override val name: String = YuzuApplication.appContext.getString(R.string.create_new_profile) +} + +data class ExistingProfileItem( +    override val name: String, +    val deleteProfile: () -> Unit, +    val saveProfile: () -> Unit, +    val loadProfile: () -> Unit +) : ProfileItem diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileDialogFragment.kt new file mode 100644 index 000000000..1bae593ae --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileDialogFragment.kt @@ -0,0 +1,148 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.settings.ui + +import android.app.Dialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.activityViewModels +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.databinding.DialogInputProfilesBinding +import org.yuzu.yuzu_emu.features.settings.model.view.InputProfileSetting +import org.yuzu.yuzu_emu.fragments.MessageDialogFragment +import org.yuzu.yuzu_emu.utils.collect + +class InputProfileDialogFragment : DialogFragment() { +    private var position = 0 + +    private val settingsViewModel: SettingsViewModel by activityViewModels() + +    private lateinit var binding: DialogInputProfilesBinding + +    private lateinit var setting: InputProfileSetting + +    override fun onCreate(savedInstanceState: Bundle?) { +        super.onCreate(savedInstanceState) +        position = requireArguments().getInt(POSITION) +    } + +    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { +        binding = DialogInputProfilesBinding.inflate(layoutInflater) + +        setting = settingsViewModel.clickedItem as InputProfileSetting +        val options = mutableListOf<ProfileItem>().apply { +            add( +                NewProfileItem( +                    createNewProfile = { +                        NewInputProfileDialogFragment.newInstance( +                            settingsViewModel, +                            setting, +                            position +                        ).show(parentFragmentManager, NewInputProfileDialogFragment.TAG) +                        dismiss() +                    } +                ) +            ) + +            val onActionDismiss = { +                settingsViewModel.setReloadListAndNotifyDataset(true) +                dismiss() +            } +            setting.getProfileNames().forEach { +                add( +                    ExistingProfileItem( +                        it, +                        deleteProfile = { +                            settingsViewModel.setShouldShowDeleteProfileDialog(it) +                        }, +                        saveProfile = { +                            if (!setting.saveProfile(it)) { +                                Toast.makeText( +                                    requireContext(), +                                    R.string.failed_to_save_profile, +                                    Toast.LENGTH_SHORT +                                ).show() +                            } +                            onActionDismiss.invoke() +                        }, +                        loadProfile = { +                            if (!setting.loadProfile(it)) { +                                Toast.makeText( +                                    requireContext(), +                                    R.string.failed_to_load_profile, +                                    Toast.LENGTH_SHORT +                                ).show() +                            } +                            onActionDismiss.invoke() +                        } +                    ) +                ) +            } +        } +        binding.listProfiles.apply { +            layoutManager = LinearLayoutManager(requireContext()) +            adapter = InputProfileAdapter(options) +        } + +        return MaterialAlertDialogBuilder(requireContext()) +            .setView(binding.root) +            .create() +    } + +    override fun onCreateView( +        inflater: LayoutInflater, +        container: ViewGroup?, +        savedInstanceState: Bundle? +    ): View { +        return binding.root +    } + +    override fun onViewCreated(view: View, savedInstanceState: Bundle?) { +        super.onViewCreated(view, savedInstanceState) + +        settingsViewModel.shouldShowDeleteProfileDialog.collect(viewLifecycleOwner) { +            if (it.isNotEmpty()) { +                MessageDialogFragment.newInstance( +                    activity = requireActivity(), +                    titleId = R.string.delete_input_profile, +                    descriptionId = R.string.delete_input_profile_description, +                    positiveAction = { +                        setting.deleteProfile(it) +                        settingsViewModel.setReloadListAndNotifyDataset(true) +                    }, +                    negativeAction = {}, +                    negativeButtonTitleId = android.R.string.cancel +                ).show(parentFragmentManager, MessageDialogFragment.TAG) +                settingsViewModel.setShouldShowDeleteProfileDialog("") +                dismiss() +            } +        } +    } + +    companion object { +        const val TAG = "InputProfileDialogFragment" + +        const val POSITION = "Position" + +        fun newInstance( +            settingsViewModel: SettingsViewModel, +            profileSetting: InputProfileSetting, +            position: Int +        ): InputProfileDialogFragment { +            settingsViewModel.clickedItem = profileSetting + +            val args = Bundle() +            args.putInt(POSITION, position) +            val fragment = InputProfileDialogFragment() +            fragment.arguments = args +            return fragment +        } +    } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/NewInputProfileDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/NewInputProfileDialogFragment.kt new file mode 100644 index 000000000..6e52bea80 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/NewInputProfileDialogFragment.kt @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.settings.ui + +import android.app.Dialog +import android.os.Bundle +import android.widget.Toast +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.activityViewModels +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.yuzu.yuzu_emu.databinding.DialogEditTextBinding +import org.yuzu.yuzu_emu.features.settings.model.view.InputProfileSetting +import org.yuzu.yuzu_emu.R + +class NewInputProfileDialogFragment : DialogFragment() { +    private var position = 0 + +    private val settingsViewModel: SettingsViewModel by activityViewModels() + +    private lateinit var binding: DialogEditTextBinding + +    override fun onCreate(savedInstanceState: Bundle?) { +        super.onCreate(savedInstanceState) +        position = requireArguments().getInt(POSITION) +    } + +    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { +        binding = DialogEditTextBinding.inflate(layoutInflater) + +        val setting = settingsViewModel.clickedItem as InputProfileSetting +        return MaterialAlertDialogBuilder(requireContext()) +            .setTitle(R.string.enter_profile_name) +            .setPositiveButton(android.R.string.ok) { _, _ -> +                val profileName = binding.editText.text.toString() +                if (!setting.isProfileNameValid(profileName)) { +                    Toast.makeText( +                        requireContext(), +                        R.string.invalid_profile_name, +                        Toast.LENGTH_SHORT +                    ).show() +                    return@setPositiveButton +                } + +                if (!setting.createProfile(profileName)) { +                    Toast.makeText( +                        requireContext(), +                        R.string.profile_name_already_exists, +                        Toast.LENGTH_SHORT +                    ).show() +                } else { +                    settingsViewModel.setAdapterItemChanged(position) +                } +            } +            .setNegativeButton(android.R.string.cancel, null) +            .setView(binding.root) +            .show() +    } + +    companion object { +        const val TAG = "NewInputProfileDialogFragment" + +        const val POSITION = "Position" + +        fun newInstance( +            settingsViewModel: SettingsViewModel, +            profileSetting: InputProfileSetting, +            position: Int +        ): NewInputProfileDialogFragment { +            settingsViewModel.clickedItem = profileSetting + +            val args = Bundle() +            args.putInt(POSITION, position) +            val fragment = NewInputProfileDialogFragment() +            fragment.arguments = args +            return fragment +        } +    } +} 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 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt @@ -13,21 +13,16 @@ import androidx.appcompat.app.AppCompatActivity  import androidx.core.view.ViewCompat  import androidx.core.view.WindowCompat  import androidx.core.view.WindowInsetsCompat -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle  import androidx.navigation.fragment.NavHostFragment  import androidx.navigation.navArgs  import com.google.android.material.color.MaterialColors -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch  import org.yuzu.yuzu_emu.NativeLibrary  import java.io.IOException  import org.yuzu.yuzu_emu.R  import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding +import org.yuzu.yuzu_emu.features.input.NativeInput  import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile  import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment -import org.yuzu.yuzu_emu.model.SettingsViewModel  import org.yuzu.yuzu_emu.utils.*  class SettingsActivity : AppCompatActivity() { @@ -70,39 +65,23 @@ class SettingsActivity : AppCompatActivity() {              )          } -        lifecycleScope.apply { -            launch { -                repeatOnLifecycle(Lifecycle.State.CREATED) { -                    settingsViewModel.shouldRecreate.collectLatest { -                        if (it) { -                            settingsViewModel.setShouldRecreate(false) -                            recreate() -                        } -                    } -                } -            } -            launch { -                repeatOnLifecycle(Lifecycle.State.CREATED) { -                    settingsViewModel.shouldNavigateBack.collectLatest { -                        if (it) { -                            settingsViewModel.setShouldNavigateBack(false) -                            navigateBack() -                        } -                    } -                } -            } -            launch { -                repeatOnLifecycle(Lifecycle.State.CREATED) { -                    settingsViewModel.shouldShowResetSettingsDialog.collectLatest { -                        if (it) { -                            settingsViewModel.setShouldShowResetSettingsDialog(false) -                            ResetSettingsDialogFragment().show( -                                supportFragmentManager, -                                ResetSettingsDialogFragment.TAG -                            ) -                        } -                    } -                } +        settingsViewModel.shouldRecreate.collect( +            this, +            resetState = { settingsViewModel.setShouldRecreate(false) } +        ) { if (it) recreate() } +        settingsViewModel.shouldNavigateBack.collect( +            this, +            resetState = { settingsViewModel.setShouldNavigateBack(false) } +        ) { if (it) navigateBack() } +        settingsViewModel.shouldShowResetSettingsDialog.collect( +            this, +            resetState = { settingsViewModel.setShouldShowResetSettingsDialog(false) } +        ) { +            if (it) { +                ResetSettingsDialogFragment().show( +                    supportFragmentManager, +                    ResetSettingsDialogFragment.TAG +                )              }          } @@ -137,6 +116,7 @@ class SettingsActivity : AppCompatActivity() {          super.onStop()          Log.info("[SettingsActivity] Settings activity stopping. Saving settings to INI...")          if (isFinishing) { +            NativeInput.reloadInputDevices()              NativeLibrary.applySettings()              if (args.game == null) {                  NativeConfig.saveGlobalConfig() diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt index be9b3031b..45c8faa10 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt @@ -8,12 +8,11 @@ import android.icu.util.Calendar  import android.icu.util.TimeZone  import android.text.format.DateFormat  import android.view.LayoutInflater +import android.view.View  import android.view.ViewGroup +import android.widget.PopupMenu  import androidx.fragment.app.Fragment -import androidx.lifecycle.Lifecycle  import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle  import androidx.navigation.findNavController  import androidx.recyclerview.widget.AsyncDifferConfig  import androidx.recyclerview.widget.DiffUtil @@ -21,16 +20,18 @@ import androidx.recyclerview.widget.ListAdapter  import com.google.android.material.datepicker.MaterialDatePicker  import com.google.android.material.timepicker.MaterialTimePicker  import com.google.android.material.timepicker.TimeFormat -import kotlinx.coroutines.launch  import org.yuzu.yuzu_emu.R  import org.yuzu.yuzu_emu.SettingsNavigationDirections  import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding +import org.yuzu.yuzu_emu.databinding.ListItemSettingInputBinding  import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding  import org.yuzu.yuzu_emu.databinding.ListItemSettingsHeaderBinding +import org.yuzu.yuzu_emu.features.input.NativeInput +import org.yuzu.yuzu_emu.features.input.model.AnalogDirection +import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting  import org.yuzu.yuzu_emu.features.settings.model.view.*  import org.yuzu.yuzu_emu.features.settings.ui.viewholder.* -import org.yuzu.yuzu_emu.fragments.SettingsDialogFragment -import org.yuzu.yuzu_emu.model.SettingsViewModel +import org.yuzu.yuzu_emu.utils.ParamPackage  class SettingsAdapter(      private val fragment: Fragment, @@ -41,19 +42,6 @@ class SettingsAdapter(      private val settingsViewModel: SettingsViewModel          get() = ViewModelProvider(fragment.requireActivity())[SettingsViewModel::class.java] -    init { -        fragment.viewLifecycleOwner.lifecycleScope.launch { -            fragment.repeatOnLifecycle(Lifecycle.State.STARTED) { -                settingsViewModel.adapterItemChanged.collect { -                    if (it != -1) { -                        notifyItemChanged(it) -                        settingsViewModel.setAdapterItemChanged(-1) -                    } -                } -            } -        } -    } -      override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder {          val inflater = LayoutInflater.from(parent.context)          return when (viewType) { @@ -85,8 +73,19 @@ class SettingsAdapter(                  RunnableViewHolder(ListItemSettingBinding.inflate(inflater), this)              } +            SettingsItem.TYPE_INPUT -> { +                InputViewHolder(ListItemSettingInputBinding.inflate(inflater), this) +            } + +            SettingsItem.TYPE_INT_SINGLE_CHOICE -> { +                SingleChoiceViewHolder(ListItemSettingBinding.inflate(inflater), this) +            } + +            SettingsItem.TYPE_INPUT_PROFILE -> { +                InputProfileViewHolder(ListItemSettingBinding.inflate(inflater), this) +            } +              else -> { -                // TODO: Create an error view since we can't return null now                  HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this)              }          } @@ -126,6 +125,15 @@ class SettingsAdapter(          ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG)      } +    fun onIntSingleChoiceClick(item: IntSingleChoiceSetting, position: Int) { +        SettingsDialogFragment.newInstance( +            settingsViewModel, +            item, +            SettingsItem.TYPE_INT_SINGLE_CHOICE, +            position +        ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG) +    } +      fun onDateTimeClick(item: DateTimeSetting, position: Int) {          val storedTime = item.getValue() * 1000 @@ -185,6 +193,205 @@ class SettingsAdapter(          fragment.view?.findNavController()?.navigate(action)      } +    fun onInputProfileClick(item: InputProfileSetting, position: Int) { +        InputProfileDialogFragment.newInstance( +            settingsViewModel, +            item, +            position +        ).show(fragment.childFragmentManager, InputProfileDialogFragment.TAG) +    } + +    fun onInputClick(item: InputSetting, position: Int) { +        InputDialogFragment.newInstance( +            settingsViewModel, +            item, +            position +        ).show(fragment.childFragmentManager, InputDialogFragment.TAG) +    } + +    fun onInputOptionsClick(anchor: View, item: InputSetting, position: Int) { +        val popup = PopupMenu(context, anchor) +        popup.menuInflater.inflate(R.menu.menu_input_options, popup.menu) + +        popup.menu.apply { +            val invertAxis = findItem(R.id.invert_axis) +            val invertButton = findItem(R.id.invert_button) +            val toggleButton = findItem(R.id.toggle_button) +            val turboButton = findItem(R.id.turbo_button) +            val setThreshold = findItem(R.id.set_threshold) +            val toggleAxis = findItem(R.id.toggle_axis) +            when (item) { +                is AnalogInputSetting -> { +                    val params = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog) + +                    invertAxis.isVisible = true +                    invertAxis.isCheckable = true +                    invertAxis.isChecked = when (item.analogDirection) { +                        AnalogDirection.Left, AnalogDirection.Right -> { +                            params.get("invert_x", "+") == "-" +                        } + +                        AnalogDirection.Up, AnalogDirection.Down -> { +                            params.get("invert_y", "+") == "-" +                        } +                    } +                    invertAxis.setOnMenuItemClickListener { +                        if (item.analogDirection == AnalogDirection.Left || +                            item.analogDirection == AnalogDirection.Right +                        ) { +                            val invertValue = params.get("invert_x", "+") == "-" +                            val invertString = if (invertValue) "+" else "-" +                            params.set("invert_x", invertString) +                        } else if ( +                            item.analogDirection == AnalogDirection.Up || +                            item.analogDirection == AnalogDirection.Down +                        ) { +                            val invertValue = params.get("invert_y", "+") == "-" +                            val invertString = if (invertValue) "+" else "-" +                            params.set("invert_y", invertString) +                        } +                        true +                    } + +                    popup.setOnDismissListener { +                        NativeInput.setStickParam(item.playerIndex, item.nativeAnalog, params) +                        settingsViewModel.setDatasetChanged(true) +                    } +                } + +                is ButtonInputSetting -> { +                    val params = NativeInput.getButtonParam(item.playerIndex, item.nativeButton) +                    if (params.has("code") || params.has("button") || params.has("hat")) { +                        val buttonInvert = params.get("inverted", false) +                        invertButton.isVisible = true +                        invertButton.isCheckable = true +                        invertButton.isChecked = buttonInvert +                        invertButton.setOnMenuItemClickListener { +                            params.set("inverted", !buttonInvert) +                            true +                        } + +                        val toggle = params.get("toggle", false) +                        toggleButton.isVisible = true +                        toggleButton.isCheckable = true +                        toggleButton.isChecked = toggle +                        toggleButton.setOnMenuItemClickListener { +                            params.set("toggle", !toggle) +                            true +                        } + +                        val turbo = params.get("turbo", false) +                        turboButton.isVisible = true +                        turboButton.isCheckable = true +                        turboButton.isChecked = turbo +                        turboButton.setOnMenuItemClickListener { +                            params.set("turbo", !turbo) +                            true +                        } +                    } else if (params.has("axis")) { +                        val axisInvert = params.get("invert", "+") == "-" +                        invertAxis.isVisible = true +                        invertAxis.isCheckable = true +                        invertAxis.isChecked = axisInvert +                        invertAxis.setOnMenuItemClickListener { +                            params.set("invert", if (!axisInvert) "-" else "+") +                            true +                        } + +                        val buttonInvert = params.get("inverted", false) +                        invertButton.isVisible = true +                        invertButton.isCheckable = true +                        invertButton.isChecked = buttonInvert +                        invertButton.setOnMenuItemClickListener { +                            params.set("inverted", !buttonInvert) +                            true +                        } + +                        setThreshold.isVisible = true +                        val thresholdSetting = object : AbstractIntSetting { +                            override val key = "" + +                            override fun getInt(needsGlobal: Boolean): Int = +                                (params.get("threshold", 0.5f) * 100).toInt() + +                            override fun setInt(value: Int) { +                                params.set("threshold", value.toFloat() / 100) +                                NativeInput.setButtonParam( +                                    item.playerIndex, +                                    item.nativeButton, +                                    params +                                ) +                            } + +                            override val defaultValue = 50 + +                            override fun getValueAsString(needsGlobal: Boolean): String = +                                getInt(needsGlobal).toString() + +                            override fun reset() = setInt(defaultValue) +                        } +                        setThreshold.setOnMenuItemClickListener { +                            onSliderClick( +                                SliderSetting(thresholdSetting, R.string.set_threshold), +                                position +                            ) +                            true +                        } + +                        val axisToggle = params.get("toggle", false) +                        toggleAxis.isVisible = true +                        toggleAxis.isCheckable = true +                        toggleAxis.isChecked = axisToggle +                        toggleAxis.setOnMenuItemClickListener { +                            params.set("toggle", !axisToggle) +                            true +                        } +                    } + +                    popup.setOnDismissListener { +                        NativeInput.setButtonParam(item.playerIndex, item.nativeButton, params) +                        settingsViewModel.setAdapterItemChanged(position) +                    } +                } + +                is ModifierInputSetting -> { +                    val stickParams = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog) +                    val modifierParams = ParamPackage(stickParams.get("modifier", "")) + +                    val invert = modifierParams.get("inverted", false) +                    invertButton.isVisible = true +                    invertButton.isCheckable = true +                    invertButton.isChecked = invert +                    invertButton.setOnMenuItemClickListener { +                        modifierParams.set("inverted", !invert) +                        stickParams.set("modifier", modifierParams.serialize()) +                        true +                    } + +                    val toggle = modifierParams.get("toggle", false) +                    toggleButton.isVisible = true +                    toggleButton.isCheckable = true +                    toggleButton.isChecked = toggle +                    toggleButton.setOnMenuItemClickListener { +                        modifierParams.set("toggle", !toggle) +                        stickParams.set("modifier", modifierParams.serialize()) +                        true +                    } + +                    popup.setOnDismissListener { +                        NativeInput.setStickParam( +                            item.playerIndex, +                            item.nativeAnalog, +                            stickParams +                        ) +                        settingsViewModel.setAdapterItemChanged(position) +                    } +                } +            } +        } +        popup.show() +    } +      fun onLongClick(item: SettingsItem, position: Int): Boolean {          SettingsDialogFragment.newInstance(              settingsViewModel, diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt index 60e029f34..a81ff6b1a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt @@ -1,7 +1,7 @@  // SPDX-FileCopyrightText: 2023 yuzu Emulator Project  // SPDX-License-Identifier: GPL-2.0-or-later -package org.yuzu.yuzu_emu.fragments +package org.yuzu.yuzu_emu.features.settings.ui  import android.app.Dialog  import android.content.DialogInterface @@ -11,19 +11,21 @@ import android.view.View  import android.view.ViewGroup  import androidx.fragment.app.DialogFragment  import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle  import com.google.android.material.dialog.MaterialAlertDialogBuilder  import com.google.android.material.slider.Slider -import kotlinx.coroutines.launch  import org.yuzu.yuzu_emu.R  import org.yuzu.yuzu_emu.databinding.DialogSliderBinding +import org.yuzu.yuzu_emu.features.input.NativeInput +import org.yuzu.yuzu_emu.features.input.model.AnalogDirection +import org.yuzu.yuzu_emu.features.settings.model.view.AnalogInputSetting +import org.yuzu.yuzu_emu.features.settings.model.view.ButtonInputSetting +import org.yuzu.yuzu_emu.features.settings.model.view.IntSingleChoiceSetting  import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem  import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting  import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting  import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting -import org.yuzu.yuzu_emu.model.SettingsViewModel +import org.yuzu.yuzu_emu.utils.ParamPackage +import org.yuzu.yuzu_emu.utils.collect  class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener {      private var type = 0 @@ -50,8 +52,49 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener                  MaterialAlertDialogBuilder(requireContext())                      .setMessage(R.string.reset_setting_confirmation)                      .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> -                        settingsViewModel.clickedItem!!.setting.reset() -                        settingsViewModel.setAdapterItemChanged(position) +                        when (val item = settingsViewModel.clickedItem) { +                            is AnalogInputSetting -> { +                                val stickParam = NativeInput.getStickParam( +                                    item.playerIndex, +                                    item.nativeAnalog +                                ) +                                if (stickParam.get("engine", "") == "analog_from_button") { +                                    when (item.analogDirection) { +                                        AnalogDirection.Up -> stickParam.erase("up") +                                        AnalogDirection.Down -> stickParam.erase("down") +                                        AnalogDirection.Left -> stickParam.erase("left") +                                        AnalogDirection.Right -> stickParam.erase("right") +                                    } +                                    NativeInput.setStickParam( +                                        item.playerIndex, +                                        item.nativeAnalog, +                                        stickParam +                                    ) +                                    settingsViewModel.setAdapterItemChanged(position) +                                } else { +                                    NativeInput.setStickParam( +                                        item.playerIndex, +                                        item.nativeAnalog, +                                        ParamPackage() +                                    ) +                                    settingsViewModel.setDatasetChanged(true) +                                } +                            } + +                            is ButtonInputSetting -> { +                                NativeInput.setButtonParam( +                                    item.playerIndex, +                                    item.nativeButton, +                                    ParamPackage() +                                ) +                                settingsViewModel.setAdapterItemChanged(position) +                            } + +                            else -> { +                                settingsViewModel.clickedItem!!.setting.reset() +                                settingsViewModel.setAdapterItemChanged(position) +                            } +                        }                      }                      .setNegativeButton(android.R.string.cancel, null)                      .create() @@ -61,7 +104,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener                  val item = settingsViewModel.clickedItem as SingleChoiceSetting                  val value = getSelectionForSingleChoiceValue(item)                  MaterialAlertDialogBuilder(requireContext()) -                    .setTitle(item.nameId) +                    .setTitle(item.title)                      .setSingleChoiceItems(item.choicesId, value, this)                      .create()              } @@ -81,7 +124,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener                  }                  MaterialAlertDialogBuilder(requireContext()) -                    .setTitle(item.nameId) +                    .setTitle(item.title)                      .setView(sliderBinding.root)                      .setPositiveButton(android.R.string.ok, this)                      .setNegativeButton(android.R.string.cancel, defaultCancelListener) @@ -91,8 +134,16 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener              SettingsItem.TYPE_STRING_SINGLE_CHOICE -> {                  val item = settingsViewModel.clickedItem as StringSingleChoiceSetting                  MaterialAlertDialogBuilder(requireContext()) -                    .setTitle(item.nameId) -                    .setSingleChoiceItems(item.choices, item.selectValueIndex, this) +                    .setTitle(item.title) +                    .setSingleChoiceItems(item.choices, item.selectedValueIndex, this) +                    .create() +            } + +            SettingsItem.TYPE_INT_SINGLE_CHOICE -> { +                val item = settingsViewModel.clickedItem as IntSingleChoiceSetting +                MaterialAlertDialogBuilder(requireContext()) +                    .setTitle(item.title) +                    .setSingleChoiceItems(item.choices, item.selectedValueIndex, this)                      .create()              } @@ -115,17 +166,11 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener          super.onViewCreated(view, savedInstanceState)          when (type) {              SettingsItem.TYPE_SLIDER -> { -                viewLifecycleOwner.lifecycleScope.launch { -                    repeatOnLifecycle(Lifecycle.State.CREATED) { -                        settingsViewModel.sliderTextValue.collect { -                            sliderBinding.textValue.text = it -                        } -                    } -                    repeatOnLifecycle(Lifecycle.State.CREATED) { -                        settingsViewModel.sliderProgress.collect { -                            sliderBinding.slider.value = it.toFloat() -                        } -                    } +                settingsViewModel.sliderTextValue.collect(viewLifecycleOwner) { +                    sliderBinding.textValue.text = it +                } +                settingsViewModel.sliderProgress.collect(viewLifecycleOwner) { +                    sliderBinding.slider.value = it.toFloat()                  }              }          } @@ -145,6 +190,12 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener                  scSetting.setSelectedValue(value)              } +            is IntSingleChoiceSetting -> { +                val scSetting = settingsViewModel.clickedItem as IntSingleChoiceSetting +                val value = scSetting.getValueAt(which) +                scSetting.setSelectedValue(value) +            } +              is SliderSetting -> {                  val sliderSetting = settingsViewModel.clickedItem as SliderSetting                  sliderSetting.setSelectedValue(settingsViewModel.sliderProgress.value) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt index 6f6e7be10..ec16f16c4 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt @@ -13,20 +13,17 @@ import androidx.core.view.WindowInsetsCompat  import androidx.core.view.updatePadding  import androidx.fragment.app.Fragment  import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle  import androidx.navigation.findNavController  import androidx.navigation.fragment.navArgs  import androidx.recyclerview.widget.LinearLayoutManager  import com.google.android.material.transition.MaterialSharedAxis -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch  import org.yuzu.yuzu_emu.R  import org.yuzu.yuzu_emu.databinding.FragmentSettingsBinding +import org.yuzu.yuzu_emu.features.input.NativeInput  import org.yuzu.yuzu_emu.features.settings.model.Settings -import org.yuzu.yuzu_emu.model.SettingsViewModel +import org.yuzu.yuzu_emu.fragments.MessageDialogFragment  import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins +import org.yuzu.yuzu_emu.utils.collect  class SettingsFragment : Fragment() {      private lateinit var presenter: SettingsFragmentPresenter @@ -45,6 +42,12 @@ class SettingsFragment : Fragment() {          returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)          reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)          exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) + +        val playerIndex = getPlayerIndex() +        if (playerIndex != -1) { +            NativeInput.loadInputProfiles() +            NativeInput.reloadInputDevices() +        }      }      override fun onCreateView( @@ -56,9 +59,9 @@ class SettingsFragment : Fragment() {          return binding.root      } -    // This is using the correct scope, lint is just acting up -    @SuppressLint("UnsafeRepeatOnLifecycleDetector") +    @SuppressLint("NotifyDataSetChanged")      override fun onViewCreated(view: View, savedInstanceState: Bundle?) { +        super.onViewCreated(view, savedInstanceState)          settingsAdapter = SettingsAdapter(this, requireContext())          presenter = SettingsFragmentPresenter(              settingsViewModel, @@ -71,7 +74,17 @@ class SettingsFragment : Fragment() {          ) {              args.game!!.title          } else { -            getString(args.menuTag.titleId) +            when (args.menuTag) { +                Settings.MenuTag.SECTION_INPUT_PLAYER_ONE -> Settings.getPlayerString(1) +                Settings.MenuTag.SECTION_INPUT_PLAYER_TWO -> Settings.getPlayerString(2) +                Settings.MenuTag.SECTION_INPUT_PLAYER_THREE -> Settings.getPlayerString(3) +                Settings.MenuTag.SECTION_INPUT_PLAYER_FOUR -> Settings.getPlayerString(4) +                Settings.MenuTag.SECTION_INPUT_PLAYER_FIVE -> Settings.getPlayerString(5) +                Settings.MenuTag.SECTION_INPUT_PLAYER_SIX -> Settings.getPlayerString(6) +                Settings.MenuTag.SECTION_INPUT_PLAYER_SEVEN -> Settings.getPlayerString(7) +                Settings.MenuTag.SECTION_INPUT_PLAYER_EIGHT -> Settings.getPlayerString(8) +                else -> getString(args.menuTag.titleId) +            }          }          binding.listSettings.apply {              adapter = settingsAdapter @@ -82,16 +95,37 @@ class SettingsFragment : Fragment() {              settingsViewModel.setShouldNavigateBack(true)          } -        viewLifecycleOwner.lifecycleScope.apply { -            launch { -                repeatOnLifecycle(Lifecycle.State.CREATED) { -                    settingsViewModel.shouldReloadSettingsList.collectLatest { -                        if (it) { -                            settingsViewModel.setShouldReloadSettingsList(false) -                            presenter.loadSettingsList() -                        } -                    } -                } +        settingsViewModel.shouldReloadSettingsList.collect( +            viewLifecycleOwner, +            resetState = { settingsViewModel.setShouldReloadSettingsList(false) } +        ) { if (it) presenter.loadSettingsList() } +        settingsViewModel.adapterItemChanged.collect( +            viewLifecycleOwner, +            resetState = { settingsViewModel.setAdapterItemChanged(-1) } +        ) { if (it != -1) settingsAdapter?.notifyItemChanged(it) } +        settingsViewModel.datasetChanged.collect( +            viewLifecycleOwner, +            resetState = { settingsViewModel.setDatasetChanged(false) } +        ) { if (it) settingsAdapter?.notifyDataSetChanged() } +        settingsViewModel.reloadListAndNotifyDataset.collect( +            viewLifecycleOwner, +            resetState = { settingsViewModel.setReloadListAndNotifyDataset(false) } +        ) { if (it) presenter.loadSettingsList(true) } +        settingsViewModel.shouldShowResetInputDialog.collect( +            viewLifecycleOwner, +            resetState = { settingsViewModel.setShouldShowResetInputDialog(false) } +        ) { +            if (it) { +                MessageDialogFragment.newInstance( +                    activity = requireActivity(), +                    titleId = R.string.reset_mapping, +                    descriptionId = R.string.reset_mapping_description, +                    positiveAction = { +                        NativeInput.resetControllerMappings(getPlayerIndex()) +                        settingsViewModel.setReloadListAndNotifyDataset(true) +                    }, +                    negativeAction = {} +                ).show(parentFragmentManager, MessageDialogFragment.TAG)              }          } @@ -115,6 +149,19 @@ class SettingsFragment : Fragment() {          setInsets()      } +    private fun getPlayerIndex(): Int = +        when (args.menuTag) { +            Settings.MenuTag.SECTION_INPUT_PLAYER_ONE -> 0 +            Settings.MenuTag.SECTION_INPUT_PLAYER_TWO -> 1 +            Settings.MenuTag.SECTION_INPUT_PLAYER_THREE -> 2 +            Settings.MenuTag.SECTION_INPUT_PLAYER_FOUR -> 3 +            Settings.MenuTag.SECTION_INPUT_PLAYER_FIVE -> 4 +            Settings.MenuTag.SECTION_INPUT_PLAYER_SIX -> 5 +            Settings.MenuTag.SECTION_INPUT_PLAYER_SEVEN -> 6 +            Settings.MenuTag.SECTION_INPUT_PLAYER_EIGHT -> 7 +            else -> -1 +        } +      private fun setInsets() {          ViewCompat.setOnApplyWindowInsetsListener(              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 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -3,11 +3,17 @@  package org.yuzu.yuzu_emu.features.settings.ui +import android.annotation.SuppressLint  import android.os.Build  import android.widget.Toast  import org.yuzu.yuzu_emu.NativeLibrary  import org.yuzu.yuzu_emu.R  import org.yuzu.yuzu_emu.YuzuApplication +import org.yuzu.yuzu_emu.features.input.NativeInput +import org.yuzu.yuzu_emu.features.input.model.AnalogDirection +import org.yuzu.yuzu_emu.features.input.model.NativeAnalog +import org.yuzu.yuzu_emu.features.input.model.NativeButton +import org.yuzu.yuzu_emu.features.input.model.NpadStyleIndex  import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting  import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting  import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting @@ -15,18 +21,21 @@ import org.yuzu.yuzu_emu.features.settings.model.ByteSetting  import org.yuzu.yuzu_emu.features.settings.model.IntSetting  import org.yuzu.yuzu_emu.features.settings.model.LongSetting  import org.yuzu.yuzu_emu.features.settings.model.Settings +import org.yuzu.yuzu_emu.features.settings.model.Settings.MenuTag  import org.yuzu.yuzu_emu.features.settings.model.ShortSetting  import org.yuzu.yuzu_emu.features.settings.model.view.* -import org.yuzu.yuzu_emu.model.SettingsViewModel +import org.yuzu.yuzu_emu.utils.InputHandler  import org.yuzu.yuzu_emu.utils.NativeConfig  class SettingsFragmentPresenter(      private val settingsViewModel: SettingsViewModel,      private val adapter: SettingsAdapter, -    private var menuTag: Settings.MenuTag +    private var menuTag: MenuTag  ) {      private var settingsList = ArrayList<SettingsItem>() +    private val context get() = YuzuApplication.appContext +      // Extension for altering settings list based on each setting's properties      fun ArrayList<SettingsItem>.add(key: String) {          val item = SettingsItem.settingsItems[key]!! @@ -53,73 +62,90 @@ class SettingsFragmentPresenter(          add(item)      } +    // Allows you to show/hide abstract settings based on the paired setting key +    fun ArrayList<SettingsItem>.addAbstract(item: SettingsItem) { +        val pairedSettingKey = item.setting.pairedSettingKey +        if (pairedSettingKey.isNotEmpty()) { +            val pairedSettingsItem = +                this.firstOrNull { it.setting.key == pairedSettingKey } ?: return +            val pairedSetting = pairedSettingsItem.setting as AbstractBooleanSetting +            if (!pairedSetting.getBoolean(!NativeConfig.isPerGameConfigLoaded())) return +        } +        add(item) +    } +      fun onViewCreated() {          loadSettingsList()      } -    fun loadSettingsList() { +    @SuppressLint("NotifyDataSetChanged") +    fun loadSettingsList(notifyDataSetChanged: Boolean = false) {          val sl = ArrayList<SettingsItem>()          when (menuTag) { -            Settings.MenuTag.SECTION_ROOT -> addConfigSettings(sl) -            Settings.MenuTag.SECTION_SYSTEM -> addSystemSettings(sl) -            Settings.MenuTag.SECTION_RENDERER -> addGraphicsSettings(sl) -            Settings.MenuTag.SECTION_AUDIO -> addAudioSettings(sl) -            Settings.MenuTag.SECTION_THEME -> addThemeSettings(sl) -            Settings.MenuTag.SECTION_DEBUG -> addDebugSettings(sl) -            else -> { -                val context = YuzuApplication.appContext -                Toast.makeText( -                    context, -                    context.getString(R.string.unimplemented_menu), -                    Toast.LENGTH_SHORT -                ).show() -                return -            } +            MenuTag.SECTION_ROOT -> addConfigSettings(sl) +            MenuTag.SECTION_SYSTEM -> addSystemSettings(sl) +            MenuTag.SECTION_RENDERER -> addGraphicsSettings(sl) +            MenuTag.SECTION_AUDIO -> addAudioSettings(sl) +            MenuTag.SECTION_INPUT -> addInputSettings(sl) +            MenuTag.SECTION_INPUT_PLAYER_ONE -> addInputPlayer(sl, 0) +            MenuTag.SECTION_INPUT_PLAYER_TWO -> addInputPlayer(sl, 1) +            MenuTag.SECTION_INPUT_PLAYER_THREE -> addInputPlayer(sl, 2) +            MenuTag.SECTION_INPUT_PLAYER_FOUR -> addInputPlayer(sl, 3) +            MenuTag.SECTION_INPUT_PLAYER_FIVE -> addInputPlayer(sl, 4) +            MenuTag.SECTION_INPUT_PLAYER_SIX -> addInputPlayer(sl, 5) +            MenuTag.SECTION_INPUT_PLAYER_SEVEN -> addInputPlayer(sl, 6) +            MenuTag.SECTION_INPUT_PLAYER_EIGHT -> addInputPlayer(sl, 7) +            MenuTag.SECTION_THEME -> addThemeSettings(sl) +            MenuTag.SECTION_DEBUG -> addDebugSettings(sl)          }          settingsList = sl -        adapter.submitList(settingsList) +        adapter.submitList(settingsList) { +            if (notifyDataSetChanged) { +                adapter.notifyDataSetChanged() +            } +        }      }      private fun addConfigSettings(sl: ArrayList<SettingsItem>) {          sl.apply {              add(                  SubmenuSetting( -                    R.string.preferences_system, -                    R.string.preferences_system_description, -                    R.drawable.ic_system_settings, -                    Settings.MenuTag.SECTION_SYSTEM +                    titleId = R.string.preferences_system, +                    descriptionId = R.string.preferences_system_description, +                    iconId = R.drawable.ic_system_settings, +                    menuKey = MenuTag.SECTION_SYSTEM                  )              )              add(                  SubmenuSetting( -                    R.string.preferences_graphics, -                    R.string.preferences_graphics_description, -                    R.drawable.ic_graphics, -                    Settings.MenuTag.SECTION_RENDERER +                    titleId = R.string.preferences_graphics, +                    descriptionId = R.string.preferences_graphics_description, +                    iconId = R.drawable.ic_graphics, +                    menuKey = MenuTag.SECTION_RENDERER                  )              )              add(                  SubmenuSetting( -                    R.string.preferences_audio, -                    R.string.preferences_audio_description, -                    R.drawable.ic_audio, -                    Settings.MenuTag.SECTION_AUDIO +                    titleId = R.string.preferences_audio, +                    descriptionId = R.string.preferences_audio_description, +                    iconId = R.drawable.ic_audio, +                    menuKey = MenuTag.SECTION_AUDIO                  )              )              add(                  SubmenuSetting( -                    R.string.preferences_debug, -                    R.string.preferences_debug_description, -                    R.drawable.ic_code, -                    Settings.MenuTag.SECTION_DEBUG +                    titleId = R.string.preferences_debug, +                    descriptionId = R.string.preferences_debug_description, +                    iconId = R.drawable.ic_code, +                    menuKey = MenuTag.SECTION_DEBUG                  )              )              add(                  RunnableSetting( -                    R.string.reset_to_default, -                    R.string.reset_to_default_description, -                    false, -                    R.drawable.ic_restore +                    titleId = R.string.reset_to_default, +                    descriptionId = R.string.reset_to_default_description, +                    isRunnable = !NativeLibrary.isRunning(), +                    iconId = R.drawable.ic_restore                  ) { settingsViewModel.setShouldShowResetSettingsDialog(true) }              )          } @@ -164,6 +190,671 @@ class SettingsFragmentPresenter(          }      } +    private fun addInputSettings(sl: ArrayList<SettingsItem>) { +        settingsViewModel.currentDevice = 0 + +        if (NativeConfig.isPerGameConfigLoaded()) { +            NativeInput.loadInputProfiles() +            val profiles = NativeInput.getInputProfileNames().toMutableList() +            profiles.add(0, "") +            val prettyProfiles = profiles.toTypedArray() +            prettyProfiles[0] = +                context.getString(R.string.use_global_input_configuration) +            sl.apply { +                for (i in 0 until 8) { +                    add( +                        IntSingleChoiceSetting( +                            getPerGameProfileSetting(profiles, i), +                            titleString = getPlayerProfileString(i + 1), +                            choices = prettyProfiles, +                            values = IntArray(profiles.size) { it }.toTypedArray() +                        ) +                    ) +                } +            } +            return +        } + +        val getConnectedIcon: (Int) -> Int = { playerIndex: Int -> +            if (NativeInput.getIsConnected(playerIndex)) { +                R.drawable.ic_controller +            } else { +                R.drawable.ic_controller_disconnected +            } +        } + +        val inputSettings = NativeConfig.getInputSettings(true) +        sl.apply { +            add( +                SubmenuSetting( +                    titleString = Settings.getPlayerString(1), +                    descriptionString = inputSettings[0].profileName, +                    menuKey = MenuTag.SECTION_INPUT_PLAYER_ONE, +                    iconId = getConnectedIcon(0) +                ) +            ) +            add( +                SubmenuSetting( +                    titleString = Settings.getPlayerString(2), +                    descriptionString = inputSettings[1].profileName, +                    menuKey = MenuTag.SECTION_INPUT_PLAYER_TWO, +                    iconId = getConnectedIcon(1) +                ) +            ) +            add( +                SubmenuSetting( +                    titleString = Settings.getPlayerString(3), +                    descriptionString = inputSettings[2].profileName, +                    menuKey = MenuTag.SECTION_INPUT_PLAYER_THREE, +                    iconId = getConnectedIcon(2) +                ) +            ) +            add( +                SubmenuSetting( +                    titleString = Settings.getPlayerString(4), +                    descriptionString = inputSettings[3].profileName, +                    menuKey = MenuTag.SECTION_INPUT_PLAYER_FOUR, +                    iconId = getConnectedIcon(3) +                ) +            ) +            add( +                SubmenuSetting( +                    titleString = Settings.getPlayerString(5), +                    descriptionString = inputSettings[4].profileName, +                    menuKey = MenuTag.SECTION_INPUT_PLAYER_FIVE, +                    iconId = getConnectedIcon(4) +                ) +            ) +            add( +                SubmenuSetting( +                    titleString = Settings.getPlayerString(6), +                    descriptionString = inputSettings[5].profileName, +                    menuKey = MenuTag.SECTION_INPUT_PLAYER_SIX, +                    iconId = getConnectedIcon(5) +                ) +            ) +            add( +                SubmenuSetting( +                    titleString = Settings.getPlayerString(7), +                    descriptionString = inputSettings[6].profileName, +                    menuKey = MenuTag.SECTION_INPUT_PLAYER_SEVEN, +                    iconId = getConnectedIcon(6) +                ) +            ) +            add( +                SubmenuSetting( +                    titleString = Settings.getPlayerString(8), +                    descriptionString = inputSettings[7].profileName, +                    menuKey = MenuTag.SECTION_INPUT_PLAYER_EIGHT, +                    iconId = getConnectedIcon(7) +                ) +            ) +        } +    } + +    private fun getPlayerProfileString(player: Int): String = +        context.getString(R.string.player_num_profile, player) + +    private fun getPerGameProfileSetting( +        profiles: List<String>, +        playerIndex: Int +    ): AbstractIntSetting { +        return object : AbstractIntSetting { +            private val players +                get() = NativeConfig.getInputSettings(false) + +            override val key = "" + +            override fun getInt(needsGlobal: Boolean): Int { +                val currentProfile = players[playerIndex].profileName +                profiles.forEachIndexed { i, profile -> +                    if (profile == currentProfile) { +                        return i +                    } +                } +                return 0 +            } + +            override fun setInt(value: Int) { +                NativeInput.loadPerGameConfiguration(playerIndex, value, profiles[value]) +                NativeInput.connectControllers(playerIndex) +                NativeConfig.saveControlPlayerValues() +            } + +            override val defaultValue = 0 + +            override fun getValueAsString(needsGlobal: Boolean): String = getInt().toString() + +            override fun reset() = setInt(defaultValue) + +            override var global = true + +            override val isRuntimeModifiable = true + +            override val isSaveable = true +        } +    } + +    private fun addInputPlayer(sl: ArrayList<SettingsItem>, playerIndex: Int) { +        sl.apply { +            val connectedSetting = object : AbstractBooleanSetting { +                override val key = "connected" + +                override fun getBoolean(needsGlobal: Boolean): Boolean = +                    NativeInput.getIsConnected(playerIndex) + +                override fun setBoolean(value: Boolean) = +                    NativeInput.connectControllers(playerIndex, value) + +                override val defaultValue = playerIndex == 0 + +                override fun getValueAsString(needsGlobal: Boolean): String = +                    getBoolean(needsGlobal).toString() + +                override fun reset() = setBoolean(defaultValue) +            } +            add(SwitchSetting(connectedSetting, R.string.connected)) + +            val styleTags = NativeInput.getSupportedStyleTags(playerIndex) +            val npadType = object : AbstractIntSetting { +                override val key = "npad_type" +                override fun getInt(needsGlobal: Boolean): Int { +                    val styleIndex = NativeInput.getStyleIndex(playerIndex) +                    return styleTags.indexOfFirst { it == styleIndex } +                } + +                override fun setInt(value: Int) { +                    NativeInput.setStyleIndex(playerIndex, styleTags[value]) +                    settingsViewModel.setReloadListAndNotifyDataset(true) +                } + +                override val defaultValue = NpadStyleIndex.Fullkey.int +                override fun getValueAsString(needsGlobal: Boolean): String = getInt().toString() +                override fun reset() = setInt(defaultValue) +                override val pairedSettingKey: String = "connected" +            } +            addAbstract( +                IntSingleChoiceSetting( +                    npadType, +                    titleId = R.string.controller_type, +                    choices = styleTags.map { context.getString(it.nameId) } +                        .toTypedArray(), +                    values = IntArray(styleTags.size) { it }.toTypedArray() +                ) +            ) + +            InputHandler.updateControllerData() + +            val autoMappingSetting = object : AbstractIntSetting { +                override val key = "auto_mapping_device" + +                override fun getInt(needsGlobal: Boolean): Int = -1 + +                override fun setInt(value: Int) { +                    val registeredController = InputHandler.registeredControllers[value + 1] +                    val displayName = registeredController.get( +                        "display", +                        context.getString(R.string.unknown) +                    ) +                    NativeInput.updateMappingsWithDefault( +                        playerIndex, +                        registeredController, +                        displayName +                    ) +                    Toast.makeText( +                        context, +                        context.getString(R.string.attempted_auto_map, displayName), +                        Toast.LENGTH_SHORT +                    ).show() +                    settingsViewModel.setReloadListAndNotifyDataset(true) +                } + +                override val defaultValue = -1 + +                override fun getValueAsString(needsGlobal: Boolean) = getInt().toString() + +                override fun reset() = setInt(defaultValue) + +                override val isRuntimeModifiable: Boolean = true +            } + +            val unknownString = context.getString(R.string.unknown) +            val prettyAutoMappingControllerList = InputHandler.registeredControllers.mapNotNull { +                val port = it.get("port", -1) +                return@mapNotNull if (port == 100 || port == -1) { +                    null +                } else { +                    it.get("display", unknownString) +                } +            }.toTypedArray() +            add( +                IntSingleChoiceSetting( +                    autoMappingSetting, +                    titleId = R.string.auto_map, +                    descriptionId = R.string.auto_map_description, +                    choices = prettyAutoMappingControllerList, +                    values = IntArray(prettyAutoMappingControllerList.size) { it }.toTypedArray() +                ) +            ) + +            val mappingFilterSetting = object : AbstractIntSetting { +                override val key = "mapping_filter" + +                override fun getInt(needsGlobal: Boolean): Int = settingsViewModel.currentDevice + +                override fun setInt(value: Int) { +                    settingsViewModel.currentDevice = value +                } + +                override val defaultValue = 0 + +                override fun getValueAsString(needsGlobal: Boolean) = getInt().toString() + +                override fun reset() = setInt(defaultValue) + +                override val isRuntimeModifiable: Boolean = true +            } + +            val prettyControllerList = InputHandler.registeredControllers.mapNotNull { +                return@mapNotNull if (it.get("port", 0) == 100) { +                    null +                } else { +                    it.get("display", unknownString) +                } +            }.toTypedArray() +            add( +                IntSingleChoiceSetting( +                    mappingFilterSetting, +                    titleId = R.string.input_mapping_filter, +                    descriptionId = R.string.input_mapping_filter_description, +                    choices = prettyControllerList, +                    values = IntArray(prettyControllerList.size) { it }.toTypedArray() +                ) +            ) + +            add(InputProfileSetting(playerIndex)) +            add( +                RunnableSetting(titleId = R.string.reset_to_default, isRunnable = true) { +                    settingsViewModel.setShouldShowResetInputDialog(true) +                } +            ) + +            val styleIndex = NativeInput.getStyleIndex(playerIndex) + +            // Buttons +            when (styleIndex) { +                NpadStyleIndex.Fullkey, +                NpadStyleIndex.Handheld, +                NpadStyleIndex.JoyconDual -> { +                    add(HeaderSetting(R.string.buttons)) +                    add(ButtonInputSetting(playerIndex, NativeButton.A, R.string.button_a)) +                    add(ButtonInputSetting(playerIndex, NativeButton.B, R.string.button_b)) +                    add(ButtonInputSetting(playerIndex, NativeButton.X, R.string.button_x)) +                    add(ButtonInputSetting(playerIndex, NativeButton.Y, R.string.button_y)) +                    add(ButtonInputSetting(playerIndex, NativeButton.Plus, R.string.button_plus)) +                    add(ButtonInputSetting(playerIndex, NativeButton.Minus, R.string.button_minus)) +                    add(ButtonInputSetting(playerIndex, NativeButton.Home, R.string.button_home)) +                    add( +                        ButtonInputSetting( +                            playerIndex, +                            NativeButton.Capture, +                            R.string.button_capture +                        ) +                    ) +                } + +                NpadStyleIndex.JoyconLeft -> { +                    add(HeaderSetting(R.string.buttons)) +                    add(ButtonInputSetting(playerIndex, NativeButton.Minus, R.string.button_minus)) +                    add( +                        ButtonInputSetting( +                            playerIndex, +                            NativeButton.Capture, +                            R.string.button_capture +                        ) +                    ) +                } + +                NpadStyleIndex.JoyconRight -> { +                    add(HeaderSetting(R.string.buttons)) +                    add(ButtonInputSetting(playerIndex, NativeButton.A, R.string.button_a)) +                    add(ButtonInputSetting(playerIndex, NativeButton.B, R.string.button_b)) +                    add(ButtonInputSetting(playerIndex, NativeButton.X, R.string.button_x)) +                    add(ButtonInputSetting(playerIndex, NativeButton.Y, R.string.button_y)) +                    add(ButtonInputSetting(playerIndex, NativeButton.Plus, R.string.button_plus)) +                    add(ButtonInputSetting(playerIndex, NativeButton.Home, R.string.button_home)) +                } + +                NpadStyleIndex.GameCube -> { +                    add(HeaderSetting(R.string.buttons)) +                    add(ButtonInputSetting(playerIndex, NativeButton.A, R.string.button_a)) +                    add(ButtonInputSetting(playerIndex, NativeButton.B, R.string.button_b)) +                    add(ButtonInputSetting(playerIndex, NativeButton.X, R.string.button_x)) +                    add(ButtonInputSetting(playerIndex, NativeButton.Y, R.string.button_y)) +                    add(ButtonInputSetting(playerIndex, NativeButton.Plus, R.string.start_pause)) +                } + +                else -> { +                    // No-op +                } +            } + +            when (styleIndex) { +                NpadStyleIndex.Fullkey, +                NpadStyleIndex.Handheld, +                NpadStyleIndex.JoyconDual, +                NpadStyleIndex.JoyconLeft -> { +                    add(HeaderSetting(R.string.dpad)) +                    add(ButtonInputSetting(playerIndex, NativeButton.DUp, R.string.up)) +                    add(ButtonInputSetting(playerIndex, NativeButton.DDown, R.string.down)) +                    add(ButtonInputSetting(playerIndex, NativeButton.DLeft, R.string.left)) +                    add(ButtonInputSetting(playerIndex, NativeButton.DRight, R.string.right)) +                } + +                else -> { +                    // No-op +                } +            } + +            // Left stick +            when (styleIndex) { +                NpadStyleIndex.Fullkey, +                NpadStyleIndex.Handheld, +                NpadStyleIndex.JoyconDual, +                NpadStyleIndex.JoyconLeft -> { +                    add(HeaderSetting(R.string.left_stick)) +                    addAll(getStickDirections(playerIndex, NativeAnalog.LStick)) +                    add(ButtonInputSetting(playerIndex, NativeButton.LStick, R.string.pressed)) +                    addAll(getExtraStickSettings(playerIndex, NativeAnalog.LStick)) +                } + +                NpadStyleIndex.GameCube -> { +                    add(HeaderSetting(R.string.control_stick)) +                    addAll(getStickDirections(playerIndex, NativeAnalog.LStick)) +                    addAll(getExtraStickSettings(playerIndex, NativeAnalog.LStick)) +                } + +                else -> { +                    // No-op +                } +            } + +            // Right stick +            when (styleIndex) { +                NpadStyleIndex.Fullkey, +                NpadStyleIndex.Handheld, +                NpadStyleIndex.JoyconDual, +                NpadStyleIndex.JoyconRight -> { +                    add(HeaderSetting(R.string.right_stick)) +                    addAll(getStickDirections(playerIndex, NativeAnalog.RStick)) +                    add(ButtonInputSetting(playerIndex, NativeButton.RStick, R.string.pressed)) +                    addAll(getExtraStickSettings(playerIndex, NativeAnalog.RStick)) +                } + +                NpadStyleIndex.GameCube -> { +                    add(HeaderSetting(R.string.c_stick)) +                    addAll(getStickDirections(playerIndex, NativeAnalog.RStick)) +                    addAll(getExtraStickSettings(playerIndex, NativeAnalog.RStick)) +                } + +                else -> { +                    // No-op +                } +            } + +            // L/R, ZL/ZR, and SL/SR +            when (styleIndex) { +                NpadStyleIndex.Fullkey, +                NpadStyleIndex.Handheld -> { +                    add(HeaderSetting(R.string.triggers)) +                    add(ButtonInputSetting(playerIndex, NativeButton.L, R.string.button_l)) +                    add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_r)) +                    add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_zl)) +                    add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_zr)) +                } + +                NpadStyleIndex.JoyconDual -> { +                    add(HeaderSetting(R.string.triggers)) +                    add(ButtonInputSetting(playerIndex, NativeButton.L, R.string.button_l)) +                    add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_r)) +                    add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_zl)) +                    add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_zr)) +                    add( +                        ButtonInputSetting( +                            playerIndex, +                            NativeButton.SLLeft, +                            R.string.button_sl_left +                        ) +                    ) +                    add( +                        ButtonInputSetting( +                            playerIndex, +                            NativeButton.SRLeft, +                            R.string.button_sr_left +                        ) +                    ) +                    add( +                        ButtonInputSetting( +                            playerIndex, +                            NativeButton.SLRight, +                            R.string.button_sl_right +                        ) +                    ) +                    add( +                        ButtonInputSetting( +                            playerIndex, +                            NativeButton.SRRight, +                            R.string.button_sr_right +                        ) +                    ) +                } + +                NpadStyleIndex.JoyconLeft -> { +                    add(HeaderSetting(R.string.triggers)) +                    add(ButtonInputSetting(playerIndex, NativeButton.L, R.string.button_l)) +                    add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_zl)) +                    add( +                        ButtonInputSetting( +                            playerIndex, +                            NativeButton.SLLeft, +                            R.string.button_sl_left +                        ) +                    ) +                    add( +                        ButtonInputSetting( +                            playerIndex, +                            NativeButton.SRLeft, +                            R.string.button_sr_left +                        ) +                    ) +                } + +                NpadStyleIndex.JoyconRight -> { +                    add(HeaderSetting(R.string.triggers)) +                    add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_r)) +                    add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_zr)) +                    add( +                        ButtonInputSetting( +                            playerIndex, +                            NativeButton.SLRight, +                            R.string.button_sl_right +                        ) +                    ) +                    add( +                        ButtonInputSetting( +                            playerIndex, +                            NativeButton.SRRight, +                            R.string.button_sr_right +                        ) +                    ) +                } + +                NpadStyleIndex.GameCube -> { +                    add(HeaderSetting(R.string.triggers)) +                    add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_z)) +                    add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_l)) +                    add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_r)) +                } + +                else -> { +                    // No-op +                } +            } + +            add(HeaderSetting(R.string.vibration)) +            val vibrationEnabledSetting = object : AbstractBooleanSetting { +                override val key = "vibration" + +                override fun getBoolean(needsGlobal: Boolean): Boolean = +                    NativeConfig.getInputSettings(true)[playerIndex].vibrationEnabled + +                override fun setBoolean(value: Boolean) { +                    val settings = NativeConfig.getInputSettings(true) +                    settings[playerIndex].vibrationEnabled = value +                    NativeConfig.setInputSettings(settings, true) +                } + +                override val defaultValue = true + +                override fun getValueAsString(needsGlobal: Boolean): String = +                    getBoolean(needsGlobal).toString() + +                override fun reset() = setBoolean(defaultValue) +            } +            add(SwitchSetting(vibrationEnabledSetting, R.string.vibration)) + +            val useSystemVibratorSetting = object : AbstractBooleanSetting { +                override val key = "" + +                override fun getBoolean(needsGlobal: Boolean): Boolean = +                    NativeConfig.getInputSettings(true)[playerIndex].useSystemVibrator + +                override fun setBoolean(value: Boolean) { +                    val settings = NativeConfig.getInputSettings(true) +                    settings[playerIndex].useSystemVibrator = value +                    NativeConfig.setInputSettings(settings, true) +                } + +                override val defaultValue = playerIndex == 0 + +                override fun getValueAsString(needsGlobal: Boolean): String = +                    getBoolean(needsGlobal).toString() + +                override fun reset() = setBoolean(defaultValue) + +                override val pairedSettingKey: String = "vibration" +            } +            addAbstract(SwitchSetting(useSystemVibratorSetting, R.string.use_system_vibrator)) + +            val vibrationStrengthSetting = object : AbstractIntSetting { +                override val key = "" + +                override fun getInt(needsGlobal: Boolean): Int = +                    NativeConfig.getInputSettings(true)[playerIndex].vibrationStrength + +                override fun setInt(value: Int) { +                    val settings = NativeConfig.getInputSettings(true) +                    settings[playerIndex].vibrationStrength = value +                    NativeConfig.setInputSettings(settings, true) +                } + +                override val defaultValue = 100 + +                override fun getValueAsString(needsGlobal: Boolean): String = +                    getInt(needsGlobal).toString() + +                override fun reset() = setInt(defaultValue) + +                override val pairedSettingKey: String = "vibration" +            } +            addAbstract( +                SliderSetting(vibrationStrengthSetting, R.string.vibration_strength, units = "%") +            ) +        } +    } + +    // Convenience function for creating AbstractIntSettings for modifier range/stick range/stick deadzones +    private fun getStickIntSettingFromParam( +        playerIndex: Int, +        paramName: String, +        stick: NativeAnalog, +        defaultValue: Int +    ): AbstractIntSetting = +        object : AbstractIntSetting { +            val params get() = NativeInput.getStickParam(playerIndex, stick) + +            override val key = "" + +            override fun getInt(needsGlobal: Boolean): Int = +                (params.get(paramName, 0.15f) * 100).toInt() + +            override fun setInt(value: Int) { +                val tempParams = params +                tempParams.set(paramName, value.toFloat() / 100) +                NativeInput.setStickParam(playerIndex, stick, tempParams) +            } + +            override val defaultValue = defaultValue + +            override fun getValueAsString(needsGlobal: Boolean): String = +                getInt(needsGlobal).toString() + +            override fun reset() = setInt(defaultValue) +        } + +    private fun getExtraStickSettings( +        playerIndex: Int, +        nativeAnalog: NativeAnalog +    ): List<SettingsItem> { +        val stickIsController = +            NativeInput.isController(NativeInput.getStickParam(playerIndex, nativeAnalog)) +        val modifierRangeSetting = +            getStickIntSettingFromParam(playerIndex, "modifier_scale", nativeAnalog, 50) +        val stickRangeSetting = +            getStickIntSettingFromParam(playerIndex, "range", nativeAnalog, 95) +        val stickDeadzoneSetting = +            getStickIntSettingFromParam(playerIndex, "deadzone", nativeAnalog, 15) + +        val out = mutableListOf<SettingsItem>().apply { +            if (stickIsController) { +                add(SliderSetting(stickRangeSetting, titleId = R.string.range, min = 25, max = 150)) +                add(SliderSetting(stickDeadzoneSetting, R.string.deadzone)) +            } else { +                add(ModifierInputSetting(playerIndex, NativeAnalog.LStick, R.string.modifier)) +                add(SliderSetting(modifierRangeSetting, R.string.modifier_range)) +            } +        } +        return out +    } + +    private fun getStickDirections(player: Int, stick: NativeAnalog): List<AnalogInputSetting> = +        listOf( +            AnalogInputSetting( +                player, +                stick, +                AnalogDirection.Up, +                R.string.up +            ), +            AnalogInputSetting( +                player, +                stick, +                AnalogDirection.Down, +                R.string.down +            ), +            AnalogInputSetting( +                player, +                stick, +                AnalogDirection.Left, +                R.string.left +            ), +            AnalogInputSetting( +                player, +                stick, +                AnalogDirection.Right, +                R.string.right +            ) +        ) +      private fun addThemeSettings(sl: ArrayList<SettingsItem>) {          sl.apply {              val theme: AbstractIntSetting = object : AbstractIntSetting { @@ -186,20 +877,18 @@ class SettingsFragmentPresenter(                  add(                      SingleChoiceSetting(                          theme, -                        R.string.change_app_theme, -                        0, -                        R.array.themeEntriesA12, -                        R.array.themeValuesA12 +                        titleId = R.string.change_app_theme, +                        choicesId = R.array.themeEntriesA12, +                        valuesId = R.array.themeValuesA12                      )                  )              } else {                  add(                      SingleChoiceSetting(                          theme, -                        R.string.change_app_theme, -                        0, -                        R.array.themeEntries, -                        R.array.themeValues +                        titleId = R.string.change_app_theme, +                        choicesId = R.array.themeEntries, +                        valuesId = R.array.themeValues                      )                  )              } @@ -228,10 +917,9 @@ class SettingsFragmentPresenter(              add(                  SingleChoiceSetting(                      themeMode, -                    R.string.change_theme_mode, -                    0, -                    R.array.themeModeEntries, -                    R.array.themeModeValues +                    titleId = R.string.change_theme_mode, +                    choicesId = R.array.themeModeEntries, +                    valuesId = R.array.themeModeValues                  )              ) @@ -262,8 +950,8 @@ class SettingsFragmentPresenter(              add(                  SwitchSetting(                      blackBackgrounds, -                    R.string.use_black_backgrounds, -                    R.string.use_black_backgrounds_description +                    titleId = R.string.use_black_backgrounds, +                    descriptionId = R.string.use_black_backgrounds_description                  )              )          } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsSearchFragment.kt index a135b80b4..ed60cf34f 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsSearchFragment.kt @@ -1,7 +1,7 @@  // SPDX-FileCopyrightText: 2023 yuzu Emulator Project  // SPDX-License-Identifier: GPL-2.0-or-later -package org.yuzu.yuzu_emu.fragments +package org.yuzu.yuzu_emu.features.settings.ui  import android.content.Context  import android.os.Bundle @@ -15,21 +15,17 @@ import androidx.core.view.updatePadding  import androidx.core.widget.doOnTextChanged  import androidx.fragment.app.Fragment  import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle  import androidx.recyclerview.widget.LinearLayoutManager  import com.google.android.material.divider.MaterialDividerItemDecoration  import com.google.android.material.transition.MaterialSharedAxis  import info.debatty.java.stringsimilarity.Cosine -import kotlinx.coroutines.launch  import org.yuzu.yuzu_emu.R  import org.yuzu.yuzu_emu.databinding.FragmentSettingsSearchBinding  import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem -import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter -import org.yuzu.yuzu_emu.model.SettingsViewModel  import org.yuzu.yuzu_emu.utils.NativeConfig +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible  import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins +import org.yuzu.yuzu_emu.utils.collect  class SettingsSearchFragment : Fragment() {      private var _binding: FragmentSettingsSearchBinding? = null @@ -85,14 +81,10 @@ class SettingsSearchFragment : Fragment() {              search()              binding.settingsList.smoothScrollToPosition(0)          } -        viewLifecycleOwner.lifecycleScope.launch { -            repeatOnLifecycle(Lifecycle.State.CREATED) { -                settingsViewModel.shouldReloadSettingsList.collect { -                    if (it) { -                        settingsViewModel.setShouldReloadSettingsList(false) -                        search() -                    } -                } +        settingsViewModel.shouldReloadSettingsList.collect(viewLifecycleOwner) { +            if (it) { +                settingsViewModel.setShouldReloadSettingsList(false) +                search()              }          } @@ -108,10 +100,9 @@ class SettingsSearchFragment : Fragment() {      private fun search() {          val searchTerm = binding.searchText.text.toString().lowercase() -        binding.clearButton.visibility = -            if (searchTerm.isEmpty()) View.INVISIBLE else View.VISIBLE +        binding.clearButton.setVisible(visible = searchTerm.isNotEmpty(), gone = false)          if (searchTerm.isEmpty()) { -            binding.noResultsView.visibility = View.VISIBLE +            binding.noResultsView.setVisible(visible = false, gone = false)              settingsAdapter?.submitList(emptyList())              return          } @@ -119,7 +110,7 @@ class SettingsSearchFragment : Fragment() {          val baseList = SettingsItem.settingsItems          val similarityAlgorithm = if (searchTerm.length > 2) Cosine() else Cosine(1)          val sortedList: List<SettingsItem> = baseList.mapNotNull { item -> -            val title = getString(item.value.nameId).lowercase() +            val title = item.value.title.lowercase()              val similarity = similarityAlgorithm.similarity(searchTerm, title)              if (similarity > 0.08) {                  Pair(similarity, item) @@ -138,8 +129,7 @@ class SettingsSearchFragment : Fragment() {              optionalSetting          }          settingsAdapter?.submitList(sortedList) -        binding.noResultsView.visibility = -            if (sortedList.isEmpty()) View.VISIBLE else View.INVISIBLE +        binding.noResultsView.setVisible(visible = sortedList.isEmpty(), gone = false)      }      private fun focusSearch() { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsViewModel.kt index 5cb6a5d57..fbdca04e9 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsViewModel.kt @@ -1,20 +1,26 @@  // SPDX-FileCopyrightText: 2023 yuzu Emulator Project  // SPDX-License-Identifier: GPL-2.0-or-later -package org.yuzu.yuzu_emu.model +package org.yuzu.yuzu_emu.features.settings.ui  import androidx.lifecycle.ViewModel  import kotlinx.coroutines.flow.MutableStateFlow  import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow  import org.yuzu.yuzu_emu.R  import org.yuzu.yuzu_emu.YuzuApplication  import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem +import org.yuzu.yuzu_emu.model.Game +import org.yuzu.yuzu_emu.utils.InputHandler +import org.yuzu.yuzu_emu.utils.ParamPackage  class SettingsViewModel : ViewModel() {      var game: Game? = null      var clickedItem: SettingsItem? = null +    var currentDevice = 0 +      val shouldRecreate: StateFlow<Boolean> get() = _shouldRecreate      private val _shouldRecreate = MutableStateFlow(false) @@ -36,6 +42,18 @@ class SettingsViewModel : ViewModel() {      val adapterItemChanged: StateFlow<Int> get() = _adapterItemChanged      private val _adapterItemChanged = MutableStateFlow(-1) +    private val _datasetChanged = MutableStateFlow(false) +    val datasetChanged = _datasetChanged.asStateFlow() + +    private val _reloadListAndNotifyDataset = MutableStateFlow(false) +    val reloadListAndNotifyDataset = _reloadListAndNotifyDataset.asStateFlow() + +    private val _shouldShowDeleteProfileDialog = MutableStateFlow("") +    val shouldShowDeleteProfileDialog = _shouldShowDeleteProfileDialog.asStateFlow() + +    private val _shouldShowResetInputDialog = MutableStateFlow(false) +    val shouldShowResetInputDialog = _shouldShowResetInputDialog.asStateFlow() +      fun setShouldRecreate(value: Boolean) {          _shouldRecreate.value = value      } @@ -68,4 +86,27 @@ class SettingsViewModel : ViewModel() {      fun setAdapterItemChanged(value: Int) {          _adapterItemChanged.value = value      } + +    fun setDatasetChanged(value: Boolean) { +        _datasetChanged.value = value +    } + +    fun setReloadListAndNotifyDataset(value: Boolean) { +        _reloadListAndNotifyDataset.value = value +    } + +    fun setShouldShowDeleteProfileDialog(profile: String) { +        _shouldShowDeleteProfileDialog.value = profile +    } + +    fun setShouldShowResetInputDialog(value: Boolean) { +        _shouldShowResetInputDialog.value = value +    } + +    fun getCurrentDeviceParams(defaultParams: ParamPackage): ParamPackage = +        try { +            InputHandler.registeredControllers[currentDevice] +        } catch (e: IndexOutOfBoundsException) { +            defaultParams +        }  } 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 100644 --- 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  import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem  import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter  import org.yuzu.yuzu_emu.utils.NativeConfig +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible  class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :      SettingViewHolder(binding.root, adapter) { @@ -21,28 +22,19 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA      override fun bind(item: SettingsItem) {          setting = item as DateTimeSetting -        binding.textSettingName.setText(item.nameId) -        if (item.descriptionId != 0) { -            binding.textSettingDescription.setText(item.descriptionId) -            binding.textSettingDescription.visibility = View.VISIBLE -        } else { -            binding.textSettingDescription.visibility = View.GONE -        } - -        binding.textSettingValue.visibility = View.VISIBLE +        binding.textSettingName.text = item.title +        binding.textSettingDescription.setVisible(item.description.isNotEmpty()) +        binding.textSettingDescription.text = item.description +        binding.textSettingValue.setVisible(true)          val epochTime = setting.getValue()          val instant = Instant.ofEpochMilli(epochTime * 1000)          val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC"))          val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)          binding.textSettingValue.text = dateFormatter.format(zonedTime) -        binding.buttonClear.visibility = if (setting.setting.global || -            !NativeConfig.isPerGameConfigLoaded() -        ) { -            View.GONE -        } else { -            View.VISIBLE -        } +        binding.buttonClear.setVisible( +            !setting.setting.global || NativeConfig.isPerGameConfigLoaded() +        )          binding.buttonClear.setOnClickListener {              adapter.onClearClick(setting, bindingAdapterPosition)          } 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 100644 --- 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      }      override fun bind(item: SettingsItem) { -        binding.textHeaderName.setText(item.nameId) +        binding.textHeaderName.text = item.title      }      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 100644 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 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.settings.ui.viewholder + +import android.view.View +import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding +import org.yuzu.yuzu_emu.features.settings.model.view.InputProfileSetting +import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem +import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible + +class InputProfileViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : +    SettingViewHolder(binding.root, adapter) { +    private lateinit var setting: InputProfileSetting + +    override fun bind(item: SettingsItem) { +        setting = item as InputProfileSetting +        binding.textSettingName.text = setting.title +        binding.textSettingValue.text = +            setting.getCurrentProfile().ifEmpty { binding.root.context.getString(R.string.not_set) } + +        binding.textSettingDescription.setVisible(false) +        binding.buttonClear.setVisible(false) +        binding.icon.setVisible(false) +        binding.buttonClear.setVisible(false) +    } + +    override fun onClick(clicked: View) = +        adapter.onInputProfileClick(setting, bindingAdapterPosition) + +    override fun onLongClick(clicked: View): Boolean = false +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputViewHolder.kt new file mode 100644 index 000000000..9d9047804 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputViewHolder.kt @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.settings.ui.viewholder + +import android.view.View +import org.yuzu.yuzu_emu.databinding.ListItemSettingInputBinding +import org.yuzu.yuzu_emu.features.input.NativeInput +import org.yuzu.yuzu_emu.features.settings.model.view.AnalogInputSetting +import org.yuzu.yuzu_emu.features.settings.model.view.ButtonInputSetting +import org.yuzu.yuzu_emu.features.settings.model.view.InputSetting +import org.yuzu.yuzu_emu.features.settings.model.view.ModifierInputSetting +import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem +import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible + +class InputViewHolder(val binding: ListItemSettingInputBinding, adapter: SettingsAdapter) : +    SettingViewHolder(binding.root, adapter) { +    private lateinit var setting: InputSetting + +    override fun bind(item: SettingsItem) { +        setting = item as InputSetting +        binding.textSettingName.text = setting.title +        binding.textSettingValue.text = setting.getSelectedValue() + +        when (item) { +            is AnalogInputSetting -> { +                val param = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog) +                binding.buttonOptions.setVisible( +                    param.get("engine", "") == "analog_from_button" || +                        param.has("axis_x") || param.has("axis_y") +                ) +            } + +            is ButtonInputSetting -> { +                val param = NativeInput.getButtonParam(item.playerIndex, item.nativeButton) +                binding.buttonOptions.setVisible( +                    param.has("code") || param.has("button") || param.has("hat") || +                        param.has("axis") +                ) +            } + +            is ModifierInputSetting -> { +                val params = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog) +                binding.buttonOptions.setVisible(params.has("modifier")) +            } +        } + +        binding.buttonOptions.setOnClickListener(null) +        binding.buttonOptions.setOnClickListener { +            adapter.onInputOptionsClick(binding.buttonOptions, setting, bindingAdapterPosition) +        } +    } + +    override fun onClick(clicked: View) = +        adapter.onInputClick(setting, bindingAdapterPosition) + +    override fun onLongClick(clicked: View): Boolean = +        adapter.onLongClick(setting, bindingAdapterPosition) +} 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 100644 --- 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  import android.view.View  import androidx.core.content.res.ResourcesCompat -import org.yuzu.yuzu_emu.NativeLibrary  import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding  import org.yuzu.yuzu_emu.features.settings.model.view.RunnableSetting  import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem  import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible  class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :      SettingViewHolder(binding.root, adapter) { @@ -17,34 +17,28 @@ class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA      override fun bind(item: SettingsItem) {          setting = item as RunnableSetting -        if (item.iconId != 0) { -            binding.icon.visibility = View.VISIBLE +        binding.icon.setVisible(setting.iconId != 0) +        if (setting.iconId != 0) {              binding.icon.setImageDrawable(                  ResourcesCompat.getDrawable(                      binding.icon.resources, -                    item.iconId, +                    setting.iconId,                      binding.icon.context.theme                  )              ) -        } else { -            binding.icon.visibility = View.GONE          } -        binding.textSettingName.setText(item.nameId) -        if (item.descriptionId != 0) { -            binding.textSettingDescription.setText(item.descriptionId) -            binding.textSettingDescription.visibility = View.VISIBLE -        } else { -            binding.textSettingDescription.visibility = View.GONE -        } -        binding.textSettingValue.visibility = View.GONE -        binding.buttonClear.visibility = View.GONE +        binding.textSettingName.text = setting.title +        binding.textSettingDescription.setVisible(setting.description.isNotEmpty()) +        binding.textSettingDescription.text = item.description +        binding.textSettingValue.setVisible(false) +        binding.buttonClear.setVisible(false)          setStyle(setting.isEditable, binding)      }      override fun onClick(clicked: View) { -        if (!setting.isRuntimeRunnable && !NativeLibrary.isRunning()) { +        if (setting.isRunnable) {              setting.runnable.invoke()          }      } 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 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt @@ -5,11 +5,13 @@ package org.yuzu.yuzu_emu.features.settings.ui.viewholder  import android.view.View  import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding +import org.yuzu.yuzu_emu.features.settings.model.view.IntSingleChoiceSetting  import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem  import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting  import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting  import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter  import org.yuzu.yuzu_emu.utils.NativeConfig +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible  class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :      SettingViewHolder(binding.root, adapter) { @@ -17,40 +19,38 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti      override fun bind(item: SettingsItem) {          setting = item -        binding.textSettingName.setText(item.nameId) -        if (item.descriptionId != 0) { -            binding.textSettingDescription.setText(item.descriptionId) -            binding.textSettingDescription.visibility = View.VISIBLE -        } else { -            binding.textSettingDescription.visibility = View.GONE -        } +        binding.textSettingName.text = setting.title +        binding.textSettingDescription.setVisible(item.description.isNotEmpty()) +        binding.textSettingDescription.text = item.description -        binding.textSettingValue.visibility = View.VISIBLE -        if (item is SingleChoiceSetting) { -            val resMgr = binding.textSettingValue.context.resources -            val values = resMgr.getIntArray(item.valuesId) -            for (i in values.indices) { -                if (values[i] == item.getSelectedValue()) { -                    binding.textSettingValue.text = resMgr.getStringArray(item.choicesId)[i] -                    break +        binding.textSettingValue.setVisible(true) +        when (item) { +            is SingleChoiceSetting -> { +                val resMgr = binding.textSettingValue.context.resources +                val values = resMgr.getIntArray(item.valuesId) +                for (i in values.indices) { +                    if (values[i] == item.getSelectedValue()) { +                        binding.textSettingValue.text = resMgr.getStringArray(item.choicesId)[i] +                        break +                    }                  }              } -        } else if (item is StringSingleChoiceSetting) { -            for (i in item.values.indices) { -                if (item.values[i] == item.getSelectedValue()) { -                    binding.textSettingValue.text = item.choices[i] -                    break -                } + +            is StringSingleChoiceSetting -> { +                binding.textSettingValue.text = item.getSelectedValue()              } -        } -        binding.buttonClear.visibility = if (setting.setting.global || -            !NativeConfig.isPerGameConfigLoaded() -        ) { -            View.GONE -        } else { -            View.VISIBLE +            is IntSingleChoiceSetting -> { +                binding.textSettingValue.text = item.getChoiceAt(item.getSelectedValue()) +            } +        } +        if (binding.textSettingValue.text.isEmpty()) { +            binding.textSettingValue.setVisible(false)          } + +        binding.buttonClear.setVisible( +            !setting.setting.global || NativeConfig.isPerGameConfigLoaded() +        )          binding.buttonClear.setOnClickListener {              adapter.onClearClick(setting, bindingAdapterPosition)          } @@ -63,16 +63,25 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti              return          } -        if (setting is SingleChoiceSetting) { -            adapter.onSingleChoiceClick( -                (setting as SingleChoiceSetting), -                bindingAdapterPosition -            ) -        } else if (setting is StringSingleChoiceSetting) { -            adapter.onStringSingleChoiceClick( -                (setting as StringSingleChoiceSetting), +        when (setting) { +            is SingleChoiceSetting -> adapter.onSingleChoiceClick( +                setting as SingleChoiceSetting,                  bindingAdapterPosition              ) + +            is StringSingleChoiceSetting -> { +                adapter.onStringSingleChoiceClick( +                    setting as StringSingleChoiceSetting, +                    bindingAdapterPosition +                ) +            } + +            is IntSingleChoiceSetting -> { +                adapter.onIntSingleChoiceClick( +                    setting as IntSingleChoiceSetting, +                    bindingAdapterPosition +                ) +            }          }      } 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 100644 --- 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  import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting  import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter  import org.yuzu.yuzu_emu.utils.NativeConfig +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible  class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :      SettingViewHolder(binding.root, adapter) { @@ -17,27 +18,19 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda      override fun bind(item: SettingsItem) {          setting = item as SliderSetting -        binding.textSettingName.setText(item.nameId) -        if (item.descriptionId != 0) { -            binding.textSettingDescription.setText(item.descriptionId) -            binding.textSettingDescription.visibility = View.VISIBLE -        } else { -            binding.textSettingDescription.visibility = View.GONE -        } -        binding.textSettingValue.visibility = View.VISIBLE +        binding.textSettingName.text = setting.title +        binding.textSettingDescription.setVisible(item.description.isNotEmpty()) +        binding.textSettingDescription.text = setting.description +        binding.textSettingValue.setVisible(true)          binding.textSettingValue.text = String.format(              binding.textSettingValue.context.getString(R.string.value_with_units),              setting.getSelectedValue(),              setting.units          ) -        binding.buttonClear.visibility = if (setting.setting.global || -            !NativeConfig.isPerGameConfigLoaded() -        ) { -            View.GONE -        } else { -            View.VISIBLE -        } +        binding.buttonClear.setVisible( +            !setting.setting.global || NativeConfig.isPerGameConfigLoaded() +        )          binding.buttonClear.setOnClickListener {              adapter.onClearClick(setting, bindingAdapterPosition)          } 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 100644 --- 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  import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem  import org.yuzu.yuzu_emu.features.settings.model.view.SubmenuSetting  import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible  class SubmenuViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :      SettingViewHolder(binding.root, adapter) { -    private lateinit var item: SubmenuSetting +    private lateinit var setting: SubmenuSetting      override fun bind(item: SettingsItem) { -        this.item = item as SubmenuSetting -        if (item.iconId != 0) { -            binding.icon.visibility = View.VISIBLE +        setting = item as SubmenuSetting +        binding.icon.setVisible(setting.iconId != 0) +        if (setting.iconId != 0) {              binding.icon.setImageDrawable(                  ResourcesCompat.getDrawable(                      binding.icon.resources, -                    item.iconId, +                    setting.iconId,                      binding.icon.context.theme                  )              ) -        } else { -            binding.icon.visibility = View.GONE          } -        binding.textSettingName.setText(item.nameId) -        if (item.descriptionId != 0) { -            binding.textSettingDescription.setText(item.descriptionId) -            binding.textSettingDescription.visibility = View.VISIBLE -        } else { -            binding.textSettingDescription.visibility = View.GONE -        } -        binding.textSettingValue.visibility = View.GONE -        binding.buttonClear.visibility = View.GONE +        binding.textSettingName.text = setting.title +        binding.textSettingDescription.setVisible(setting.description.isNotEmpty()) +        binding.textSettingDescription.text = setting.description +        binding.textSettingValue.setVisible(false) +        binding.buttonClear.setVisible(false)      }      override fun onClick(clicked: View) { -        adapter.onSubmenuClick(item) +        adapter.onSubmenuClick(setting)      }      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 100644 --- 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  import org.yuzu.yuzu_emu.features.settings.model.view.SwitchSetting  import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter  import org.yuzu.yuzu_emu.utils.NativeConfig +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible  class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter: SettingsAdapter) :      SettingViewHolder(binding.root, adapter) { @@ -18,28 +19,19 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter      override fun bind(item: SettingsItem) {          setting = item as SwitchSetting -        binding.textSettingName.setText(item.nameId) -        if (item.descriptionId != 0) { -            binding.textSettingDescription.setText(item.descriptionId) -            binding.textSettingDescription.visibility = View.VISIBLE -        } else { -            binding.textSettingDescription.text = "" -            binding.textSettingDescription.visibility = View.GONE -        } +        binding.textSettingName.text = setting.title +        binding.textSettingDescription.setVisible(setting.description.isNotEmpty()) +        binding.textSettingDescription.text = setting.description          binding.switchWidget.setOnCheckedChangeListener(null)          binding.switchWidget.isChecked = setting.getIsChecked(setting.needsRuntimeGlobal)          binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean -> -            adapter.onBooleanClick(item, binding.switchWidget.isChecked, bindingAdapterPosition) +            adapter.onBooleanClick(setting, binding.switchWidget.isChecked, bindingAdapterPosition)          } -        binding.buttonClear.visibility = if (setting.setting.global || -            !NativeConfig.isPerGameConfigLoaded() -        ) { -            View.GONE -        } else { -            View.VISIBLE -        } +        binding.buttonClear.setVisible( +            !setting.setting.global || NativeConfig.isPerGameConfigLoaded() +        )          binding.buttonClear.setOnClickListener {              adapter.onClearClick(setting, bindingAdapterPosition)          } 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 100644 --- 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 @@  package org.yuzu.yuzu_emu.fragments -import android.annotation.SuppressLint  import android.content.Intent  import android.os.Bundle  import android.view.LayoutInflater @@ -16,9 +15,6 @@ import androidx.core.view.updatePadding  import androidx.documentfile.provider.DocumentFile  import androidx.fragment.app.Fragment  import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle  import androidx.navigation.findNavController  import androidx.navigation.fragment.navArgs  import androidx.recyclerview.widget.LinearLayoutManager @@ -32,6 +28,7 @@ import org.yuzu.yuzu_emu.model.HomeViewModel  import org.yuzu.yuzu_emu.utils.AddonUtil  import org.yuzu.yuzu_emu.utils.FileUtil.copyFilesTo  import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins +import org.yuzu.yuzu_emu.utils.collect  import java.io.File  class AddonsFragment : Fragment() { @@ -60,8 +57,6 @@ class AddonsFragment : Fragment() {          return binding.root      } -    // This is using the correct scope, lint is just acting up -    @SuppressLint("UnsafeRepeatOnLifecycleDetector")      override fun onViewCreated(view: View, savedInstanceState: Bundle?) {          super.onViewCreated(view, savedInstanceState)          homeViewModel.setNavigationVisibility(visible = false, animated = false) @@ -78,57 +73,41 @@ class AddonsFragment : Fragment() {              adapter = AddonAdapter(addonViewModel)          } -        viewLifecycleOwner.lifecycleScope.apply { -            launch { -                repeatOnLifecycle(Lifecycle.State.STARTED) { -                    addonViewModel.addonList.collect { -                        (binding.listAddons.adapter as AddonAdapter).submitList(it) -                    } -                } -            } -            launch { -                repeatOnLifecycle(Lifecycle.State.STARTED) { -                    addonViewModel.showModInstallPicker.collect { -                        if (it) { -                            installAddon.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) -                            addonViewModel.showModInstallPicker(false) -                        } -                    } -                } -            } -            launch { -                repeatOnLifecycle(Lifecycle.State.STARTED) { -                    addonViewModel.showModNoticeDialog.collect { -                        if (it) { -                            MessageDialogFragment.newInstance( -                                requireActivity(), -                                titleId = R.string.addon_notice, -                                descriptionId = R.string.addon_notice_description, -                                dismissible = false, -                                positiveAction = { addonViewModel.showModInstallPicker(true) }, -                                negativeAction = {}, -                                negativeButtonTitleId = R.string.close -                            ).show(parentFragmentManager, MessageDialogFragment.TAG) -                            addonViewModel.showModNoticeDialog(false) -                        } -                    } -                } +        addonViewModel.addonList.collect(viewLifecycleOwner) { +            (binding.listAddons.adapter as AddonAdapter).submitList(it) +        } +        addonViewModel.showModInstallPicker.collect( +            viewLifecycleOwner, +            resetState = { addonViewModel.showModInstallPicker(false) } +        ) { if (it) installAddon.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) } +        addonViewModel.showModNoticeDialog.collect( +            viewLifecycleOwner, +            resetState = { addonViewModel.showModNoticeDialog(false) } +        ) { +            if (it) { +                MessageDialogFragment.newInstance( +                    requireActivity(), +                    titleId = R.string.addon_notice, +                    descriptionId = R.string.addon_notice_description, +                    dismissible = false, +                    positiveAction = { addonViewModel.showModInstallPicker(true) }, +                    negativeAction = {}, +                    negativeButtonTitleId = R.string.close +                ).show(parentFragmentManager, MessageDialogFragment.TAG)              } -            launch { -                repeatOnLifecycle(Lifecycle.State.STARTED) { -                    addonViewModel.addonToDelete.collect { -                        if (it != null) { -                            MessageDialogFragment.newInstance( -                                requireActivity(), -                                titleId = R.string.confirm_uninstall, -                                descriptionId = R.string.confirm_uninstall_description, -                                positiveAction = { addonViewModel.onDeleteAddon(it) }, -                                negativeAction = {} -                            ).show(parentFragmentManager, MessageDialogFragment.TAG) -                            addonViewModel.setAddonToDelete(null) -                        } -                    } -                } +        } +        addonViewModel.addonToDelete.collect( +            viewLifecycleOwner, +            resetState = { addonViewModel.setAddonToDelete(null) } +        ) { +            if (it != null) { +                MessageDialogFragment.newInstance( +                    requireActivity(), +                    titleId = R.string.confirm_uninstall, +                    descriptionId = R.string.confirm_uninstall_description, +                    positiveAction = { addonViewModel.onDeleteAddon(it) }, +                    negativeAction = {} +                ).show(parentFragmentManager, MessageDialogFragment.TAG)              }          } 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 100644 index 000000000..299f8da71 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/CoreErrorDialogFragment.kt @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.fragments + +import android.app.Dialog +import android.content.DialogInterface +import android.os.Bundle +import androidx.fragment.app.DialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.yuzu.yuzu_emu.NativeLibrary +import org.yuzu.yuzu_emu.R + +class CoreErrorDialogFragment : DialogFragment() { +    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = +        MaterialAlertDialogBuilder(requireActivity()) +            .setTitle(requireArguments().getString(TITLE)) +            .setMessage(requireArguments().getString(MESSAGE)) +            .setPositiveButton(R.string.continue_button, null) +            .setNegativeButton(R.string.abort_button) { _: DialogInterface?, _: Int -> +                NativeLibrary.coreErrorAlertResult = false +                synchronized(NativeLibrary.coreErrorAlertLock) { +                    NativeLibrary.coreErrorAlertLock.notify() +                } +            } +            .create() + +    override fun onDismiss(dialog: DialogInterface) { +        super.onDismiss(dialog) +        NativeLibrary.coreErrorAlertResult = true +        synchronized(NativeLibrary.coreErrorAlertLock) { NativeLibrary.coreErrorAlertLock.notify() } +    } + +    companion object { +        const val TITLE = "Title" +        const val MESSAGE = "Message" + +        fun newInstance(title: String, message: String): CoreErrorDialogFragment { +            val frag = CoreErrorDialogFragment() +            val args = Bundle() +            args.putString(TITLE, title) +            args.putString(MESSAGE, message) +            frag.arguments = args +            return frag +        } +    } +} 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 100644 --- 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 @@  package org.yuzu.yuzu_emu.fragments -import android.annotation.SuppressLint  import android.os.Bundle  import android.view.LayoutInflater  import android.view.View @@ -14,9 +13,6 @@ import androidx.core.view.WindowInsetsCompat  import androidx.core.view.updatePadding  import androidx.fragment.app.Fragment  import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle  import androidx.navigation.findNavController  import androidx.navigation.fragment.navArgs  import androidx.recyclerview.widget.GridLayoutManager @@ -35,6 +31,7 @@ import org.yuzu.yuzu_emu.utils.FileUtil  import org.yuzu.yuzu_emu.utils.GpuDriverHelper  import org.yuzu.yuzu_emu.utils.NativeConfig  import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins +import org.yuzu.yuzu_emu.utils.collect  import java.io.File  import java.io.IOException @@ -63,8 +60,6 @@ class DriverManagerFragment : Fragment() {          return binding.root      } -    // This is using the correct scope, lint is just acting up -    @SuppressLint("UnsafeRepeatOnLifecycleDetector")      override fun onViewCreated(view: View, savedInstanceState: Bundle?) {          super.onViewCreated(view, savedInstanceState)          homeViewModel.setNavigationVisibility(visible = false, animated = true) @@ -89,15 +84,8 @@ class DriverManagerFragment : Fragment() {                  }              } -            viewLifecycleOwner.lifecycleScope.apply { -                launch { -                    repeatOnLifecycle(Lifecycle.State.STARTED) { -                        driverViewModel.showClearButton.collect { -                            binding.toolbarDrivers.menu -                                .findItem(R.id.menu_driver_use_global).isVisible = it -                        } -                    } -                } +            driverViewModel.showClearButton.collect(viewLifecycleOwner) { +                binding.toolbarDrivers.menu.findItem(R.id.menu_driver_use_global).isVisible = it              }          } 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 100644 --- 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  import android.view.ViewGroup  import androidx.fragment.app.DialogFragment  import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle  import com.google.android.material.dialog.MaterialAlertDialogBuilder -import kotlinx.coroutines.launch  import org.yuzu.yuzu_emu.R  import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding  import org.yuzu.yuzu_emu.model.DriverViewModel +import org.yuzu.yuzu_emu.utils.collect  class DriversLoadingDialogFragment : DialogFragment() {      private val driverViewModel: DriverViewModel by activityViewModels() @@ -44,13 +41,7 @@ class DriversLoadingDialogFragment : DialogFragment() {      override fun onViewCreated(view: View, savedInstanceState: Bundle?) {          super.onViewCreated(view, savedInstanceState) -        viewLifecycleOwner.lifecycleScope.apply { -            launch { -                repeatOnLifecycle(Lifecycle.State.RESUMED) { -                    driverViewModel.isInteractionAllowed.collect { if (it) dismiss() } -                } -            } -        } +        driverViewModel.isInteractionAllowed.collect(viewLifecycleOwner) { if (it) dismiss() }      }      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 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt @@ -32,9 +32,6 @@ import androidx.drawerlayout.widget.DrawerLayout  import androidx.drawerlayout.widget.DrawerLayout.DrawerListener  import androidx.fragment.app.Fragment  import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle  import androidx.navigation.findNavController  import androidx.navigation.fragment.navArgs  import androidx.window.layout.FoldingFeature @@ -42,9 +39,6 @@ import androidx.window.layout.WindowInfoTracker  import androidx.window.layout.WindowLayoutInfo  import com.google.android.material.dialog.MaterialAlertDialogBuilder  import com.google.android.material.slider.Slider -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch  import org.yuzu.yuzu_emu.HomeNavigationDirections  import org.yuzu.yuzu_emu.NativeLibrary  import org.yuzu.yuzu_emu.R @@ -63,6 +57,7 @@ import org.yuzu.yuzu_emu.model.EmulationViewModel  import org.yuzu.yuzu_emu.overlay.model.OverlayControl  import org.yuzu.yuzu_emu.overlay.model.OverlayLayout  import org.yuzu.yuzu_emu.utils.* +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible  import java.lang.NullPointerException  class EmulationFragment : Fragment(), SurfaceHolder.Callback { @@ -90,14 +85,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {          if (context is EmulationActivity) {              emulationActivity = context              NativeLibrary.setEmulationActivity(context) - -            lifecycleScope.launch(Dispatchers.Main) { -                lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { -                    WindowInfoTracker.getOrCreate(context) -                        .windowLayoutInfo(context) -                        .collect { updateFoldableLayout(context, it) } -                } -            }          } else {              throw IllegalStateException("EmulationFragment must have EmulationActivity parent")          } @@ -168,8 +155,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {          return binding.root      } -    // This is using the correct scope, lint is just acting up -    @SuppressLint("UnsafeRepeatOnLifecycleDetector")      override fun onViewCreated(view: View, savedInstanceState: Bundle?) {          super.onViewCreated(view, savedInstanceState)          if (requireActivity().isFinishing) { @@ -277,6 +262,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {                      true                  } +                R.id.menu_controls -> { +                    val action = HomeNavigationDirections.actionGlobalSettingsActivity( +                        null, +                        Settings.MenuTag.SECTION_INPUT +                    ) +                    binding.root.findNavController().navigate(action) +                    true +                } +                  R.id.menu_overlay_controls -> {                      showOverlayOptions()                      true @@ -341,129 +335,86 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {          binding.loadingTitle.isSelected = true          binding.loadingText.isSelected = true -        viewLifecycleOwner.lifecycleScope.apply { -            launch { -                repeatOnLifecycle(Lifecycle.State.STARTED) { -                    WindowInfoTracker.getOrCreate(requireContext()) -                        .windowLayoutInfo(requireActivity()) -                        .collect { -                            updateFoldableLayout(requireActivity() as EmulationActivity, it) -                        } -                } +        WindowInfoTracker.getOrCreate(requireContext()) +            .windowLayoutInfo(requireActivity()).collect(viewLifecycleOwner) { +                updateFoldableLayout(requireActivity() as EmulationActivity, it)              } -            launch { -                repeatOnLifecycle(Lifecycle.State.CREATED) { -                    emulationViewModel.shaderProgress.collectLatest { -                        if (it > 0 && it != emulationViewModel.totalShaders.value) { -                            binding.loadingProgressIndicator.isIndeterminate = false - -                            if (it < binding.loadingProgressIndicator.max) { -                                binding.loadingProgressIndicator.progress = it -                            } -                        } +        emulationViewModel.shaderProgress.collect(viewLifecycleOwner) { +            if (it > 0 && it != emulationViewModel.totalShaders.value) { +                binding.loadingProgressIndicator.isIndeterminate = false -                        if (it == emulationViewModel.totalShaders.value) { -                            binding.loadingText.setText(R.string.loading) -                            binding.loadingProgressIndicator.isIndeterminate = true -                        } -                    } +                if (it < binding.loadingProgressIndicator.max) { +                    binding.loadingProgressIndicator.progress = it                  }              } -            launch { -                repeatOnLifecycle(Lifecycle.State.CREATED) { -                    emulationViewModel.totalShaders.collectLatest { -                        binding.loadingProgressIndicator.max = it -                    } -                } -            } -            launch { -                repeatOnLifecycle(Lifecycle.State.CREATED) { -                    emulationViewModel.shaderMessage.collectLatest { -                        if (it.isNotEmpty()) { -                            binding.loadingText.text = it -                        } -                    } -                } + +            if (it == emulationViewModel.totalShaders.value) { +                binding.loadingText.setText(R.string.loading) +                binding.loadingProgressIndicator.isIndeterminate = true              } -            launch { -                repeatOnLifecycle(Lifecycle.State.RESUMED) { -                    driverViewModel.isInteractionAllowed.collect { -                        if (it) { -                            startEmulation() -                        } -                    } -                } +        } +        emulationViewModel.totalShaders.collect(viewLifecycleOwner) { +            binding.loadingProgressIndicator.max = it +        } +        emulationViewModel.shaderMessage.collect(viewLifecycleOwner) { +            if (it.isNotEmpty()) { +                binding.loadingText.text = it              } -            launch { -                repeatOnLifecycle(Lifecycle.State.CREATED) { -                    emulationViewModel.emulationStarted.collectLatest { -                        if (it) { -                            binding.drawerLayout.setDrawerLockMode(IntSetting.LOCK_DRAWER.getInt()) -                            ViewUtils.showView(binding.surfaceInputOverlay) -                            ViewUtils.hideView(binding.loadingIndicator) - -                            emulationState.updateSurface() - -                            // Setup overlays -                            updateShowFpsOverlay() -                            updateThermalOverlay() -                        } -                    } -                } +        } + +        emulationViewModel.emulationStarted.collect(viewLifecycleOwner) { +            if (it) { +                binding.drawerLayout.setDrawerLockMode(IntSetting.LOCK_DRAWER.getInt()) +                ViewUtils.showView(binding.surfaceInputOverlay) +                ViewUtils.hideView(binding.loadingIndicator) + +                emulationState.updateSurface() + +                // Setup overlays +                updateShowFpsOverlay() +                updateThermalOverlay()              } -            launch { -                repeatOnLifecycle(Lifecycle.State.CREATED) { -                    emulationViewModel.isEmulationStopping.collectLatest { -                        if (it) { -                            binding.loadingText.setText(R.string.shutting_down) -                            ViewUtils.showView(binding.loadingIndicator) -                            ViewUtils.hideView(binding.inputContainer) -                            ViewUtils.hideView(binding.showFpsText) -                        } -                    } -                } +        } +        emulationViewModel.isEmulationStopping.collect(viewLifecycleOwner) { +            if (it) { +                binding.loadingText.setText(R.string.shutting_down) +                ViewUtils.showView(binding.loadingIndicator) +                ViewUtils.hideView(binding.inputContainer) +                ViewUtils.hideView(binding.showFpsText)              } -            launch { -                repeatOnLifecycle(Lifecycle.State.CREATED) { -                    emulationViewModel.drawerOpen.collect { -                        if (it) { -                            binding.drawerLayout.open() -                            binding.inGameMenu.requestFocus() -                        } else { -                            binding.drawerLayout.close() -                        } -                    } -                } +        } +        emulationViewModel.drawerOpen.collect(viewLifecycleOwner) { +            if (it) { +                binding.drawerLayout.open() +                binding.inGameMenu.requestFocus() +            } else { +                binding.drawerLayout.close()              } -            launch { -                repeatOnLifecycle(Lifecycle.State.CREATED) { -                    emulationViewModel.programChanged.collect { -                        if (it != 0) { -                            emulationViewModel.setEmulationStarted(false) -                            binding.drawerLayout.close() -                            binding.drawerLayout -                                .setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) -                            ViewUtils.hideView(binding.surfaceInputOverlay) -                            ViewUtils.showView(binding.loadingIndicator) -                        } -                    } -                } +        } +        emulationViewModel.programChanged.collect(viewLifecycleOwner) { +            if (it != 0) { +                emulationViewModel.setEmulationStarted(false) +                binding.drawerLayout.close() +                binding.drawerLayout +                    .setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) +                ViewUtils.hideView(binding.surfaceInputOverlay) +                ViewUtils.showView(binding.loadingIndicator)              } -            launch { -                repeatOnLifecycle(Lifecycle.State.CREATED) { -                    emulationViewModel.emulationStopped.collect { -                        if (it && emulationViewModel.programChanged.value != -1) { -                            if (perfStatsUpdater != null) { -                                perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!) -                            } -                            emulationState.changeProgram(emulationViewModel.programChanged.value) -                            emulationViewModel.setProgramChanged(-1) -                            emulationViewModel.setEmulationStopped(false) -                        } -                    } +        } +        emulationViewModel.emulationStopped.collect(viewLifecycleOwner) { +            if (it && emulationViewModel.programChanged.value != -1) { +                if (perfStatsUpdater != null) { +                    perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!)                  } +                emulationState.changeProgram(emulationViewModel.programChanged.value) +                emulationViewModel.setProgramChanged(-1) +                emulationViewModel.setEmulationStopped(false)              }          } + +        driverViewModel.isInteractionAllowed.collect(viewLifecycleOwner) { +            if (it) startEmulation() +        }      }      private fun startEmulation(programIndex: Int = 0) { @@ -491,14 +442,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {                  binding.drawerLayout.close()              }              if (showInputOverlay) { -                binding.surfaceInputOverlay.visibility = View.INVISIBLE +                binding.surfaceInputOverlay.setVisible(visible = false, gone = false)              }          } else { -            if (showInputOverlay && emulationViewModel.emulationStarted.value) { -                binding.surfaceInputOverlay.visibility = View.VISIBLE -            } else { -                binding.surfaceInputOverlay.visibility = View.INVISIBLE -            } +            binding.surfaceInputOverlay.setVisible( +                showInputOverlay && emulationViewModel.emulationStarted.value +            )              if (!isInFoldableLayout) {                  if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {                      binding.surfaceInputOverlay.layout = OverlayLayout.Portrait @@ -535,7 +484,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {      }      private fun updateShowFpsOverlay() { -        if (BooleanSetting.SHOW_PERFORMANCE_OVERLAY.getBoolean()) { +        val showOverlay = BooleanSetting.SHOW_PERFORMANCE_OVERLAY.getBoolean() +        binding.showFpsText.setVisible(showOverlay) +        if (showOverlay) {              val SYSTEM_FPS = 0              val FPS = 1              val FRAMETIME = 2 @@ -555,17 +506,17 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {                  }              }              perfStatsUpdateHandler.post(perfStatsUpdater!!) -            binding.showFpsText.visibility = View.VISIBLE          } else {              if (perfStatsUpdater != null) {                  perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!)              } -            binding.showFpsText.visibility = View.GONE          }      }      private fun updateThermalOverlay() { -        if (BooleanSetting.SHOW_THERMAL_OVERLAY.getBoolean()) { +        val showOverlay = BooleanSetting.SHOW_THERMAL_OVERLAY.getBoolean() +        binding.showThermalsText.setVisible(showOverlay) +        if (showOverlay) {              thermalStatsUpdater = {                  if (emulationViewModel.emulationStarted.value &&                      !emulationViewModel.isEmulationStopping.value @@ -587,12 +538,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {                  }              }              thermalStatsUpdateHandler.post(thermalStatsUpdater!!) -            binding.showThermalsText.visibility = View.VISIBLE          } else {              if (thermalStatsUpdater != null) {                  thermalStatsUpdateHandler.removeCallbacks(thermalStatsUpdater!!)              } -            binding.showThermalsText.visibility = View.GONE          }      } @@ -861,12 +810,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {                      }              }          } -        binding.doneControlConfig.visibility = View.VISIBLE +        binding.doneControlConfig.setVisible(false)          binding.surfaceInputOverlay.setIsInEditMode(true)      }      private fun stopConfiguringControls() { -        binding.doneControlConfig.visibility = View.GONE +        binding.doneControlConfig.setVisible(false)          binding.surfaceInputOverlay.setIsInEditMode(false)          // Unlock the orientation if it was locked for editing          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 100644 --- 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  import androidx.core.view.updatePadding  import androidx.fragment.app.Fragment  import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle  import androidx.navigation.findNavController  import androidx.recyclerview.widget.GridLayoutManager  import com.google.android.material.transition.MaterialSharedAxis @@ -27,6 +24,7 @@ import org.yuzu.yuzu_emu.model.GamesViewModel  import org.yuzu.yuzu_emu.model.HomeViewModel  import org.yuzu.yuzu_emu.ui.main.MainActivity  import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins +import org.yuzu.yuzu_emu.utils.collect  class GameFoldersFragment : Fragment() {      private var _binding: FragmentFoldersBinding? = null @@ -70,12 +68,8 @@ class GameFoldersFragment : Fragment() {              adapter = FolderAdapter(requireActivity(), gamesViewModel)          } -        viewLifecycleOwner.lifecycleScope.launch { -            repeatOnLifecycle(Lifecycle.State.CREATED) { -                gamesViewModel.folders.collect { -                    (binding.listFolders.adapter as FolderAdapter).submitList(it) -                } -            } +        gamesViewModel.folders.collect(viewLifecycleOwner) { +            (binding.listFolders.adapter as FolderAdapter).submitList(it)          }          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 100644 --- 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  import org.yuzu.yuzu_emu.model.GameVerificationResult  import org.yuzu.yuzu_emu.model.HomeViewModel  import org.yuzu.yuzu_emu.utils.GameMetadata +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible  import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins  class GameInfoFragment : Fragment() { @@ -85,7 +86,7 @@ class GameInfoFragment : Fragment() {                      copyToClipboard(getString(R.string.developer), args.game.developer)                  }              } else { -                developer.visibility = View.GONE +                developer.setVisible(false)              }              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 100644 --- 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 @@  package org.yuzu.yuzu_emu.fragments -import android.annotation.SuppressLint  import android.content.pm.ShortcutInfo  import android.content.pm.ShortcutManager  import android.os.Bundle -import android.text.TextUtils  import android.view.LayoutInflater  import android.view.View  import android.view.ViewGroup @@ -18,9 +16,7 @@ import androidx.core.view.WindowInsetsCompat  import androidx.core.view.updatePadding  import androidx.fragment.app.Fragment  import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle  import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle  import androidx.navigation.findNavController  import androidx.navigation.fragment.navArgs  import androidx.recyclerview.widget.GridLayoutManager @@ -46,7 +42,9 @@ import org.yuzu.yuzu_emu.utils.FileUtil  import org.yuzu.yuzu_emu.utils.GameIconUtils  import org.yuzu.yuzu_emu.utils.GpuDriverHelper  import org.yuzu.yuzu_emu.utils.MemoryUtil +import org.yuzu.yuzu_emu.utils.ViewUtils.marquee  import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins +import org.yuzu.yuzu_emu.utils.collect  import java.io.BufferedOutputStream  import java.io.File @@ -76,8 +74,6 @@ class GamePropertiesFragment : Fragment() {          return binding.root      } -    // This is using the correct scope, lint is just acting up -    @SuppressLint("UnsafeRepeatOnLifecycleDetector")      override fun onViewCreated(view: View, savedInstanceState: Bundle?) {          super.onViewCreated(view, savedInstanceState)          homeViewModel.setNavigationVisibility(visible = false, animated = true) @@ -107,13 +103,7 @@ class GamePropertiesFragment : Fragment() {          GameIconUtils.loadGameIcon(args.game, binding.imageGameScreen)          binding.title.text = args.game.title -        binding.title.postDelayed( -            { -                binding.title.ellipsize = TextUtils.TruncateAt.MARQUEE -                binding.title.isSelected = true -            }, -            3000 -        ) +        binding.title.marquee()          binding.buttonStart.setOnClickListener {              LaunchGameDialogFragment.newInstance(args.game) @@ -122,28 +112,14 @@ class GamePropertiesFragment : Fragment() {          reloadList() -        viewLifecycleOwner.lifecycleScope.apply { -            launch { -                repeatOnLifecycle(Lifecycle.State.STARTED) { -                    homeViewModel.openImportSaves.collect { -                        if (it) { -                            importSaves.launch(arrayOf("application/zip")) -                            homeViewModel.setOpenImportSaves(false) -                        } -                    } -                } -            } -            launch { -                repeatOnLifecycle(Lifecycle.State.STARTED) { -                    homeViewModel.reloadPropertiesList.collect { -                        if (it) { -                            reloadList() -                            homeViewModel.reloadPropertiesList(false) -                        } -                    } -                } -            } -        } +        homeViewModel.openImportSaves.collect( +            viewLifecycleOwner, +            resetState = { homeViewModel.setOpenImportSaves(false) } +        ) { if (it) importSaves.launch(arrayOf("application/zip")) } +        homeViewModel.reloadPropertiesList.collect( +            viewLifecycleOwner, +            resetState = { homeViewModel.reloadPropertiesList(false) } +        ) { if (it) reloadList() }          setInsets()      } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt index 87e130d3e..14a2504b6 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt @@ -91,6 +91,20 @@ class HomeSettingsFragment : Fragment() {              )              add(                  HomeSetting( +                    R.string.preferences_controls, +                    R.string.preferences_controls_description, +                    R.drawable.ic_controller, +                    { +                        val action = HomeNavigationDirections.actionGlobalSettingsActivity( +                            null, +                            Settings.MenuTag.SECTION_INPUT +                        ) +                        binding.root.findNavController().navigate(action) +                    } +                ) +            ) +            add( +                HomeSetting(                      R.string.gpu_driver_manager,                      R.string.install_gpu_driver_description,                      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 100644 --- 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  import androidx.core.view.updatePadding  import androidx.fragment.app.Fragment  import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle  import androidx.navigation.findNavController  import androidx.recyclerview.widget.GridLayoutManager  import com.google.android.material.transition.MaterialSharedAxis @@ -35,6 +32,7 @@ import org.yuzu.yuzu_emu.ui.main.MainActivity  import org.yuzu.yuzu_emu.utils.DirectoryInitialization  import org.yuzu.yuzu_emu.utils.FileUtil  import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins +import org.yuzu.yuzu_emu.utils.collect  import java.io.BufferedOutputStream  import java.io.File  import java.math.BigInteger @@ -75,14 +73,10 @@ class InstallableFragment : Fragment() {              binding.root.findNavController().popBackStack()          } -        viewLifecycleOwner.lifecycleScope.launch { -            repeatOnLifecycle(Lifecycle.State.CREATED) { -                homeViewModel.openImportSaves.collect { -                    if (it) { -                        importSaves.launch(arrayOf("application/zip")) -                        homeViewModel.setOpenImportSaves(false) -                    } -                } +        homeViewModel.openImportSaves.collect(viewLifecycleOwner) { +            if (it) { +                importSaves.launch(arrayOf("application/zip")) +                homeViewModel.setOpenImportSaves(false)              }          } 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 100644 --- 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  import androidx.fragment.app.DialogFragment  import androidx.fragment.app.FragmentActivity  import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle  import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle  import com.google.android.material.dialog.MaterialAlertDialogBuilder -import kotlinx.coroutines.launch  import org.yuzu.yuzu_emu.R  import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding  import org.yuzu.yuzu_emu.model.TaskViewModel +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible +import org.yuzu.yuzu_emu.utils.collect  class ProgressDialogFragment : DialogFragment() {      private val taskViewModel: TaskViewModel by activityViewModels() @@ -64,72 +62,50 @@ class ProgressDialogFragment : DialogFragment() {      override fun onViewCreated(view: View, savedInstanceState: Bundle?) {          super.onViewCreated(view, savedInstanceState)          binding.message.isSelected = true -        viewLifecycleOwner.lifecycleScope.apply { -            launch { -                repeatOnLifecycle(Lifecycle.State.CREATED) { -                    taskViewModel.isComplete.collect { -                        if (it) { -                            dismiss() -                            when (val result = taskViewModel.result.value) { -                                is String -> Toast.makeText( -                                    requireContext(), -                                    result, -                                    Toast.LENGTH_LONG -                                ).show() - -                                is MessageDialogFragment -> result.show( -                                    requireActivity().supportFragmentManager, -                                    MessageDialogFragment.TAG -                                ) - -                                else -> { -                                    // Do nothing -                                } -                            } -                            taskViewModel.clear() -                        } +        taskViewModel.isComplete.collect(viewLifecycleOwner) { +            if (it) { +                dismiss() +                when (val result = taskViewModel.result.value) { +                    is String -> Toast.makeText( +                        requireContext(), +                        result, +                        Toast.LENGTH_LONG +                    ).show() + +                    is MessageDialogFragment -> result.show( +                        requireActivity().supportFragmentManager, +                        MessageDialogFragment.TAG +                    ) + +                    else -> { +                        // Do nothing                      }                  } +                taskViewModel.clear()              } -            launch { -                repeatOnLifecycle(Lifecycle.State.CREATED) { -                    taskViewModel.cancelled.collect { -                        if (it) { -                            dialog?.setTitle(R.string.cancelling) -                        } -                    } -                } -            } -            launch { -                repeatOnLifecycle(Lifecycle.State.CREATED) { -                    taskViewModel.progress.collect { -                        if (it != 0.0) { -                            binding.progressBar.apply { -                                isIndeterminate = false -                                progress = ( -                                    (it / taskViewModel.maxProgress.value) * -                                        PROGRESS_BAR_RESOLUTION -                                    ).toInt() -                                min = 0 -                                max = PROGRESS_BAR_RESOLUTION -                            } -                        } -                    } -                } +        } +        taskViewModel.cancelled.collect(viewLifecycleOwner) { +            if (it) { +                dialog?.setTitle(R.string.cancelling)              } -            launch { -                repeatOnLifecycle(Lifecycle.State.CREATED) { -                    taskViewModel.message.collect { -                        if (it.isEmpty()) { -                            binding.message.visibility = View.GONE -                        } else { -                            binding.message.visibility = View.VISIBLE -                            binding.message.text = it -                        } -                    } +        } +        taskViewModel.progress.collect(viewLifecycleOwner) { +            if (it != 0.0) { +                binding.progressBar.apply { +                    isIndeterminate = false +                    progress = ( +                        (it / taskViewModel.maxProgress.value) * +                            PROGRESS_BAR_RESOLUTION +                        ).toInt() +                    min = 0 +                    max = PROGRESS_BAR_RESOLUTION                  }              }          } +        taskViewModel.message.collect(viewLifecycleOwner) { +            binding.message.setVisible(it.isNotEmpty()) +            binding.message.text = it +        }      }      // 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 100644 --- 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 @@  package org.yuzu.yuzu_emu.fragments -import android.annotation.SuppressLint  import android.content.Context  import android.content.SharedPreferences  import android.os.Bundle @@ -18,14 +17,9 @@ import androidx.core.view.updatePadding  import androidx.core.widget.doOnTextChanged  import androidx.fragment.app.Fragment  import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle  import androidx.preference.PreferenceManager  import info.debatty.java.stringsimilarity.Jaccard  import info.debatty.java.stringsimilarity.JaroWinkler -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch  import java.util.Locale  import org.yuzu.yuzu_emu.R  import org.yuzu.yuzu_emu.YuzuApplication @@ -35,6 +29,8 @@ import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager  import org.yuzu.yuzu_emu.model.Game  import org.yuzu.yuzu_emu.model.GamesViewModel  import org.yuzu.yuzu_emu.model.HomeViewModel +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible +import org.yuzu.yuzu_emu.utils.collect  class SearchFragment : Fragment() {      private var _binding: FragmentSearchBinding? = null @@ -58,8 +54,6 @@ class SearchFragment : Fragment() {          return binding.root      } -    // This is using the correct scope, lint is just acting up -    @SuppressLint("UnsafeRepeatOnLifecycleDetector")      override fun onViewCreated(view: View, savedInstanceState: Bundle?) {          super.onViewCreated(view, savedInstanceState)          homeViewModel.setNavigationVisibility(visible = true, animated = true) @@ -81,42 +75,18 @@ class SearchFragment : Fragment() {          binding.chipGroup.setOnCheckedStateChangeListener { _, _ -> filterAndSearch() }          binding.searchText.doOnTextChanged { text: CharSequence?, _: Int, _: Int, _: Int -> -            if (text.toString().isNotEmpty()) { -                binding.clearButton.visibility = View.VISIBLE -            } else { -                binding.clearButton.visibility = View.INVISIBLE -            } +            binding.clearButton.setVisible(text.toString().isNotEmpty())              filterAndSearch()          } -        viewLifecycleOwner.lifecycleScope.apply { -            launch { -                repeatOnLifecycle(Lifecycle.State.CREATED) { -                    gamesViewModel.searchFocused.collect { -                        if (it) { -                            focusSearch() -                            gamesViewModel.setSearchFocused(false) -                        } -                    } -                } -            } -            launch { -                repeatOnLifecycle(Lifecycle.State.CREATED) { -                    gamesViewModel.games.collectLatest { filterAndSearch() } -                } -            } -            launch { -                repeatOnLifecycle(Lifecycle.State.CREATED) { -                    gamesViewModel.searchedGames.collect { -                        (binding.gridGamesSearch.adapter as GameAdapter).submitList(it) -                        if (it.isEmpty()) { -                            binding.noResultsView.visibility = View.VISIBLE -                        } else { -                            binding.noResultsView.visibility = View.GONE -                        } -                    } -                } -            } +        gamesViewModel.searchFocused.collect( +            viewLifecycleOwner, +            resetState = { gamesViewModel.setSearchFocused(false) } +        ) { if (it) focusSearch() } +        gamesViewModel.games.collect(viewLifecycleOwner) { filterAndSearch() } +        gamesViewModel.searchedGames.collect(viewLifecycleOwner) { +            (binding.gridGamesSearch.adapter as GameAdapter).submitList(it) +            binding.noResultsView.setVisible(it.isNotEmpty())          }          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 100644 --- 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 @@  package org.yuzu.yuzu_emu.fragments  import android.Manifest -import android.annotation.SuppressLint  import android.content.Intent  import android.os.Build  import android.os.Bundle @@ -23,9 +22,6 @@ import androidx.core.view.isVisible  import androidx.core.view.updatePadding  import androidx.fragment.app.Fragment  import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle  import androidx.navigation.findNavController  import androidx.preference.PreferenceManager  import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback @@ -46,6 +42,8 @@ import org.yuzu.yuzu_emu.ui.main.MainActivity  import org.yuzu.yuzu_emu.utils.DirectoryInitialization  import org.yuzu.yuzu_emu.utils.NativeConfig  import org.yuzu.yuzu_emu.utils.ViewUtils +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible +import org.yuzu.yuzu_emu.utils.collect  class SetupFragment : Fragment() {      private var _binding: FragmentSetupBinding? = null @@ -77,8 +75,6 @@ class SetupFragment : Fragment() {          return binding.root      } -    // This is using the correct scope, lint is just acting up -    @SuppressLint("UnsafeRepeatOnLifecycleDetector")      override fun onViewCreated(view: View, savedInstanceState: Bundle?) {          mainActivity = requireActivity() as MainActivity @@ -210,28 +206,14 @@ class SetupFragment : Fragment() {              )          } -        viewLifecycleOwner.lifecycleScope.apply { -            launch { -                repeatOnLifecycle(Lifecycle.State.CREATED) { -                    homeViewModel.shouldPageForward.collect { -                        if (it) { -                            pageForward() -                            homeViewModel.setShouldPageForward(false) -                        } -                    } -                } -            } -            launch { -                repeatOnLifecycle(Lifecycle.State.CREATED) { -                    homeViewModel.gamesDirSelected.collect { -                        if (it) { -                            gamesDirCallback.onStepCompleted() -                            homeViewModel.setGamesDirSelected(false) -                        } -                    } -                } -            } -        } +        homeViewModel.shouldPageForward.collect( +            viewLifecycleOwner, +            resetState = { homeViewModel.setShouldPageForward(false) } +        ) { if (it) pageForward() } +        homeViewModel.gamesDirSelected.collect( +            viewLifecycleOwner, +            resetState = { homeViewModel.setGamesDirSelected(false) } +        ) { if (it) gamesDirCallback.onStepCompleted() }          binding.viewPager2.apply {              adapter = SetupAdapter(requireActivity() as AppCompatActivity, pages) @@ -292,12 +274,8 @@ class SetupFragment : Fragment() {              val backIsVisible = savedInstanceState.getBoolean(KEY_BACK_VISIBILITY)              hasBeenWarned = savedInstanceState.getBooleanArray(KEY_HAS_BEEN_WARNED)!! -            if (nextIsVisible) { -                binding.buttonNext.visibility = View.VISIBLE -            } -            if (backIsVisible) { -                binding.buttonBack.visibility = View.VISIBLE -            } +            binding.buttonNext.setVisible(nextIsVisible) +            binding.buttonBack.setVisible(backIsVisible)          } else {              hasBeenWarned = BooleanArray(pages.size)          } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt index c87486c90..66907085a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt @@ -24,10 +24,10 @@ import androidx.core.content.ContextCompat  import androidx.window.layout.WindowMetricsCalculator  import kotlin.math.max  import kotlin.math.min -import org.yuzu.yuzu_emu.NativeLibrary -import org.yuzu.yuzu_emu.NativeLibrary.ButtonType -import org.yuzu.yuzu_emu.NativeLibrary.StickType +import org.yuzu.yuzu_emu.features.input.NativeInput  import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.features.input.model.NativeAnalog +import org.yuzu.yuzu_emu.features.input.model.NativeButton  import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting  import org.yuzu.yuzu_emu.features.settings.model.IntSetting  import org.yuzu.yuzu_emu.overlay.model.OverlayControl @@ -100,19 +100,19 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :          var shouldUpdateView = false          val playerIndex = -            if (NativeLibrary.isHandheldOnly()) { -                NativeLibrary.ConsoleDevice +            if (NativeInput.isHandheldOnly()) { +                NativeInput.ConsoleDevice              } else { -                NativeLibrary.Player1Device +                NativeInput.Player1Device              }          for (button in overlayButtons) {              if (!button.updateStatus(event)) {                  continue              } -            NativeLibrary.onGamePadButtonEvent( +            NativeInput.onOverlayButtonEvent(                  playerIndex, -                button.buttonId, +                button.button,                  button.status              )              playHaptics(event) @@ -123,24 +123,24 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :              if (!dpad.updateStatus(event, BooleanSetting.DPAD_SLIDE.getBoolean())) {                  continue              } -            NativeLibrary.onGamePadButtonEvent( +            NativeInput.onOverlayButtonEvent(                  playerIndex, -                dpad.upId, +                dpad.up,                  dpad.upStatus              ) -            NativeLibrary.onGamePadButtonEvent( +            NativeInput.onOverlayButtonEvent(                  playerIndex, -                dpad.downId, +                dpad.down,                  dpad.downStatus              ) -            NativeLibrary.onGamePadButtonEvent( +            NativeInput.onOverlayButtonEvent(                  playerIndex, -                dpad.leftId, +                dpad.left,                  dpad.leftStatus              ) -            NativeLibrary.onGamePadButtonEvent( +            NativeInput.onOverlayButtonEvent(                  playerIndex, -                dpad.rightId, +                dpad.right,                  dpad.rightStatus              )              playHaptics(event) @@ -151,16 +151,15 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :              if (!joystick.updateStatus(event)) {                  continue              } -            val axisID = joystick.joystickId -            NativeLibrary.onGamePadJoystickEvent( +            NativeInput.onOverlayJoystickEvent(                  playerIndex, -                axisID, +                joystick.joystick,                  joystick.xAxis,                  joystick.realYAxis              ) -            NativeLibrary.onGamePadButtonEvent( +            NativeInput.onOverlayButtonEvent(                  playerIndex, -                joystick.buttonId, +                joystick.button,                  joystick.buttonStatus              )              playHaptics(event) @@ -187,7 +186,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :              motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP          if (isActionDown && !isTouchInputConsumed(pointerId)) { -            NativeLibrary.onTouchPressed(pointerId, xPosition.toFloat(), yPosition.toFloat()) +            NativeInput.onTouchPressed(pointerId, xPosition.toFloat(), yPosition.toFloat())          }          if (isActionMove) { @@ -196,12 +195,12 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :                  if (isTouchInputConsumed(fingerId)) {                      continue                  } -                NativeLibrary.onTouchMoved(fingerId, event.getX(i), event.getY(i)) +                NativeInput.onTouchMoved(fingerId, event.getX(i), event.getY(i))              }          }          if (isActionUp && !isTouchInputConsumed(pointerId)) { -            NativeLibrary.onTouchReleased(pointerId) +            NativeInput.onTouchReleased(pointerId)          }          return true @@ -359,7 +358,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :                              windowSize,                              R.drawable.facebutton_a,                              R.drawable.facebutton_a_depressed, -                            ButtonType.BUTTON_A, +                            NativeButton.A,                              data,                              position                          ) @@ -373,7 +372,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :                              windowSize,                              R.drawable.facebutton_b,                              R.drawable.facebutton_b_depressed, -                            ButtonType.BUTTON_B, +                            NativeButton.B,                              data,                              position                          ) @@ -387,7 +386,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :                              windowSize,                              R.drawable.facebutton_x,                              R.drawable.facebutton_x_depressed, -                            ButtonType.BUTTON_X, +                            NativeButton.X,                              data,                              position                          ) @@ -401,7 +400,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :                              windowSize,                              R.drawable.facebutton_y,                              R.drawable.facebutton_y_depressed, -                            ButtonType.BUTTON_Y, +                            NativeButton.Y,                              data,                              position                          ) @@ -415,7 +414,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :                              windowSize,                              R.drawable.facebutton_plus,                              R.drawable.facebutton_plus_depressed, -                            ButtonType.BUTTON_PLUS, +                            NativeButton.Plus,                              data,                              position                          ) @@ -429,7 +428,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :                              windowSize,                              R.drawable.facebutton_minus,                              R.drawable.facebutton_minus_depressed, -                            ButtonType.BUTTON_MINUS, +                            NativeButton.Minus,                              data,                              position                          ) @@ -443,7 +442,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :                              windowSize,                              R.drawable.facebutton_home,                              R.drawable.facebutton_home_depressed, -                            ButtonType.BUTTON_HOME, +                            NativeButton.Home,                              data,                              position                          ) @@ -457,7 +456,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :                              windowSize,                              R.drawable.facebutton_screenshot,                              R.drawable.facebutton_screenshot_depressed, -                            ButtonType.BUTTON_CAPTURE, +                            NativeButton.Capture,                              data,                              position                          ) @@ -471,7 +470,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :                              windowSize,                              R.drawable.l_shoulder,                              R.drawable.l_shoulder_depressed, -                            ButtonType.TRIGGER_L, +                            NativeButton.L,                              data,                              position                          ) @@ -485,7 +484,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :                              windowSize,                              R.drawable.r_shoulder,                              R.drawable.r_shoulder_depressed, -                            ButtonType.TRIGGER_R, +                            NativeButton.R,                              data,                              position                          ) @@ -499,7 +498,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :                              windowSize,                              R.drawable.zl_trigger,                              R.drawable.zl_trigger_depressed, -                            ButtonType.TRIGGER_ZL, +                            NativeButton.ZL,                              data,                              position                          ) @@ -513,7 +512,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :                              windowSize,                              R.drawable.zr_trigger,                              R.drawable.zr_trigger_depressed, -                            ButtonType.TRIGGER_ZR, +                            NativeButton.ZR,                              data,                              position                          ) @@ -527,7 +526,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :                              windowSize,                              R.drawable.button_l3,                              R.drawable.button_l3_depressed, -                            ButtonType.STICK_L, +                            NativeButton.LStick,                              data,                              position                          ) @@ -541,7 +540,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :                              windowSize,                              R.drawable.button_r3,                              R.drawable.button_r3_depressed, -                            ButtonType.STICK_R, +                            NativeButton.RStick,                              data,                              position                          ) @@ -556,8 +555,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :                              R.drawable.joystick_range,                              R.drawable.joystick,                              R.drawable.joystick_depressed, -                            StickType.STICK_L, -                            ButtonType.STICK_L, +                            NativeAnalog.LStick, +                            NativeButton.LStick,                              data,                              position                          ) @@ -572,8 +571,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :                              R.drawable.joystick_range,                              R.drawable.joystick,                              R.drawable.joystick_depressed, -                            StickType.STICK_R, -                            ButtonType.STICK_R, +                            NativeAnalog.RStick, +                            NativeButton.RStick,                              data,                              position                          ) @@ -835,7 +834,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :              windowSize: Pair<Point, Point>,              defaultResId: Int,              pressedResId: Int, -            buttonId: Int, +            button: NativeButton,              overlayControlData: OverlayControlData,              position: Pair<Double, Double>          ): InputOverlayDrawableButton { @@ -869,7 +868,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :                  res,                  defaultStateBitmap,                  pressedStateBitmap, -                buttonId, +                button,                  overlayControlData              ) @@ -940,11 +939,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :                  res,                  defaultStateBitmap,                  pressedOneDirectionStateBitmap, -                pressedTwoDirectionsStateBitmap, -                ButtonType.DPAD_UP, -                ButtonType.DPAD_DOWN, -                ButtonType.DPAD_LEFT, -                ButtonType.DPAD_RIGHT +                pressedTwoDirectionsStateBitmap              )              // 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?) :              resOuter: Int,              defaultResInner: Int,              pressedResInner: Int, -            joystick: Int, -            buttonId: Int, +            joystick: NativeAnalog, +            button: NativeButton,              overlayControlData: OverlayControlData,              position: Pair<Double, Double>          ): InputOverlayDrawableJoystick { @@ -1042,7 +1037,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :                  outerRect,                  innerRect,                  joystick, -                buttonId, +                button,                  overlayControlData.id              ) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt index b14a4f96e..fee3d04ee 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt @@ -9,7 +9,8 @@ import android.graphics.Canvas  import android.graphics.Rect  import android.graphics.drawable.BitmapDrawable  import android.view.MotionEvent -import org.yuzu.yuzu_emu.NativeLibrary.ButtonState +import org.yuzu.yuzu_emu.features.input.NativeInput.ButtonState +import org.yuzu.yuzu_emu.features.input.model.NativeButton  import org.yuzu.yuzu_emu.overlay.model.OverlayControlData  /** @@ -19,13 +20,13 @@ import org.yuzu.yuzu_emu.overlay.model.OverlayControlData   * @param res                [Resources] instance.   * @param defaultStateBitmap [Bitmap] to use with the default state Drawable.   * @param pressedStateBitmap [Bitmap] to use with the pressed state Drawable. - * @param buttonId           Identifier for this type of button. + * @param button             [NativeButton] for this type of button.   */  class InputOverlayDrawableButton(      res: Resources,      defaultStateBitmap: Bitmap,      pressedStateBitmap: Bitmap, -    val buttonId: Int, +    val button: NativeButton,      val overlayControlData: OverlayControlData  ) {      // The ID value what motion event is tracking diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt index 8aef6f5a5..0cb6ff244 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt @@ -9,7 +9,8 @@ import android.graphics.Canvas  import android.graphics.Rect  import android.graphics.drawable.BitmapDrawable  import android.view.MotionEvent -import org.yuzu.yuzu_emu.NativeLibrary.ButtonState +import org.yuzu.yuzu_emu.features.input.NativeInput.ButtonState +import org.yuzu.yuzu_emu.features.input.model.NativeButton  /**   * Custom [BitmapDrawable] that is capable @@ -19,20 +20,12 @@ import org.yuzu.yuzu_emu.NativeLibrary.ButtonState   * @param defaultStateBitmap              [Bitmap] of the default state.   * @param pressedOneDirectionStateBitmap  [Bitmap] of the pressed state in one direction.   * @param pressedTwoDirectionsStateBitmap [Bitmap] of the pressed state in two direction. - * @param buttonUp                        Identifier for the up button. - * @param buttonDown                      Identifier for the down button. - * @param buttonLeft                      Identifier for the left button. - * @param buttonRight                     Identifier for the right button.   */  class InputOverlayDrawableDpad(      res: Resources,      defaultStateBitmap: Bitmap,      pressedOneDirectionStateBitmap: Bitmap, -    pressedTwoDirectionsStateBitmap: Bitmap, -    buttonUp: Int, -    buttonDown: Int, -    buttonLeft: Int, -    buttonRight: Int +    pressedTwoDirectionsStateBitmap: Bitmap  ) {      /**       * Gets one of the InputOverlayDrawableDpad's button IDs. @@ -40,10 +33,10 @@ class InputOverlayDrawableDpad(       * @return the requested InputOverlayDrawableDpad's button ID.       */      // The ID identifying what type of button this Drawable represents. -    val upId: Int -    val downId: Int -    val leftId: Int -    val rightId: Int +    val up = NativeButton.DUp +    val down = NativeButton.DDown +    val left = NativeButton.DLeft +    val right = NativeButton.DRight      var trackId: Int      val width: Int @@ -69,10 +62,6 @@ class InputOverlayDrawableDpad(          this.pressedTwoDirectionsStateBitmap = BitmapDrawable(res, pressedTwoDirectionsStateBitmap)          width = this.defaultStateBitmap.intrinsicWidth          height = this.defaultStateBitmap.intrinsicHeight -        upId = buttonUp -        downId = buttonDown -        leftId = buttonLeft -        rightId = buttonRight          trackId = -1      } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt index 113bf7c24..4b07107fc 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt @@ -13,7 +13,9 @@ import kotlin.math.atan2  import kotlin.math.cos  import kotlin.math.sin  import kotlin.math.sqrt -import org.yuzu.yuzu_emu.NativeLibrary +import org.yuzu.yuzu_emu.features.input.NativeInput.ButtonState +import org.yuzu.yuzu_emu.features.input.model.NativeAnalog +import org.yuzu.yuzu_emu.features.input.model.NativeButton  import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting  /** @@ -26,8 +28,8 @@ import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting   * @param bitmapInnerPressed [Bitmap] which represents the pressed inner movable part of the joystick.   * @param rectOuter          [Rect] which represents the outer joystick bounds.   * @param rectInner          [Rect] which represents the inner joystick bounds. - * @param joystickId         The ID value what type of joystick this Drawable represents. - * @param buttonId           The ID value what type of button this Drawable represents. + * @param joystick           The [NativeAnalog] this Drawable represents. + * @param button             The [NativeButton] this Drawable represents.   */  class InputOverlayDrawableJoystick(      res: Resources, @@ -36,8 +38,8 @@ class InputOverlayDrawableJoystick(      bitmapInnerPressed: Bitmap,      rectOuter: Rect,      rectInner: Rect, -    val joystickId: Int, -    val buttonId: Int, +    val joystick: NativeAnalog, +    val button: NativeButton,      val prefId: String  ) {      // The ID value what motion event is tracking @@ -69,8 +71,7 @@ class InputOverlayDrawableJoystick(      // TODO: Add button support      val buttonStatus: Int -        get() = -            NativeLibrary.ButtonState.RELEASED +        get() = ButtonState.RELEASED      var bounds: Rect          get() = outerBitmap.bounds          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 100644 --- 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 @@  package org.yuzu.yuzu_emu.ui -import android.annotation.SuppressLint  import android.os.Bundle  import android.view.LayoutInflater  import android.view.View @@ -14,19 +13,16 @@ import androidx.core.view.WindowInsetsCompat  import androidx.core.view.updatePadding  import androidx.fragment.app.Fragment  import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle  import com.google.android.material.color.MaterialColors -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch  import org.yuzu.yuzu_emu.R  import org.yuzu.yuzu_emu.adapters.GameAdapter  import org.yuzu.yuzu_emu.databinding.FragmentGamesBinding  import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager  import org.yuzu.yuzu_emu.model.GamesViewModel  import org.yuzu.yuzu_emu.model.HomeViewModel +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible  import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins +import org.yuzu.yuzu_emu.utils.collect  class GamesFragment : Fragment() {      private var _binding: FragmentGamesBinding? = null @@ -44,8 +40,6 @@ class GamesFragment : Fragment() {          return binding.root      } -    // This is using the correct scope, lint is just acting up -    @SuppressLint("UnsafeRepeatOnLifecycleDetector")      override fun onViewCreated(view: View, savedInstanceState: Bundle?) {          super.onViewCreated(view, savedInstanceState)          homeViewModel.setNavigationVisibility(visible = true, animated = true) @@ -88,49 +82,28 @@ class GamesFragment : Fragment() {              }          } -        viewLifecycleOwner.lifecycleScope.apply { -            launch { -                repeatOnLifecycle(Lifecycle.State.RESUMED) { -                    gamesViewModel.isReloading.collect { -                        binding.swipeRefresh.isRefreshing = it -                        if (gamesViewModel.games.value.isEmpty() && !it) { -                            binding.noticeText.visibility = View.VISIBLE -                        } else { -                            binding.noticeText.visibility = View.INVISIBLE -                        } -                    } -                } -            } -            launch { -                repeatOnLifecycle(Lifecycle.State.RESUMED) { -                    gamesViewModel.games.collectLatest { -                        (binding.gridGames.adapter as GameAdapter).submitList(it) -                    } -                } -            } -            launch { -                repeatOnLifecycle(Lifecycle.State.RESUMED) { -                    gamesViewModel.shouldSwapData.collect { -                        if (it) { -                            (binding.gridGames.adapter as GameAdapter).submitList( -                                gamesViewModel.games.value -                            ) -                            gamesViewModel.setShouldSwapData(false) -                        } -                    } -                } -            } -            launch { -                repeatOnLifecycle(Lifecycle.State.RESUMED) { -                    gamesViewModel.shouldScrollToTop.collect { -                        if (it) { -                            scrollToTop() -                            gamesViewModel.setShouldScrollToTop(false) -                        } -                    } -                } +        gamesViewModel.isReloading.collect(viewLifecycleOwner) { +            binding.swipeRefresh.isRefreshing = it +            binding.noticeText.setVisible( +                visible = gamesViewModel.games.value.isEmpty() && !it, +                gone = false +            ) +        } +        gamesViewModel.games.collect(viewLifecycleOwner) { +            (binding.gridGames.adapter as GameAdapter).submitList(it) +        } +        gamesViewModel.shouldSwapData.collect( +            viewLifecycleOwner, +            resetState = { gamesViewModel.setShouldSwapData(false) } +        ) { +            if (it) { +                (binding.gridGames.adapter as GameAdapter).submitList(gamesViewModel.games.value)              }          } +        gamesViewModel.shouldScrollToTop.collect( +            viewLifecycleOwner, +            resetState = { gamesViewModel.setShouldScrollToTop(false) } +        ) { if (it) scrollToTop() }          setInsets()      } 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..757463a0b 100644 --- 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  import androidx.core.view.ViewCompat  import androidx.core.view.WindowCompat  import androidx.core.view.WindowInsetsCompat -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle  import androidx.navigation.NavController  import androidx.navigation.fragment.NavHostFragment  import androidx.navigation.ui.setupWithNavController @@ -30,7 +27,6 @@ import com.google.android.material.color.MaterialColors  import com.google.android.material.navigation.NavigationBarView  import java.io.File  import java.io.FilenameFilter -import kotlinx.coroutines.launch  import org.yuzu.yuzu_emu.HomeNavigationDirections  import org.yuzu.yuzu_emu.NativeLibrary  import org.yuzu.yuzu_emu.R @@ -47,6 +43,7 @@ import org.yuzu.yuzu_emu.model.InstallResult  import org.yuzu.yuzu_emu.model.TaskState  import org.yuzu.yuzu_emu.model.TaskViewModel  import org.yuzu.yuzu_emu.utils.* +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible  import java.io.BufferedInputStream  import java.io.BufferedOutputStream  import java.util.zip.ZipEntry @@ -139,42 +136,23 @@ class MainActivity : AppCompatActivity(), ThemeProvider {          // Prevents navigation from being drawn for a short time on recreation if set to hidden          if (!homeViewModel.navigationVisible.value.first) { -            binding.navigationView.visibility = View.INVISIBLE -            binding.statusBarShade.visibility = View.INVISIBLE +            binding.navigationView.setVisible(visible = false, gone = false) +            binding.statusBarShade.setVisible(visible = false, gone = false)          } -        lifecycleScope.apply { -            launch { -                repeatOnLifecycle(Lifecycle.State.CREATED) { -                    homeViewModel.navigationVisible.collect { showNavigation(it.first, it.second) } -                } -            } -            launch { -                repeatOnLifecycle(Lifecycle.State.CREATED) { -                    homeViewModel.statusBarShadeVisible.collect { showStatusBarShade(it) } -                } -            } -            launch { -                repeatOnLifecycle(Lifecycle.State.CREATED) { -                    homeViewModel.contentToInstall.collect { -                        if (it != null) { -                            installContent(it) -                            homeViewModel.setContentToInstall(null) -                        } -                    } -                } -            } -            launch { -                repeatOnLifecycle(Lifecycle.State.CREATED) { -                    homeViewModel.checkKeys.collect { -                        if (it) { -                            checkKeys() -                            homeViewModel.setCheckKeys(false) -                        } -                    } -                } +        homeViewModel.navigationVisible.collect(this) { showNavigation(it.first, it.second) } +        homeViewModel.statusBarShadeVisible.collect(this) { showStatusBarShade(it) } +        homeViewModel.contentToInstall.collect( +            this, +            resetState = { homeViewModel.setContentToInstall(null) } +        ) { +            if (it != null) { +                installContent(it)              }          } +        homeViewModel.checkKeys.collect(this, resetState = { homeViewModel.setCheckKeys(false) }) { +            if (it) checkKeys() +        }          setInsets()      } @@ -214,18 +192,14 @@ class MainActivity : AppCompatActivity(), ThemeProvider {      private fun showNavigation(visible: Boolean, animated: Boolean) {          if (!animated) { -            if (visible) { -                binding.navigationView.visibility = View.VISIBLE -            } else { -                binding.navigationView.visibility = View.INVISIBLE -            } +            binding.navigationView.setVisible(visible)              return          }          val smallLayout = resources.getBoolean(R.bool.small_layout)          binding.navigationView.animate().apply {              if (visible) { -                binding.navigationView.visibility = View.VISIBLE +                binding.navigationView.setVisible(true)                  duration = 300                  interpolator = PathInterpolator(0.05f, 0.7f, 0.1f, 1f) @@ -264,7 +238,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {              }          }.withEndAction {              if (!visible) { -                binding.navigationView.visibility = View.INVISIBLE +                binding.navigationView.setVisible(visible = false, gone = false)              }          }.start()      } @@ -272,7 +246,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {      private fun showStatusBarShade(visible: Boolean) {          binding.statusBarShade.animate().apply {              if (visible) { -                binding.statusBarShade.visibility = View.VISIBLE +                binding.statusBarShade.setVisible(true)                  binding.statusBarShade.translationY = binding.statusBarShade.height.toFloat() * -2                  duration = 300                  translationY(0f) @@ -284,7 +258,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {              }          }.withEndAction {              if (!visible) { -                binding.statusBarShade.visibility = View.INVISIBLE +                binding.statusBarShade.setVisible(visible = false, gone = false)              }          }.start()      } @@ -524,7 +498,8 @@ class MainActivity : AppCompatActivity(), ThemeProvider {                      this@MainActivity,                      titleId = R.string.content_install_notice,                      descriptionId = R.string.content_install_notice_description, -                    positiveAction = { homeViewModel.setContentToInstall(documents) } +                    positiveAction = { homeViewModel.setContentToInstall(documents) }, +                    negativeAction = {}                  )              }          }.show(supportFragmentManager, ProgressDialogFragment.TAG) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt index e63382e1d..2c7356e6a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt @@ -6,439 +6,89 @@ package org.yuzu.yuzu_emu.utils  import android.view.InputDevice  import android.view.KeyEvent  import android.view.MotionEvent -import kotlin.math.sqrt -import org.yuzu.yuzu_emu.NativeLibrary +import org.yuzu.yuzu_emu.features.input.NativeInput +import org.yuzu.yuzu_emu.features.input.YuzuInputOverlayDevice +import org.yuzu.yuzu_emu.features.input.YuzuPhysicalDevice  object InputHandler { -    private var controllerIds = getGameControllerIds() - -    fun initialize() { -        // Connect first controller -        NativeLibrary.onGamePadConnectEvent(getPlayerNumber(NativeLibrary.Player1Device)) -    } - -    fun updateControllerIds() { -        controllerIds = getGameControllerIds() -    } +    var androidControllers = mapOf<Int, YuzuPhysicalDevice>() +    var registeredControllers = mutableListOf<ParamPackage>()      fun dispatchKeyEvent(event: KeyEvent): Boolean { -        val button: Int = when (event.device.vendorId) { -            0x045E -> getInputXboxButtonKey(event.keyCode) -            0x054C -> getInputDS5ButtonKey(event.keyCode) -            0x057E -> getInputJoyconButtonKey(event.keyCode) -            0x1532 -> getInputRazerButtonKey(event.keyCode) -            0x3537 -> getInputRedmagicButtonKey(event.keyCode) -            0x358A -> getInputBackboneLabsButtonKey(event.keyCode) -            else -> getInputGenericButtonKey(event.keyCode) -        } -          val action = when (event.action) { -            KeyEvent.ACTION_DOWN -> NativeLibrary.ButtonState.PRESSED -            KeyEvent.ACTION_UP -> NativeLibrary.ButtonState.RELEASED +            KeyEvent.ACTION_DOWN -> NativeInput.ButtonState.PRESSED +            KeyEvent.ACTION_UP -> NativeInput.ButtonState.RELEASED              else -> return false          } -        // Ignore invalid buttons -        if (button < 0) { -            return false +        var controllerData = androidControllers[event.device.controllerNumber] +        if (controllerData == null) { +            updateControllerData() +            controllerData = androidControllers[event.device.controllerNumber] ?: return false          } -        return NativeLibrary.onGamePadButtonEvent( -            getPlayerNumber(event.device.controllerNumber, event.deviceId), -            button, +        NativeInput.onGamePadButtonEvent( +            controllerData.getGUID(), +            controllerData.getPort(), +            event.keyCode,              action          ) +        return true      }      fun dispatchGenericMotionEvent(event: MotionEvent): Boolean { -        val device = event.device -        // Check every axis input available on the controller -        for (range in device.motionRanges) { -            val axis = range.axis -            when (device.vendorId) { -                0x045E -> setGenericAxisInput(event, axis) -                0x054C -> setGenericAxisInput(event, axis) -                0x057E -> setJoyconAxisInput(event, axis) -                0x1532 -> setRazerAxisInput(event, axis) -                else -> setGenericAxisInput(event, axis) -            } +        val controllerData = +            androidControllers[event.device.controllerNumber] ?: return false +        event.device.motionRanges.forEach { +            NativeInput.onGamePadAxisEvent( +                controllerData.getGUID(), +                controllerData.getPort(), +                it.axis, +                event.getAxisValue(it.axis) +            )          } -          return true      } -    private fun getPlayerNumber(index: Int, deviceId: Int = -1): Int { -        var deviceIndex = index -        if (deviceId != -1) { -            deviceIndex = controllerIds[deviceId] ?: 0 -        } - -        // TODO: Joycons are handled as different controllers. Find a way to merge them. -        return when (deviceIndex) { -            2 -> NativeLibrary.Player2Device -            3 -> NativeLibrary.Player3Device -            4 -> NativeLibrary.Player4Device -            5 -> NativeLibrary.Player5Device -            6 -> NativeLibrary.Player6Device -            7 -> NativeLibrary.Player7Device -            8 -> NativeLibrary.Player8Device -            else -> if (NativeLibrary.isHandheldOnly()) { -                NativeLibrary.ConsoleDevice -            } else { -                NativeLibrary.Player1Device -            } -        } -    } - -    private fun setStickState(playerNumber: Int, index: Int, xAxis: Float, yAxis: Float) { -        // Calculate vector size -        val r2 = xAxis * xAxis + yAxis * yAxis -        var r = sqrt(r2.toDouble()).toFloat() - -        // Adjust range of joystick -        val deadzone = 0.15f -        var x = xAxis -        var y = yAxis - -        if (r > deadzone) { -            val deadzoneFactor = 1.0f / r * (r - deadzone) / (1.0f - deadzone) -            x *= deadzoneFactor -            y *= deadzoneFactor -            r *= deadzoneFactor -        } else { -            x = 0.0f -            y = 0.0f -        } - -        // Normalize joystick -        if (r > 1.0f) { -            x /= r -            y /= r -        } - -        NativeLibrary.onGamePadJoystickEvent( -            playerNumber, -            index, -            x, -            -y -        ) -    } - -    private fun getAxisToButton(axis: Float): Int { -        return if (axis > 0.5f) { -            NativeLibrary.ButtonState.PRESSED -        } else { -            NativeLibrary.ButtonState.RELEASED -        } -    } - -    private fun setAxisDpadState(playerNumber: Int, xAxis: Float, yAxis: Float) { -        NativeLibrary.onGamePadButtonEvent( -            playerNumber, -            NativeLibrary.ButtonType.DPAD_UP, -            getAxisToButton(-yAxis) -        ) -        NativeLibrary.onGamePadButtonEvent( -            playerNumber, -            NativeLibrary.ButtonType.DPAD_DOWN, -            getAxisToButton(yAxis) -        ) -        NativeLibrary.onGamePadButtonEvent( -            playerNumber, -            NativeLibrary.ButtonType.DPAD_LEFT, -            getAxisToButton(-xAxis) -        ) -        NativeLibrary.onGamePadButtonEvent( -            playerNumber, -            NativeLibrary.ButtonType.DPAD_RIGHT, -            getAxisToButton(xAxis) -        ) -    } - -    private fun getInputDS5ButtonKey(key: Int): Int { -        // The missing ds5 buttons are axis -        return when (key) { -            KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B -            KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A -            KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y -            KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X -            KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L -            KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R -            KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L -            KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R -            KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS -            KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS -            else -> -1 -        } -    } - -    private fun getInputJoyconButtonKey(key: Int): Int { -        // Joycon support is half dead. A lot of buttons can't be mapped -        return when (key) { -            KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B -            KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A -            KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_X -            KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_Y -            KeyEvent.KEYCODE_DPAD_UP -> NativeLibrary.ButtonType.DPAD_UP -            KeyEvent.KEYCODE_DPAD_DOWN -> NativeLibrary.ButtonType.DPAD_DOWN -            KeyEvent.KEYCODE_DPAD_LEFT -> NativeLibrary.ButtonType.DPAD_LEFT -            KeyEvent.KEYCODE_DPAD_RIGHT -> NativeLibrary.ButtonType.DPAD_RIGHT -            KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L -            KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R -            KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL -            KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR -            KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L -            KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R -            KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS -            KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS -            else -> -1 -        } -    } - -    private fun getInputXboxButtonKey(key: Int): Int { -        // The missing xbox buttons are axis -        return when (key) { -            KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_A -            KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_B -            KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_X -            KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_Y -            KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L -            KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R -            KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L -            KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R -            KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS -            KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS -            else -> -1 -        } -    } - -    private fun getInputRazerButtonKey(key: Int): Int { -        // The missing xbox buttons are axis -        return when (key) { -            KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B -            KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A -            KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y -            KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X -            KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L -            KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R -            KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L -            KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R -            KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS -            KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS -            else -> -1 -        } -    } - -    private fun getInputRedmagicButtonKey(key: Int): Int { -        return when (key) { -            KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B -            KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A -            KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y -            KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X -            KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L -            KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R -            KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL -            KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR -            KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L -            KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R -            KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS -            KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS -            else -> -1 -        } -    } - -    private fun getInputBackboneLabsButtonKey(key: Int): Int { -        return when (key) { -            KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B -            KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A -            KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y -            KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X -            KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L -            KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R -            KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL -            KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR -            KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L -            KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R -            KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS -            KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS -            else -> -1 -        } -    } - -    private fun getInputGenericButtonKey(key: Int): Int { -        return when (key) { -            KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_A -            KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_B -            KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_X -            KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_Y -            KeyEvent.KEYCODE_DPAD_UP -> NativeLibrary.ButtonType.DPAD_UP -            KeyEvent.KEYCODE_DPAD_DOWN -> NativeLibrary.ButtonType.DPAD_DOWN -            KeyEvent.KEYCODE_DPAD_LEFT -> NativeLibrary.ButtonType.DPAD_LEFT -            KeyEvent.KEYCODE_DPAD_RIGHT -> NativeLibrary.ButtonType.DPAD_RIGHT -            KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L -            KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R -            KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL -            KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR -            KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L -            KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R -            KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS -            KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS -            else -> -1 -        } -    } - -    private fun setGenericAxisInput(event: MotionEvent, axis: Int) { -        val playerNumber = getPlayerNumber(event.device.controllerNumber, event.deviceId) - -        when (axis) { -            MotionEvent.AXIS_X, MotionEvent.AXIS_Y -> -                setStickState( -                    playerNumber, -                    NativeLibrary.StickType.STICK_L, -                    event.getAxisValue(MotionEvent.AXIS_X), -                    event.getAxisValue(MotionEvent.AXIS_Y) -                ) -            MotionEvent.AXIS_RX, MotionEvent.AXIS_RY -> -                setStickState( -                    playerNumber, -                    NativeLibrary.StickType.STICK_R, -                    event.getAxisValue(MotionEvent.AXIS_RX), -                    event.getAxisValue(MotionEvent.AXIS_RY) -                ) -            MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ -> -                setStickState( -                    playerNumber, -                    NativeLibrary.StickType.STICK_R, -                    event.getAxisValue(MotionEvent.AXIS_Z), -                    event.getAxisValue(MotionEvent.AXIS_RZ) -                ) -            MotionEvent.AXIS_LTRIGGER -> -                NativeLibrary.onGamePadButtonEvent( -                    playerNumber, -                    NativeLibrary.ButtonType.TRIGGER_ZL, -                    getAxisToButton(event.getAxisValue(MotionEvent.AXIS_LTRIGGER)) -                ) -            MotionEvent.AXIS_BRAKE -> -                NativeLibrary.onGamePadButtonEvent( -                    playerNumber, -                    NativeLibrary.ButtonType.TRIGGER_ZL, -                    getAxisToButton(event.getAxisValue(MotionEvent.AXIS_BRAKE)) -                ) -            MotionEvent.AXIS_RTRIGGER -> -                NativeLibrary.onGamePadButtonEvent( -                    playerNumber, -                    NativeLibrary.ButtonType.TRIGGER_ZR, -                    getAxisToButton(event.getAxisValue(MotionEvent.AXIS_RTRIGGER)) -                ) -            MotionEvent.AXIS_GAS -> -                NativeLibrary.onGamePadButtonEvent( -                    playerNumber, -                    NativeLibrary.ButtonType.TRIGGER_ZR, -                    getAxisToButton(event.getAxisValue(MotionEvent.AXIS_GAS)) -                ) -            MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y -> -                setAxisDpadState( -                    playerNumber, -                    event.getAxisValue(MotionEvent.AXIS_HAT_X), -                    event.getAxisValue(MotionEvent.AXIS_HAT_Y) -                ) -        } -    } - -    private fun setJoyconAxisInput(event: MotionEvent, axis: Int) { -        // Joycon support is half dead. Right joystick doesn't work -        val playerNumber = getPlayerNumber(event.device.controllerNumber, event.deviceId) - -        when (axis) { -            MotionEvent.AXIS_X, MotionEvent.AXIS_Y -> -                setStickState( -                    playerNumber, -                    NativeLibrary.StickType.STICK_L, -                    event.getAxisValue(MotionEvent.AXIS_X), -                    event.getAxisValue(MotionEvent.AXIS_Y) -                ) -            MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ -> -                setStickState( -                    playerNumber, -                    NativeLibrary.StickType.STICK_R, -                    event.getAxisValue(MotionEvent.AXIS_Z), -                    event.getAxisValue(MotionEvent.AXIS_RZ) -                ) -            MotionEvent.AXIS_RX, MotionEvent.AXIS_RY -> -                setStickState( -                    playerNumber, -                    NativeLibrary.StickType.STICK_R, -                    event.getAxisValue(MotionEvent.AXIS_RX), -                    event.getAxisValue(MotionEvent.AXIS_RY) -                ) -        } -    } - -    private fun setRazerAxisInput(event: MotionEvent, axis: Int) { -        val playerNumber = getPlayerNumber(event.device.controllerNumber, event.deviceId) - -        when (axis) { -            MotionEvent.AXIS_X, MotionEvent.AXIS_Y -> -                setStickState( -                    playerNumber, -                    NativeLibrary.StickType.STICK_L, -                    event.getAxisValue(MotionEvent.AXIS_X), -                    event.getAxisValue(MotionEvent.AXIS_Y) -                ) -            MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ -> -                setStickState( -                    playerNumber, -                    NativeLibrary.StickType.STICK_R, -                    event.getAxisValue(MotionEvent.AXIS_Z), -                    event.getAxisValue(MotionEvent.AXIS_RZ) -                ) -            MotionEvent.AXIS_BRAKE -> -                NativeLibrary.onGamePadButtonEvent( -                    playerNumber, -                    NativeLibrary.ButtonType.TRIGGER_ZL, -                    getAxisToButton(event.getAxisValue(MotionEvent.AXIS_BRAKE)) -                ) -            MotionEvent.AXIS_GAS -> -                NativeLibrary.onGamePadButtonEvent( -                    playerNumber, -                    NativeLibrary.ButtonType.TRIGGER_ZR, -                    getAxisToButton(event.getAxisValue(MotionEvent.AXIS_GAS)) -                ) -            MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y -> -                setAxisDpadState( -                    playerNumber, -                    event.getAxisValue(MotionEvent.AXIS_HAT_X), -                    event.getAxisValue(MotionEvent.AXIS_HAT_Y) -                ) -        } -    } - -    fun getGameControllerIds(): Map<Int, Int> { -        val gameControllerDeviceIds = mutableMapOf<Int, Int>() +    fun getDevices(): Map<Int, YuzuPhysicalDevice> { +        val gameControllerDeviceIds = mutableMapOf<Int, YuzuPhysicalDevice>()          val deviceIds = InputDevice.getDeviceIds() -        var controllerSlot = 1 +        var port = 0 +        val inputSettings = NativeConfig.getInputSettings(true)          deviceIds.forEach { deviceId ->              InputDevice.getDevice(deviceId)?.apply { -                // Don't over-assign controllers -                if (controllerSlot >= 8) { -                    return gameControllerDeviceIds -                } -                  // Verify that the device has gamepad buttons, control sticks, or both.                  if (sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD ||                      sources and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK                  ) { -                    // This device is a game controller. Store its device ID. -                    if (deviceId and id and vendorId and productId != 0) { -                        // Additionally filter out devices that have no ID -                        gameControllerDeviceIds -                            .takeIf { !it.contains(deviceId) } -                            ?.put(deviceId, controllerSlot) -                        controllerSlot++ +                    if (!gameControllerDeviceIds.contains(controllerNumber)) { +                        gameControllerDeviceIds[controllerNumber] = YuzuPhysicalDevice( +                            this, +                            port, +                            inputSettings[port].useSystemVibrator +                        )                      } +                    port++                  }              }          }          return gameControllerDeviceIds      } + +    fun updateControllerData() { +        androidControllers = getDevices() +        androidControllers.forEach { +            NativeInput.registerController(it.value) +        } + +        // Register the input overlay on a dedicated port for all player 1 vibrations +        NativeInput.registerController(YuzuInputOverlayDevice(androidControllers.isEmpty(), 100)) +        registeredControllers.clear() +        NativeInput.getInputDevices().forEach { +            registeredControllers.add(ParamPackage(it)) +        } +        registeredControllers.sortBy { it.get("port", 0) } +    } + +    fun InputDevice.getGUID(): String = String.format("%016x%016x", productId, vendorId)  } 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 100644 index 000000000..d5c19c681 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/LifecycleUtils.kt @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.utils + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch + +/** + * Collects this [Flow] with a given [LifecycleOwner]. + * @param scope [LifecycleOwner] that this [Flow] will be collected with. + * @param repeatState When to repeat collection on this [Flow]. + * @param resetState Optional lambda to reset state of an underlying [MutableStateFlow] after + * [stateCollector] has been run. + * @param stateCollector Lambda that receives new state. + */ +inline fun <reified T> Flow<T>.collect( +    scope: LifecycleOwner, +    repeatState: Lifecycle.State = Lifecycle.State.CREATED, +    crossinline resetState: () -> Unit = {}, +    crossinline stateCollector: (state: T) -> Unit +) { +    scope.apply { +        lifecycleScope.launch { +            repeatOnLifecycle(repeatState) { +                this@collect.collect { +                    stateCollector(it) +                    resetState() +                } +            } +        } +    } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt index a4c14b3a7..7228f25d2 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt @@ -6,6 +6,8 @@ package org.yuzu.yuzu_emu.utils  import org.yuzu.yuzu_emu.model.GameDir  import org.yuzu.yuzu_emu.overlay.model.OverlayControlData +import org.yuzu.yuzu_emu.features.input.model.PlayerInput +  object NativeConfig {      /**       * Loads global config. @@ -168,4 +170,17 @@ object NativeConfig {       */      @Synchronized      external fun setOverlayControlData(overlayControlData: Array<OverlayControlData>) + +    @Synchronized +    external fun getInputSettings(global: Boolean): Array<PlayerInput> + +    @Synchronized +    external fun setInputSettings(value: Array<PlayerInput>, global: Boolean) + +    /** +     * Saves control values for a specific player +     * Must be used when per game config is loaded +     */ +    @Synchronized +    external fun saveControlPlayerValues()  } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NfcReader.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NfcReader.kt index 68ed66565..331b7ddca 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NfcReader.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NfcReader.kt @@ -14,7 +14,7 @@ import android.os.Build  import android.os.Handler  import android.os.Looper  import java.io.IOException -import org.yuzu.yuzu_emu.NativeLibrary +import org.yuzu.yuzu_emu.features.input.NativeInput  class NfcReader(private val activity: Activity) {      private var nfcAdapter: NfcAdapter? = null @@ -76,12 +76,12 @@ class NfcReader(private val activity: Activity) {          amiibo.connect()          val tagData = ntag215ReadAll(amiibo) ?: return -        NativeLibrary.onReadNfcTag(tagData) +        NativeInput.onReadNfcTag(tagData)          nfcAdapter?.ignore(              tag,              1000, -            { NativeLibrary.onRemoveNfcTag() }, +            { NativeInput.onRemoveNfcTag() },              Handler(Looper.getMainLooper())          )      } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ParamPackage.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ParamPackage.kt new file mode 100644 index 000000000..83fc7da3c --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ParamPackage.kt @@ -0,0 +1,141 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.utils + +// Kotlin version of src/common/param_package.h +class ParamPackage(serialized: String = "") { +    private val KEY_VALUE_SEPARATOR = ":" +    private val PARAM_SEPARATOR = "," + +    private val ESCAPE_CHARACTER = "$" +    private val KEY_VALUE_SEPARATOR_ESCAPE = "$0" +    private val PARAM_SEPARATOR_ESCAPE = "$1" +    private val ESCAPE_CHARACTER_ESCAPE = "$2" + +    private val EMPTY_PLACEHOLDER = "[empty]" + +    val data = mutableMapOf<String, String>() + +    init { +        val pairs = serialized.split(PARAM_SEPARATOR) +        for (pair in pairs) { +            val keyValue = pair.split(KEY_VALUE_SEPARATOR).toMutableList() +            if (keyValue.size != 2) { +                Log.error("[ParamPackage] Invalid key pair $keyValue") +                continue +            } + +            keyValue.forEachIndexed { i: Int, _: String -> +                keyValue[i] = keyValue[i].replace(KEY_VALUE_SEPARATOR_ESCAPE, KEY_VALUE_SEPARATOR) +                keyValue[i] = keyValue[i].replace(PARAM_SEPARATOR_ESCAPE, PARAM_SEPARATOR) +                keyValue[i] = keyValue[i].replace(ESCAPE_CHARACTER_ESCAPE, ESCAPE_CHARACTER) +            } + +            set(keyValue[0], keyValue[1]) +        } +    } + +    constructor(params: List<Pair<String, String>>) : this() { +        params.forEach { +            data[it.first] = it.second +        } +    } + +    fun serialize(): String { +        if (data.isEmpty()) { +            return EMPTY_PLACEHOLDER +        } + +        val result = StringBuilder() +        data.forEach { +            val keyValue = mutableListOf(it.key, it.value) +            keyValue.forEachIndexed { i, _ -> +                keyValue[i] = keyValue[i].replace(ESCAPE_CHARACTER, ESCAPE_CHARACTER_ESCAPE) +                keyValue[i] = keyValue[i].replace(PARAM_SEPARATOR, PARAM_SEPARATOR_ESCAPE) +                keyValue[i] = keyValue[i].replace(KEY_VALUE_SEPARATOR, KEY_VALUE_SEPARATOR_ESCAPE) +            } +            result.append("${keyValue[0]}$KEY_VALUE_SEPARATOR${keyValue[1]}$PARAM_SEPARATOR") +        } +        return result.removeSuffix(PARAM_SEPARATOR).toString() +    } + +    fun get(key: String, defaultValue: String): String = +        if (has(key)) { +            data[key]!! +        } else { +            Log.debug("[ParamPackage] key $key not found") +            defaultValue +        } + +    fun get(key: String, defaultValue: Int): Int = +        if (has(key)) { +            try { +                data[key]!!.toInt() +            } catch (e: NumberFormatException) { +                Log.debug("[ParamPackage] failed to convert ${data[key]!!} to int") +                defaultValue +            } +        } else { +            Log.debug("[ParamPackage] key $key not found") +            defaultValue +        } + +    private fun Int.toBoolean(): Boolean = +        if (this == 1) { +            true +        } else if (this == 0) { +            false +        } else { +            throw Exception("Tried to convert a value to a boolean that was not 0 or 1!") +        } + +    fun get(key: String, defaultValue: Boolean): Boolean = +        if (has(key)) { +            try { +                get(key, if (defaultValue) 1 else 0).toBoolean() +            } catch (e: Exception) { +                Log.debug("[ParamPackage] failed to convert ${data[key]!!} to boolean") +                defaultValue +            } +        } else { +            Log.debug("[ParamPackage] key $key not found") +            defaultValue +        } + +    fun get(key: String, defaultValue: Float): Float = +        if (has(key)) { +            try { +                data[key]!!.toFloat() +            } catch (e: NumberFormatException) { +                Log.debug("[ParamPackage] failed to convert ${data[key]!!} to float") +                defaultValue +            } +        } else { +            Log.debug("[ParamPackage] key $key not found") +            defaultValue +        } + +    fun set(key: String, value: String) { +        data[key] = value +    } + +    fun set(key: String, value: Int) { +        data[key] = value.toString() +    } + +    fun Boolean.toInt(): Int = if (this) 1 else 0 +    fun set(key: String, value: Boolean) { +        data[key] = value.toInt().toString() +    } + +    fun set(key: String, value: Float) { +        data[key] = value.toString() +    } + +    fun has(key: String): Boolean = data.containsKey(key) + +    fun erase(key: String) = data.remove(key) + +    fun clear() = data.clear() +} 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 100644 --- 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 @@  package org.yuzu.yuzu_emu.utils +import android.text.TextUtils  import android.view.View  import android.view.ViewGroup +import android.widget.TextView  object ViewUtils {      fun showView(view: View, length: Long = 300) { @@ -57,4 +59,35 @@ object ViewUtils {          }          this.layoutParams = layoutParams      } + +    /** +     * Shows or hides a view. +     * @param visible Whether a view will be made View.VISIBLE or View.INVISIBLE/GONE. +     * @param gone Optional parameter for hiding a view. Uses View.GONE if true and View.INVISIBLE otherwise. +     */ +    fun View.setVisible(visible: Boolean, gone: Boolean = true) { +        visibility = if (visible) { +            View.VISIBLE +        } else { +            if (gone) { +                View.GONE +            } else { +                View.INVISIBLE +            } +        } +    } + +    /** +     * Starts a marquee on some text. +     * @param delay Optional parameter for changing the start delay. 3 seconds of delay by default. +     */ +    fun TextView.marquee(delay: Long = 3000) { +        ellipsize = null +        marqueeRepeatLimit = -1 +        isSingleLine = true +        postDelayed({ +            ellipsize = TextUtils.TruncateAt.MARQUEE +            isSelected = true +        }, delay) +    }  } diff --git a/src/android/app/src/main/jni/CMakeLists.txt b/src/android/app/src/main/jni/CMakeLists.txt index 20b319c12..ec8ae5c57 100644 --- a/src/android/app/src/main/jni/CMakeLists.txt +++ b/src/android/app/src/main/jni/CMakeLists.txt @@ -12,6 +12,7 @@ add_library(yuzu-android SHARED      native_log.cpp      android_config.cpp      android_config.h +    native_input.cpp  )  set_property(TARGET yuzu-android PROPERTY IMPORTED_LOCATION ${FFmpeg_LIBRARY_DIR}) diff --git a/src/android/app/src/main/jni/android_config.cpp b/src/android/app/src/main/jni/android_config.cpp index e147560c3..a79a64afb 100644 --- a/src/android/app/src/main/jni/android_config.cpp +++ b/src/android/app/src/main/jni/android_config.cpp @@ -1,6 +1,8 @@  // SPDX-FileCopyrightText: 2023 yuzu Emulator Project  // SPDX-License-Identifier: GPL-2.0-or-later +#include <common/logging/log.h> +#include <input_common/main.h>  #include "android_config.h"  #include "android_settings.h"  #include "common/settings_setting.h" @@ -32,6 +34,7 @@ void AndroidConfig::ReadAndroidValues() {          ReadOverlayValues();      }      ReadDriverValues(); +    ReadAndroidControlValues();  }  void AndroidConfig::ReadAndroidUIValues() { @@ -107,6 +110,76 @@ void AndroidConfig::ReadOverlayValues() {      EndGroup();  } +void AndroidConfig::ReadAndroidPlayerValues(std::size_t player_index) { +    std::string player_prefix; +    if (type != ConfigType::InputProfile) { +        player_prefix.append("player_").append(ToString(player_index)).append("_"); +    } + +    auto& player = Settings::values.players.GetValue()[player_index]; +    if (IsCustomConfig()) { +        const auto profile_name = +            ReadStringSetting(std::string(player_prefix).append("profile_name")); +        if (profile_name.empty()) { +            // Use the global input config +            player = Settings::values.players.GetValue(true)[player_index]; +            player.profile_name = ""; +            return; +        } +    } + +    // Android doesn't have default options for controllers. We have the input overlay for that. +    for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) { +        const std::string default_param; +        auto& player_buttons = player.buttons[i]; + +        player_buttons = ReadStringSetting( +            std::string(player_prefix).append(Settings::NativeButton::mapping[i]), default_param); +        if (player_buttons.empty()) { +            player_buttons = default_param; +        } +    } +    for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) { +        const std::string default_param; +        auto& player_analogs = player.analogs[i]; + +        player_analogs = ReadStringSetting( +            std::string(player_prefix).append(Settings::NativeAnalog::mapping[i]), default_param); +        if (player_analogs.empty()) { +            player_analogs = default_param; +        } +    } +    for (int i = 0; i < Settings::NativeMotion::NumMotions; ++i) { +        const std::string default_param; +        auto& player_motions = player.motions[i]; + +        player_motions = ReadStringSetting( +            std::string(player_prefix).append(Settings::NativeMotion::mapping[i]), default_param); +        if (player_motions.empty()) { +            player_motions = default_param; +        } +    } +    player.use_system_vibrator = ReadBooleanSetting( +        std::string(player_prefix).append("use_system_vibrator"), player_index == 0); +} + +void AndroidConfig::ReadAndroidControlValues() { +    BeginGroup(Settings::TranslateCategory(Settings::Category::Controls)); + +    Settings::values.players.SetGlobal(!IsCustomConfig()); +    for (std::size_t p = 0; p < Settings::values.players.GetValue().size(); ++p) { +        ReadAndroidPlayerValues(p); +    } +    if (IsCustomConfig()) { +        EndGroup(); +        return; +    } +    // ReadDebugControlValues(); +    // ReadHidbusValues(); + +    EndGroup(); +} +  void AndroidConfig::SaveAndroidValues() {      if (global) {          SaveAndroidUIValues(); @@ -114,6 +187,7 @@ void AndroidConfig::SaveAndroidValues() {          SaveOverlayValues();      }      SaveDriverValues(); +    SaveAndroidControlValues();      WriteToIni();  } @@ -187,6 +261,52 @@ void AndroidConfig::SaveOverlayValues() {      EndGroup();  } +void AndroidConfig::SaveAndroidPlayerValues(std::size_t player_index) { +    std::string player_prefix; +    if (type != ConfigType::InputProfile) { +        player_prefix = std::string("player_").append(ToString(player_index)).append("_"); +    } + +    const auto& player = Settings::values.players.GetValue()[player_index]; +    if (IsCustomConfig() && player.profile_name.empty()) { +        // No custom profile selected +        return; +    } + +    const std::string default_param; +    for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) { +        WriteStringSetting(std::string(player_prefix).append(Settings::NativeButton::mapping[i]), +                           player.buttons[i], std::make_optional(default_param)); +    } +    for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) { +        WriteStringSetting(std::string(player_prefix).append(Settings::NativeAnalog::mapping[i]), +                           player.analogs[i], std::make_optional(default_param)); +    } +    for (int i = 0; i < Settings::NativeMotion::NumMotions; ++i) { +        WriteStringSetting(std::string(player_prefix).append(Settings::NativeMotion::mapping[i]), +                           player.motions[i], std::make_optional(default_param)); +    } +    WriteBooleanSetting(std::string(player_prefix).append("use_system_vibrator"), +                        player.use_system_vibrator, std::make_optional(player_index == 0)); +} + +void AndroidConfig::SaveAndroidControlValues() { +    BeginGroup(Settings::TranslateCategory(Settings::Category::Controls)); + +    Settings::values.players.SetGlobal(!IsCustomConfig()); +    for (std::size_t p = 0; p < Settings::values.players.GetValue().size(); ++p) { +        SaveAndroidPlayerValues(p); +    } +    if (IsCustomConfig()) { +        EndGroup(); +        return; +    } +    // SaveDebugControlValues(); +    // SaveHidbusValues(); + +    EndGroup(); +} +  std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::Category category) {      auto& map = Settings::values.linkage.by_category;      if (map.contains(category)) { @@ -194,3 +314,24 @@ std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::      }      return AndroidSettings::values.linkage.by_category[category];  } + +void AndroidConfig::ReadAndroidControlPlayerValues(std::size_t player_index) { +    BeginGroup(Settings::TranslateCategory(Settings::Category::Controls)); + +    ReadPlayerValues(player_index); +    ReadAndroidPlayerValues(player_index); + +    EndGroup(); +} + +void AndroidConfig::SaveAndroidControlPlayerValues(std::size_t player_index) { +    BeginGroup(Settings::TranslateCategory(Settings::Category::Controls)); + +    LOG_DEBUG(Config, "Saving players control configuration values"); +    SavePlayerValues(player_index); +    SaveAndroidPlayerValues(player_index); + +    EndGroup(); + +    WriteToIni(); +} diff --git a/src/android/app/src/main/jni/android_config.h b/src/android/app/src/main/jni/android_config.h index 693e1e3f0..28ef5d0a8 100644 --- a/src/android/app/src/main/jni/android_config.h +++ b/src/android/app/src/main/jni/android_config.h @@ -13,7 +13,12 @@ public:      void ReloadAllValues() override;      void SaveAllValues() override; +    void ReadAndroidControlPlayerValues(std::size_t player_index); +    void SaveAndroidControlPlayerValues(std::size_t player_index); +  protected: +    void ReadAndroidPlayerValues(std::size_t player_index); +    void ReadAndroidControlValues();      void ReadAndroidValues();      void ReadAndroidUIValues();      void ReadDriverValues(); @@ -27,6 +32,8 @@ protected:      void ReadUILayoutValues() override {}      void ReadMultiplayerValues() override {} +    void SaveAndroidPlayerValues(std::size_t player_index); +    void SaveAndroidControlValues();      void SaveAndroidValues();      void SaveAndroidUIValues();      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..06db55369 100644 --- a/src/android/app/src/main/jni/emu_window/emu_window.cpp +++ b/src/android/app/src/main/jni/emu_window/emu_window.cpp @@ -5,6 +5,7 @@  #include "common/android/id_cache.h"  #include "common/logging/log.h" +#include "input_common/drivers/android.h"  #include "input_common/drivers/touch_screen.h"  #include "input_common/drivers/virtual_amiibo.h"  #include "input_common/drivers/virtual_gamepad.h" @@ -24,39 +25,18 @@ void EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) {  void EmuWindow_Android::OnTouchPressed(int id, float x, float y) {      const auto [touch_x, touch_y] = MapToTouchScreen(x, y); -    m_input_subsystem->GetTouchScreen()->TouchPressed(touch_x, touch_y, id); +    EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchPressed(touch_x, +                                                                                       touch_y, id);  }  void EmuWindow_Android::OnTouchMoved(int id, float x, float y) {      const auto [touch_x, touch_y] = MapToTouchScreen(x, y); -    m_input_subsystem->GetTouchScreen()->TouchMoved(touch_x, touch_y, id); +    EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchMoved(touch_x, +                                                                                     touch_y, id);  }  void EmuWindow_Android::OnTouchReleased(int id) { -    m_input_subsystem->GetTouchScreen()->TouchReleased(id); -} - -void EmuWindow_Android::OnGamepadButtonEvent(int player_index, int button_id, bool pressed) { -    m_input_subsystem->GetVirtualGamepad()->SetButtonState(player_index, button_id, pressed); -} - -void EmuWindow_Android::OnGamepadJoystickEvent(int player_index, int stick_id, float x, float y) { -    m_input_subsystem->GetVirtualGamepad()->SetStickPosition(player_index, stick_id, x, y); -} - -void EmuWindow_Android::OnGamepadMotionEvent(int player_index, u64 delta_timestamp, float gyro_x, -                                             float gyro_y, float gyro_z, float accel_x, -                                             float accel_y, float accel_z) { -    m_input_subsystem->GetVirtualGamepad()->SetMotionState( -        player_index, delta_timestamp, gyro_x, gyro_y, gyro_z, accel_x, accel_y, accel_z); -} - -void EmuWindow_Android::OnReadNfcTag(std::span<u8> data) { -    m_input_subsystem->GetVirtualAmiibo()->LoadAmiibo(data); -} - -void EmuWindow_Android::OnRemoveNfcTag() { -    m_input_subsystem->GetVirtualAmiibo()->CloseAmiibo(); +    EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchReleased(id);  }  void EmuWindow_Android::OnFrameDisplayed() { @@ -67,10 +47,9 @@ void EmuWindow_Android::OnFrameDisplayed() {      }  } -EmuWindow_Android::EmuWindow_Android(InputCommon::InputSubsystem* input_subsystem, -                                     ANativeWindow* surface, +EmuWindow_Android::EmuWindow_Android(ANativeWindow* surface,                                       std::shared_ptr<Common::DynamicLibrary> driver_library) -    : m_input_subsystem{input_subsystem}, m_driver_library{driver_library} { +    : m_driver_library{driver_library} {      LOG_INFO(Frontend, "initializing");      if (!surface) { @@ -80,10 +59,4 @@ EmuWindow_Android::EmuWindow_Android(InputCommon::InputSubsystem* input_subsyste      OnSurfaceChanged(surface);      window_info.type = Core::Frontend::WindowSystemType::Android; - -    m_input_subsystem->Initialize(); -} - -EmuWindow_Android::~EmuWindow_Android() { -    m_input_subsystem->Shutdown();  } 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..d7b5fc6da 100644 --- a/src/android/app/src/main/jni/emu_window/emu_window.h +++ b/src/android/app/src/main/jni/emu_window/emu_window.h @@ -30,22 +30,17 @@ private:  class EmuWindow_Android final : public Core::Frontend::EmuWindow {  public: -    EmuWindow_Android(InputCommon::InputSubsystem* input_subsystem, ANativeWindow* surface, +    EmuWindow_Android(ANativeWindow* surface,                        std::shared_ptr<Common::DynamicLibrary> driver_library); -    ~EmuWindow_Android(); +    ~EmuWindow_Android() = default;      void OnSurfaceChanged(ANativeWindow* surface); +    void OnFrameDisplayed() override; +      void OnTouchPressed(int id, float x, float y);      void OnTouchMoved(int id, float x, float y);      void OnTouchReleased(int id); -    void OnGamepadButtonEvent(int player_index, int button_id, bool pressed); -    void OnGamepadJoystickEvent(int player_index, int stick_id, float x, float y); -    void OnGamepadMotionEvent(int player_index, u64 delta_timestamp, float gyro_x, float gyro_y, -                              float gyro_z, float accel_x, float accel_y, float accel_z); -    void OnReadNfcTag(std::span<u8> data); -    void OnRemoveNfcTag(); -    void OnFrameDisplayed() override;      std::unique_ptr<Core::Frontend::GraphicsContext> CreateSharedContext() const override {          return {std::make_unique<GraphicsContext_Android>(m_driver_library)}; @@ -55,8 +50,6 @@ public:      };  private: -    InputCommon::InputSubsystem* m_input_subsystem{}; -      float m_window_width{};      float m_window_height{}; diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index a4d8454e8..50cef5d2a 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -88,6 +88,10 @@ FileSys::ManualContentProvider* EmulationSession::GetContentProvider() {      return m_manual_provider.get();  } +InputCommon::InputSubsystem& EmulationSession::GetInputSubsystem() { +    return m_input_subsystem; +} +  const EmuWindow_Android& EmulationSession::Window() const {      return *m_window;  } @@ -198,6 +202,8 @@ void EmulationSession::InitializeSystem(bool reload) {          Common::Log::Initialize();          Common::Log::SetColorConsoleBackendEnabled(true);          Common::Log::Start(); + +        m_input_subsystem.Initialize();      }      // Initialize filesystem. @@ -222,8 +228,7 @@ Core::SystemResultStatus EmulationSession::InitializeEmulation(const std::string      std::scoped_lock lock(m_mutex);      // Create the render window. -    m_window = -        std::make_unique<EmuWindow_Android>(&m_input_subsystem, m_native_window, m_vulkan_library); +    m_window = std::make_unique<EmuWindow_Android>(m_native_window, m_vulkan_library);      // Initialize system.      jauto android_keyboard = std::make_unique<Common::Android::SoftwareKeyboard::AndroidKeyboard>(); @@ -355,60 +360,6 @@ void EmulationSession::RunEmulation() {      m_applet_id = static_cast<int>(Service::AM::AppletId::Application);  } -bool EmulationSession::IsHandheldOnly() { -    jconst npad_style_set = m_system.HIDCore().GetSupportedStyleTag(); - -    if (npad_style_set.fullkey == 1) { -        return false; -    } - -    if (npad_style_set.handheld == 0) { -        return false; -    } - -    return !Settings::IsDockedMode(); -} - -void EmulationSession::SetDeviceType([[maybe_unused]] int index, int type) { -    jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index); -    controller->SetNpadStyleIndex(static_cast<Core::HID::NpadStyleIndex>(type)); -} - -void EmulationSession::OnGamepadConnectEvent([[maybe_unused]] int index) { -    jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index); - -    // Ensure that player1 is configured correctly and handheld disconnected -    if (controller->GetNpadIdType() == Core::HID::NpadIdType::Player1) { -        jauto handheld = m_system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Handheld); - -        if (controller->GetNpadStyleIndex() == Core::HID::NpadStyleIndex::Handheld) { -            handheld->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Fullkey); -            controller->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Fullkey); -            handheld->Disconnect(); -        } -    } - -    // Ensure that handheld is configured correctly and player 1 disconnected -    if (controller->GetNpadIdType() == Core::HID::NpadIdType::Handheld) { -        jauto player1 = m_system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1); - -        if (controller->GetNpadStyleIndex() != Core::HID::NpadStyleIndex::Handheld) { -            player1->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Handheld); -            controller->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Handheld); -            player1->Disconnect(); -        } -    } - -    if (!controller->IsConnected()) { -        controller->Connect(); -    } -} - -void EmulationSession::OnGamepadDisconnectEvent([[maybe_unused]] int index) { -    jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index); -    controller->Disconnect(); -} -  Common::Android::SoftwareKeyboard::AndroidKeyboard* EmulationSession::SoftwareKeyboard() {      return m_software_keyboard;  } @@ -574,14 +525,14 @@ jobjectArray Java_org_yuzu_yuzu_1emu_utils_GpuDriverHelper_getSystemDriverInfo(                                               nullptr, nullptr, file_redirect_dir_, nullptr);      auto driver_library = std::make_shared<Common::DynamicLibrary>(handle);      InputCommon::InputSubsystem input_subsystem; -    auto m_window = std::make_unique<EmuWindow_Android>( -        &input_subsystem, ANativeWindow_fromSurface(env, j_surf), driver_library); +    auto window = +        std::make_unique<EmuWindow_Android>(ANativeWindow_fromSurface(env, j_surf), driver_library);      Vulkan::vk::InstanceDispatch dld;      Vulkan::vk::Instance vk_instance = Vulkan::CreateInstance(          *driver_library, dld, VK_API_VERSION_1_1, Core::Frontend::WindowSystemType::Android); -    auto surface = Vulkan::CreateSurface(vk_instance, m_window->GetWindowInfo()); +    auto surface = Vulkan::CreateSurface(vk_instance, window->GetWindowInfo());      auto device = Vulkan::CreateDevice(vk_instance, dld, *surface); @@ -622,103 +573,6 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isPaused(JNIEnv* env, jclass claz      return static_cast<jboolean>(EmulationSession::GetInstance().IsPaused());  } -jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isHandheldOnly(JNIEnv* env, jclass clazz) { -    return EmulationSession::GetInstance().IsHandheldOnly(); -} - -jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_setDeviceType(JNIEnv* env, jclass clazz, -                                                             jint j_device, jint j_type) { -    if (EmulationSession::GetInstance().IsRunning()) { -        EmulationSession::GetInstance().SetDeviceType(j_device, j_type); -    } -    return static_cast<jboolean>(true); -} - -jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadConnectEvent(JNIEnv* env, jclass clazz, -                                                                     jint j_device) { -    if (EmulationSession::GetInstance().IsRunning()) { -        EmulationSession::GetInstance().OnGamepadConnectEvent(j_device); -    } -    return static_cast<jboolean>(true); -} - -jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadDisconnectEvent(JNIEnv* env, jclass clazz, -                                                                        jint j_device) { -    if (EmulationSession::GetInstance().IsRunning()) { -        EmulationSession::GetInstance().OnGamepadDisconnectEvent(j_device); -    } -    return static_cast<jboolean>(true); -} -jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadButtonEvent(JNIEnv* env, jclass clazz, -                                                                    jint j_device, jint j_button, -                                                                    jint action) { -    if (EmulationSession::GetInstance().IsRunning()) { -        // Ensure gamepad is connected -        EmulationSession::GetInstance().OnGamepadConnectEvent(j_device); -        EmulationSession::GetInstance().Window().OnGamepadButtonEvent(j_device, j_button, -                                                                      action != 0); -    } -    return static_cast<jboolean>(true); -} - -jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadJoystickEvent(JNIEnv* env, jclass clazz, -                                                                      jint j_device, jint stick_id, -                                                                      jfloat x, jfloat y) { -    if (EmulationSession::GetInstance().IsRunning()) { -        EmulationSession::GetInstance().Window().OnGamepadJoystickEvent(j_device, stick_id, x, y); -    } -    return static_cast<jboolean>(true); -} - -jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadMotionEvent( -    JNIEnv* env, jclass clazz, jint j_device, jlong delta_timestamp, jfloat gyro_x, jfloat gyro_y, -    jfloat gyro_z, jfloat accel_x, jfloat accel_y, jfloat accel_z) { -    if (EmulationSession::GetInstance().IsRunning()) { -        EmulationSession::GetInstance().Window().OnGamepadMotionEvent( -            j_device, delta_timestamp, gyro_x, gyro_y, gyro_z, accel_x, accel_y, accel_z); -    } -    return static_cast<jboolean>(true); -} - -jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onReadNfcTag(JNIEnv* env, jclass clazz, -                                                            jbyteArray j_data) { -    jboolean isCopy{false}; -    std::span<u8> data(reinterpret_cast<u8*>(env->GetByteArrayElements(j_data, &isCopy)), -                       static_cast<size_t>(env->GetArrayLength(j_data))); - -    if (EmulationSession::GetInstance().IsRunning()) { -        EmulationSession::GetInstance().Window().OnReadNfcTag(data); -    } -    return static_cast<jboolean>(true); -} - -jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onRemoveNfcTag(JNIEnv* env, jclass clazz) { -    if (EmulationSession::GetInstance().IsRunning()) { -        EmulationSession::GetInstance().Window().OnRemoveNfcTag(); -    } -    return static_cast<jboolean>(true); -} - -void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchPressed(JNIEnv* env, jclass clazz, jint id, -                                                          jfloat x, jfloat y) { -    if (EmulationSession::GetInstance().IsRunning()) { -        EmulationSession::GetInstance().Window().OnTouchPressed(id, x, y); -    } -} - -void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchMoved(JNIEnv* env, jclass clazz, jint id, -                                                        jfloat x, jfloat y) { -    if (EmulationSession::GetInstance().IsRunning()) { -        EmulationSession::GetInstance().Window().OnTouchMoved(id, x, y); -    } -} - -void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchReleased(JNIEnv* env, jclass clazz, jint id) { -    if (EmulationSession::GetInstance().IsRunning()) { -        EmulationSession::GetInstance().Window().OnTouchReleased(id); -    } -} -  void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeSystem(JNIEnv* env, jclass clazz,                                                              jboolean reload) {      // Initialize the emulated system. @@ -759,6 +613,7 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getGpuDriver(JNIEnv* env, jobject  void Java_org_yuzu_yuzu_1emu_NativeLibrary_applySettings(JNIEnv* env, jobject jobj) {      EmulationSession::GetInstance().System().ApplySettings(); +    EmulationSession::GetInstance().System().HIDCore().ReloadInputDevices();  }  void Java_org_yuzu_yuzu_1emu_NativeLibrary_logSettings(JNIEnv* env, jobject jobj) { diff --git a/src/android/app/src/main/jni/native.h b/src/android/app/src/main/jni/native.h index 47936e305..6a4551ada 100644 --- a/src/android/app/src/main/jni/native.h +++ b/src/android/app/src/main/jni/native.h @@ -23,6 +23,7 @@ public:      const Core::System& System() const;      Core::System& System();      FileSys::ManualContentProvider* GetContentProvider(); +    InputCommon::InputSubsystem& GetInputSubsystem();      const EmuWindow_Android& Window() const;      EmuWindow_Android& Window(); @@ -50,10 +51,6 @@ public:                                                   const std::size_t program_index,                                                   const bool frontend_initiated); -    bool IsHandheldOnly(); -    void SetDeviceType([[maybe_unused]] int index, int type); -    void OnGamepadConnectEvent([[maybe_unused]] int index); -    void OnGamepadDisconnectEvent([[maybe_unused]] int index);      Common::Android::SoftwareKeyboard::AndroidKeyboard* SoftwareKeyboard();      static void OnEmulationStarted(); diff --git a/src/android/app/src/main/jni/native_config.cpp b/src/android/app/src/main/jni/native_config.cpp index 8ae10fbc7..0b26280c6 100644 --- a/src/android/app/src/main/jni/native_config.cpp +++ b/src/android/app/src/main/jni/native_config.cpp @@ -3,7 +3,6 @@  #include <string> -#include <common/fs/fs_util.h>  #include <jni.h>  #include "android_config.h" @@ -425,4 +424,120 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setOverlayControlData(      }  } +jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getInputSettings(JNIEnv* env, jobject obj, +                                                                         jboolean j_global) { +    Settings::values.players.SetGlobal(static_cast<bool>(j_global)); +    auto& players = Settings::values.players.GetValue(); +    jobjectArray j_input_settings = +        env->NewObjectArray(players.size(), Common::Android::GetPlayerInputClass(), nullptr); +    for (size_t i = 0; i < players.size(); ++i) { +        auto j_connected = static_cast<jboolean>(players[i].connected); + +        jobjectArray j_buttons = env->NewObjectArray( +            players[i].buttons.size(), Common::Android::GetStringClass(), env->NewStringUTF("")); +        for (size_t j = 0; j < players[i].buttons.size(); ++j) { +            env->SetObjectArrayElement(j_buttons, j, +                                       Common::Android::ToJString(env, players[i].buttons[j])); +        } +        jobjectArray j_analogs = env->NewObjectArray( +            players[i].analogs.size(), Common::Android::GetStringClass(), env->NewStringUTF("")); +        for (size_t j = 0; j < players[i].analogs.size(); ++j) { +            env->SetObjectArrayElement(j_analogs, j, +                                       Common::Android::ToJString(env, players[i].analogs[j])); +        } +        jobjectArray j_motions = env->NewObjectArray( +            players[i].motions.size(), Common::Android::GetStringClass(), env->NewStringUTF("")); +        for (size_t j = 0; j < players[i].motions.size(); ++j) { +            env->SetObjectArrayElement(j_motions, j, +                                       Common::Android::ToJString(env, players[i].motions[j])); +        } + +        auto j_vibration_enabled = static_cast<jboolean>(players[i].vibration_enabled); +        auto j_vibration_strength = static_cast<jint>(players[i].vibration_strength); + +        auto j_body_color_left = static_cast<jlong>(players[i].body_color_left); +        auto j_body_color_right = static_cast<jlong>(players[i].body_color_right); +        auto j_button_color_left = static_cast<jlong>(players[i].button_color_left); +        auto j_button_color_right = static_cast<jlong>(players[i].button_color_right); + +        auto j_profile_name = Common::Android::ToJString(env, players[i].profile_name); + +        auto j_use_system_vibrator = players[i].use_system_vibrator; + +        jobject playerInput = env->NewObject( +            Common::Android::GetPlayerInputClass(), Common::Android::GetPlayerInputConstructor(), +            j_connected, j_buttons, j_analogs, j_motions, j_vibration_enabled, j_vibration_strength, +            j_body_color_left, j_body_color_right, j_button_color_left, j_button_color_right, +            j_profile_name, j_use_system_vibrator); +        env->SetObjectArrayElement(j_input_settings, i, playerInput); +    } +    return j_input_settings; +} + +void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setInputSettings(JNIEnv* env, jobject obj, +                                                                 jobjectArray j_value, +                                                                 jboolean j_global) { +    auto& players = Settings::values.players.GetValue(static_cast<bool>(j_global)); +    int playersSize = env->GetArrayLength(j_value); +    for (int i = 0; i < playersSize; ++i) { +        jobject jplayer = env->GetObjectArrayElement(j_value, i); + +        players[i].connected = static_cast<bool>( +            env->GetBooleanField(jplayer, Common::Android::GetPlayerInputConnectedField())); + +        auto j_buttons_array = static_cast<jobjectArray>( +            env->GetObjectField(jplayer, Common::Android::GetPlayerInputButtonsField())); +        int buttons_size = env->GetArrayLength(j_buttons_array); +        for (int j = 0; j < buttons_size; ++j) { +            auto button = static_cast<jstring>(env->GetObjectArrayElement(j_buttons_array, j)); +            players[i].buttons[j] = Common::Android::GetJString(env, button); +        } +        auto j_analogs_array = static_cast<jobjectArray>( +            env->GetObjectField(jplayer, Common::Android::GetPlayerInputAnalogsField())); +        int analogs_size = env->GetArrayLength(j_analogs_array); +        for (int j = 0; j < analogs_size; ++j) { +            auto analog = static_cast<jstring>(env->GetObjectArrayElement(j_analogs_array, j)); +            players[i].analogs[j] = Common::Android::GetJString(env, analog); +        } +        auto j_motions_array = static_cast<jobjectArray>( +            env->GetObjectField(jplayer, Common::Android::GetPlayerInputMotionsField())); +        int motions_size = env->GetArrayLength(j_motions_array); +        for (int j = 0; j < motions_size; ++j) { +            auto motion = static_cast<jstring>(env->GetObjectArrayElement(j_motions_array, j)); +            players[i].motions[j] = Common::Android::GetJString(env, motion); +        } + +        players[i].vibration_enabled = static_cast<bool>( +            env->GetBooleanField(jplayer, Common::Android::GetPlayerInputVibrationEnabledField())); +        players[i].vibration_strength = static_cast<int>( +            env->GetIntField(jplayer, Common::Android::GetPlayerInputVibrationStrengthField())); + +        players[i].body_color_left = static_cast<u32>( +            env->GetLongField(jplayer, Common::Android::GetPlayerInputBodyColorLeftField())); +        players[i].body_color_right = static_cast<u32>( +            env->GetLongField(jplayer, Common::Android::GetPlayerInputBodyColorRightField())); +        players[i].button_color_left = static_cast<u32>( +            env->GetLongField(jplayer, Common::Android::GetPlayerInputButtonColorLeftField())); +        players[i].button_color_right = static_cast<u32>( +            env->GetLongField(jplayer, Common::Android::GetPlayerInputButtonColorRightField())); + +        auto profileName = static_cast<jstring>( +            env->GetObjectField(jplayer, Common::Android::GetPlayerInputProfileNameField())); +        players[i].profile_name = Common::Android::GetJString(env, profileName); + +        players[i].use_system_vibrator = +            env->GetBooleanField(jplayer, Common::Android::GetPlayerInputUseSystemVibratorField()); +    } +} + +void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_saveControlPlayerValues(JNIEnv* env, jobject obj) { +    Settings::values.players.SetGlobal(false); + +    // Clear all controls from the config in case the user reverted back to globals +    per_game_config->ClearControlPlayerValues(); +    for (size_t index = 0; index < Settings::values.players.GetValue().size(); ++index) { +        per_game_config->SaveAndroidControlPlayerValues(index); +    } +} +  } // extern "C" diff --git a/src/android/app/src/main/jni/native_input.cpp b/src/android/app/src/main/jni/native_input.cpp new file mode 100644 index 000000000..37a65f2b8 --- /dev/null +++ b/src/android/app/src/main/jni/native_input.cpp @@ -0,0 +1,629 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <common/fs/fs.h> +#include <common/fs/path_util.h> +#include <common/settings.h> +#include <hid_core/hid_types.h> +#include <jni.h> + +#include "android_config.h" +#include "common/android/android_common.h" +#include "common/android/id_cache.h" +#include "hid_core/frontend/emulated_controller.h" +#include "hid_core/hid_core.h" +#include "input_common/drivers/android.h" +#include "input_common/drivers/touch_screen.h" +#include "input_common/drivers/virtual_amiibo.h" +#include "input_common/drivers/virtual_gamepad.h" +#include "native.h" + +std::unordered_map<std::string, std::unique_ptr<AndroidConfig>> map_profiles; + +bool IsHandheldOnly() { +    const auto npad_style_set = +        EmulationSession::GetInstance().System().HIDCore().GetSupportedStyleTag(); + +    if (npad_style_set.fullkey == 1) { +        return false; +    } + +    if (npad_style_set.handheld == 0) { +        return false; +    } + +    return !Settings::IsDockedMode(); +} + +std::filesystem::path GetNameWithoutExtension(std::filesystem::path filename) { +    return filename.replace_extension(); +} + +bool IsProfileNameValid(std::string_view profile_name) { +    return profile_name.find_first_of("<>:;\"/\\|,.!?*") == std::string::npos; +} + +bool ProfileExistsInFilesystem(std::string_view profile_name) { +    return Common::FS::Exists(Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir) / "input" / +                              fmt::format("{}.ini", profile_name)); +} + +bool ProfileExistsInMap(const std::string& profile_name) { +    return map_profiles.find(profile_name) != map_profiles.end(); +} + +bool SaveProfile(const std::string& profile_name, std::size_t player_index) { +    if (!ProfileExistsInMap(profile_name)) { +        return false; +    } + +    Settings::values.players.GetValue()[player_index].profile_name = profile_name; +    map_profiles[profile_name]->SaveAndroidControlPlayerValues(player_index); +    return true; +} + +bool LoadProfile(std::string& profile_name, std::size_t player_index) { +    if (!ProfileExistsInMap(profile_name)) { +        return false; +    } + +    if (!ProfileExistsInFilesystem(profile_name)) { +        map_profiles.erase(profile_name); +        return false; +    } + +    LOG_INFO(Config, "Loading input profile `{}`", profile_name); + +    Settings::values.players.GetValue()[player_index].profile_name = profile_name; +    map_profiles[profile_name]->ReadAndroidControlPlayerValues(player_index); +    return true; +} + +void ApplyControllerConfig(size_t player_index, +                           const std::function<void(Core::HID::EmulatedController*)>& apply) { +    auto& hid_core = EmulationSession::GetInstance().System().HIDCore(); +    if (player_index == 0) { +        auto* handheld = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld); +        auto* player_one = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1); +        handheld->EnableConfiguration(); +        player_one->EnableConfiguration(); +        apply(handheld); +        apply(player_one); +        handheld->DisableConfiguration(); +        player_one->DisableConfiguration(); +        handheld->SaveCurrentConfig(); +        player_one->SaveCurrentConfig(); +    } else { +        auto* controller = hid_core.GetEmulatedControllerByIndex(player_index); +        controller->EnableConfiguration(); +        apply(controller); +        controller->DisableConfiguration(); +        controller->SaveCurrentConfig(); +    } +} + +void ConnectController(size_t player_index, bool connected) { +    auto& hid_core = EmulationSession::GetInstance().System().HIDCore(); +    if (player_index == 0) { +        auto* handheld = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld); +        auto* player_one = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1); +        handheld->EnableConfiguration(); +        player_one->EnableConfiguration(); +        if (player_one->GetNpadStyleIndex(true) == Core::HID::NpadStyleIndex::Handheld) { +            if (connected) { +                handheld->Connect(); +            } else { +                handheld->Disconnect(); +            } +            player_one->Disconnect(); +        } else { +            if (connected) { +                player_one->Connect(); +            } else { +                player_one->Disconnect(); +            } +            handheld->Disconnect(); +        } +        handheld->DisableConfiguration(); +        player_one->DisableConfiguration(); +        handheld->SaveCurrentConfig(); +        player_one->SaveCurrentConfig(); +    } else { +        auto* controller = hid_core.GetEmulatedControllerByIndex(player_index); +        controller->EnableConfiguration(); +        if (connected) { +            controller->Connect(); +        } else { +            controller->Disconnect(); +        } +        controller->DisableConfiguration(); +        controller->SaveCurrentConfig(); +    } +} + +extern "C" { + +jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_isHandheldOnly(JNIEnv* env, +                                                                           jobject j_obj) { +    return IsHandheldOnly(); +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onGamePadButtonEvent( +    JNIEnv* env, jobject j_obj, jstring j_guid, jint j_port, jint j_button_id, jint j_action) { +    EmulationSession::GetInstance().GetInputSubsystem().GetAndroid()->SetButtonState( +        Common::Android::GetJString(env, j_guid), j_port, j_button_id, j_action != 0); +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onGamePadAxisEvent( +    JNIEnv* env, jobject j_obj, jstring j_guid, jint j_port, jint j_stick_id, jfloat j_value) { +    EmulationSession::GetInstance().GetInputSubsystem().GetAndroid()->SetAxisPosition( +        Common::Android::GetJString(env, j_guid), j_port, j_stick_id, j_value); +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onGamePadMotionEvent( +    JNIEnv* env, jobject j_obj, jstring j_guid, jint j_port, jlong j_delta_timestamp, +    jfloat j_x_gyro, jfloat j_y_gyro, jfloat j_z_gyro, jfloat j_x_accel, jfloat j_y_accel, +    jfloat j_z_accel) { +    EmulationSession::GetInstance().GetInputSubsystem().GetAndroid()->SetMotionState( +        Common::Android::GetJString(env, j_guid), j_port, j_delta_timestamp, j_x_gyro, j_y_gyro, +        j_z_gyro, j_x_accel, j_y_accel, j_z_accel); +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onReadNfcTag(JNIEnv* env, jobject j_obj, +                                                                     jbyteArray j_data) { +    jboolean isCopy{false}; +    std::span<u8> data(reinterpret_cast<u8*>(env->GetByteArrayElements(j_data, &isCopy)), +                       static_cast<size_t>(env->GetArrayLength(j_data))); + +    if (EmulationSession::GetInstance().IsRunning()) { +        EmulationSession::GetInstance().GetInputSubsystem().GetVirtualAmiibo()->LoadAmiibo(data); +    } +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onRemoveNfcTag(JNIEnv* env, jobject j_obj) { +    if (EmulationSession::GetInstance().IsRunning()) { +        EmulationSession::GetInstance().GetInputSubsystem().GetVirtualAmiibo()->CloseAmiibo(); +    } +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onTouchPressed(JNIEnv* env, jobject j_obj, +                                                                       jint j_id, jfloat j_x_axis, +                                                                       jfloat j_y_axis) { +    if (EmulationSession::GetInstance().IsRunning()) { +        EmulationSession::GetInstance().Window().OnTouchPressed(j_id, j_x_axis, j_y_axis); +    } +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onTouchMoved(JNIEnv* env, jobject j_obj, +                                                                     jint j_id, jfloat j_x_axis, +                                                                     jfloat j_y_axis) { +    if (EmulationSession::GetInstance().IsRunning()) { +        EmulationSession::GetInstance().Window().OnTouchMoved(j_id, j_x_axis, j_y_axis); +    } +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onTouchReleased(JNIEnv* env, jobject j_obj, +                                                                        jint j_id) { +    if (EmulationSession::GetInstance().IsRunning()) { +        EmulationSession::GetInstance().Window().OnTouchReleased(j_id); +    } +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onOverlayButtonEventImpl( +    JNIEnv* env, jobject j_obj, jint j_port, jint j_button_id, jint j_action) { +    if (EmulationSession::GetInstance().IsRunning()) { +        EmulationSession::GetInstance().GetInputSubsystem().GetVirtualGamepad()->SetButtonState( +            j_port, j_button_id, j_action == 1); +    } +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onOverlayJoystickEventImpl( +    JNIEnv* env, jobject j_obj, jint j_port, jint j_stick_id, jfloat j_x_axis, jfloat j_y_axis) { +    if (EmulationSession::GetInstance().IsRunning()) { +        EmulationSession::GetInstance().GetInputSubsystem().GetVirtualGamepad()->SetStickPosition( +            j_port, j_stick_id, j_x_axis, j_y_axis); +    } +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onDeviceMotionEvent( +    JNIEnv* env, jobject j_obj, jint j_port, jlong j_delta_timestamp, jfloat j_x_gyro, +    jfloat j_y_gyro, jfloat j_z_gyro, jfloat j_x_accel, jfloat j_y_accel, jfloat j_z_accel) { +    if (EmulationSession::GetInstance().IsRunning()) { +        EmulationSession::GetInstance().GetInputSubsystem().GetVirtualGamepad()->SetMotionState( +            j_port, j_delta_timestamp, j_x_gyro, j_y_gyro, j_z_gyro, j_x_accel, j_y_accel, +            j_z_accel); +    } +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_reloadInputDevices(JNIEnv* env, +                                                                           jobject j_obj) { +    EmulationSession::GetInstance().System().HIDCore().ReloadInputDevices(); +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_registerController(JNIEnv* env, +                                                                           jobject j_obj, +                                                                           jobject j_device) { +    EmulationSession::GetInstance().GetInputSubsystem().GetAndroid()->RegisterController(j_device); +} + +jobjectArray Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getInputDevices(JNIEnv* env, +                                                                                jobject j_obj) { +    auto devices = EmulationSession::GetInstance().GetInputSubsystem().GetInputDevices(); +    jobjectArray jdevices = env->NewObjectArray(devices.size(), Common::Android::GetStringClass(), +                                                Common::Android::ToJString(env, "")); +    for (size_t i = 0; i < devices.size(); ++i) { +        env->SetObjectArrayElement(jdevices, i, +                                   Common::Android::ToJString(env, devices[i].Serialize())); +    } +    return jdevices; +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_loadInputProfiles(JNIEnv* env, +                                                                          jobject j_obj) { +    map_profiles.clear(); +    const auto input_profile_loc = +        Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir) / "input"; + +    if (Common::FS::IsDir(input_profile_loc)) { +        Common::FS::IterateDirEntries( +            input_profile_loc, +            [&](const std::filesystem::path& full_path) { +                const auto filename = full_path.filename(); +                const auto name_without_ext = +                    Common::FS::PathToUTF8String(GetNameWithoutExtension(filename)); + +                if (filename.extension() == ".ini" && IsProfileNameValid(name_without_ext)) { +                    map_profiles.insert_or_assign( +                        name_without_ext, std::make_unique<AndroidConfig>( +                                              name_without_ext, Config::ConfigType::InputProfile)); +                } + +                return true; +            }, +            Common::FS::DirEntryFilter::File); +    } +} + +jobjectArray Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getInputProfileNames( +    JNIEnv* env, jobject j_obj) { +    std::vector<std::string> profile_names; +    profile_names.reserve(map_profiles.size()); + +    auto it = map_profiles.cbegin(); +    while (it != map_profiles.cend()) { +        const auto& [profile_name, config] = *it; +        if (!ProfileExistsInFilesystem(profile_name)) { +            it = map_profiles.erase(it); +            continue; +        } + +        profile_names.push_back(profile_name); +        ++it; +    } + +    std::stable_sort(profile_names.begin(), profile_names.end()); + +    jobjectArray j_profile_names = +        env->NewObjectArray(profile_names.size(), Common::Android::GetStringClass(), +                            Common::Android::ToJString(env, "")); +    for (size_t i = 0; i < profile_names.size(); ++i) { +        env->SetObjectArrayElement(j_profile_names, i, +                                   Common::Android::ToJString(env, profile_names[i])); +    } + +    return j_profile_names; +} + +jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_isProfileNameValid(JNIEnv* env, +                                                                               jobject j_obj, +                                                                               jstring j_name) { +    return Common::Android::GetJString(env, j_name).find_first_of("<>:;\"/\\|,.!?*") == +           std::string::npos; +} + +jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_createProfile(JNIEnv* env, +                                                                          jobject j_obj, +                                                                          jstring j_name, +                                                                          jint j_player_index) { +    auto profile_name = Common::Android::GetJString(env, j_name); +    if (ProfileExistsInMap(profile_name)) { +        return false; +    } + +    map_profiles.insert_or_assign( +        profile_name, +        std::make_unique<AndroidConfig>(profile_name, Config::ConfigType::InputProfile)); + +    return SaveProfile(profile_name, j_player_index); +} + +jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_deleteProfile(JNIEnv* env, +                                                                          jobject j_obj, +                                                                          jstring j_name, +                                                                          jint j_player_index) { +    auto profile_name = Common::Android::GetJString(env, j_name); +    if (!ProfileExistsInMap(profile_name)) { +        return false; +    } + +    if (!ProfileExistsInFilesystem(profile_name) || +        Common::FS::RemoveFile(map_profiles[profile_name]->GetConfigFilePath())) { +        map_profiles.erase(profile_name); +    } + +    Settings::values.players.GetValue()[j_player_index].profile_name = ""; +    return !ProfileExistsInMap(profile_name) && !ProfileExistsInFilesystem(profile_name); +} + +jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_loadProfile(JNIEnv* env, jobject j_obj, +                                                                        jstring j_name, +                                                                        jint j_player_index) { +    auto profile_name = Common::Android::GetJString(env, j_name); +    return LoadProfile(profile_name, j_player_index); +} + +jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_saveProfile(JNIEnv* env, jobject j_obj, +                                                                        jstring j_name, +                                                                        jint j_player_index) { +    auto profile_name = Common::Android::GetJString(env, j_name); +    return SaveProfile(profile_name, j_player_index); +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_loadPerGameConfiguration( +    JNIEnv* env, jobject j_obj, jint j_player_index, jint j_selected_index, +    jstring j_selected_profile_name) { +    static constexpr size_t HANDHELD_INDEX = 8; + +    auto& hid_core = EmulationSession::GetInstance().System().HIDCore(); +    Settings::values.players.SetGlobal(false); + +    auto profile_name = Common::Android::GetJString(env, j_selected_profile_name); +    auto* emulated_controller = hid_core.GetEmulatedControllerByIndex(j_player_index); + +    if (j_selected_index == 0) { +        Settings::values.players.GetValue()[j_player_index].profile_name = ""; +        if (j_player_index == 0) { +            Settings::values.players.GetValue()[HANDHELD_INDEX] = {}; +        } +        Settings::values.players.SetGlobal(true); +        emulated_controller->ReloadFromSettings(); +        return; +    } +    if (profile_name.empty()) { +        return; +    } +    auto& player = Settings::values.players.GetValue()[j_player_index]; +    auto& global_player = Settings::values.players.GetValue(true)[j_player_index]; +    player.profile_name = profile_name; +    global_player.profile_name = profile_name; +    // Read from the profile into the custom player settings +    LoadProfile(profile_name, j_player_index); +    // Make sure the controller is connected +    player.connected = true; + +    emulated_controller->ReloadFromSettings(); + +    if (j_player_index > 0) { +        return; +    } +    // Handle Handheld cases +    auto& handheld_player = Settings::values.players.GetValue()[HANDHELD_INDEX]; +    auto* handheld_controller = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld); +    if (player.controller_type == Settings::ControllerType::Handheld) { +        handheld_player = player; +    } else { +        handheld_player = {}; +    } +    handheld_controller->ReloadFromSettings(); +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_beginMapping(JNIEnv* env, jobject j_obj, +                                                                     jint jtype) { +    EmulationSession::GetInstance().GetInputSubsystem().BeginMapping( +        static_cast<InputCommon::Polling::InputType>(jtype)); +} + +jstring Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getNextInput(JNIEnv* env, +                                                                        jobject j_obj) { +    return Common::Android::ToJString( +        env, EmulationSession::GetInstance().GetInputSubsystem().GetNextInput().Serialize()); +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_stopMapping(JNIEnv* env, jobject j_obj) { +    EmulationSession::GetInstance().GetInputSubsystem().StopMapping(); +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_updateMappingsWithDefaultImpl( +    JNIEnv* env, jobject j_obj, jint j_player_index, jstring j_device_params, +    jstring j_display_name) { +    auto& input_subsystem = EmulationSession::GetInstance().GetInputSubsystem(); + +    // Clear all previous mappings +    for (int button_id = 0; button_id < Settings::NativeButton::NumButtons; ++button_id) { +        ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { +            controller->SetButtonParam(button_id, {}); +        }); +    } +    for (int analog_id = 0; analog_id < Settings::NativeAnalog::NumAnalogs; ++analog_id) { +        ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { +            controller->SetStickParam(analog_id, {}); +        }); +    } + +    // Apply new mappings +    auto device = Common::ParamPackage(Common::Android::GetJString(env, j_device_params)); +    auto button_mappings = input_subsystem.GetButtonMappingForDevice(device); +    auto analog_mappings = input_subsystem.GetAnalogMappingForDevice(device); +    auto display_name = Common::Android::GetJString(env, j_display_name); +    for (const auto& button_mapping : button_mappings) { +        const std::size_t index = button_mapping.first; +        auto named_mapping = button_mapping.second; +        named_mapping.Set("display", display_name); +        ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { +            controller->SetButtonParam(index, named_mapping); +        }); +    } +    for (const auto& analog_mapping : analog_mappings) { +        const std::size_t index = analog_mapping.first; +        auto named_mapping = analog_mapping.second; +        named_mapping.Set("display", display_name); +        ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { +            controller->SetStickParam(index, named_mapping); +        }); +    } +} + +jstring Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getButtonParamImpl(JNIEnv* env, +                                                                              jobject j_obj, +                                                                              jint j_player_index, +                                                                              jint j_button) { +    return Common::Android::ToJString(env, EmulationSession::GetInstance() +                                               .System() +                                               .HIDCore() +                                               .GetEmulatedControllerByIndex(j_player_index) +                                               ->GetButtonParam(j_button) +                                               .Serialize()); +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_setButtonParamImpl( +    JNIEnv* env, jobject j_obj, jint j_player_index, jint j_button_id, jstring j_param) { +    ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { +        controller->SetButtonParam(j_button_id, +                                   Common::ParamPackage(Common::Android::GetJString(env, j_param))); +    }); +} + +jstring Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getStickParamImpl(JNIEnv* env, +                                                                             jobject j_obj, +                                                                             jint j_player_index, +                                                                             jint j_stick) { +    return Common::Android::ToJString(env, EmulationSession::GetInstance() +                                               .System() +                                               .HIDCore() +                                               .GetEmulatedControllerByIndex(j_player_index) +                                               ->GetStickParam(j_stick) +                                               .Serialize()); +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_setStickParamImpl( +    JNIEnv* env, jobject j_obj, jint j_player_index, jint j_stick_id, jstring j_param) { +    ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { +        controller->SetStickParam(j_stick_id, +                                  Common::ParamPackage(Common::Android::GetJString(env, j_param))); +    }); +} + +jint Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getButtonNameImpl(JNIEnv* env, +                                                                          jobject j_obj, +                                                                          jstring j_param) { +    return static_cast<jint>(EmulationSession::GetInstance().GetInputSubsystem().GetButtonName( +        Common::ParamPackage(Common::Android::GetJString(env, j_param)))); +} + +jintArray Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getSupportedStyleTagsImpl( +    JNIEnv* env, jobject j_obj, jint j_player_index) { +    auto& hid_core = EmulationSession::GetInstance().System().HIDCore(); +    const auto npad_style_set = hid_core.GetSupportedStyleTag(); +    std::vector<s32> supported_indexes; +    if (npad_style_set.fullkey == 1) { +        supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::Fullkey)); +    } + +    if (npad_style_set.joycon_dual == 1) { +        supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::JoyconDual)); +    } + +    if (npad_style_set.joycon_left == 1) { +        supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::JoyconLeft)); +    } + +    if (npad_style_set.joycon_right == 1) { +        supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::JoyconRight)); +    } + +    if (j_player_index == 0 && npad_style_set.handheld == 1) { +        supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::Handheld)); +    } + +    if (npad_style_set.gamecube == 1) { +        supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::GameCube)); +    } + +    jintArray j_supported_indexes = env->NewIntArray(supported_indexes.size()); +    env->SetIntArrayRegion(j_supported_indexes, 0, supported_indexes.size(), +                           supported_indexes.data()); +    return j_supported_indexes; +} + +jint Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getStyleIndexImpl(JNIEnv* env, +                                                                          jobject j_obj, +                                                                          jint j_player_index) { +    return static_cast<s32>(EmulationSession::GetInstance() +                                .System() +                                .HIDCore() +                                .GetEmulatedControllerByIndex(j_player_index) +                                ->GetNpadStyleIndex(true)); +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_setStyleIndexImpl(JNIEnv* env, +                                                                          jobject j_obj, +                                                                          jint j_player_index, +                                                                          jint j_style_index) { +    auto& hid_core = EmulationSession::GetInstance().System().HIDCore(); +    auto type = static_cast<Core::HID::NpadStyleIndex>(j_style_index); +    ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { +        controller->SetNpadStyleIndex(type); +    }); +    if (j_player_index == 0) { +        auto* handheld = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld); +        auto* player_one = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1); +        ConnectController(j_player_index, +                          player_one->IsConnected(true) || handheld->IsConnected(true)); +    } +} + +jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_isControllerImpl(JNIEnv* env, +                                                                             jobject j_obj, +                                                                             jstring jparams) { +    return static_cast<jint>(EmulationSession::GetInstance().GetInputSubsystem().IsController( +        Common::ParamPackage(Common::Android::GetJString(env, jparams)))); +} + +jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getIsConnected(JNIEnv* env, +                                                                           jobject j_obj, +                                                                           jint j_player_index) { +    auto& hid_core = EmulationSession::GetInstance().System().HIDCore(); +    auto* controller = hid_core.GetEmulatedControllerByIndex(static_cast<size_t>(j_player_index)); +    if (j_player_index == 0 && +        controller->GetNpadStyleIndex(true) == Core::HID::NpadStyleIndex::Handheld) { +        return hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld)->IsConnected(true); +    } +    return controller->IsConnected(true); +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_connectControllersImpl( +    JNIEnv* env, jobject j_obj, jbooleanArray j_connected) { +    jboolean isCopy = false; +    auto j_connected_array_size = env->GetArrayLength(j_connected); +    jboolean* j_connected_array = env->GetBooleanArrayElements(j_connected, &isCopy); +    for (int i = 0; i < j_connected_array_size; ++i) { +        ConnectController(i, j_connected_array[i]); +    } +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_resetControllerMappings( +    JNIEnv* env, jobject j_obj, jint j_player_index) { +    // Clear all previous mappings +    for (int button_id = 0; button_id < Settings::NativeButton::NumButtons; ++button_id) { +        ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { +            controller->SetButtonParam(button_id, {}); +        }); +    } +    for (int analog_id = 0; analog_id < Settings::NativeAnalog::NumAnalogs; ++analog_id) { +        ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { +            controller->SetStickParam(analog_id, {}); +        }); +    } +} + +} // extern "C" diff --git a/src/android/app/src/main/res/drawable/button_anim.xml b/src/android/app/src/main/res/drawable/button_anim.xml new file mode 100644 index 000000000..ccdc5ca6a --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_anim.xml @@ -0,0 +1,142 @@ +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" +    xmlns:aapt="http://schemas.android.com/aapt"> +    <aapt:attr name="android:drawable"> +        <vector +            android:width="1000dp" +            android:height="1000dp" +            android:viewportWidth="1000" +            android:viewportHeight="1000"> +            <group android:name="_R_G"> +                <group +                    android:name="_R_G_L_0_G" +                    android:pivotX="100" +                    android:pivotY="100" +                    android:scaleX="4.5" +                    android:scaleY="4.5" +                    android:translateX="400" +                    android:translateY="400"> +                    <path +                        android:name="_R_G_L_0_G_D_0_P_0" +                        android:fillAlpha="1" +                        android:fillColor="?attr/colorSecondaryContainer" +                        android:fillType="nonZero" +                        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 " /> +                    <path +                        android:name="_R_G_L_0_G_D_2_P_0" +                        android:fillAlpha="0.8" +                        android:fillColor="?attr/colorOnSecondaryContainer" +                        android:fillType="nonZero" +                        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 " /> +                </group> +            </group> +            <group android:name="time_group" /> +        </vector> +    </aapt:attr> +    <target android:name="_R_G_L_0_G"> +        <aapt:attr name="android:animation"> +            <set android:ordering="together"> +                <objectAnimator +                    android:duration="100" +                    android:propertyName="scaleX" +                    android:startOffset="0" +                    android:valueFrom="4.5" +                    android:valueTo="3.75" +                    android:valueType="floatType"> +                    <aapt:attr name="android:interpolator"> +                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> +                    </aapt:attr> +                </objectAnimator> +                <objectAnimator +                    android:duration="100" +                    android:propertyName="scaleY" +                    android:startOffset="0" +                    android:valueFrom="4.5" +                    android:valueTo="3.75" +                    android:valueType="floatType"> +                    <aapt:attr name="android:interpolator"> +                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> +                    </aapt:attr> +                </objectAnimator> +                <objectAnimator +                    android:duration="234" +                    android:propertyName="scaleX" +                    android:startOffset="100" +                    android:valueFrom="3.75" +                    android:valueTo="3.75" +                    android:valueType="floatType"> +                    <aapt:attr name="android:interpolator"> +                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> +                    </aapt:attr> +                </objectAnimator> +                <objectAnimator +                    android:duration="234" +                    android:propertyName="scaleY" +                    android:startOffset="100" +                    android:valueFrom="3.75" +                    android:valueTo="3.75" +                    android:valueType="floatType"> +                    <aapt:attr name="android:interpolator"> +                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> +                    </aapt:attr> +                </objectAnimator> +                <objectAnimator +                    android:duration="167" +                    android:propertyName="scaleX" +                    android:startOffset="334" +                    android:valueFrom="3.75" +                    android:valueTo="4.75" +                    android:valueType="floatType"> +                    <aapt:attr name="android:interpolator"> +                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> +                    </aapt:attr> +                </objectAnimator> +                <objectAnimator +                    android:duration="167" +                    android:propertyName="scaleY" +                    android:startOffset="334" +                    android:valueFrom="3.75" +                    android:valueTo="4.75" +                    android:valueType="floatType"> +                    <aapt:attr name="android:interpolator"> +                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> +                    </aapt:attr> +                </objectAnimator> +                <objectAnimator +                    android:duration="67" +                    android:propertyName="scaleX" +                    android:startOffset="501" +                    android:valueFrom="4.75" +                    android:valueTo="4.5" +                    android:valueType="floatType"> +                    <aapt:attr name="android:interpolator"> +                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> +                    </aapt:attr> +                </objectAnimator> +                <objectAnimator +                    android:duration="67" +                    android:propertyName="scaleY" +                    android:startOffset="501" +                    android:valueFrom="4.75" +                    android:valueTo="4.5" +                    android:valueType="floatType"> +                    <aapt:attr name="android:interpolator"> +                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> +                    </aapt:attr> +                </objectAnimator> +            </set> +        </aapt:attr> +    </target> +    <target android:name="time_group"> +        <aapt:attr name="android:animation"> +            <set android:ordering="together"> +                <objectAnimator +                    android:duration="1034" +                    android:propertyName="translateX" +                    android:startOffset="0" +                    android:valueFrom="0" +                    android:valueTo="1" +                    android:valueType="floatType" /> +            </set> +        </aapt:attr> +    </target> +</animated-vector> diff --git a/src/android/app/src/main/res/drawable/ic_controller_disconnected.xml b/src/android/app/src/main/res/drawable/ic_controller_disconnected.xml new file mode 100644 index 000000000..8e3c66f74 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_controller_disconnected.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" +    android:width="24dp" +    android:height="24dp" +    android:viewportWidth="960" +    android:viewportHeight="960"> +    <path +        android:fillColor="?attr/colorControlNormal" +        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" /> +</vector> diff --git a/src/android/app/src/main/res/drawable/ic_more_vert.xml b/src/android/app/src/main/res/drawable/ic_more_vert.xml new file mode 100644 index 000000000..9f62ac595 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_more_vert.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" +    android:height="24dp" +    android:viewportHeight="24" +    android:viewportWidth="24" +    android:width="24dp"> +    <path +        android:fillColor="?attr/colorControlNormal" +        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" /> +</vector> diff --git a/src/android/app/src/main/res/drawable/ic_new_label.xml b/src/android/app/src/main/res/drawable/ic_new_label.xml new file mode 100644 index 000000000..fac562c26 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_new_label.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" +    android:width="24dp" +    android:height="24dp" +    android:viewportWidth="24" +    android:viewportHeight="24"> +    <path +        android:fillColor="?attr/colorControlNormal" +        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" /> +</vector> diff --git a/src/android/app/src/main/res/drawable/ic_overlay.xml b/src/android/app/src/main/res/drawable/ic_overlay.xml new file mode 100644 index 000000000..c7986c5a2 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_overlay.xml @@ -0,0 +1,21 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" +    android:width="24dp" +    android:height="24dp" +    android:viewportWidth="24" +    android:viewportHeight="24"> +    <path +        android:fillColor="?attr/colorControlNormal" +        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" /> +    <path +        android:fillColor="?attr/colorControlNormal" +        android:pathData="M15,11.25h1.5v1.5h-1.5z" /> +    <path +        android:fillColor="?attr/colorControlNormal" +        android:pathData="M12.5,11.25h1.5v1.5h-1.5z" /> +    <path +        android:fillColor="?attr/colorControlNormal" +        android:pathData="M10,11.25h1.5v1.5h-1.5z" /> +    <path +        android:fillColor="?attr/colorControlNormal" +        android:pathData="M7.5,11.25h1.5v1.5h-1.5z" /> +</vector> diff --git a/src/android/app/src/main/res/drawable/ic_share.xml b/src/android/app/src/main/res/drawable/ic_share.xml new file mode 100644 index 000000000..3fc2f3c99 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_share.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" +    android:width="24dp" +    android:height="24dp" +    android:viewportWidth="24" +    android:viewportHeight="24"> +    <path +        android:fillColor="?attr/colorControlNormal" +        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" /> +</vector> diff --git a/src/android/app/src/main/res/drawable/stick_one_direction_anim.xml b/src/android/app/src/main/res/drawable/stick_one_direction_anim.xml new file mode 100644 index 000000000..a1da1316f --- /dev/null +++ b/src/android/app/src/main/res/drawable/stick_one_direction_anim.xml @@ -0,0 +1,118 @@ +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" +    xmlns:aapt="http://schemas.android.com/aapt"> +    <aapt:attr name="android:drawable"> +        <vector +            android:width="1000dp" +            android:height="1000dp" +            android:viewportWidth="1000" +            android:viewportHeight="1000"> +            <group android:name="_R_G"> +                <group +                    android:name="_R_G_L_1_G" +                    android:pivotX="100" +                    android:pivotY="100" +                    android:scaleX="5" +                    android:scaleY="5" +                    android:translateX="400" +                    android:translateY="400"> +                    <path +                        android:name="_R_G_L_1_G_D_0_P_0" +                        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 " +                        android:strokeWidth="1" +                        android:strokeAlpha="0.6" +                        android:strokeColor="?attr/colorOutline" +                        android:strokeLineCap="round" +                        android:strokeLineJoin="round" /> +                </group> +                <group +                    android:name="_R_G_L_0_G_T_1" +                    android:scaleX="5" +                    android:scaleY="5" +                    android:translateX="500" +                    android:translateY="500"> +                    <group +                        android:name="_R_G_L_0_G" +                        android:translateX="-100" +                        android:translateY="-100"> +                        <path +                            android:name="_R_G_L_0_G_D_0_P_0" +                            android:fillAlpha="1" +                            android:fillColor="?attr/colorSecondaryContainer" +                            android:fillType="nonZero" +                            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 " /> +                        <path +                            android:name="_R_G_L_0_G_D_2_P_0" +                            android:fillAlpha="0.8" +                            android:fillColor="?attr/colorOnSecondaryContainer" +                            android:fillType="nonZero" +                            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 " /> +                    </group> +                </group> +            </group> +            <group android:name="time_group" /> +        </vector> +    </aapt:attr> +    <target android:name="_R_G_L_0_G_T_1"> +        <aapt:attr name="android:animation"> +            <set android:ordering="together"> +                <objectAnimator +                    android:duration="267" +                    android:pathData="M 500,500C 500,500 364,500 364,500" +                    android:propertyName="translateXY" +                    android:propertyXName="translateX" +                    android:propertyYName="translateY" +                    android:startOffset="0"> +                    <aapt:attr name="android:interpolator"> +                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> +                    </aapt:attr> +                </objectAnimator> +                <objectAnimator +                    android:duration="234" +                    android:pathData="M 364,500C 364,500 364,500 364,500" +                    android:propertyName="translateXY" +                    android:propertyXName="translateX" +                    android:propertyYName="translateY" +                    android:startOffset="267"> +                    <aapt:attr name="android:interpolator"> +                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0.333 0.667,0.667 1.0,1.0" /> +                    </aapt:attr> +                </objectAnimator> +                <objectAnimator +                    android:duration="133" +                    android:pathData="M 364,500C 364,500 525,500 525,500" +                    android:propertyName="translateXY" +                    android:propertyXName="translateX" +                    android:propertyYName="translateY" +                    android:startOffset="501"> +                    <aapt:attr name="android:interpolator"> +                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> +                    </aapt:attr> +                </objectAnimator> +                <objectAnimator +                    android:duration="100" +                    android:pathData="M 525,500C 525,500 500,500 500,500" +                    android:propertyName="translateXY" +                    android:propertyXName="translateX" +                    android:propertyYName="translateY" +                    android:startOffset="634"> +                    <aapt:attr name="android:interpolator"> +                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> +                    </aapt:attr> +                </objectAnimator> +            </set> +        </aapt:attr> +    </target> +    <target android:name="time_group"> +        <aapt:attr name="android:animation"> +            <set android:ordering="together"> +                <objectAnimator +                    android:duration="968" +                    android:propertyName="translateX" +                    android:startOffset="0" +                    android:valueFrom="0" +                    android:valueTo="1" +                    android:valueType="floatType" /> +            </set> +        </aapt:attr> +    </target> +</animated-vector> diff --git a/src/android/app/src/main/res/drawable/stick_two_direction_anim.xml b/src/android/app/src/main/res/drawable/stick_two_direction_anim.xml new file mode 100644 index 000000000..bc71adcbd --- /dev/null +++ b/src/android/app/src/main/res/drawable/stick_two_direction_anim.xml @@ -0,0 +1,173 @@ +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" +    xmlns:aapt="http://schemas.android.com/aapt"> +    <aapt:attr name="android:drawable"> +        <vector +            android:width="1000dp" +            android:height="1000dp" +            android:viewportWidth="1000" +            android:viewportHeight="1000"> +            <group android:name="_R_G"> +                <group +                    android:name="_R_G_L_1_G" +                    android:pivotX="100" +                    android:pivotY="100" +                    android:scaleX="5" +                    android:scaleY="5" +                    android:translateX="400" +                    android:translateY="400"> +                    <path +                        android:name="_R_G_L_1_G_D_0_P_0" +                        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 " +                        android:strokeWidth="1" +                        android:strokeAlpha="0.6" +                        android:strokeColor="?attr/colorOutline" +                        android:strokeLineCap="round" +                        android:strokeLineJoin="round" /> +                </group> +                <group +                    android:name="_R_G_L_0_G_T_1" +                    android:scaleX="5" +                    android:scaleY="5" +                    android:translateX="500" +                    android:translateY="500"> +                    <group +                        android:name="_R_G_L_0_G" +                        android:translateX="-100" +                        android:translateY="-100"> +                        <path +                            android:name="_R_G_L_0_G_D_0_P_0" +                            android:fillAlpha="1" +                            android:fillColor="?attr/colorSecondaryContainer" +                            android:fillType="nonZero" +                            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 " /> +                        <path +                            android:name="_R_G_L_0_G_D_2_P_0" +                            android:fillAlpha="0.8" +                            android:fillColor="?attr/colorOnSecondaryContainer" +                            android:fillType="nonZero" +                            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 " /> +                    </group> +                </group> +            </group> +            <group android:name="time_group" /> +        </vector> +    </aapt:attr> +    <target android:name="_R_G_L_0_G_T_1"> +        <aapt:attr name="android:animation"> +            <set android:ordering="together"> +                <objectAnimator +                    android:duration="267" +                    android:pathData="M 500,500C 500,500 364,500 364,500" +                    android:propertyName="translateXY" +                    android:propertyXName="translateX" +                    android:propertyYName="translateY" +                    android:startOffset="0"> +                    <aapt:attr name="android:interpolator"> +                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> +                    </aapt:attr> +                </objectAnimator> +                <objectAnimator +                    android:duration="234" +                    android:pathData="M 364,500C 364,500 364,500 364,500" +                    android:propertyName="translateXY" +                    android:propertyXName="translateX" +                    android:propertyYName="translateY" +                    android:startOffset="267"> +                    <aapt:attr name="android:interpolator"> +                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0.333 0.667,0.667 1.0,1.0" /> +                    </aapt:attr> +                </objectAnimator> +                <objectAnimator +                    android:duration="133" +                    android:pathData="M 364,500C 364,500 525,500 525,500" +                    android:propertyName="translateXY" +                    android:propertyXName="translateX" +                    android:propertyYName="translateY" +                    android:startOffset="501"> +                    <aapt:attr name="android:interpolator"> +                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> +                    </aapt:attr> +                </objectAnimator> +                <objectAnimator +                    android:duration="100" +                    android:pathData="M 525,500C 525,500 500,500 500,500" +                    android:propertyName="translateXY" +                    android:propertyXName="translateX" +                    android:propertyYName="translateY" +                    android:startOffset="634"> +                    <aapt:attr name="android:interpolator"> +                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> +                    </aapt:attr> +                </objectAnimator> +                <objectAnimator +                    android:duration="400" +                    android:pathData="M 500,500C 500,500 500,500 500,500" +                    android:propertyName="translateXY" +                    android:propertyXName="translateX" +                    android:propertyYName="translateY" +                    android:startOffset="734"> +                    <aapt:attr name="android:interpolator"> +                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0.333 0.667,0.667 1.0,1.0" /> +                    </aapt:attr> +                </objectAnimator> +                <objectAnimator +                    android:duration="267" +                    android:pathData="M 500,500C 500,500 500,364 500,364" +                    android:propertyName="translateXY" +                    android:propertyXName="translateX" +                    android:propertyYName="translateY" +                    android:startOffset="1134"> +                    <aapt:attr name="android:interpolator"> +                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> +                    </aapt:attr> +                </objectAnimator> +                <objectAnimator +                    android:duration="234" +                    android:pathData="M 500,364C 500,364 500,364 500,364" +                    android:propertyName="translateXY" +                    android:propertyXName="translateX" +                    android:propertyYName="translateY" +                    android:startOffset="1401"> +                    <aapt:attr name="android:interpolator"> +                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0.333 0.667,0.667 1.0,1.0" /> +                    </aapt:attr> +                </objectAnimator> +                <objectAnimator +                    android:duration="133" +                    android:pathData="M 500,364C 500,364 500,535 500,535" +                    android:propertyName="translateXY" +                    android:propertyXName="translateX" +                    android:propertyYName="translateY" +                    android:startOffset="1635"> +                    <aapt:attr name="android:interpolator"> +                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> +                    </aapt:attr> +                </objectAnimator> +                <objectAnimator +                    android:duration="100" +                    android:pathData="M 500,535C 500,535 500,500 500,500" +                    android:propertyName="translateXY" +                    android:propertyXName="translateX" +                    android:propertyYName="translateY" +                    android:startOffset="1768"> +                    <aapt:attr name="android:interpolator"> +                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> +                    </aapt:attr> +                </objectAnimator> +            </set> +        </aapt:attr> +    </target> +    <target android:name="time_group"> +        <aapt:attr name="android:animation"> +            <set android:ordering="together"> +                <objectAnimator +                    android:duration="2269" +                    android:propertyName="translateX" +                    android:startOffset="0" +                    android:valueFrom="0" +                    android:valueTo="1" +                    android:valueType="floatType" /> +            </set> +        </aapt:attr> +    </target> +</animated-vector> diff --git a/src/android/app/src/main/res/layout-ldrtl/list_item_setting_input.xml b/src/android/app/src/main/res/layout-ldrtl/list_item_setting_input.xml new file mode 100644 index 000000000..583620dc6 --- /dev/null +++ b/src/android/app/src/main/res/layout-ldrtl/list_item_setting_input.xml @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" +    xmlns:app="http://schemas.android.com/apk/res-auto" +    xmlns:tools="http://schemas.android.com/tools" +    android:id="@+id/setting_body" +    android:layout_width="match_parent" +    android:layout_height="wrap_content" +    android:background="?android:attr/selectableItemBackground" +    android:clickable="true" +    android:focusable="true" +    android:gravity="center_vertical" +    android:minHeight="72dp" +    android:padding="16dp" +    android:nextFocusLeft="@id/button_options"> + +    <LinearLayout +        android:layout_width="match_parent" +        android:layout_height="wrap_content" +        android:gravity="center_vertical" +        android:orientation="horizontal"> + +        <LinearLayout +            android:layout_width="0dp" +            android:layout_height="wrap_content" +            android:orientation="vertical" +            android:layout_weight="1"> + +            <com.google.android.material.textview.MaterialTextView +                android:id="@+id/text_setting_name" +                style="@style/TextAppearance.Material3.HeadlineMedium" +                android:layout_width="match_parent" +                android:layout_height="wrap_content" +                android:textAlignment="viewStart" +                android:textSize="17sp" +                app:lineHeight="22dp" +                tools:text="Setting Name" /> + +            <com.google.android.material.textview.MaterialTextView +                android:id="@+id/text_setting_value" +                style="@style/TextAppearance.Material3.LabelMedium" +                android:layout_width="match_parent" +                android:layout_height="wrap_content" +                android:layout_marginTop="@dimen/spacing_small" +                android:textAlignment="viewStart" +                android:textStyle="bold" +                android:textSize="13sp" +                tools:text="1x" /> + +        </LinearLayout> + +        <Button +            android:id="@+id/button_options" +            style="?attr/materialIconButtonStyle" +            android:layout_width="wrap_content" +            android:layout_height="wrap_content" +            android:nextFocusRight="@id/setting_body" +            app:icon="@drawable/ic_more_vert" +            app:iconSize="24dp" +            app:iconTint="?attr/colorOnSurface" /> + +    </LinearLayout> + +</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 100644 --- 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 @@                  style="@style/TextAppearance.Material3.TitleMedium"                  android:layout_width="match_parent"                  android:layout_height="wrap_content" -                android:ellipsize="none" -                android:marqueeRepeatLimit="marquee_forever"                  android:requiresFadingEdge="horizontal" -                android:singleLine="true"                  android:textAlignment="viewStart"                  tools:text="@string/select_gpu_driver_default" /> @@ -52,10 +49,7 @@                  android:layout_width="match_parent"                  android:layout_height="wrap_content"                  android:layout_marginTop="6dp" -                android:ellipsize="none" -                android:marqueeRepeatLimit="marquee_forever"                  android:requiresFadingEdge="horizontal" -                android:singleLine="true"                  android:textAlignment="viewStart"                  tools:text="@string/install_gpu_driver_description" /> @@ -65,10 +59,7 @@                  android:layout_width="match_parent"                  android:layout_height="wrap_content"                  android:layout_marginTop="6dp" -                android:ellipsize="none" -                android:marqueeRepeatLimit="marquee_forever"                  android:requiresFadingEdge="horizontal" -                android:singleLine="true"                  android:textAlignment="viewStart"                  tools:text="@string/install_gpu_driver_description" /> 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 100644 --- 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 @@              android:layout_width="0dp"              android:layout_height="wrap_content"              android:layout_gravity="center_vertical|start" -            android:ellipsize="none" -            android:marqueeRepeatLimit="marquee_forever"              android:requiresFadingEdge="horizontal" -            android:singleLine="true"              android:textAlignment="viewStart"              app:layout_constraintBottom_toBottomOf="parent"              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 100644 --- 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 @@                  android:layout_width="0dp"                  android:layout_height="wrap_content"                  android:layout_marginTop="8dp" -                android:ellipsize="none" -                android:marqueeRepeatLimit="marquee_forever"                  android:requiresFadingEdge="horizontal" -                android:singleLine="true"                  android:textAlignment="center"                  android:textSize="14sp"                  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 100644 --- 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 @@                  android:textAlignment="viewStart"                  android:textSize="14sp"                  android:textStyle="bold" -                android:singleLine="true" -                android:marqueeRepeatLimit="marquee_forever" -                android:ellipsize="none"                  android:requiresFadingEdge="horizontal"                  android:layout_marginTop="6dp"                  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 100644 index 000000000..6ad76fe41 --- /dev/null +++ b/src/android/app/src/main/res/layout/dialog_input_profiles.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android" +    android:id="@+id/list_profiles" +    android:layout_width="match_parent" +    android:layout_height="wrap_content" +    android:fadeScrollbars="false" /> diff --git a/src/android/app/src/main/res/layout/dialog_mapping.xml b/src/android/app/src/main/res/layout/dialog_mapping.xml new file mode 100644 index 000000000..06190b8d2 --- /dev/null +++ b/src/android/app/src/main/res/layout/dialog_mapping.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" +    android:layout_width="match_parent" +    android:layout_height="wrap_content" +    xmlns:tools="http://schemas.android.com/tools" +    android:defaultFocusHighlightEnabled="false" +    android:focusable="true" +    android:focusableInTouchMode="true" +    android:focusedByDefault="true" +    android:orientation="horizontal" +    android:gravity="center"> + +    <ImageView +        android:id="@+id/image_stick_animation" +        android:layout_width="@dimen/mapping_anim_size" +        android:layout_height="@dimen/mapping_anim_size" +        tools:src="@drawable/stick_two_direction_anim" /> + +    <ImageView +        android:id="@+id/image_button_animation" +        android:layout_width="@dimen/mapping_anim_size" +        android:layout_height="@dimen/mapping_anim_size" +        android:layout_marginStart="48dp" +        tools:src="@drawable/button_anim" /> + +</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 100644 --- 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 @@                  android:layout_marginTop="12dp"                  android:layout_marginBottom="12dp"                  android:layout_marginHorizontal="16dp" -                android:ellipsize="none" -                android:marqueeRepeatLimit="marquee_forever"                  android:requiresFadingEdge="horizontal" -                android:singleLine="true"                  android:textAlignment="center"                  tools:text="deko_basic" /> diff --git a/src/android/app/src/main/res/layout/list_item_input_profile.xml b/src/android/app/src/main/res/layout/list_item_input_profile.xml new file mode 100644 index 000000000..a08dccf0c --- /dev/null +++ b/src/android/app/src/main/res/layout/list_item_input_profile.xml @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" +    xmlns:app="http://schemas.android.com/apk/res-auto" +    xmlns:tools="http://schemas.android.com/tools" +    android:layout_width="match_parent" +    android:layout_height="wrap_content" +    android:focusable="false" +    android:paddingHorizontal="20dp" +    android:paddingVertical="16dp"> + +    <com.google.android.material.textview.MaterialTextView +        android:id="@+id/title" +        style="@style/TextAppearance.Material3.HeadlineMedium" +        android:layout_width="0dp" +        android:layout_height="0dp" +        android:textAlignment="viewStart" +        android:gravity="start|center_vertical" +        android:textSize="17sp" +        android:layout_marginEnd="16dp" +        app:layout_constraintBottom_toBottomOf="@+id/button_layout" +        app:layout_constraintEnd_toStartOf="@+id/button_layout" +        app:layout_constraintStart_toStartOf="parent" +        app:layout_constraintTop_toTopOf="parent" +        app:lineHeight="28dp" +        tools:text="My profile" /> + +    <LinearLayout +        android:id="@+id/button_layout" +        android:layout_width="wrap_content" +        android:layout_height="wrap_content" +        android:gravity="center_vertical" +        android:orientation="horizontal" +        app:layout_constraintEnd_toEndOf="parent" +        app:layout_constraintTop_toTopOf="parent"> + +        <Button +            android:id="@+id/button_new" +            style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" +            android:layout_width="wrap_content" +            android:layout_height="wrap_content" +            android:contentDescription="@string/create_new_profile" +            android:tooltipText="@string/create_new_profile" +            app:icon="@drawable/ic_new_label" /> + +        <Button +            android:id="@+id/button_delete" +            style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" +            android:layout_width="wrap_content" +            android:layout_height="wrap_content" +            android:contentDescription="@string/delete" +            android:tooltipText="@string/delete" +            app:icon="@drawable/ic_delete" /> + +        <Button +            android:id="@+id/button_save" +            style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" +            android:layout_width="wrap_content" +            android:layout_height="wrap_content" +            android:contentDescription="@string/save" +            android:tooltipText="@string/save" +            app:icon="@drawable/ic_save" /> + +        <Button +            android:id="@+id/button_load" +            style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" +            android:layout_width="wrap_content" +            android:layout_height="wrap_content" +            android:contentDescription="@string/load" +            android:tooltipText="@string/load" +            app:icon="@drawable/ic_import" /> + +    </LinearLayout> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/src/android/app/src/main/res/layout/list_item_setting_input.xml b/src/android/app/src/main/res/layout/list_item_setting_input.xml new file mode 100644 index 000000000..d67cbe245 --- /dev/null +++ b/src/android/app/src/main/res/layout/list_item_setting_input.xml @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" +    xmlns:app="http://schemas.android.com/apk/res-auto" +    xmlns:tools="http://schemas.android.com/tools" +    android:id="@+id/setting_body" +    android:layout_width="match_parent" +    android:layout_height="wrap_content" +    android:background="?android:attr/selectableItemBackground" +    android:clickable="true" +    android:focusable="true" +    android:gravity="center_vertical" +    android:minHeight="72dp" +    android:padding="16dp" +    android:nextFocusRight="@id/button_options"> + +    <LinearLayout +        android:layout_width="match_parent" +        android:layout_height="wrap_content" +        android:gravity="center_vertical" +        android:orientation="horizontal"> + +        <LinearLayout +            android:layout_width="0dp" +            android:layout_height="wrap_content" +            android:orientation="vertical" +            android:layout_weight="1"> + +            <com.google.android.material.textview.MaterialTextView +                android:id="@+id/text_setting_name" +                style="@style/TextAppearance.Material3.HeadlineMedium" +                android:layout_width="match_parent" +                android:layout_height="wrap_content" +                android:textAlignment="viewStart" +                android:textSize="17sp" +                app:lineHeight="22dp" +                tools:text="Setting Name" /> + +            <com.google.android.material.textview.MaterialTextView +                android:id="@+id/text_setting_value" +                style="@style/TextAppearance.Material3.LabelMedium" +                android:layout_width="match_parent" +                android:layout_height="wrap_content" +                android:layout_marginTop="@dimen/spacing_small" +                android:textAlignment="viewStart" +                android:textStyle="bold" +                android:textSize="13sp" +                tools:text="1x" /> + +        </LinearLayout> + +        <Button +            android:id="@+id/button_options" +            style="?attr/materialIconButtonStyle" +            android:layout_width="wrap_content" +            android:layout_height="wrap_content" +            android:nextFocusLeft="@id/setting_body" +            app:icon="@drawable/ic_more_vert" +            app:iconSize="24dp" +            app:iconTint="?attr/colorOnSurface" /> + +    </LinearLayout> + +</RelativeLayout> diff --git a/src/android/app/src/main/res/menu/menu_in_game.xml b/src/android/app/src/main/res/menu/menu_in_game.xml index eecb0563b..867197ebc 100644 --- a/src/android/app/src/main/res/menu/menu_in_game.xml +++ b/src/android/app/src/main/res/menu/menu_in_game.xml @@ -17,8 +17,13 @@          android:title="@string/per_game_settings" />      <item -        android:id="@+id/menu_overlay_controls" +        android:id="@+id/menu_controls"          android:icon="@drawable/ic_controller" +        android:title="@string/preferences_controls" /> + +    <item +        android:id="@+id/menu_overlay_controls" +        android:icon="@drawable/ic_overlay"          android:title="@string/emulation_input_overlay" />      <item diff --git a/src/android/app/src/main/res/menu/menu_input_options.xml b/src/android/app/src/main/res/menu/menu_input_options.xml new file mode 100644 index 000000000..81ea5043f --- /dev/null +++ b/src/android/app/src/main/res/menu/menu_input_options.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + +    <item +        android:id="@+id/invert_axis" +        android:title="@string/invert_axis" +        android:visible="false" /> + +    <item +        android:id="@+id/invert_button" +        android:title="@string/invert_button" +        android:visible="false" /> + +    <item +        android:id="@+id/toggle_button" +        android:title="@string/toggle_button" +        android:visible="false" /> + +    <item +        android:id="@+id/turbo_button" +        android:title="@string/turbo_button" +        android:visible="false" /> + +    <item +        android:id="@+id/set_threshold" +        android:title="@string/set_threshold" +        android:visible="false" /> + +    <item +        android:id="@+id/toggle_axis" +        android:title="@string/toggle_axis" +        android:visible="false" /> + +</menu> diff --git a/src/android/app/src/main/res/navigation/settings_navigation.xml b/src/android/app/src/main/res/navigation/settings_navigation.xml index 1d87d36b3..e4c66e7d5 100644 --- a/src/android/app/src/main/res/navigation/settings_navigation.xml +++ b/src/android/app/src/main/res/navigation/settings_navigation.xml @@ -26,7 +26,7 @@      <fragment          android:id="@+id/settingsSearchFragment" -        android:name="org.yuzu.yuzu_emu.fragments.SettingsSearchFragment" +        android:name="org.yuzu.yuzu_emu.features.settings.ui.SettingsSearchFragment"          android:label="SettingsSearchFragment" />  </navigation> diff --git a/src/android/app/src/main/res/values-w600dp/dimens.xml b/src/android/app/src/main/res/values-w600dp/dimens.xml index 128319e27..0e2d40876 100644 --- a/src/android/app/src/main/res/values-w600dp/dimens.xml +++ b/src/android/app/src/main/res/values-w600dp/dimens.xml @@ -2,4 +2,6 @@  <resources>      <dimen name="spacing_navigation">0dp</dimen>      <dimen name="spacing_navigation_rail">80dp</dimen> + +    <dimen name="mapping_anim_size">100dp</dimen>  </resources> diff --git a/src/android/app/src/main/res/values/dimens.xml b/src/android/app/src/main/res/values/dimens.xml index 992b5ae44..bf733637f 100644 --- a/src/android/app/src/main/res/values/dimens.xml +++ b/src/android/app/src/main/res/values/dimens.xml @@ -18,4 +18,6 @@      <dimen name="dialog_margin">20dp</dimen>      <dimen name="elevated_app_bar">3dp</dimen> + +    <dimen name="mapping_anim_size">75dp</dimen>  </resources> diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 78a4c958a..6a631f664 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -255,6 +255,92 @@      <string name="audio_volume">Volume</string>      <string name="audio_volume_description">Specifies the volume of audio output.</string> +    <!-- Input strings --> +    <string name="buttons">Buttons</string> +    <string name="button_a">A</string> +    <string name="button_b">B</string> +    <string name="button_x">X</string> +    <string name="button_y">Y</string> +    <string name="button_plus">Plus</string> +    <string name="button_minus">Minus</string> +    <string name="button_home">Home</string> +    <string name="button_capture">Capture</string> +    <string name="start_pause">Start/Pause</string> +    <string name="dpad">D-Pad</string> +    <string name="up">Up</string> +    <string name="down">Down</string> +    <string name="left">Left</string> +    <string name="right">Right</string> +    <string name="left_stick">Left stick</string> +    <string name="control_stick">Control stick</string> +    <string name="right_stick">Right stick</string> +    <string name="c_stick">C-Stick</string> +    <string name="pressed">Pressed</string> +    <string name="range">Range</string> +    <string name="deadzone">Deadzone</string> +    <string name="modifier">Modifier</string> +    <string name="modifier_range">Modifier range</string> +    <string name="triggers">Triggers</string> +    <string name="button_l">L</string> +    <string name="button_r">R</string> +    <string name="button_zl">ZL</string> +    <string name="button_zr">ZR</string> +    <string name="button_sl_left">Left SL</string> +    <string name="button_sr_left">Left SR</string> +    <string name="button_sl_right">Right SL</string> +    <string name="button_sr_right">Right SR</string> +    <string name="button_z">Z</string> +    <string name="invalid">Invalid</string> +    <string name="not_set">Not set</string> +    <string name="unknown">Unknown</string> +    <string name="qualified_hat">%1$s%2$s%3$sHat %4$s</string> +    <string name="qualified_button_stick_axis">%1$s%2$s%3$sAxis %4$s</string> +    <string name="qualified_button">%1$s%2$s%3$sButton %4$s</string> +    <string name="qualified_axis">Axis %1$s%2$s</string> +    <string name="unused">Unused</string> +    <string name="input_prompt">Move or press an input</string> +    <string name="unsupported_input">Unsupported input type</string> +    <string name="input_mapping_filter">Input mapping filter</string> +    <string name="input_mapping_filter_description">Select a device to filter mapping inputs</string> +    <string name="auto_map">Auto-map a controller</string> +    <string name="auto_map_description">Select a device to attempt auto-mapping</string> +    <string name="attempted_auto_map">Attempted auto-map with %1$s</string> +    <string name="controller_type">Controller type</string> +    <string name="pro_controller">Pro Controller</string> +    <string name="handheld">Handheld</string> +    <string name="dual_joycons">Dual Joycons</string> +    <string name="left_joycon">Left Joycon</string> +    <string name="right_joycon">Right Joycon</string> +    <string name="gamecube_controller">GameCube Controller</string> +    <string name="invert_axis">Invert axis</string> +    <string name="invert_button">Invert button</string> +    <string name="toggle_button">Toggle button</string> +    <string name="turbo_button">Turbo button</string> +    <string name="set_threshold">Set threshold</string> +    <string name="toggle_axis">Toggle axis</string> +    <string name="connected">Connected</string> +    <string name="use_system_vibrator">Use system vibrator</string> +    <string name="input_overlay">Input overlay</string> +    <string name="vibration">Vibration</string> +    <string name="vibration_strength">Vibration strength</string> +    <string name="profile">Profile</string> +    <string name="create_new_profile">Create new profile</string> +    <string name="enter_profile_name">Enter profile name</string> +    <string name="profile_name_already_exists">Profile name already exists</string> +    <string name="invalid_profile_name">Invalid profile name</string> +    <string name="use_global_input_configuration">Use global input configuration</string> +    <string name="player_num_profile">Player %d profile</string> +    <string name="delete_input_profile">Delete input profile</string> +    <string name="delete_input_profile_description">Are you sure that you want to delete this profile? This is not recoverable.</string> +    <string name="stick_map_description">Move a stick left and then up or press a button</string> +    <string name="button_map_description">Press a button or move a trigger/stick</string> +    <string name="map_dpad_direction">Map to D-Pad %1$s</string> +    <string name="map_control">Map to %1$s</string> +    <string name="failed_to_load_profile">Failed to load profile</string> +    <string name="failed_to_save_profile">Failed to save profile</string> +    <string name="reset_mapping">Reset mappings</string> +    <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> +      <!-- Miscellaneous -->      <string name="slider_default">Default</string>      <string name="ini_saved">Saved settings</string> @@ -292,6 +378,10 @@      <string name="more_options">More options</string>      <string name="use_global_setting">Use global setting</string>      <string name="operation_completed_successfully">The operation completed successfully</string> +    <string name="retry">Retry</string> +    <string name="confirm">Confirm</string> +    <string name="load">Load</string> +    <string name="save">Save</string>      <!-- GPU driver installation -->      <string name="select_gpu_driver">Select GPU driver</string> @@ -313,6 +403,9 @@      <string name="preferences_graphics_description">Accuracy level, resolution, shader cache</string>      <string name="preferences_audio">Audio</string>      <string name="preferences_audio_description">Output engine, volume</string> +    <string name="preferences_controls">Controls</string> +    <string name="preferences_controls_description">Map controller input</string> +    <string name="preferences_player">Player %d</string>      <string name="preferences_theme">Theme and color</string>      <string name="preferences_debug">Debug</string>      <string name="preferences_debug_description">CPU/GPU debugging, graphics API, fastmem</string> diff --git a/src/common/android/id_cache.cpp b/src/common/android/id_cache.cpp index f39262db9..1145cbdf2 100644 --- a/src/common/android/id_cache.cpp +++ b/src/common/android/id_cache.cpp @@ -65,6 +65,30 @@ static jclass s_boolean_class;  static jmethodID s_boolean_constructor;  static jfieldID s_boolean_value_field; +static jclass s_player_input_class; +static jmethodID s_player_input_constructor; +static jfieldID s_player_input_connected_field; +static jfieldID s_player_input_buttons_field; +static jfieldID s_player_input_analogs_field; +static jfieldID s_player_input_motions_field; +static jfieldID s_player_input_vibration_enabled_field; +static jfieldID s_player_input_vibration_strength_field; +static jfieldID s_player_input_body_color_left_field; +static jfieldID s_player_input_body_color_right_field; +static jfieldID s_player_input_button_color_left_field; +static jfieldID s_player_input_button_color_right_field; +static jfieldID s_player_input_profile_name_field; +static jfieldID s_player_input_use_system_vibrator_field; + +static jclass s_yuzu_input_device_interface; +static jmethodID s_yuzu_input_device_get_name; +static jmethodID s_yuzu_input_device_get_guid; +static jmethodID s_yuzu_input_device_get_port; +static jmethodID s_yuzu_input_device_get_supports_vibration; +static jmethodID s_yuzu_input_device_vibrate; +static jmethodID s_yuzu_input_device_get_axes; +static jmethodID s_yuzu_input_device_has_keys; +  static constexpr jint JNI_VERSION = JNI_VERSION_1_6;  namespace Common::Android { @@ -276,6 +300,94 @@ jfieldID GetBooleanValueField() {      return s_boolean_value_field;  } +jclass GetPlayerInputClass() { +    return s_player_input_class; +} + +jmethodID GetPlayerInputConstructor() { +    return s_player_input_constructor; +} + +jfieldID GetPlayerInputConnectedField() { +    return s_player_input_connected_field; +} + +jfieldID GetPlayerInputButtonsField() { +    return s_player_input_buttons_field; +} + +jfieldID GetPlayerInputAnalogsField() { +    return s_player_input_analogs_field; +} + +jfieldID GetPlayerInputMotionsField() { +    return s_player_input_motions_field; +} + +jfieldID GetPlayerInputVibrationEnabledField() { +    return s_player_input_vibration_enabled_field; +} + +jfieldID GetPlayerInputVibrationStrengthField() { +    return s_player_input_vibration_strength_field; +} + +jfieldID GetPlayerInputBodyColorLeftField() { +    return s_player_input_body_color_left_field; +} + +jfieldID GetPlayerInputBodyColorRightField() { +    return s_player_input_body_color_right_field; +} + +jfieldID GetPlayerInputButtonColorLeftField() { +    return s_player_input_button_color_left_field; +} + +jfieldID GetPlayerInputButtonColorRightField() { +    return s_player_input_button_color_right_field; +} + +jfieldID GetPlayerInputProfileNameField() { +    return s_player_input_profile_name_field; +} + +jfieldID GetPlayerInputUseSystemVibratorField() { +    return s_player_input_use_system_vibrator_field; +} + +jclass GetYuzuInputDeviceInterface() { +    return s_yuzu_input_device_interface; +} + +jmethodID GetYuzuDeviceGetName() { +    return s_yuzu_input_device_get_name; +} + +jmethodID GetYuzuDeviceGetGUID() { +    return s_yuzu_input_device_get_guid; +} + +jmethodID GetYuzuDeviceGetPort() { +    return s_yuzu_input_device_get_port; +} + +jmethodID GetYuzuDeviceGetSupportsVibration() { +    return s_yuzu_input_device_get_supports_vibration; +} + +jmethodID GetYuzuDeviceVibrate() { +    return s_yuzu_input_device_vibrate; +} + +jmethodID GetYuzuDeviceGetAxes() { +    return s_yuzu_input_device_get_axes; +} + +jmethodID GetYuzuDeviceHasKeys() { +    return s_yuzu_input_device_has_keys; +} +  #ifdef __cplusplus  extern "C" {  #endif @@ -387,6 +499,55 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {      s_boolean_value_field = env->GetFieldID(boolean_class, "value", "Z");      env->DeleteLocalRef(boolean_class); +    const jclass player_input_class = +        env->FindClass("org/yuzu/yuzu_emu/features/input/model/PlayerInput"); +    s_player_input_class = reinterpret_cast<jclass>(env->NewGlobalRef(player_input_class)); +    s_player_input_constructor = env->GetMethodID( +        player_input_class, "<init>", +        "(Z[Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;ZIJJJJLjava/lang/String;Z)V"); +    s_player_input_connected_field = env->GetFieldID(player_input_class, "connected", "Z"); +    s_player_input_buttons_field = +        env->GetFieldID(player_input_class, "buttons", "[Ljava/lang/String;"); +    s_player_input_analogs_field = +        env->GetFieldID(player_input_class, "analogs", "[Ljava/lang/String;"); +    s_player_input_motions_field = +        env->GetFieldID(player_input_class, "motions", "[Ljava/lang/String;"); +    s_player_input_vibration_enabled_field = +        env->GetFieldID(player_input_class, "vibrationEnabled", "Z"); +    s_player_input_vibration_strength_field = +        env->GetFieldID(player_input_class, "vibrationStrength", "I"); +    s_player_input_body_color_left_field = +        env->GetFieldID(player_input_class, "bodyColorLeft", "J"); +    s_player_input_body_color_right_field = +        env->GetFieldID(player_input_class, "bodyColorRight", "J"); +    s_player_input_button_color_left_field = +        env->GetFieldID(player_input_class, "buttonColorLeft", "J"); +    s_player_input_button_color_right_field = +        env->GetFieldID(player_input_class, "buttonColorRight", "J"); +    s_player_input_profile_name_field = +        env->GetFieldID(player_input_class, "profileName", "Ljava/lang/String;"); +    s_player_input_use_system_vibrator_field = +        env->GetFieldID(player_input_class, "useSystemVibrator", "Z"); +    env->DeleteLocalRef(player_input_class); + +    const jclass yuzu_input_device_interface = +        env->FindClass("org/yuzu/yuzu_emu/features/input/YuzuInputDevice"); +    s_yuzu_input_device_interface = +        reinterpret_cast<jclass>(env->NewGlobalRef(yuzu_input_device_interface)); +    s_yuzu_input_device_get_name = +        env->GetMethodID(yuzu_input_device_interface, "getName", "()Ljava/lang/String;"); +    s_yuzu_input_device_get_guid = +        env->GetMethodID(yuzu_input_device_interface, "getGUID", "()Ljava/lang/String;"); +    s_yuzu_input_device_get_port = env->GetMethodID(yuzu_input_device_interface, "getPort", "()I"); +    s_yuzu_input_device_get_supports_vibration = +        env->GetMethodID(yuzu_input_device_interface, "getSupportsVibration", "()Z"); +    s_yuzu_input_device_vibrate = env->GetMethodID(yuzu_input_device_interface, "vibrate", "(F)V"); +    s_yuzu_input_device_get_axes = +        env->GetMethodID(yuzu_input_device_interface, "getAxes", "()[Ljava/lang/Integer;"); +    s_yuzu_input_device_has_keys = +        env->GetMethodID(yuzu_input_device_interface, "hasKeys", "([I)[Z"); +    env->DeleteLocalRef(yuzu_input_device_interface); +      // Initialize Android Storage      Common::FS::Android::RegisterCallbacks(env, s_native_library_class); @@ -416,6 +577,8 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) {      env->DeleteGlobalRef(s_double_class);      env->DeleteGlobalRef(s_integer_class);      env->DeleteGlobalRef(s_boolean_class); +    env->DeleteGlobalRef(s_player_input_class); +    env->DeleteGlobalRef(s_yuzu_input_device_interface);      // UnInitialize applets      SoftwareKeyboard::CleanupJNI(env); diff --git a/src/common/android/id_cache.h b/src/common/android/id_cache.h index 47802f96c..cd2844dcc 100644 --- a/src/common/android/id_cache.h +++ b/src/common/android/id_cache.h @@ -85,4 +85,28 @@ jclass GetBooleanClass();  jmethodID GetBooleanConstructor();  jfieldID GetBooleanValueField(); +jclass GetPlayerInputClass(); +jmethodID GetPlayerInputConstructor(); +jfieldID GetPlayerInputConnectedField(); +jfieldID GetPlayerInputButtonsField(); +jfieldID GetPlayerInputAnalogsField(); +jfieldID GetPlayerInputMotionsField(); +jfieldID GetPlayerInputVibrationEnabledField(); +jfieldID GetPlayerInputVibrationStrengthField(); +jfieldID GetPlayerInputBodyColorLeftField(); +jfieldID GetPlayerInputBodyColorRightField(); +jfieldID GetPlayerInputButtonColorLeftField(); +jfieldID GetPlayerInputButtonColorRightField(); +jfieldID GetPlayerInputProfileNameField(); +jfieldID GetPlayerInputUseSystemVibratorField(); + +jclass GetYuzuInputDeviceInterface(); +jmethodID GetYuzuDeviceGetName(); +jmethodID GetYuzuDeviceGetGUID(); +jmethodID GetYuzuDeviceGetPort(); +jmethodID GetYuzuDeviceGetSupportsVibration(); +jmethodID GetYuzuDeviceVibrate(); +jmethodID GetYuzuDeviceGetAxes(); +jmethodID GetYuzuDeviceHasKeys(); +  } // namespace Common::Android diff --git a/src/common/settings_input.h b/src/common/settings_input.h index 53a95ef8f..a99bb0892 100644 --- a/src/common/settings_input.h +++ b/src/common/settings_input.h @@ -395,6 +395,10 @@ struct PlayerInput {      u32 button_color_left;      u32 button_color_right;      std::string profile_name; + +    // This is meant to tell the Android frontend whether to use a device's built-in vibration +    // motor or a controller's vibrations. +    bool use_system_vibrator;  };  struct TouchscreenInput { diff --git a/src/core/core.cpp b/src/core/core.cpp index 435ef6793..bd5f11d53 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -242,7 +242,7 @@ struct System::Impl {      void Run() {          std::unique_lock<std::mutex> lk(suspend_guard); -        kernel.SuspendApplication(false); +        kernel.SuspendEmulation(false);          core_timing.SyncPause(false);          is_paused.store(false, std::memory_order_relaxed);      } @@ -251,7 +251,7 @@ struct System::Impl {          std::unique_lock<std::mutex> lk(suspend_guard);          core_timing.SyncPause(true); -        kernel.SuspendApplication(true); +        kernel.SuspendEmulation(true);          is_paused.store(true, std::memory_order_relaxed);      } @@ -261,7 +261,7 @@ struct System::Impl {      std::unique_lock<std::mutex> StallApplication() {          std::unique_lock<std::mutex> lk(suspend_guard); -        kernel.SuspendApplication(true); +        kernel.SuspendEmulation(true);          core_timing.SyncPause(true);          return lk;      } @@ -269,7 +269,7 @@ struct System::Impl {      void UnstallApplication() {          if (!IsPaused()) {              core_timing.SyncPause(false); -            kernel.SuspendApplication(false); +            kernel.SuspendEmulation(false);          }      } @@ -459,7 +459,7 @@ struct System::Impl {          }          Network::CancelPendingSocketOperations(); -        kernel.SuspendApplication(true); +        kernel.SuspendEmulation(true);          if (services) {              services->KillNVNFlinger();          } diff --git a/src/core/file_sys/control_metadata.h b/src/core/file_sys/control_metadata.h index 555b9d8f7..667efbbab 100644 --- a/src/core/file_sys/control_metadata.h +++ b/src/core/file_sys/control_metadata.h @@ -64,8 +64,8 @@ struct RawNACP {      u64_le cache_storage_size;      u64_le cache_storage_journal_size;      u64_le cache_storage_data_and_journal_max_size; -    u64_le cache_storage_max_index; -    INSERT_PADDING_BYTES(0xE70); +    u16_le cache_storage_max_index; +    INSERT_PADDING_BYTES(0xE76);  };  static_assert(sizeof(RawNACP) == 0x4000, "RawNACP has incorrect size."); diff --git a/src/core/hle/kernel/k_thread.h b/src/core/hle/kernel/k_thread.h index f13e232b2..e928cfebc 100644 --- a/src/core/hle/kernel/k_thread.h +++ b/src/core/hle/kernel/k_thread.h @@ -66,6 +66,7 @@ enum class SuspendType : u32 {      Debug = 2,      Backtrace = 3,      Init = 4, +    System = 5,      Count,  }; @@ -84,8 +85,9 @@ enum class ThreadState : u16 {      DebugSuspended = (1 << (2 + SuspendShift)),      BacktraceSuspended = (1 << (3 + SuspendShift)),      InitSuspended = (1 << (4 + SuspendShift)), +    SystemSuspended = (1 << (5 + SuspendShift)), -    SuspendFlagMask = ((1 << 5) - 1) << SuspendShift, +    SuspendFlagMask = ((1 << 6) - 1) << SuspendShift,  };  DECLARE_ENUM_FLAG_OPERATORS(ThreadState); diff --git a/src/core/hle/kernel/kernel.cpp b/src/core/hle/kernel/kernel.cpp index 34b25be66..4f4b02fac 100644 --- a/src/core/hle/kernel/kernel.cpp +++ b/src/core/hle/kernel/kernel.cpp @@ -1204,39 +1204,48 @@ const Kernel::KSharedMemory& KernelCore::GetHidBusSharedMem() const {      return *impl->hidbus_shared_mem;  } -void KernelCore::SuspendApplication(bool suspended) { +void KernelCore::SuspendEmulation(bool suspended) {      const bool should_suspend{exception_exited || suspended}; -    const auto activity = -        should_suspend ? Svc::ProcessActivity::Paused : Svc::ProcessActivity::Runnable; +    auto processes = GetProcessList(); -    // Get the application process. -    KScopedAutoObject<KProcess> process = ApplicationProcess(); -    if (process.IsNull()) { -        return; +    for (auto& process : processes) { +        KScopedLightLock ll{process->GetListLock()}; + +        for (auto& thread : process->GetThreadList()) { +            if (should_suspend) { +                thread.RequestSuspend(SuspendType::System); +            } else { +                thread.Resume(SuspendType::System); +            } +        }      } -    // Set the new activity. -    process->SetActivity(activity); +    if (!should_suspend) { +        return; +    }      // Wait for process execution to stop. -    bool must_wait{should_suspend}; - -    // KernelCore::SuspendApplication must be called from locked context, -    // or we could race another call to SetActivity, interfering with waiting. -    while (must_wait) { +    // KernelCore::SuspendEmulation must be called from locked context, +    // or we could race another call, interfering with waiting. +    const auto TryWait = [&]() {          KScopedSchedulerLock sl{*this}; -        // Assume that all threads have finished running. -        must_wait = false; - -        for (auto i = 0; i < static_cast<s32>(Core::Hardware::NUM_CPU_CORES); ++i) { -            if (Scheduler(i).GetSchedulerCurrentThread()->GetOwnerProcess() == -                process.GetPointerUnsafe()) { -                // A thread has not finished running yet. -                // Continue waiting. -                must_wait = true; +        for (auto& process : processes) { +            for (auto i = 0; i < static_cast<s32>(Core::Hardware::NUM_CPU_CORES); ++i) { +                if (Scheduler(i).GetSchedulerCurrentThread()->GetOwnerProcess() == +                    process.GetPointerUnsafe()) { +                    // A thread has not finished running yet. +                    // Continue waiting. +                    return false; +                }              }          } + +        return true; +    }; + +    while (!TryWait()) { +        // ...      }  } @@ -1260,7 +1269,7 @@ bool KernelCore::IsShuttingDown() const {  void KernelCore::ExceptionalExitApplication() {      exception_exited = true; -    SuspendApplication(true); +    SuspendEmulation(true);  }  void KernelCore::EnterSVCProfile() { diff --git a/src/core/hle/kernel/kernel.h b/src/core/hle/kernel/kernel.h index 8ea5bed1c..57182c0c8 100644 --- a/src/core/hle/kernel/kernel.h +++ b/src/core/hle/kernel/kernel.h @@ -258,8 +258,8 @@ public:      /// Gets the shared memory object for HIDBus services.      const Kernel::KSharedMemory& GetHidBusSharedMem() const; -    /// Suspend/unsuspend application process. -    void SuspendApplication(bool suspend); +    /// Suspend/unsuspend emulated processes. +    void SuspendEmulation(bool suspend);      /// Exceptional exit application process.      void ExceptionalExitApplication(); diff --git a/src/core/hle/service/am/service/application_functions.cpp b/src/core/hle/service/am/service/application_functions.cpp index b788fddd4..63dd12a47 100644 --- a/src/core/hle/service/am/service/application_functions.cpp +++ b/src/core/hle/service/am/service/application_functions.cpp @@ -15,6 +15,7 @@  #include "core/hle/service/cmif_serialization.h"  #include "core/hle/service/filesystem/filesystem.h"  #include "core/hle/service/filesystem/save_data_controller.h" +#include "core/hle/service/glue/glue_manager.h"  #include "core/hle/service/ns/ns.h"  #include "core/hle/service/sm/sm.h" @@ -40,7 +41,7 @@ IApplicationFunctions::IApplicationFunctions(Core::System& system_, std::shared_          {26, D<&IApplicationFunctions::GetSaveDataSize>, "GetSaveDataSize"},          {27, D<&IApplicationFunctions::CreateCacheStorage>, "CreateCacheStorage"},          {28, D<&IApplicationFunctions::GetSaveDataSizeMax>, "GetSaveDataSizeMax"}, -        {29, nullptr, "GetCacheStorageMax"}, +        {29, D<&IApplicationFunctions::GetCacheStorageMax>, "GetCacheStorageMax"},          {30, D<&IApplicationFunctions::BeginBlockingHomeButtonShortAndLongPressed>, "BeginBlockingHomeButtonShortAndLongPressed"},          {31, D<&IApplicationFunctions::EndBlockingHomeButtonShortAndLongPressed>, "EndBlockingHomeButtonShortAndLongPressed"},          {32, D<&IApplicationFunctions::BeginBlockingHomeButton>, "BeginBlockingHomeButton"}, @@ -267,6 +268,22 @@ Result IApplicationFunctions::GetSaveDataSizeMax(Out<u64> out_max_normal_size,      R_SUCCEED();  } +Result IApplicationFunctions::GetCacheStorageMax(Out<u32> out_cache_storage_index_max, +                                                 Out<u64> out_max_journal_size) { +    LOG_DEBUG(Service_AM, "called"); + +    std::vector<u8> nacp; +    R_TRY(system.GetARPManager().GetControlProperty(&nacp, m_applet->program_id)); + +    auto raw_nacp = std::make_unique<FileSys::RawNACP>(); +    std::memcpy(raw_nacp.get(), nacp.data(), std::min(sizeof(*raw_nacp), nacp.size())); + +    *out_cache_storage_index_max = static_cast<u32>(raw_nacp->cache_storage_max_index); +    *out_max_journal_size = static_cast<u64>(raw_nacp->cache_storage_data_and_journal_max_size); + +    R_SUCCEED(); +} +  Result IApplicationFunctions::BeginBlockingHomeButtonShortAndLongPressed(s64 unused) {      LOG_WARNING(Service_AM, "(STUBBED) called"); diff --git a/src/core/hle/service/am/service/application_functions.h b/src/core/hle/service/am/service/application_functions.h index 3548202f8..10025a152 100644 --- a/src/core/hle/service/am/service/application_functions.h +++ b/src/core/hle/service/am/service/application_functions.h @@ -40,6 +40,7 @@ private:      Result CreateCacheStorage(Out<u32> out_target_media, Out<u64> out_required_size, u16 index,                                u64 normal_size, u64 journal_size);      Result GetSaveDataSizeMax(Out<u64> out_max_normal_size, Out<u64> out_max_journal_size); +    Result GetCacheStorageMax(Out<u32> out_cache_storage_index_max, Out<u64> out_max_journal_size);      Result BeginBlockingHomeButtonShortAndLongPressed(s64 unused);      Result EndBlockingHomeButtonShortAndLongPressed();      Result BeginBlockingHomeButton(s64 timeout_ns); diff --git a/src/core/hle/service/filesystem/fsp/fsp_srv.cpp b/src/core/hle/service/filesystem/fsp/fsp_srv.cpp index 63c2d3a58..2d49f30c8 100644 --- a/src/core/hle/service/filesystem/fsp/fsp_srv.cpp +++ b/src/core/hle/service/filesystem/fsp/fsp_srv.cpp @@ -336,7 +336,7 @@ FSP_SRV::FSP_SRV(Core::System& system_)          {1012, nullptr, "GetFsStackUsage"},          {1013, nullptr, "UnsetSaveDataRootPath"},          {1014, nullptr, "OutputMultiProgramTagAccessLog"}, -        {1016, nullptr, "FlushAccessLogOnSdCard"}, +        {1016, &FSP_SRV::FlushAccessLogOnSdCard, "FlushAccessLogOnSdCard"},          {1017, nullptr, "OutputApplicationInfoAccessLog"},          {1018, nullptr, "SetDebugOption"},          {1019, nullptr, "UnsetDebugOption"}, @@ -706,6 +706,13 @@ void FSP_SRV::GetProgramIndexForAccessLog(HLERequestContext& ctx) {      rb.Push(access_log_program_index);  } +void FSP_SRV::FlushAccessLogOnSdCard(HLERequestContext& ctx) { +    LOG_DEBUG(Service_FS, "(STUBBED) called"); + +    IPC::ResponseBuilder rb{ctx, 2}; +    rb.Push(ResultSuccess); +} +  void FSP_SRV::GetCacheStorageSize(HLERequestContext& ctx) {      IPC::RequestParser rp{ctx};      const auto index{rp.Pop<s32>()}; diff --git a/src/core/hle/service/filesystem/fsp/fsp_srv.h b/src/core/hle/service/filesystem/fsp/fsp_srv.h index 26980af99..59406e6f9 100644 --- a/src/core/hle/service/filesystem/fsp/fsp_srv.h +++ b/src/core/hle/service/filesystem/fsp/fsp_srv.h @@ -58,6 +58,7 @@ private:      void SetGlobalAccessLogMode(HLERequestContext& ctx);      void GetGlobalAccessLogMode(HLERequestContext& ctx);      void OutputAccessLogToSdCard(HLERequestContext& ctx); +    void FlushAccessLogOnSdCard(HLERequestContext& ctx);      void GetProgramIndexForAccessLog(HLERequestContext& ctx);      void OpenMultiCommitManager(HLERequestContext& ctx);      void GetCacheStorageSize(HLERequestContext& ctx); diff --git a/src/core/hle/service/glue/time/manager.cpp b/src/core/hle/service/glue/time/manager.cpp index cad755fa7..059ac3fc9 100644 --- a/src/core/hle/service/glue/time/manager.cpp +++ b/src/core/hle/service/glue/time/manager.cpp @@ -186,6 +186,10 @@ TimeManager::TimeManager(Core::System& system)      }  } +TimeManager::~TimeManager() { +    ResetTimeZoneBinary(); +} +  Result TimeManager::SetupStandardSteadyClockCore() {      Common::UUID external_clock_source_id{};      auto res = m_set_sys->GetExternalSteadyClockSourceId(&external_clock_source_id); diff --git a/src/core/hle/service/glue/time/manager.h b/src/core/hle/service/glue/time/manager.h index 1de93f8f9..bb4b65049 100644 --- a/src/core/hle/service/glue/time/manager.h +++ b/src/core/hle/service/glue/time/manager.h @@ -26,6 +26,7 @@ namespace Service::Glue::Time {  class TimeManager {  public:      explicit TimeManager(Core::System& system); +    ~TimeManager();      std::shared_ptr<Service::Set::ISystemSettingsServer> m_set_sys; diff --git a/src/core/memory/cheat_engine.cpp b/src/core/memory/cheat_engine.cpp index b84b57d92..d8921e565 100644 --- a/src/core/memory/cheat_engine.cpp +++ b/src/core/memory/cheat_engine.cpp @@ -117,9 +117,9 @@ bool StandardVmCallbacks::IsAddressInRange(VAddr in) const {          (in < metadata.heap_extents.base ||           in >= metadata.heap_extents.base + metadata.heap_extents.size) &&          (in < metadata.alias_extents.base || -         in >= metadata.heap_extents.base + metadata.alias_extents.size) && +         in >= metadata.alias_extents.base + metadata.alias_extents.size) &&          (in < metadata.aslr_extents.base || -         in >= metadata.heap_extents.base + metadata.aslr_extents.size)) { +         in >= metadata.aslr_extents.base + metadata.aslr_extents.size)) {          LOG_DEBUG(CheatEngine,                    "Cheat attempting to access memory at invalid address={:016X}, if this "                    "persists, " diff --git a/src/frontend_common/config.cpp b/src/frontend_common/config.cpp index 2bebfeef9..95f8c8c36 100644 --- 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) {          if (profile_name.empty()) {              // Use the global input config              player = Settings::values.players.GetValue(true)[player_index]; +            player.profile_name = "";              return;          }          player.profile_name = profile_name; diff --git a/src/frontend_common/content_manager.h b/src/frontend_common/content_manager.h index f3efe3465..c4e97a47b 100644 --- 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&   * \param callback Callback to report the progress of the installation. The first size_t   * parameter is the total size of the installed contents and the second is the current progress. If   * you return true to the callback, it will cancel the installation as soon as possible. + * \param firmware_only Set to true to only scan system nand NCAs (firmware), post firmware install.   * \return A list of entries that failed to install. Returns an empty vector if successful.   */  inline std::vector<std::string> VerifyInstalledContents(      Core::System& system, FileSys::ManualContentProvider& provider, -    const std::function<bool(size_t, size_t)>& callback) { +    const std::function<bool(size_t, size_t)>& callback, bool firmware_only = false) {      // Get content registries.      auto bis_contents = system.GetFileSystemController().GetSystemNANDContents();      auto user_contents = system.GetFileSystemController().GetUserNANDContents(); @@ -264,7 +265,7 @@ inline std::vector<std::string> VerifyInstalledContents(      if (bis_contents) {          content_providers.push_back(bis_contents);      } -    if (user_contents) { +    if (user_contents && !firmware_only) {          content_providers.push_back(user_contents);      } diff --git a/src/hid_core/frontend/emulated_controller.cpp b/src/hid_core/frontend/emulated_controller.cpp index 819460eb5..3fa06d188 100644 --- a/src/hid_core/frontend/emulated_controller.cpp +++ b/src/hid_core/frontend/emulated_controller.cpp @@ -176,16 +176,19 @@ void EmulatedController::LoadDevices() {          camera_params[1] = Common::ParamPackage{"engine:camera,camera:1"};          ring_params[1] = Common::ParamPackage{"engine:joycon,axis_x:100,axis_y:101"};          nfc_params[0] = Common::ParamPackage{"engine:virtual_amiibo,nfc:1"}; +        android_params = Common::ParamPackage{"engine:android,port:100"};      }      output_params[LeftIndex] = left_joycon;      output_params[RightIndex] = right_joycon;      output_params[2] = camera_params[1];      output_params[3] = nfc_params[0]; +    output_params[4] = android_params;      output_params[LeftIndex].Set("output", true);      output_params[RightIndex].Set("output", true);      output_params[2].Set("output", true);      output_params[3].Set("output", true); +    output_params[4].Set("output", true);      LoadTASParams();      LoadVirtualGamepadParams(); @@ -578,6 +581,9 @@ void EmulatedController::DisableConfiguration() {      // Get Joycon colors before turning on the controller      for (const auto& color_device : color_devices) { +        if (color_device == nullptr) { +            continue; +        }          color_device->ForceUpdate();      } @@ -1277,6 +1283,10 @@ bool EmulatedController::SetVibration(DeviceIndex device_index, const VibrationV          .high_frequency = vibration.high_frequency,          .type = type,      }; + +    // Send vibrations to Android's input overlay +    output_devices[4]->SetVibration(status); +      return output_devices[index]->SetVibration(status) == Common::Input::DriverResult::Success;  } diff --git a/src/hid_core/frontend/emulated_controller.h b/src/hid_core/frontend/emulated_controller.h index 701b38300..ab3c6fcd3 100644 --- a/src/hid_core/frontend/emulated_controller.h +++ b/src/hid_core/frontend/emulated_controller.h @@ -21,7 +21,7 @@  namespace Core::HID {  const std::size_t max_emulated_controllers = 2; -const std::size_t output_devices_size = 4; +const std::size_t output_devices_size = 5;  struct ControllerMotionInfo {      Common::Input::MotionStatus raw_status{};      MotionInput emulated{}; @@ -597,6 +597,7 @@ private:      CameraParams camera_params;      RingAnalogParams ring_params;      NfcParams nfc_params; +    Common::ParamPackage android_params;      OutputParams output_params;      ButtonDevices button_devices; diff --git a/src/input_common/CMakeLists.txt b/src/input_common/CMakeLists.txt index d0a71a15b..d455323e0 100644 --- a/src/input_common/CMakeLists.txt +++ b/src/input_common/CMakeLists.txt @@ -2,8 +2,6 @@  # SPDX-License-Identifier: GPL-2.0-or-later  add_library(input_common STATIC -    drivers/android.cpp -    drivers/android.h      drivers/camera.cpp      drivers/camera.h      drivers/keyboard.cpp @@ -94,3 +92,11 @@ target_link_libraries(input_common PUBLIC hid_core PRIVATE common Boost::headers  if (YUZU_USE_PRECOMPILED_HEADERS)      target_precompile_headers(input_common PRIVATE precompiled_headers.h)  endif() + +if (ANDROID) +    target_sources(input_common PRIVATE +        drivers/android.cpp +        drivers/android.h +    ) +    target_link_libraries(input_common PRIVATE android) +endif() diff --git a/src/input_common/drivers/android.cpp b/src/input_common/drivers/android.cpp index b6a03fdc0..e859cc538 100644 --- a/src/input_common/drivers/android.cpp +++ b/src/input_common/drivers/android.cpp @@ -1,30 +1,47 @@  // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project  // SPDX-License-Identifier: GPL-3.0-or-later +#include <set> +#include <common/settings_input.h> +#include <jni.h> +#include "common/android/android_common.h" +#include "common/android/id_cache.h"  #include "input_common/drivers/android.h"  namespace InputCommon {  Android::Android(std::string input_engine_) : InputEngine(std::move(input_engine_)) {} -void Android::RegisterController(std::size_t controller_number) { -    PreSetController(GetIdentifier(controller_number)); +void Android::RegisterController(jobject j_input_device) { +    auto env = Common::Android::GetEnvForThread(); +    const std::string guid = Common::Android::GetJString( +        env, static_cast<jstring>( +                 env->CallObjectMethod(j_input_device, Common::Android::GetYuzuDeviceGetGUID()))); +    const s32 port = env->CallIntMethod(j_input_device, Common::Android::GetYuzuDeviceGetPort()); +    const auto identifier = GetIdentifier(guid, static_cast<size_t>(port)); +    PreSetController(identifier); + +    if (input_devices.find(identifier) != input_devices.end()) { +        env->DeleteGlobalRef(input_devices[identifier]); +    } +    auto new_device = env->NewGlobalRef(j_input_device); +    input_devices[identifier] = new_device;  } -void Android::SetButtonState(std::size_t controller_number, int button_id, bool value) { -    const auto identifier = GetIdentifier(controller_number); +void Android::SetButtonState(std::string guid, size_t port, int button_id, bool value) { +    const auto identifier = GetIdentifier(guid, port);      SetButton(identifier, button_id, value);  } -void Android::SetAxisState(std::size_t controller_number, int axis_id, float value) { -    const auto identifier = GetIdentifier(controller_number); +void Android::SetAxisPosition(std::string guid, size_t port, int axis_id, float value) { +    const auto identifier = GetIdentifier(guid, port);      SetAxis(identifier, axis_id, value);  } -void Android::SetMotionState(std::size_t controller_number, u64 delta_timestamp, float gyro_x, +void Android::SetMotionState(std::string guid, size_t port, u64 delta_timestamp, float gyro_x,                               float gyro_y, float gyro_z, float accel_x, float accel_y,                               float accel_z) { -    const auto identifier = GetIdentifier(controller_number); +    const auto identifier = GetIdentifier(guid, port);      const BasicMotion motion_data{          .gyro_x = gyro_x,          .gyro_y = gyro_y, @@ -37,10 +54,295 @@ void Android::SetMotionState(std::size_t controller_number, u64 delta_timestamp,      SetMotion(identifier, 0, motion_data);  } -PadIdentifier Android::GetIdentifier(std::size_t controller_number) const { +Common::Input::DriverResult Android::SetVibration( +    [[maybe_unused]] const PadIdentifier& identifier, +    [[maybe_unused]] const Common::Input::VibrationStatus& vibration) { +    auto device = input_devices.find(identifier); +    if (device != input_devices.end()) { +        Common::Android::RunJNIOnFiber<void>([&](JNIEnv* env) { +            float average_intensity = +                static_cast<float>((vibration.high_amplitude + vibration.low_amplitude) / 2.0); +            env->CallVoidMethod(device->second, Common::Android::GetYuzuDeviceVibrate(), +                                average_intensity); +        }); +        return Common::Input::DriverResult::Success; +    } +    return Common::Input::DriverResult::NotSupported; +} + +bool Android::IsVibrationEnabled([[maybe_unused]] const PadIdentifier& identifier) { +    auto device = input_devices.find(identifier); +    if (device != input_devices.end()) { +        return Common::Android::RunJNIOnFiber<bool>([&](JNIEnv* env) { +            return static_cast<bool>(env->CallBooleanMethod( +                device->second, Common::Android::GetYuzuDeviceGetSupportsVibration())); +        }); +    } +    return false; +} + +std::vector<Common::ParamPackage> Android::GetInputDevices() const { +    std::vector<Common::ParamPackage> devices; +    auto env = Common::Android::GetEnvForThread(); +    for (const auto& [key, value] : input_devices) { +        auto name_object = static_cast<jstring>( +            env->CallObjectMethod(value, Common::Android::GetYuzuDeviceGetName())); +        const std::string name = +            fmt::format("{} {}", Common::Android::GetJString(env, name_object), key.port); +        devices.emplace_back(Common::ParamPackage{ +            {"engine", GetEngineName()}, +            {"display", std::move(name)}, +            {"guid", key.guid.RawString()}, +            {"port", std::to_string(key.port)}, +        }); +    } +    return devices; +} + +std::set<s32> Android::GetDeviceAxes(JNIEnv* env, jobject& j_device) const { +    auto j_axes = static_cast<jobjectArray>( +        env->CallObjectMethod(j_device, Common::Android::GetYuzuDeviceGetAxes())); +    std::set<s32> axes; +    for (int i = 0; i < env->GetArrayLength(j_axes); ++i) { +        jobject axis = env->GetObjectArrayElement(j_axes, i); +        axes.insert(env->GetIntField(axis, Common::Android::GetIntegerValueField())); +    } +    return axes; +} + +Common::ParamPackage Android::BuildParamPackageForAnalog(PadIdentifier identifier, int axis_x, +                                                         int axis_y) const { +    Common::ParamPackage params; +    params.Set("engine", GetEngineName()); +    params.Set("port", static_cast<int>(identifier.port)); +    params.Set("guid", identifier.guid.RawString()); +    params.Set("axis_x", axis_x); +    params.Set("axis_y", axis_y); +    params.Set("offset_x", 0); +    params.Set("offset_y", 0); +    params.Set("invert_x", "+"); + +    // Invert Y-Axis by default +    params.Set("invert_y", "-"); +    return params; +} + +Common::ParamPackage Android::BuildAnalogParamPackageForButton(PadIdentifier identifier, s32 axis, +                                                               bool invert) const { +    Common::ParamPackage params{}; +    params.Set("engine", GetEngineName()); +    params.Set("port", static_cast<int>(identifier.port)); +    params.Set("guid", identifier.guid.RawString()); +    params.Set("axis", axis); +    params.Set("threshold", "0.5"); +    params.Set("invert", invert ? "-" : "+"); +    return params; +} + +Common::ParamPackage Android::BuildButtonParamPackageForButton(PadIdentifier identifier, +                                                               s32 button) const { +    Common::ParamPackage params{}; +    params.Set("engine", GetEngineName()); +    params.Set("port", static_cast<int>(identifier.port)); +    params.Set("guid", identifier.guid.RawString()); +    params.Set("button", button); +    return params; +} + +bool Android::MatchVID(Common::UUID device, const std::vector<std::string>& vids) const { +    for (size_t i = 0; i < vids.size(); ++i) { +        auto fucker = device.RawString(); +        if (fucker.find(vids[i]) != std::string::npos) { +            return true; +        } +    } +    return false; +} + +AnalogMapping Android::GetAnalogMappingForDevice(const Common::ParamPackage& params) { +    if (!params.Has("guid") || !params.Has("port")) { +        return {}; +    } + +    auto identifier = +        GetIdentifier(params.Get("guid", ""), static_cast<size_t>(params.Get("port", 0))); +    auto& j_device = input_devices[identifier]; +    if (j_device == nullptr) { +        return {}; +    } + +    auto env = Common::Android::GetEnvForThread(); +    std::set<s32> axes = GetDeviceAxes(env, j_device); +    if (axes.size() == 0) { +        return {}; +    } + +    AnalogMapping mapping = {}; +    if (axes.find(AXIS_X) != axes.end() && axes.find(AXIS_Y) != axes.end()) { +        mapping.insert_or_assign(Settings::NativeAnalog::LStick, +                                 BuildParamPackageForAnalog(identifier, AXIS_X, AXIS_Y)); +    } + +    if (axes.find(AXIS_RX) != axes.end() && axes.find(AXIS_RY) != axes.end()) { +        mapping.insert_or_assign(Settings::NativeAnalog::RStick, +                                 BuildParamPackageForAnalog(identifier, AXIS_RX, AXIS_RY)); +    } else if (axes.find(AXIS_Z) != axes.end() && axes.find(AXIS_RZ) != axes.end()) { +        mapping.insert_or_assign(Settings::NativeAnalog::RStick, +                                 BuildParamPackageForAnalog(identifier, AXIS_Z, AXIS_RZ)); +    } +    return mapping; +} + +ButtonMapping Android::GetButtonMappingForDevice(const Common::ParamPackage& params) { +    if (!params.Has("guid") || !params.Has("port")) { +        return {}; +    } + +    auto identifier = +        GetIdentifier(params.Get("guid", ""), static_cast<size_t>(params.Get("port", 0))); +    auto& j_device = input_devices[identifier]; +    if (j_device == nullptr) { +        return {}; +    } + +    auto env = Common::Android::GetEnvForThread(); +    jintArray j_keys = env->NewIntArray(static_cast<int>(keycode_ids.size())); +    env->SetIntArrayRegion(j_keys, 0, static_cast<int>(keycode_ids.size()), keycode_ids.data()); +    auto j_has_keys_object = static_cast<jbooleanArray>( +        env->CallObjectMethod(j_device, Common::Android::GetYuzuDeviceHasKeys(), j_keys)); +    jboolean isCopy = false; +    jboolean* j_has_keys = env->GetBooleanArrayElements(j_has_keys_object, &isCopy); + +    std::set<s32> available_keys; +    for (size_t i = 0; i < keycode_ids.size(); ++i) { +        if (j_has_keys[i]) { +            available_keys.insert(keycode_ids[i]); +        } +    } + +    // Some devices use axes instead of buttons for certain controls so we need all the axes here +    std::set<s32> axes = GetDeviceAxes(env, j_device); + +    ButtonMapping mapping = {}; +    if (axes.find(AXIS_HAT_X) != axes.end() && axes.find(AXIS_HAT_Y) != axes.end()) { +        mapping.insert_or_assign(Settings::NativeButton::DUp, +                                 BuildAnalogParamPackageForButton(identifier, AXIS_HAT_Y, true)); +        mapping.insert_or_assign(Settings::NativeButton::DDown, +                                 BuildAnalogParamPackageForButton(identifier, AXIS_HAT_Y, false)); +        mapping.insert_or_assign(Settings::NativeButton::DLeft, +                                 BuildAnalogParamPackageForButton(identifier, AXIS_HAT_X, true)); +        mapping.insert_or_assign(Settings::NativeButton::DRight, +                                 BuildAnalogParamPackageForButton(identifier, AXIS_HAT_X, false)); +    } else if (available_keys.find(KEYCODE_DPAD_UP) != available_keys.end() && +               available_keys.find(KEYCODE_DPAD_DOWN) != available_keys.end() && +               available_keys.find(KEYCODE_DPAD_LEFT) != available_keys.end() && +               available_keys.find(KEYCODE_DPAD_RIGHT) != available_keys.end()) { +        mapping.insert_or_assign(Settings::NativeButton::DUp, +                                 BuildButtonParamPackageForButton(identifier, KEYCODE_DPAD_UP)); +        mapping.insert_or_assign(Settings::NativeButton::DDown, +                                 BuildButtonParamPackageForButton(identifier, KEYCODE_DPAD_DOWN)); +        mapping.insert_or_assign(Settings::NativeButton::DLeft, +                                 BuildButtonParamPackageForButton(identifier, KEYCODE_DPAD_LEFT)); +        mapping.insert_or_assign(Settings::NativeButton::DRight, +                                 BuildButtonParamPackageForButton(identifier, KEYCODE_DPAD_RIGHT)); +    } + +    if (axes.find(AXIS_LTRIGGER) != axes.end()) { +        mapping.insert_or_assign(Settings::NativeButton::ZL, BuildAnalogParamPackageForButton( +                                                                 identifier, AXIS_LTRIGGER, false)); +    } else if (available_keys.find(KEYCODE_BUTTON_L2) != available_keys.end()) { +        mapping.insert_or_assign(Settings::NativeButton::ZL, +                                 BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_L2)); +    } + +    if (axes.find(AXIS_RTRIGGER) != axes.end()) { +        mapping.insert_or_assign(Settings::NativeButton::ZR, BuildAnalogParamPackageForButton( +                                                                 identifier, AXIS_RTRIGGER, false)); +    } else if (available_keys.find(KEYCODE_BUTTON_R2) != available_keys.end()) { +        mapping.insert_or_assign(Settings::NativeButton::ZR, +                                 BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_R2)); +    } + +    if (available_keys.find(KEYCODE_BUTTON_A) != available_keys.end()) { +        if (MatchVID(identifier.guid, flipped_ab_vids)) { +            mapping.insert_or_assign(Settings::NativeButton::B, BuildButtonParamPackageForButton( +                                                                    identifier, KEYCODE_BUTTON_A)); +        } else { +            mapping.insert_or_assign(Settings::NativeButton::A, BuildButtonParamPackageForButton( +                                                                    identifier, KEYCODE_BUTTON_A)); +        } +    } +    if (available_keys.find(KEYCODE_BUTTON_B) != available_keys.end()) { +        if (MatchVID(identifier.guid, flipped_ab_vids)) { +            mapping.insert_or_assign(Settings::NativeButton::A, BuildButtonParamPackageForButton( +                                                                    identifier, KEYCODE_BUTTON_B)); +        } else { +            mapping.insert_or_assign(Settings::NativeButton::B, BuildButtonParamPackageForButton( +                                                                    identifier, KEYCODE_BUTTON_B)); +        } +    } +    if (available_keys.find(KEYCODE_BUTTON_X) != available_keys.end()) { +        if (MatchVID(identifier.guid, flipped_xy_vids)) { +            mapping.insert_or_assign(Settings::NativeButton::Y, BuildButtonParamPackageForButton( +                                                                    identifier, KEYCODE_BUTTON_X)); +        } else { +            mapping.insert_or_assign(Settings::NativeButton::X, BuildButtonParamPackageForButton( +                                                                    identifier, KEYCODE_BUTTON_X)); +        } +    } +    if (available_keys.find(KEYCODE_BUTTON_Y) != available_keys.end()) { +        if (MatchVID(identifier.guid, flipped_xy_vids)) { +            mapping.insert_or_assign(Settings::NativeButton::X, BuildButtonParamPackageForButton( +                                                                    identifier, KEYCODE_BUTTON_Y)); +        } else { +            mapping.insert_or_assign(Settings::NativeButton::Y, BuildButtonParamPackageForButton( +                                                                    identifier, KEYCODE_BUTTON_Y)); +        } +    } + +    if (available_keys.find(KEYCODE_BUTTON_L1) != available_keys.end()) { +        mapping.insert_or_assign(Settings::NativeButton::L, +                                 BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_L1)); +    } +    if (available_keys.find(KEYCODE_BUTTON_R1) != available_keys.end()) { +        mapping.insert_or_assign(Settings::NativeButton::R, +                                 BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_R1)); +    } + +    if (available_keys.find(KEYCODE_BUTTON_THUMBL) != available_keys.end()) { +        mapping.insert_or_assign( +            Settings::NativeButton::LStick, +            BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_THUMBL)); +    } +    if (available_keys.find(KEYCODE_BUTTON_THUMBR) != available_keys.end()) { +        mapping.insert_or_assign( +            Settings::NativeButton::RStick, +            BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_THUMBR)); +    } + +    if (available_keys.find(KEYCODE_BUTTON_START) != available_keys.end()) { +        mapping.insert_or_assign( +            Settings::NativeButton::Plus, +            BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_START)); +    } +    if (available_keys.find(KEYCODE_BUTTON_SELECT) != available_keys.end()) { +        mapping.insert_or_assign( +            Settings::NativeButton::Minus, +            BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_SELECT)); +    } + +    return mapping; +} + +Common::Input::ButtonNames Android::GetUIName( +    [[maybe_unused]] const Common::ParamPackage& params) const { +    return Common::Input::ButtonNames::Value; +} + +PadIdentifier Android::GetIdentifier(const std::string& guid, size_t port) const {      return { -        .guid = Common::UUID{}, -        .port = controller_number, +        .guid = Common::UUID{guid}, +        .port = port,          .pad = 0,      };  } diff --git a/src/input_common/drivers/android.h b/src/input_common/drivers/android.h index 3f01817f6..ac60e3598 100644 --- a/src/input_common/drivers/android.h +++ b/src/input_common/drivers/android.h @@ -3,6 +3,8 @@  #pragma once +#include <set> +#include <jni.h>  #include "input_common/input_engine.h"  namespace InputCommon { @@ -15,40 +17,121 @@ public:      explicit Android(std::string input_engine_);      /** -     * Registers controller number to accept new inputs -     * @param controller_number the controller number that will take this action +     * Registers controller number to accept new inputs. +     * @param j_input_device YuzuInputDevice object from the Android frontend to register.       */ -    void RegisterController(std::size_t controller_number); +    void RegisterController(jobject j_input_device);      /** -     * Sets the status of all buttons bound with the key to pressed -     * @param controller_number the controller number that will take this action -     * @param button_id the id of the button -     * @param value indicates if the button is pressed or not +     * Sets the status of a button on a specific controller. +     * @param guid 32 character hexadecimal string consisting of the controller's PID+VID. +     * @param port Port determined by controller connection order. +     * @param button_id The Android Keycode corresponding to this event. +     * @param value Whether the button is pressed or not.       */ -    void SetButtonState(std::size_t controller_number, int button_id, bool value); +    void SetButtonState(std::string guid, size_t port, int button_id, bool value);      /** -     * Sets the status of a analog input to a specific player index -     * @param controller_number the controller number that will take this action -     * @param axis_id the id of the axis to move -     * @param value the analog position of the axis +     * Sets the status of an axis on a specific controller. +     * @param guid 32 character hexadecimal string consisting of the controller's PID+VID. +     * @param port Port determined by controller connection order. +     * @param axis_id The Android axis ID corresponding to this event. +     * @param value Value along the given axis.       */ -    void SetAxisState(std::size_t controller_number, int axis_id, float value); +    void SetAxisPosition(std::string guid, size_t port, int axis_id, float value);      /** -     * Sets the status of the motion sensor to a specific player index -     * @param controller_number the controller number that will take this action -     * @param delta_timestamp time passed since last reading -     * @param gyro_x,gyro_y,gyro_z the gyro sensor readings -     * @param accel_x,accel_y,accel_z the accelerometer reading +     * Sets the status of the motion sensor on a specific controller +     * @param guid 32 character hexadecimal string consisting of the controller's PID+VID. +     * @param port Port determined by controller connection order. +     * @param delta_timestamp Time passed since the last read. +     * @param gyro_x,gyro_y,gyro_z Gyro sensor readings. +     * @param accel_x,accel_y,accel_z Accelerometer sensor readings.       */ -    void SetMotionState(std::size_t controller_number, u64 delta_timestamp, float gyro_x, +    void SetMotionState(std::string guid, size_t port, u64 delta_timestamp, float gyro_x,                          float gyro_y, float gyro_z, float accel_x, float accel_y, float accel_z); +    Common::Input::DriverResult SetVibration( +        const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) override; + +    bool IsVibrationEnabled(const PadIdentifier& identifier) override; + +    std::vector<Common::ParamPackage> GetInputDevices() const override; + +    /** +     * Gets the axes reported by the YuzuInputDevice. +     * @param env JNI environment pointer. +     * @param j_device YuzuInputDevice from the Android frontend. +     * @return Set of the axes reported by the underlying Android InputDevice +     */ +    std::set<s32> GetDeviceAxes(JNIEnv* env, jobject& j_device) const; + +    Common::ParamPackage BuildParamPackageForAnalog(PadIdentifier identifier, int axis_x, +                                                    int axis_y) const; + +    Common::ParamPackage BuildAnalogParamPackageForButton(PadIdentifier identifier, s32 axis, +                                                          bool invert) const; + +    Common::ParamPackage BuildButtonParamPackageForButton(PadIdentifier identifier, +                                                          s32 button) const; + +    bool MatchVID(Common::UUID device, const std::vector<std::string>& vids) const; + +    AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override; + +    ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) override; + +    Common::Input::ButtonNames GetUIName(const Common::ParamPackage& params) const override; +  private: +    std::unordered_map<PadIdentifier, jobject> input_devices; +      /// Returns the correct identifier corresponding to the player index -    PadIdentifier GetIdentifier(std::size_t controller_number) const; +    PadIdentifier GetIdentifier(const std::string& guid, size_t port) const; + +    static constexpr s32 AXIS_X = 0; +    static constexpr s32 AXIS_Y = 1; +    static constexpr s32 AXIS_Z = 11; +    static constexpr s32 AXIS_RX = 12; +    static constexpr s32 AXIS_RY = 13; +    static constexpr s32 AXIS_RZ = 14; +    static constexpr s32 AXIS_HAT_X = 15; +    static constexpr s32 AXIS_HAT_Y = 16; +    static constexpr s32 AXIS_LTRIGGER = 17; +    static constexpr s32 AXIS_RTRIGGER = 18; + +    static constexpr s32 KEYCODE_DPAD_UP = 19; +    static constexpr s32 KEYCODE_DPAD_DOWN = 20; +    static constexpr s32 KEYCODE_DPAD_LEFT = 21; +    static constexpr s32 KEYCODE_DPAD_RIGHT = 22; +    static constexpr s32 KEYCODE_BUTTON_A = 96; +    static constexpr s32 KEYCODE_BUTTON_B = 97; +    static constexpr s32 KEYCODE_BUTTON_X = 99; +    static constexpr s32 KEYCODE_BUTTON_Y = 100; +    static constexpr s32 KEYCODE_BUTTON_L1 = 102; +    static constexpr s32 KEYCODE_BUTTON_R1 = 103; +    static constexpr s32 KEYCODE_BUTTON_L2 = 104; +    static constexpr s32 KEYCODE_BUTTON_R2 = 105; +    static constexpr s32 KEYCODE_BUTTON_THUMBL = 106; +    static constexpr s32 KEYCODE_BUTTON_THUMBR = 107; +    static constexpr s32 KEYCODE_BUTTON_START = 108; +    static constexpr s32 KEYCODE_BUTTON_SELECT = 109; +    const std::vector<s32> keycode_ids{ +        KEYCODE_DPAD_UP,       KEYCODE_DPAD_DOWN,     KEYCODE_DPAD_LEFT,    KEYCODE_DPAD_RIGHT, +        KEYCODE_BUTTON_A,      KEYCODE_BUTTON_B,      KEYCODE_BUTTON_X,     KEYCODE_BUTTON_Y, +        KEYCODE_BUTTON_L1,     KEYCODE_BUTTON_R1,     KEYCODE_BUTTON_L2,    KEYCODE_BUTTON_R2, +        KEYCODE_BUTTON_THUMBL, KEYCODE_BUTTON_THUMBR, KEYCODE_BUTTON_START, KEYCODE_BUTTON_SELECT, +    }; + +    const std::string sony_vid{"054c"}; +    const std::string nintendo_vid{"057e"}; +    const std::string razer_vid{"1532"}; +    const std::string redmagic_vid{"3537"}; +    const std::string backbone_labs_vid{"358a"}; +    const std::vector<std::string> flipped_ab_vids{sony_vid, nintendo_vid, razer_vid, redmagic_vid, +                                                   backbone_labs_vid}; +    const std::vector<std::string> flipped_xy_vids{sony_vid, razer_vid, redmagic_vid, +                                                   backbone_labs_vid};  };  } // namespace InputCommon diff --git a/src/input_common/main.cpp b/src/input_common/main.cpp index f8749ebbf..62a7ae40f 100644 --- a/src/input_common/main.cpp +++ b/src/input_common/main.cpp @@ -4,7 +4,6 @@  #include <memory>  #include "common/input.h"  #include "common/param_package.h" -#include "input_common/drivers/android.h"  #include "input_common/drivers/camera.h"  #include "input_common/drivers/keyboard.h"  #include "input_common/drivers/mouse.h" @@ -28,6 +27,10 @@  #include "input_common/drivers/sdl_driver.h"  #endif +#ifdef ANDROID +#include "input_common/drivers/android.h" +#endif +  namespace InputCommon {  /// Dummy engine to get periodic updates @@ -79,7 +82,9 @@ struct InputSubsystem::Impl {          RegisterEngine("cemuhookudp", udp_client);          RegisterEngine("tas", tas_input);          RegisterEngine("camera", camera); +#ifdef ANDROID          RegisterEngine("android", android); +#endif          RegisterEngine("virtual_amiibo", virtual_amiibo);          RegisterEngine("virtual_gamepad", virtual_gamepad);  #ifdef HAVE_SDL2 @@ -111,7 +116,9 @@ struct InputSubsystem::Impl {          UnregisterEngine(udp_client);          UnregisterEngine(tas_input);          UnregisterEngine(camera); +#ifdef ANDROID          UnregisterEngine(android); +#endif          UnregisterEngine(virtual_amiibo);          UnregisterEngine(virtual_gamepad);  #ifdef HAVE_SDL2 @@ -128,12 +135,16 @@ struct InputSubsystem::Impl {              Common::ParamPackage{{"display", "Any"}, {"engine", "any"}},          }; +#ifndef ANDROID          auto keyboard_devices = keyboard->GetInputDevices();          devices.insert(devices.end(), keyboard_devices.begin(), keyboard_devices.end());          auto mouse_devices = mouse->GetInputDevices();          devices.insert(devices.end(), mouse_devices.begin(), mouse_devices.end()); +#endif +#ifdef ANDROID          auto android_devices = android->GetInputDevices();          devices.insert(devices.end(), android_devices.begin(), android_devices.end()); +#endif  #ifdef HAVE_LIBUSB          auto gcadapter_devices = gcadapter->GetInputDevices();          devices.insert(devices.end(), gcadapter_devices.begin(), gcadapter_devices.end()); @@ -162,9 +173,11 @@ struct InputSubsystem::Impl {          if (engine == mouse->GetEngineName()) {              return mouse;          } +#ifdef ANDROID          if (engine == android->GetEngineName()) {              return android;          } +#endif  #ifdef HAVE_LIBUSB          if (engine == gcadapter->GetEngineName()) {              return gcadapter; @@ -245,9 +258,11 @@ struct InputSubsystem::Impl {          if (engine == mouse->GetEngineName()) {              return true;          } +#ifdef ANDROID          if (engine == android->GetEngineName()) {              return true;          } +#endif  #ifdef HAVE_LIBUSB          if (engine == gcadapter->GetEngineName()) {              return true; @@ -276,7 +291,9 @@ struct InputSubsystem::Impl {      void BeginConfiguration() {          keyboard->BeginConfiguration();          mouse->BeginConfiguration(); +#ifdef ANDROID          android->BeginConfiguration(); +#endif  #ifdef HAVE_LIBUSB          gcadapter->BeginConfiguration();  #endif @@ -290,7 +307,9 @@ struct InputSubsystem::Impl {      void EndConfiguration() {          keyboard->EndConfiguration();          mouse->EndConfiguration(); +#ifdef ANDROID          android->EndConfiguration(); +#endif  #ifdef HAVE_LIBUSB          gcadapter->EndConfiguration();  #endif @@ -321,7 +340,6 @@ struct InputSubsystem::Impl {      std::shared_ptr<TasInput::Tas> tas_input;      std::shared_ptr<CemuhookUDP::UDPClient> udp_client;      std::shared_ptr<Camera> camera; -    std::shared_ptr<Android> android;      std::shared_ptr<VirtualAmiibo> virtual_amiibo;      std::shared_ptr<VirtualGamepad> virtual_gamepad; @@ -333,6 +351,10 @@ struct InputSubsystem::Impl {      std::shared_ptr<SDLDriver> sdl;      std::shared_ptr<Joycons> joycon;  #endif + +#ifdef ANDROID +    std::shared_ptr<Android> android; +#endif  };  InputSubsystem::InputSubsystem() : impl{std::make_unique<Impl>()} {} @@ -387,6 +409,7 @@ const Camera* InputSubsystem::GetCamera() const {      return impl->camera.get();  } +#ifdef ANDROID  Android* InputSubsystem::GetAndroid() {      return impl->android.get();  } @@ -394,6 +417,7 @@ Android* InputSubsystem::GetAndroid() {  const Android* InputSubsystem::GetAndroid() const {      return impl->android.get();  } +#endif  VirtualAmiibo* InputSubsystem::GetVirtualAmiibo() {      return impl->virtual_amiibo.get(); diff --git a/src/yuzu/configuration/qt_config.cpp b/src/yuzu/configuration/qt_config.cpp index 1051031f2..37951b9c8 100644 --- 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) {          if (profile_name.empty()) {              // Use the global input config              player = Settings::values.players.GetValue(true)[player_index]; +            player.profile_name = "";              return;          }      } diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index dfa50006a..0d16bfd65 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -1603,6 +1603,7 @@ void GMainWindow::ConnectMenuEvents() {      // Help      connect_menu(ui->action_Open_yuzu_Folder, &GMainWindow::OnOpenYuzuFolder);      connect_menu(ui->action_Verify_installed_contents, &GMainWindow::OnVerifyInstalledContents); +    connect_menu(ui->action_Install_Firmware, &GMainWindow::OnInstallFirmware);      connect_menu(ui->action_About, &GMainWindow::OnAbout);  } @@ -1631,6 +1632,8 @@ void GMainWindow::UpdateMenuState() {          action->setEnabled(emulation_running);      } +    ui->action_Install_Firmware->setEnabled(!emulation_running); +      for (QAction* action : applet_actions) {          action->setEnabled(is_firmware_available && !emulation_running);      } @@ -4150,6 +4153,146 @@ void GMainWindow::OnVerifyInstalledContents() {      }  } +void GMainWindow::OnInstallFirmware() { +    // Don't do this while emulation is running, that'd probably be a bad idea. +    if (emu_thread != nullptr && emu_thread->IsRunning()) { +        return; +    } + +    // Check for installed keys, error out, suggest restart? +    if (!ContentManager::AreKeysPresent()) { +        QMessageBox::information( +            this, tr("Keys not installed"), +            tr("Install decryption keys and restart yuzu before attempting to install firmware.")); +        return; +    } + +    QString firmware_source_location = +        QFileDialog::getExistingDirectory(this, tr("Select Dumped Firmware Source Location"), +                                          QString::fromStdString(""), QFileDialog::ShowDirsOnly); +    if (firmware_source_location.isEmpty()) { +        return; +    } + +    QProgressDialog progress(tr("Installing Firmware..."), tr("Cancel"), 0, 100, this); +    progress.setWindowModality(Qt::WindowModal); +    progress.setMinimumDuration(100); +    progress.setAutoClose(false); +    progress.setAutoReset(false); +    progress.show(); + +    // Declare progress callback. +    auto QtProgressCallback = [&](size_t total_size, size_t processed_size) { +        progress.setValue(static_cast<int>((processed_size * 100) / total_size)); +        return progress.wasCanceled(); +    }; + +    LOG_INFO(Frontend, "Installing firmware from {}", firmware_source_location.toStdString()); + +    // Check for a reasonable number of .nca files (don't hardcode them, just see if there's some in +    // there.) +    std::filesystem::path firmware_source_path = firmware_source_location.toStdString(); +    if (!Common::FS::IsDir(firmware_source_path)) { +        progress.close(); +        return; +    } + +    std::vector<std::filesystem::path> out; +    const Common::FS::DirEntryCallable callback = +        [&out](const std::filesystem::directory_entry& entry) { +            if (entry.path().has_extension() && entry.path().extension() == ".nca") +                out.emplace_back(entry.path()); + +            return true; +        }; + +    QtProgressCallback(100, 10); + +    Common::FS::IterateDirEntries(firmware_source_path, callback, Common::FS::DirEntryFilter::File); +    if (out.size() <= 0) { +        progress.close(); +        QMessageBox::warning(this, tr("Firmware install failed"), +                             tr("Unable to locate potential firmware NCA files")); +        return; +    } + +    // Locate and erase the content of nand/system/Content/registered/*.nca, if any. +    auto sysnand_content_vdir = system->GetFileSystemController().GetSystemNANDContentDirectory(); +    if (!sysnand_content_vdir->CleanSubdirectoryRecursive("registered")) { +        progress.close(); +        QMessageBox::critical(this, tr("Firmware install failed"), +                              tr("Failed to delete one or more firmware file.")); +        return; +    } + +    LOG_INFO(Frontend, +             "Cleaned nand/system/Content/registered folder in preparation for new firmware."); + +    QtProgressCallback(100, 20); + +    auto firmware_vdir = sysnand_content_vdir->GetDirectoryRelative("registered"); + +    bool success = true; +    bool cancelled = false; +    int i = 0; +    for (const auto& firmware_src_path : out) { +        i++; +        auto firmware_src_vfile = +            vfs->OpenFile(firmware_src_path.generic_string(), FileSys::OpenMode::Read); +        auto firmware_dst_vfile = +            firmware_vdir->CreateFileRelative(firmware_src_path.filename().string()); + +        if (!VfsRawCopy(firmware_src_vfile, firmware_dst_vfile)) { +            LOG_ERROR(Frontend, "Failed to copy firmware file {} to {} in registered folder!", +                      firmware_src_path.generic_string(), firmware_src_path.filename().string()); +            success = false; +        } + +        if (QtProgressCallback(100, 20 + (int)(((float)(i) / (float)out.size()) * 70.0))) { +            success = false; +            cancelled = true; +            break; +        } +    } + +    if (!success && !cancelled) { +        progress.close(); +        QMessageBox::critical(this, tr("Firmware install failed"), +                              tr("One or more firmware files failed to copy into NAND.")); +        return; +    } else if (cancelled) { +        progress.close(); +        QMessageBox::warning(this, tr("Firmware install failed"), +                             tr("Firmware installation cancelled, firmware may be in bad state, " +                                "restart yuzu or re-install firmware.")); +        return; +    } + +    // Re-scan VFS for the newly placed firmware files. +    system->GetFileSystemController().CreateFactories(*vfs); + +    auto VerifyFirmwareCallback = [&](size_t total_size, size_t processed_size) { +        progress.setValue(90 + static_cast<int>((processed_size * 10) / total_size)); +        return progress.wasCanceled(); +    }; + +    auto result = +        ContentManager::VerifyInstalledContents(*system, *provider, VerifyFirmwareCallback, true); + +    if (result.size() > 0) { +        const auto failed_names = +            QString::fromStdString(fmt::format("{}", fmt::join(result, "\n"))); +        progress.close(); +        QMessageBox::critical( +            this, tr("Firmware integrity verification failed!"), +            tr("Verification failed for the following files:\n\n%1").arg(failed_names)); +        return; +    } + +    progress.close(); +    OnCheckFirmwareDecryption(); +} +  void GMainWindow::OnAbout() {      AboutDialog aboutDialog(this);      aboutDialog.exec(); diff --git a/src/yuzu/main.h b/src/yuzu/main.h index aba61e388..1f0e35c67 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -380,6 +380,7 @@ private slots:      void OnLoadAmiibo();      void OnOpenYuzuFolder();      void OnVerifyInstalledContents(); +    void OnInstallFirmware();      void OnAbout();      void OnToggleFilterBar();      void OnToggleStatusBar(); diff --git a/src/yuzu/main.ui b/src/yuzu/main.ui index 6a6b0821f..6ff444a22 100644 --- a/src/yuzu/main.ui +++ b/src/yuzu/main.ui @@ -25,7 +25,16 @@    </property>    <widget class="QWidget" name="centralwidget">     <layout class="QHBoxLayout" name="horizontalLayout"> -    <property name="margin" stdset="0"> +    <property name="leftMargin"> +     <number>0</number> +    </property> +    <property name="topMargin"> +     <number>0</number> +    </property> +    <property name="rightMargin"> +     <number>0</number> +    </property> +    <property name="bottomMargin">       <number>0</number>      </property>     </layout> @@ -156,8 +165,8 @@       <addaction name="separator"/>       <addaction name="action_Configure_Tas"/>      </widget> -    <addaction name="action_Rederive"/>      <addaction name="action_Verify_installed_contents"/> +    <addaction name="action_Install_Firmware"/>      <addaction name="separator"/>      <addaction name="menu_cabinet_applet"/>      <addaction name="action_Load_Album"/> @@ -455,6 +464,11 @@      <string>Open &Controller Menu</string>     </property>    </action> +  <action name="action_Install_Firmware"> +   <property name="text"> +    <string>Install Firmware</string> +   </property> +  </action>   </widget>   <resources>    <include location="yuzu.qrc"/> diff --git a/src/yuzu_cmd/sdl_config.cpp b/src/yuzu_cmd/sdl_config.cpp index 995114510..6e0f254b6 100644 --- 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) {          if (profile_name.empty()) {              // Use the global input config              player = Settings::values.players.GetValue(true)[player_index]; +            player.profile_name = "";              return;          }      }  | 
