diff options
Diffstat (limited to '')
88 files changed, 1559 insertions, 631 deletions
diff --git a/.gitignore b/.gitignore index a5f7248c7..fbadb208b 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,8 @@ CMakeSettings.json # OSX global filetypes # Created by Finder or Spotlight in directories for various OS functionality (indexing, etc) .DS_Store +.DS_Store? +._* .AppleDouble .LSOverride .Spotlight-V100 diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index e48137080..7cce27d51 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -63,8 +63,9 @@ if (YUZU_USE_EXTERNAL_SDL2) # Yuzu itself needs: Atomic Audio Events Joystick Haptic Sensor Threads Timers # Since 2.0.18 Atomic+Threads required for HIDAPI/libusb (see https://github.com/libsdl-org/SDL/issues/5095) # Yuzu-cmd also needs: Video (depends on Loadso/Dlopen) + # CPUinfo also required for SDL Audio, at least until 2.28.0 (see https://github.com/libsdl-org/SDL/issues/7809) set(SDL_UNUSED_SUBSYSTEMS - CPUinfo File Filesystem + File Filesystem Locale Power Render) foreach(_SUB ${SDL_UNUSED_SUBSYSTEMS}) string(TOUPPER ${_SUB} _OPT) diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts index a637db78a..bab4f4d0f 100644 --- a/src/android/app/build.gradle.kts +++ b/src/android/app/build.gradle.kts @@ -2,7 +2,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later import android.annotation.SuppressLint +import kotlin.collections.setOf import org.jetbrains.kotlin.konan.properties.Properties +import org.jlleitschuh.gradle.ktlint.reporter.ReporterType plugins { id("com.android.application") @@ -10,6 +12,7 @@ plugins { id("kotlin-parcelize") kotlin("plugin.serialization") version "1.8.21" id("androidx.navigation.safeargs.kotlin") + id("org.jlleitschuh.gradle.ktlint") version "11.4.0" } /** @@ -44,16 +47,6 @@ android { jniLibs.useLegacyPackaging = true } - lint { - // This is important as it will run lint but not abort on error - // Lint has some overly obnoxious "errors" that should really be warnings - abortOnError = false - - //Uncomment disable lines for test builds... - //disable 'MissingTranslation'bin - //disable 'ExtraTranslation' - } - defaultConfig { // TODO If this is ever modified, change application_id in strings.xml applicationId = "org.yuzu.yuzu_emu" @@ -167,6 +160,24 @@ android { } } +tasks.getByPath("preBuild").dependsOn("ktlintCheck") + +ktlint { + version.set("0.47.1") + android.set(true) + ignoreFailures.set(false) + disabledRules.set( + setOf( + "no-wildcard-imports", + "package-name", + "import-ordering" + ) + ) + reporters { + reporter(ReporterType.CHECKSTYLE) + } +} + dependencies { implementation("androidx.core:core-ktx:1.10.1") implementation("androidx.appcompat:appcompat:1.6.1") diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml index b474ddb0b..e31ad69e2 100644 --- a/src/android/app/src/main/AndroidManifest.xml +++ b/src/android/app/src/main/AndroidManifest.xml @@ -54,6 +54,8 @@ SPDX-License-Identifier: GPL-3.0-or-later android:name="org.yuzu.yuzu_emu.activities.EmulationActivity" android:theme="@style/Theme.Yuzu.Main" android:screenOrientation="userLandscape" + android:supportsPictureInPicture="true" + android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|uiMode" android:exported="true"> <intent-filter> 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 4be9ade14..f860cdd4b 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 @@ -14,16 +14,18 @@ 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.YuzuApplication.Companion.appContext import org.yuzu.yuzu_emu.activities.EmulationActivity import org.yuzu.yuzu_emu.utils.DocumentsTree.Companion.isNativePath +import org.yuzu.yuzu_emu.utils.FileUtil.exists import org.yuzu.yuzu_emu.utils.FileUtil.getFileSize +import org.yuzu.yuzu_emu.utils.FileUtil.isDirectory import org.yuzu.yuzu_emu.utils.FileUtil.openContentUri import org.yuzu.yuzu_emu.utils.Log.error import org.yuzu.yuzu_emu.utils.Log.verbose import org.yuzu.yuzu_emu.utils.Log.warning import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable -import java.lang.ref.WeakReference /** * Class which contains methods that interact @@ -74,7 +76,9 @@ object NativeLibrary { fun openContentUri(path: String?, openmode: String?): Int { return if (isNativePath(path!!)) { YuzuApplication.documentsTree!!.openContentUri(path, openmode) - } else openContentUri(appContext, path, openmode) + } else { + openContentUri(appContext, path, openmode) + } } @Keep @@ -82,7 +86,29 @@ object NativeLibrary { fun getSize(path: String?): Long { return if (isNativePath(path!!)) { YuzuApplication.documentsTree!!.getFileSize(path) - } else getFileSize(appContext, path) + } else { + getFileSize(appContext, path) + } + } + + @Keep + @JvmStatic + fun exists(path: String?): Boolean { + return if (isNativePath(path!!)) { + YuzuApplication.documentsTree!!.exists(path) + } else { + exists(appContext, path) + } + } + + @Keep + @JvmStatic + fun isDirectory(path: String?): Boolean { + return if (isNativePath(path!!)) { + YuzuApplication.documentsTree!!.isDirectory(path) + } else { + isDirectory(appContext, path) + } } /** @@ -283,6 +309,11 @@ object NativeLibrary { external fun isRunning(): Boolean /** + * Returns true if emulation is paused. + */ + external fun isPaused(): Boolean + + /** * Returns the performance stats for the current game */ external fun getPerfStats(): DoubleArray @@ -431,7 +462,9 @@ object NativeLibrary { Html.FROM_HTML_MODE_LEGACY ) ) - .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> emulationActivity.finish() } + .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> + emulationActivity.finish() + } .setOnDismissListener { emulationActivity.finish() } emulationActivity.runOnUiThread { val alert = builder.create() 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 4c947b786..04ab6a220 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,12 +7,12 @@ import android.app.Application import android.app.NotificationChannel import android.app.NotificationManager import android.content.Context +import java.io.File import org.yuzu.yuzu_emu.utils.DirectoryInitialization import org.yuzu.yuzu_emu.utils.DocumentsTree import org.yuzu.yuzu_emu.utils.GpuDriverHelper -import java.io.File -fun Context.getPublicFilesDir() : File = getExternalFilesDir(null) ?: filesDir +fun Context.getPublicFilesDir(): File = getExternalFilesDir(null) ?: filesDir class YuzuApplication : Application() { private fun createNotificationChannels() { @@ -21,7 +21,9 @@ class YuzuApplication : Application() { getString(R.string.emulation_notification_channel_name), NotificationManager.IMPORTANCE_LOW ) - emulationChannel.description = getString(R.string.emulation_notification_channel_description) + emulationChannel.description = getString( + R.string.emulation_notification_channel_description + ) emulationChannel.setSound(null, null) emulationChannel.vibrationPattern = null @@ -48,7 +50,7 @@ class YuzuApplication : Application() { GpuDriverHelper.initializeDriverParameters(applicationContext) NativeLibrary.logDeviceInfo() - createNotificationChannels(); + createNotificationChannels() } companion object { 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 caf660348..f0a6753a9 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 @@ -4,14 +4,23 @@ package org.yuzu.yuzu_emu.activities import android.app.Activity +import android.app.PendingIntent +import android.app.PictureInPictureParams +import android.app.RemoteAction +import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.content.IntentFilter +import android.content.res.Configuration import android.graphics.Rect +import android.graphics.drawable.Icon import android.hardware.Sensor import android.hardware.SensorEvent import android.hardware.SensorEventListener import android.hardware.SensorManager +import android.os.Build import android.os.Bundle +import android.util.Rational import android.view.InputDevice import android.view.KeyEvent import android.view.MotionEvent @@ -24,9 +33,12 @@ import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import androidx.navigation.fragment.NavHostFragment +import kotlin.math.roundToInt import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.ActivityEmulationBinding +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.SettingsViewModel import org.yuzu.yuzu_emu.model.Game import org.yuzu.yuzu_emu.utils.ControllerMappingHelper @@ -34,7 +46,6 @@ import org.yuzu.yuzu_emu.utils.ForegroundService import org.yuzu.yuzu_emu.utils.InputHandler import org.yuzu.yuzu_emu.utils.NfcReader import org.yuzu.yuzu_emu.utils.ThemeHelper -import kotlin.math.roundToInt class EmulationActivity : AppCompatActivity(), SensorEventListener { private lateinit var binding: ActivityEmulationBinding @@ -50,6 +61,9 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { private var motionTimestamp: Long = 0 private var flipMotionOrientation: Boolean = false + private val actionPause = "ACTION_EMULATOR_PAUSE" + private val actionPlay = "ACTION_EMULATOR_PLAY" + private val settingsViewModel: SettingsViewModel by viewModels() override fun onDestroy() { @@ -120,6 +134,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { super.onResume() nfcReader.startScanning() startMotionSensorListener() + + buildPictureInPictureParams() } override fun onPause() { @@ -128,6 +144,16 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { stopMotionSensorListener() } + override fun onUserLeaveHint() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + if (BooleanSetting.PICTURE_IN_PICTURE.boolean && !isInPictureInPictureMode) { + val pictureInPictureParamsBuilder = PictureInPictureParams.Builder() + .getPictureInPictureActionsBuilder().getPictureInPictureAspectBuilder() + enterPictureInPictureMode(pictureInPictureParamsBuilder.build()) + } + } + } + override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) setIntent(intent) @@ -230,6 +256,100 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { } } + private fun PictureInPictureParams.Builder.getPictureInPictureAspectBuilder(): + PictureInPictureParams.Builder { + val aspectRatio = when (IntSetting.RENDERER_ASPECT_RATIO.int) { + 0 -> Rational(16, 9) + 1 -> Rational(4, 3) + 2 -> Rational(21, 9) + 3 -> Rational(16, 10) + else -> null // Best fit + } + return this.apply { aspectRatio?.let { setAspectRatio(it) } } + } + + private fun PictureInPictureParams.Builder.getPictureInPictureActionsBuilder(): + PictureInPictureParams.Builder { + val pictureInPictureActions: MutableList<RemoteAction> = mutableListOf() + val pendingFlags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + + if (NativeLibrary.isPaused()) { + val playIcon = Icon.createWithResource(this@EmulationActivity, R.drawable.ic_pip_play) + val playPendingIntent = PendingIntent.getBroadcast( + this@EmulationActivity, + R.drawable.ic_pip_play, + Intent(actionPlay), + pendingFlags + ) + val playRemoteAction = RemoteAction( + playIcon, + getString(R.string.play), + getString(R.string.play), + playPendingIntent + ) + pictureInPictureActions.add(playRemoteAction) + } else { + val pauseIcon = Icon.createWithResource(this@EmulationActivity, R.drawable.ic_pip_pause) + val pausePendingIntent = PendingIntent.getBroadcast( + this@EmulationActivity, + R.drawable.ic_pip_pause, + Intent(actionPause), + pendingFlags + ) + val pauseRemoteAction = RemoteAction( + pauseIcon, + getString(R.string.pause), + getString(R.string.pause), + pausePendingIntent + ) + pictureInPictureActions.add(pauseRemoteAction) + } + + return this.apply { setActions(pictureInPictureActions) } + } + + fun buildPictureInPictureParams() { + val pictureInPictureParamsBuilder = PictureInPictureParams.Builder() + .getPictureInPictureActionsBuilder().getPictureInPictureAspectBuilder() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + pictureInPictureParamsBuilder.setAutoEnterEnabled( + BooleanSetting.PICTURE_IN_PICTURE.boolean + ) + } + setPictureInPictureParams(pictureInPictureParamsBuilder.build()) + } + + private var pictureInPictureReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent) { + if (intent.action == actionPlay) { + if (NativeLibrary.isPaused()) NativeLibrary.unPauseEmulation() + } else if (intent.action == actionPause) { + if (!NativeLibrary.isPaused()) NativeLibrary.pauseEmulation() + } + buildPictureInPictureParams() + } + } + + override fun onPictureInPictureModeChanged( + isInPictureInPictureMode: Boolean, + newConfig: Configuration + ) { + super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) + if (isInPictureInPictureMode) { + IntentFilter().apply { + addAction(actionPause) + addAction(actionPlay) + }.also { + registerReceiver(pictureInPictureReceiver, it) + } + } else { + try { + unregisterReceiver(pictureInPictureReceiver) + } catch (ignored: Exception) { + } + } + } + private fun startMotionSensorListener() { val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) 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 83d08841b..e91277d35 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 @@ -28,10 +28,9 @@ import org.yuzu.yuzu_emu.HomeNavigationDirections import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication +import org.yuzu.yuzu_emu.adapters.GameAdapter.GameViewHolder import org.yuzu.yuzu_emu.databinding.CardGameBinding -import org.yuzu.yuzu_emu.activities.EmulationActivity import org.yuzu.yuzu_emu.model.Game -import org.yuzu.yuzu_emu.adapters.GameAdapter.GameViewHolder import org.yuzu.yuzu_emu.model.GamesViewModel class GameAdapter(private val activity: AppCompatActivity) : @@ -60,7 +59,10 @@ class GameAdapter(private val activity: AppCompatActivity) : override fun onClick(view: View) { val holder = view.tag as GameViewHolder - val gameExists = DocumentFile.fromSingleUri(YuzuApplication.appContext, Uri.parse(holder.game.path))?.exists() == true + val gameExists = DocumentFile.fromSingleUri( + YuzuApplication.appContext, + Uri.parse(holder.game.path) + )?.exists() == true if (!gameExists) { Toast.makeText( YuzuApplication.appContext, 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 b719dd539..d3df3bc81 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 @@ -58,11 +58,12 @@ class HomeSettingAdapter(private val activity: AppCompatActivity, var options: L ) when (option.titleId) { - R.string.get_early_access -> binding.optionLayout.background = - ContextCompat.getDrawable( - binding.optionCard.context, - R.drawable.premium_background - ) + R.string.get_early_access -> + binding.optionLayout.background = + ContextCompat.getDrawable( + binding.optionCard.context, + R.drawable.premium_background + ) } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard.kt index 82a6712b6..e058067c9 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard.kt @@ -12,10 +12,10 @@ import android.view.WindowInsets import android.view.inputmethod.InputMethodManager import androidx.annotation.Keep import androidx.core.view.ViewCompat +import java.io.Serializable import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.applets.keyboard.ui.KeyboardDialogFragment -import java.io.Serializable @Keep object SoftwareKeyboard { @@ -40,19 +40,22 @@ object SoftwareKeyboard { // There isn't a good way to know that the IMM is dismissed, so poll every 500ms to submit inline keyboard result. val handler = Handler(Looper.myLooper()!!) val delayMs = 500 - handler.postDelayed(object : Runnable { - override fun run() { - val insets = ViewCompat.getRootWindowInsets(overlayView) - val isKeyboardVisible = insets!!.isVisible(WindowInsets.Type.ime()) - if (isKeyboardVisible) { - handler.postDelayed(this, delayMs.toLong()) - return - } + handler.postDelayed( + object : Runnable { + override fun run() { + val insets = ViewCompat.getRootWindowInsets(overlayView) + val isKeyboardVisible = insets!!.isVisible(WindowInsets.Type.ime()) + if (isKeyboardVisible) { + handler.postDelayed(this, delayMs.toLong()) + return + } - // No longer visible, submit the result. - NativeLibrary.submitInlineKeyboardInput(KeyEvent.KEYCODE_ENTER) - } - }, delayMs.toLong()) + // No longer visible, submit the result. + NativeLibrary.submitInlineKeyboardInput(KeyEvent.KEYCODE_ENTER) + } + }, + delayMs.toLong() + ) } @JvmStatic diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/disk_shader_cache/DiskShaderCacheProgress.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/disk_shader_cache/DiskShaderCacheProgress.kt index 3b1559c80..a18efef19 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/disk_shader_cache/DiskShaderCacheProgress.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/disk_shader_cache/DiskShaderCacheProgress.kt @@ -20,7 +20,10 @@ object DiskShaderCacheProgress { emulationActivity.getString(R.string.loading), emulationActivity.getString(R.string.preparing_shaders) ) - fragment.show(emulationActivity.supportFragmentManager, ShaderProgressDialogFragment.TAG) + fragment.show( + emulationActivity.supportFragmentManager, + ShaderProgressDialogFragment.TAG + ) } synchronized(finishLock) { finishLock.wait() } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/disk_shader_cache/ui/ShaderProgressDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/disk_shader_cache/ui/ShaderProgressDialogFragment.kt index 2c68c9ac3..8a8e0a6e8 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/disk_shader_cache/ui/ShaderProgressDialogFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/disk_shader_cache/ui/ShaderProgressDialogFragment.kt @@ -62,7 +62,9 @@ class ShaderProgressDialogFragment : DialogFragment() { shaderProgressViewModel.message.observe(viewLifecycleOwner) { msg -> alertDialog.setMessage(msg) } - synchronized(DiskShaderCacheProgress.finishLock) { DiskShaderCacheProgress.finishLock.notifyAll() } + synchronized(DiskShaderCacheProgress.finishLock) { + DiskShaderCacheProgress.finishLock.notifyAll() + } } override fun onDestroyView() { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/DocumentProvider.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/DocumentProvider.kt index 4c3a9ca80..f3be156b5 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/DocumentProvider.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/DocumentProvider.kt @@ -13,11 +13,11 @@ import android.os.ParcelFileDescriptor import android.provider.DocumentsContract import android.provider.DocumentsProvider import android.webkit.MimeTypeMap +import java.io.* import org.yuzu.yuzu_emu.BuildConfig import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.getPublicFilesDir -import java.io.* class DocumentProvider : DocumentsProvider() { private val baseDirectory: File @@ -44,7 +44,7 @@ class DocumentProvider : DocumentsProvider() { DocumentsContract.Document.COLUMN_SIZE ) - const val AUTHORITY : String = BuildConfig.APPLICATION_ID + ".user" + const val AUTHORITY: String = BuildConfig.APPLICATION_ID + ".user" const val ROOT_ID: String = "root" } @@ -58,7 +58,11 @@ class DocumentProvider : DocumentsProvider() { private fun getFile(documentId: String): File { if (documentId.startsWith(ROOT_ID)) { val file = baseDirectory.resolve(documentId.drop(ROOT_ID.length + 1)) - if (!file.exists()) throw FileNotFoundException("${file.absolutePath} ($documentId) not found") + if (!file.exists()) { + throw FileNotFoundException( + "${file.absolutePath} ($documentId) not found" + ) + } return file } else { throw FileNotFoundException("'$documentId' is not in any known root") @@ -80,7 +84,8 @@ class DocumentProvider : DocumentsProvider() { add(DocumentsContract.Root.COLUMN_SUMMARY, null) add( DocumentsContract.Root.COLUMN_FLAGS, - DocumentsContract.Root.FLAG_SUPPORTS_CREATE or DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD + DocumentsContract.Root.FLAG_SUPPORTS_CREATE or + DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD ) add(DocumentsContract.Root.COLUMN_TITLE, context!!.getString(R.string.app_name)) add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, getDocumentId(baseDirectory)) @@ -127,11 +132,13 @@ class DocumentProvider : DocumentsProvider() { try { if (DocumentsContract.Document.MIME_TYPE_DIR == mimeType) { - if (!newFile.mkdir()) + if (!newFile.mkdir()) { throw IOException("Failed to create directory") + } } else { - if (!newFile.createNewFile()) + if (!newFile.createNewFile()) { throw IOException("Failed to create file") + } } } catch (e: IOException) { throw FileNotFoundException("Couldn't create document '${newFile.path}': ${e.message}") @@ -142,8 +149,9 @@ class DocumentProvider : DocumentsProvider() { override fun deleteDocument(documentId: String?) { val file = getFile(documentId!!) - if (!file.delete()) + if (!file.delete()) { throw FileNotFoundException("Couldn't delete document with ID '$documentId'") + } } override fun removeDocument(documentId: String, parentDocumentId: String?) { @@ -151,38 +159,55 @@ class DocumentProvider : DocumentsProvider() { val file = getFile(documentId) if (parent == file || file.parentFile == null || file.parentFile!! == parent) { - if (!file.delete()) + if (!file.delete()) { throw FileNotFoundException("Couldn't delete document with ID '$documentId'") + } } else { throw FileNotFoundException("Couldn't delete document with ID '$documentId'") } } override fun renameDocument(documentId: String?, displayName: String?): String { - if (displayName == null) - throw FileNotFoundException("Couldn't rename document '$documentId' as the new name is null") + if (displayName == null) { + throw FileNotFoundException( + "Couldn't rename document '$documentId' as the new name is null" + ) + } val sourceFile = getFile(documentId!!) val sourceParentFile = sourceFile.parentFile - ?: throw FileNotFoundException("Couldn't rename document '$documentId' as it has no parent") + ?: throw FileNotFoundException( + "Couldn't rename document '$documentId' as it has no parent" + ) val destFile = sourceParentFile.resolve(displayName) try { - if (!sourceFile.renameTo(destFile)) - throw FileNotFoundException("Couldn't rename document from '${sourceFile.name}' to '${destFile.name}'") + if (!sourceFile.renameTo(destFile)) { + throw FileNotFoundException( + "Couldn't rename document from '${sourceFile.name}' to '${destFile.name}'" + ) + } } catch (e: Exception) { - throw FileNotFoundException("Couldn't rename document from '${sourceFile.name}' to '${destFile.name}': ${e.message}") + throw FileNotFoundException( + "Couldn't rename document from '${sourceFile.name}' to '${destFile.name}': " + + "${e.message}" + ) } return getDocumentId(destFile) } private fun copyDocument( - sourceDocumentId: String, sourceParentDocumentId: String, + sourceDocumentId: String, + sourceParentDocumentId: String, targetParentDocumentId: String? ): String { - if (!isChildDocument(sourceParentDocumentId, sourceDocumentId)) - throw FileNotFoundException("Couldn't copy document '$sourceDocumentId' as its parent is not '$sourceParentDocumentId'") + if (!isChildDocument(sourceParentDocumentId, sourceDocumentId)) { + throw FileNotFoundException( + "Couldn't copy document '$sourceDocumentId' as its parent is not " + + "'$sourceParentDocumentId'" + ) + } return copyDocument(sourceDocumentId, targetParentDocumentId) } @@ -193,8 +218,13 @@ class DocumentProvider : DocumentsProvider() { val newFile = parent.resolveWithoutConflict(oldFile.name) try { - if (!(newFile.createNewFile() && newFile.setWritable(true) && newFile.setReadable(true))) + if (!( + newFile.createNewFile() && newFile.setWritable(true) && + newFile.setReadable(true) + ) + ) { throw IOException("Couldn't create new file") + } FileInputStream(oldFile).use { inStream -> FileOutputStream(newFile).use { outStream -> @@ -209,12 +239,14 @@ class DocumentProvider : DocumentsProvider() { } override fun moveDocument( - sourceDocumentId: String, sourceParentDocumentId: String?, + sourceDocumentId: String, + sourceParentDocumentId: String?, targetParentDocumentId: String? ): String { try { val newDocumentId = copyDocument( - sourceDocumentId, sourceParentDocumentId!!, + sourceDocumentId, + sourceParentDocumentId!!, targetParentDocumentId ) removeDocument(sourceDocumentId, sourceParentDocumentId) @@ -245,24 +277,30 @@ class DocumentProvider : DocumentsProvider() { add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, localDocumentId) add( DocumentsContract.Document.COLUMN_DISPLAY_NAME, - if (localFile == baseDirectory) context!!.getString(R.string.app_name) else localFile.name + if (localFile == baseDirectory) { + context!!.getString(R.string.app_name) + } else { + localFile.name + } ) add(DocumentsContract.Document.COLUMN_SIZE, localFile.length()) add(DocumentsContract.Document.COLUMN_MIME_TYPE, getTypeForFile(localFile)) add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, localFile.lastModified()) add(DocumentsContract.Document.COLUMN_FLAGS, flags) - if (localFile == baseDirectory) + if (localFile == baseDirectory) { add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_yuzu) + } } return cursor } private fun getTypeForFile(file: File): Any { - return if (file.isDirectory) + return if (file.isDirectory) { DocumentsContract.Document.MIME_TYPE_DIR - else + } else { getTypeForName(file.name) + } } private fun getTypeForName(name: String): Any { @@ -270,8 +308,9 @@ class DocumentProvider : DocumentsProvider() { if (lastDot >= 0) { val extension = name.substring(lastDot + 1) val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) - if (mime != null) + if (mime != null) { return mime + } } return "application/octect-stream" } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt index 3dfd66779..d41933766 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt @@ -8,6 +8,10 @@ enum class BooleanSetting( override val section: String, override val defaultValue: Boolean ) : AbstractBooleanSetting { + CPU_DEBUG_MODE("cpu_debug_mode", Settings.SECTION_CPU, false), + FASTMEM("cpuopt_fastmem", Settings.SECTION_CPU, true), + FASTMEM_EXCLUSIVES("cpuopt_fastmem_exclusives", Settings.SECTION_CPU, true), + PICTURE_IN_PICTURE("picture_in_picture", Settings.SECTION_GENERAL, true), USE_CUSTOM_RTC("custom_rtc_enabled", Settings.SECTION_SYSTEM, false); override var boolean: Boolean = defaultValue @@ -27,6 +31,7 @@ enum class BooleanSetting( companion object { private val NOT_RUNTIME_EDITABLE = listOf( + PICTURE_IN_PICTURE, USE_CUSTOM_RTC ) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt index fa84f94f5..4427a7d9d 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt @@ -93,6 +93,11 @@ enum class IntSetting( Settings.SECTION_RENDERER, 0 ), + RENDERER_SCREEN_LAYOUT( + "screen_layout", + Settings.SECTION_RENDERER, + Settings.LayoutOption_MobileLandscape + ), RENDERER_ASPECT_RATIO( "aspect_ratio", Settings.SECTION_RENDERER, 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 8df20b928..88afb2223 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,11 +4,11 @@ package org.yuzu.yuzu_emu.features.settings.model import android.text.TextUtils +import java.util.* import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivityView import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile -import java.util.* class Settings { private var gameId: String? = null @@ -133,7 +133,6 @@ class Settings { const val PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER = "EmulationMenuSettings_JoystickRelCenter" const val PREF_MENU_SETTINGS_DPAD_SLIDE = "EmulationMenuSettings_DpadSlideEnable" const val PREF_MENU_SETTINGS_HAPTICS = "EmulationMenuSettings_Haptics" - const val PREF_MENU_SETTINGS_LANDSCAPE = "EmulationMenuSettings_LandscapeScreenLayout" const val PREF_MENU_SETTINGS_SHOW_FPS = "EmulationMenuSettings_ShowFps" const val PREF_MENU_SETTINGS_SHOW_OVERLAY = "EmulationMenuSettings_ShowOverlay" @@ -144,6 +143,10 @@ class Settings { private val configFileSectionsMap: MutableMap<String, List<String>> = HashMap() + const val LayoutOption_Unspecified = 0 + const val LayoutOption_MobilePortrait = 4 + const val LayoutOption_MobileLandscape = 5 + init { configFileSectionsMap[SettingsFile.FILE_NAME_CONFIG] = listOf( diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt index 63f95690c..6621289fd 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt @@ -8,6 +8,7 @@ enum class StringSetting( override val section: String, override val defaultValue: String ) : AbstractStringSetting { + AUDIO_OUTPUT_ENGINE("output_engine", Settings.SECTION_AUDIO, "auto"), CUSTOM_RTC("custom_rtc", Settings.SECTION_SYSTEM, "0"); override var string: String = defaultValue 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 0f8edbfb0..a67001311 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,12 +3,8 @@ package org.yuzu.yuzu_emu.features.settings.model.view -import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting - class HeaderSetting( - setting: AbstractSetting?, - titleId: Int, - descriptionId: Int -) : SettingsItem(setting, titleId, descriptionId) { + titleId: Int +) : SettingsItem(null, titleId, 0) { override val type = TYPE_HEADER } 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 9eac9904e..7306ec458 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 @@ -4,7 +4,6 @@ package org.yuzu.yuzu_emu.features.settings.model.view import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting -import org.yuzu.yuzu_emu.features.settings.model.IntSetting class SingleChoiceSetting( setting: AbstractIntSetting?, 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 842648ce4..92d0167ae 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,13 +3,11 @@ package org.yuzu.yuzu_emu.features.settings.model.view +import kotlin.math.roundToInt import org.yuzu.yuzu_emu.features.settings.model.AbstractFloatSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting -import org.yuzu.yuzu_emu.features.settings.model.FloatSetting -import org.yuzu.yuzu_emu.features.settings.model.IntSetting import org.yuzu.yuzu_emu.utils.Log -import kotlin.math.roundToInt class SliderSetting( setting: AbstractSetting?, @@ -19,7 +17,7 @@ class SliderSetting( val max: Int, val units: String, val key: String? = null, - val defaultValue: Int? = null, + val defaultValue: Int? = null ) : SettingsItem(setting, titleId, descriptionId) { override val type = TYPE_SLIDER 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 9e9b00d10..3b6731dcd 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 @@ -5,24 +5,25 @@ package org.yuzu.yuzu_emu.features.settings.model.view import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting -import org.yuzu.yuzu_emu.features.settings.model.StringSetting class StringSingleChoiceSetting( - val key: String? = null, setting: AbstractSetting?, titleId: Int, descriptionId: Int, - val choicesId: Array<String>, - private val valuesId: Array<String>?, + val choices: Array<String>, + val values: Array<String>?, + val key: String? = null, private val defaultValue: String? = null ) : SettingsItem(setting, titleId, descriptionId) { override val type = TYPE_STRING_SINGLE_CHOICE fun getValueAt(index: Int): String? { - if (valuesId == null) return null - return if (index >= 0 && index < valuesId.size) { - valuesId[index] - } else "" + if (values == null) return null + return if (index >= 0 && index < values.size) { + values[index] + } else { + "" + } } val selectedValue: String @@ -35,8 +36,8 @@ class StringSingleChoiceSetting( val selectValueIndex: Int get() { val selectedValue = selectedValue - for (i in valuesId!!.indices) { - if (valuesId[i] == selectedValue) { + for (i in values!!.indices) { + if (values[i] == selectedValue) { return i } } 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 a3ef59c2f..8a9d13a92 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 @@ -3,8 +3,6 @@ package org.yuzu.yuzu_emu.features.settings.model.view -import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting - class SubmenuSetting( titleId: Int, descriptionId: Int, 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 72e2cce2a..a5af5a7ae 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 @@ -8,17 +8,18 @@ import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.View +import android.view.ViewGroup.MarginLayoutParams import android.widget.Toast +import androidx.activity.OnBackPressedCallback +import androidx.activity.result.ActivityResultLauncher import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat -import android.view.ViewGroup.MarginLayoutParams -import androidx.activity.OnBackPressedCallback import androidx.core.view.updatePadding import com.google.android.material.color.MaterialColors -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.settings.model.BooleanSetting @@ -29,7 +30,6 @@ import org.yuzu.yuzu_emu.features.settings.model.SettingsViewModel import org.yuzu.yuzu_emu.features.settings.model.StringSetting import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile import org.yuzu.yuzu_emu.utils.* -import java.io.IOException class SettingsActivity : AppCompatActivity(), SettingsActivityView { private val presenter = SettingsActivityPresenter(this) @@ -59,7 +59,9 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView { setSupportActionBar(binding.toolbarSettings) supportActionBar!!.setDisplayHomeAsUpEnabled(true) - if (InsetsHelper.getSystemGestureType(applicationContext) != InsetsHelper.GESTURE_NAVIGATION) { + if (InsetsHelper.getSystemGestureType(applicationContext) != + InsetsHelper.GESTURE_NAVIGATION + ) { binding.navigationBarShade.setBackgroundColor( ThemeHelper.getColorWithOpacity( MaterialColors.getColor( @@ -75,7 +77,8 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView { this, object : OnBackPressedCallback(true) { override fun handleOnBackPressed() = navigateBack() - }) + } + ) setInsets() } @@ -148,11 +151,13 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView { private fun areSystemAnimationsEnabled(): Boolean { val duration = android.provider.Settings.Global.getFloat( contentResolver, - android.provider.Settings.Global.ANIMATOR_DURATION_SCALE, 1f + android.provider.Settings.Global.ANIMATOR_DURATION_SCALE, + 1f ) val transition = android.provider.Settings.Global.getFloat( contentResolver, - android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE, 1f + android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE, + 1f ) return duration != 0f && transition != 0f } @@ -207,7 +212,9 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView { get() = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as SettingsFragment? private fun setInsets() { - ViewCompat.setOnApplyWindowInsetsListener(binding.frameContent) { view: View, windowInsets: WindowInsetsCompat -> + ViewCompat.setOnApplyWindowInsetsListener( + binding.frameContent + ) { view: View, windowInsets: WindowInsetsCompat -> val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) view.updatePadding( @@ -239,5 +246,17 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView { settings.putExtra(ARG_GAME_ID, gameId) context.startActivity(settings) } + + fun launch( + context: Context, + launcher: ActivityResultLauncher<Intent>, + menuTag: String?, + gameId: String? + ) { + val settings = Intent(context, SettingsActivity::class.java) + settings.putExtra(ARG_MENU_TAG, menuTag) + settings.putExtra(ARG_GAME_ID, gameId) + launcher.launch(settings) + } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityPresenter.kt index 4361d95fb..93e677b21 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityPresenter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityPresenter.kt @@ -6,12 +6,12 @@ package org.yuzu.yuzu_emu.features.settings.ui import android.content.Context import android.os.Bundle import android.text.TextUtils +import java.io.File import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile import org.yuzu.yuzu_emu.utils.DirectoryInitialization import org.yuzu.yuzu_emu.utils.Log -import java.io.File class SettingsActivityPresenter(private val activityView: SettingsActivityView) { val settings: Settings get() = activityView.settings @@ -46,9 +46,15 @@ class SettingsActivityPresenter(private val activityView: SettingsActivityView) private fun prepareDirectoriesIfNeeded() { val configFile = - File(DirectoryInitialization.userDirectory + "/config/" + SettingsFile.FILE_NAME_CONFIG + ".ini") + File( + "${DirectoryInitialization.userDirectory}/config/" + + "${SettingsFile.FILE_NAME_CONFIG}.ini" + ) if (!configFile.exists()) { - Log.error(DirectoryInitialization.userDirectory + "/config/" + SettingsFile.FILE_NAME_CONFIG + ".ini") + Log.error( + "${DirectoryInitialization.userDirectory}/config/" + + "${SettingsFile.FILE_NAME_CONFIG}.ini" + ) Log.error("yuzu config file could not be found!") } 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 1eb4899fc..ce0b92c90 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 @@ -13,7 +13,6 @@ import android.view.ViewGroup import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity -import androidx.fragment.app.setFragmentResultListener import androidx.recyclerview.widget.RecyclerView import com.google.android.material.datepicker.MaterialDatePicker import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -139,7 +138,7 @@ class SettingsAdapter( clickedItem = item dialog = MaterialAlertDialogBuilder(context) .setTitle(item.nameId) - .setSingleChoiceItems(item.choicesId, item.selectValueIndex, this) + .setSingleChoiceItems(item.choices, item.selectValueIndex, this) .show() } 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 867147950..70a74c4dd 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 @@ -50,7 +50,10 @@ class SettingsFragment : Fragment(), SettingsFragmentView { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { settingsAdapter = SettingsAdapter(this, requireActivity()) - val dividerDecoration = MaterialDividerItemDecoration(requireContext(), LinearLayoutManager.VERTICAL) + val dividerDecoration = MaterialDividerItemDecoration( + requireContext(), + LinearLayoutManager.VERTICAL + ) dividerDecoration.isLastItemDecorated = false binding.listSettings.apply { adapter = settingsAdapter @@ -99,7 +102,9 @@ class SettingsFragment : Fragment(), SettingsFragmentView { } private fun setInsets() { - ViewCompat.setOnApplyWindowInsetsListener(binding.listSettings) { view: View, windowInsets: WindowInsetsCompat -> + ViewCompat.setOnApplyWindowInsetsListener( + binding.listSettings + ) { view: View, windowInsets: WindowInsetsCompat -> val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) view.updatePadding(bottom = insets.bottom) windowInsets 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 1ceaa6fb4..59c1d9d54 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 @@ -7,7 +7,6 @@ import android.content.SharedPreferences import android.os.Build import android.text.TextUtils import androidx.preference.PreferenceManager -import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting @@ -43,7 +42,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) } fun putSetting(setting: AbstractSetting) { - if (setting.section == null) { + if (setting.section == null || setting.key == null) { return } @@ -166,6 +165,15 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) IntSetting.CPU_ACCURACY.defaultValue ) ) + add( + SwitchSetting( + BooleanSetting.PICTURE_IN_PICTURE, + R.string.picture_in_picture, + R.string.picture_in_picture_description, + BooleanSetting.PICTURE_IN_PICTURE.key, + BooleanSetting.PICTURE_IN_PICTURE.defaultValue + ) + ) } } @@ -227,7 +235,6 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) private fun addGraphicsSettings(sl: ArrayList<SettingsItem>) { settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_graphics)) sl.apply { - add( SingleChoiceSetting( IntSetting.RENDERER_ACCURACY, @@ -285,6 +292,17 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SingleChoiceSetting( + IntSetting.RENDERER_SCREEN_LAYOUT, + R.string.renderer_screen_layout, + 0, + R.array.rendererScreenLayoutNames, + R.array.rendererScreenLayoutValues, + IntSetting.RENDERER_SCREEN_LAYOUT.key, + IntSetting.RENDERER_SCREEN_LAYOUT.defaultValue + ) + ) + add( + SingleChoiceSetting( IntSetting.RENDERER_ASPECT_RATIO, R.string.renderer_aspect_ratio, 0, @@ -335,18 +353,31 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) private fun addAudioSettings(sl: ArrayList<SettingsItem>) { settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_audio)) - sl.add( - SliderSetting( - IntSetting.AUDIO_VOLUME, - R.string.audio_volume, - R.string.audio_volume_description, - 0, - 100, - "%", - IntSetting.AUDIO_VOLUME.key, - IntSetting.AUDIO_VOLUME.defaultValue - ) - ) + sl.apply { + add( + StringSingleChoiceSetting( + StringSetting.AUDIO_OUTPUT_ENGINE, + R.string.audio_output_engine, + 0, + settingsActivity.resources.getStringArray(R.array.outputEngineEntries), + settingsActivity.resources.getStringArray(R.array.outputEngineValues), + StringSetting.AUDIO_OUTPUT_ENGINE.key, + StringSetting.AUDIO_OUTPUT_ENGINE.defaultValue + ) + ) + add( + SliderSetting( + IntSetting.AUDIO_VOLUME, + R.string.audio_volume, + R.string.audio_volume_description, + 0, + 100, + "%", + IntSetting.AUDIO_VOLUME.key, + IntSetting.AUDIO_VOLUME.defaultValue + ) + ) + } } private fun addThemeSettings(sl: ArrayList<SettingsItem>) { @@ -449,6 +480,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) private fun addDebugSettings(sl: ArrayList<SettingsItem>) { settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_debug)) sl.apply { + add(HeaderSetting(R.string.gpu)) add( SingleChoiceSetting( IntSetting.RENDERER_BACKEND, @@ -469,6 +501,39 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) IntSetting.RENDERER_DEBUG.defaultValue ) ) + + add(HeaderSetting(R.string.cpu)) + add( + SwitchSetting( + BooleanSetting.CPU_DEBUG_MODE, + R.string.cpu_debug_mode, + R.string.cpu_debug_mode_description, + BooleanSetting.CPU_DEBUG_MODE.key, + BooleanSetting.CPU_DEBUG_MODE.defaultValue + ) + ) + + val fastmem = object : AbstractBooleanSetting { + override var boolean: Boolean + get() = + BooleanSetting.FASTMEM.boolean && BooleanSetting.FASTMEM_EXCLUSIVES.boolean + set(value) { + BooleanSetting.FASTMEM.boolean = value + BooleanSetting.FASTMEM_EXCLUSIVES.boolean = value + } + override val key: String? = null + override val section: String = Settings.SECTION_CPU + override val isRuntimeEditable: Boolean = false + override val valueAsString: String = "" + override val defaultValue: Any = true + } + add( + SwitchSetting( + fastmem, + R.string.fastmem, + 0 + ) + ) } } } 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 04c045e77..7955532ee 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 @@ -4,15 +4,15 @@ 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.DateTimeSetting -import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem -import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter import java.time.Instant import java.time.ZoneId import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import java.time.format.FormatStyle +import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding +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 class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : SettingViewHolder(binding.root, adapter) { 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 de764a27f..e4e321bd3 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 @@ -26,6 +26,14 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti for (i in values.indices) { if (values[i] == item.selectedValue) { binding.textSettingDescription.text = resMgr.getStringArray(item.choicesId)[i] + return + } + } + } else if (item is StringSingleChoiceSetting) { + for (i in item.values!!.indices) { + if (item.values[i] == item.selectedValue) { + binding.textSettingDescription.text = item.choices[i] + return } } } else { 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 b163bd6ca..54f531795 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 @@ -6,8 +6,8 @@ package org.yuzu.yuzu_emu.features.settings.ui.viewholder import android.view.View import android.widget.CompoundButton import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding -import org.yuzu.yuzu_emu.features.settings.model.view.SwitchSetting 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 class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter: SettingsAdapter) : diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt index e29bca11d..70a52df5d 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt @@ -3,6 +3,8 @@ package org.yuzu.yuzu_emu.features.settings.utils +import java.io.* +import java.util.* import org.ini4j.Wini import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R @@ -13,8 +15,6 @@ import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivityView import org.yuzu.yuzu_emu.utils.BiMap import org.yuzu.yuzu_emu.utils.DirectoryInitialization import org.yuzu.yuzu_emu.utils.Log -import java.io.* -import java.util.* /** * Contains static methods for interacting with .ini files in which settings are stored. @@ -137,9 +137,12 @@ object SettingsFile { for (settingKey in sortedKeySet) { val setting = settings[settingKey] NativeLibrary.setUserSetting( - gameId, mapSectionNameFromIni( + gameId, + mapSectionNameFromIni( section.name - ), setting!!.key, setting.valueAsString + ), + setting!!.key, + setting.valueAsString ) } } @@ -148,13 +151,17 @@ object SettingsFile { private fun mapSectionNameFromIni(generalSectionName: String): String? { return if (sectionsMap.getForward(generalSectionName) != null) { sectionsMap.getForward(generalSectionName) - } else generalSectionName + } else { + generalSectionName + } } private fun mapSectionNameToIni(generalSectionName: String): String { return if (sectionsMap.getBackward(generalSectionName) != null) { sectionsMap.getBackward(generalSectionName).toString() - } else generalSectionName + } else { + generalSectionName + } } fun getSettingsFile(fileName: String): File { @@ -237,5 +244,21 @@ object SettingsFile { val setting = settings[key] parser.put(header, setting!!.key, setting.valueAsString) } + + BooleanSetting.values().forEach { + if (!keySet.contains(it.key)) { + parser.put(header, it.key, it.valueAsString) + } + } + IntSetting.values().forEach { + if (!keySet.contains(it.key)) { + parser.put(header, it.key, it.valueAsString) + } + } + StringSetting.values().forEach { + if (!keySet.contains(it.key)) { + parser.put(header, it.key, it.valueAsString) + } + } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt index c92e2755c..2ff827c6b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt @@ -66,7 +66,11 @@ class AboutFragment : Fragment() { true } - binding.buttonContributors.setOnClickListener { openLink(getString(R.string.contributors_link)) } + binding.buttonContributors.setOnClickListener { + openLink( + getString(R.string.contributors_link) + ) + } binding.buttonLicenses.setOnClickListener { exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) binding.root.findNavController().navigate(R.id.action_aboutFragment_to_licensesFragment) @@ -101,7 +105,9 @@ class AboutFragment : Fragment() { } private fun setInsets() = - ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _: View, windowInsets: WindowInsetsCompat -> + ViewCompat.setOnApplyWindowInsetsListener( + binding.root + ) { _: View, windowInsets: WindowInsetsCompat -> val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EarlyAccessFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EarlyAccessFragment.kt index d8bbc1ce4..dbc16da4a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EarlyAccessFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EarlyAccessFragment.kt @@ -49,7 +49,11 @@ class EarlyAccessFragment : Fragment() { parentFragmentManager.primaryNavigationFragment?.findNavController()?.popBackStack() } - binding.getEarlyAccessButton.setOnClickListener { openLink(getString(R.string.play_store_link)) } + binding.getEarlyAccessButton.setOnClickListener { + openLink( + getString(R.string.play_store_link) + ) + } setInsets() } @@ -60,7 +64,9 @@ class EarlyAccessFragment : Fragment() { } private fun setInsets() = - ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _: View, windowInsets: WindowInsetsCompat -> + ViewCompat.setOnApplyWindowInsetsListener( + binding.root + ) { _: View, windowInsets: WindowInsetsCompat -> val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) 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 02bfcdb1e..4643418c1 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 @@ -7,24 +7,26 @@ import android.annotation.SuppressLint import android.app.AlertDialog import android.content.Context import android.content.DialogInterface +import android.content.Intent import android.content.SharedPreferences import android.content.pm.ActivityInfo -import android.content.res.Resources +import android.content.res.Configuration import android.graphics.Color import android.os.Bundle import android.os.Handler import android.os.Looper import android.util.Rational -import android.util.TypedValue import android.view.* import android.widget.TextView import androidx.activity.OnBackPressedCallback +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.widget.PopupMenu import androidx.core.content.res.ResourcesCompat import androidx.core.graphics.Insets import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat -import androidx.core.view.updatePadding +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope @@ -48,6 +50,7 @@ import org.yuzu.yuzu_emu.features.settings.model.IntSetting import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile +import org.yuzu.yuzu_emu.overlay.InputOverlay import org.yuzu.yuzu_emu.utils.* class EmulationFragment : Fragment(), SurfaceHolder.Callback { @@ -61,11 +64,28 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { val args by navArgs<EmulationFragmentArgs>() + private var isInFoldableLayout = false + + private lateinit var onReturnFromSettings: ActivityResultLauncher<Intent> + override fun onAttach(context: Context) { super.onAttach(context) 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) } + } + } + + onReturnFromSettings = context.activityResultRegistry.register( + "SettingsResult", + ActivityResultContracts.StartActivityForResult() + ) { updateScreenLayout() } } else { throw IllegalStateException("EmulationFragment must have EmulationActivity parent") } @@ -129,7 +149,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } R.id.menu_settings -> { - SettingsActivity.launch(requireContext(), SettingsFile.FILE_NAME_CONFIG, "") + SettingsActivity.launch( + requireContext(), + onReturnFromSettings, + SettingsFile.FILE_NAME_CONFIG, + "" + ) true } @@ -154,15 +179,46 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { requireActivity(), object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { - if (binding.drawerLayout.isOpen) binding.drawerLayout.close() else binding.drawerLayout.open() + if (binding.drawerLayout.isOpen) { + binding.drawerLayout.close() + } else { + binding.drawerLayout.open() + } } - }) + } + ) viewLifecycleOwner.lifecycleScope.launch(Dispatchers.Main) { lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { WindowInfoTracker.getOrCreate(requireContext()) .windowLayoutInfo(requireActivity()) - .collect { updateCurrentLayout(requireActivity() as EmulationActivity, it) } + .collect { updateFoldableLayout(requireActivity() as EmulationActivity, it) } + } + } + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + if (emulationActivity?.isInPictureInPictureMode == true) { + if (binding.drawerLayout.isOpen) { + binding.drawerLayout.close() + } + if (EmulationMenuSettings.showOverlay) { + binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.isVisible = false } + } + } else { + if (EmulationMenuSettings.showOverlay) { + binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.isVisible = true } + } + if (!isInFoldableLayout) { + if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { + binding.surfaceInputOverlay.orientation = InputOverlay.PORTRAIT + } else { + binding.surfaceInputOverlay.orientation = InputOverlay.LANDSCAPE + } + } + if (!binding.surfaceInputOverlay.isInEditMode) { + refreshInputOverlay() } } } @@ -173,16 +229,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { DirectoryInitialization.start(requireContext()) } - binding.surfaceEmulation.setAspectRatio( - when (IntSetting.RENDERER_ASPECT_RATIO.int) { - 0 -> Rational(16, 9) - 1 -> Rational(4, 3) - 2 -> Rational(21, 9) - 3 -> Rational(16, 10) - 4 -> null // Stretch - else -> Rational(16, 9) - } - ) + updateScreenLayout() emulationState.run(emulationActivity!!.isActivityRecreated) } @@ -243,31 +290,72 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } } - private val Number.toPx get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), Resources.getSystem().displayMetrics).toInt() - - fun updateCurrentLayout(emulationActivity: EmulationActivity, newLayoutInfo: WindowLayoutInfo) { - val isFolding = (newLayoutInfo.displayFeatures.find { it is FoldingFeature } as? FoldingFeature)?.let { - if (it.isSeparating) { - emulationActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED - if (it.orientation == FoldingFeature.Orientation.HORIZONTAL) { - binding.surfaceEmulation.layoutParams.height = it.bounds.top - binding.inGameMenu.layoutParams.height = it.bounds.bottom - binding.overlayContainer.layoutParams.height = it.bounds.bottom - 48.toPx - binding.overlayContainer.updatePadding(0, 0, 0, 24.toPx) - } + @SuppressLint("SourceLockedOrientationActivity") + private fun updateOrientation() { + emulationActivity?.let { + it.requestedOrientation = when (IntSetting.RENDERER_SCREEN_LAYOUT.int) { + Settings.LayoutOption_MobileLandscape -> + ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE + Settings.LayoutOption_MobilePortrait -> + ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT + Settings.LayoutOption_Unspecified -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + else -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE } - it.isSeparating - } ?: false + } + } + + private fun updateScreenLayout() { + binding.surfaceEmulation.setAspectRatio( + when (IntSetting.RENDERER_ASPECT_RATIO.int) { + 0 -> Rational(16, 9) + 1 -> Rational(4, 3) + 2 -> Rational(21, 9) + 3 -> Rational(16, 10) + 4 -> null // Stretch + else -> Rational(16, 9) + } + ) + emulationActivity?.buildPictureInPictureParams() + updateOrientation() + } + + private fun updateFoldableLayout( + emulationActivity: EmulationActivity, + newLayoutInfo: WindowLayoutInfo + ) { + val isFolding = + (newLayoutInfo.displayFeatures.find { it is FoldingFeature } as? FoldingFeature)?.let { + if (it.isSeparating) { + emulationActivity.requestedOrientation = + ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + if (it.orientation == FoldingFeature.Orientation.HORIZONTAL) { + // Restrict emulation and overlays to the top of the screen + binding.emulationContainer.layoutParams.height = it.bounds.top + binding.overlayContainer.layoutParams.height = it.bounds.top + // Restrict input and menu drawer to the bottom of the screen + binding.inputContainer.layoutParams.height = it.bounds.bottom + binding.inGameMenu.layoutParams.height = it.bounds.bottom + + isInFoldableLayout = true + binding.surfaceInputOverlay.orientation = InputOverlay.FOLDABLE + refreshInputOverlay() + } + } + it.isSeparating + } ?: false if (!isFolding) { - binding.surfaceEmulation.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT - binding.inGameMenu.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT + binding.emulationContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT + binding.inputContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT binding.overlayContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT - binding.overlayContainer.updatePadding(0, 0, 0, 0) - emulationActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + binding.inGameMenu.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT + isInFoldableLayout = false + updateOrientation() + onConfigurationChanged(resources.configuration) } - binding.surfaceInputOverlay.requestLayout() - binding.inGameMenu.requestLayout() + binding.emulationContainer.requestLayout() + binding.inputContainer.requestLayout() binding.overlayContainer.requestLayout() + binding.inGameMenu.requestLayout() } override fun surfaceCreated(holder: SurfaceHolder) { @@ -397,7 +485,19 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { popup.show() } + @SuppressLint("SourceLockedOrientationActivity") private fun startConfiguringControls() { + // Lock the current orientation to prevent editing inconsistencies + if (IntSetting.RENDERER_SCREEN_LAYOUT.int == Settings.LayoutOption_Unspecified) { + emulationActivity?.let { + it.requestedOrientation = + if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { + ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT + } else { + ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE + } + } + } binding.doneControlConfig.visibility = View.VISIBLE binding.surfaceInputOverlay.setIsInEditMode(true) } @@ -405,6 +505,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { private fun stopConfiguringControls() { binding.doneControlConfig.visibility = View.GONE binding.surfaceInputOverlay.setIsInEditMode(false) + // Unlock the orientation if it was locked for editing + if (IntSetting.RENDERER_SCREEN_LAYOUT.int == Settings.LayoutOption_Unspecified) { + emulationActivity?.let { + it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + } + } } @SuppressLint("SetTextI18n") @@ -414,18 +520,22 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { inputScaleSlider.apply { valueTo = 150F value = preferences.getInt(Settings.PREF_CONTROL_SCALE, 50).toFloat() - addOnChangeListener(Slider.OnChangeListener { _, value, _ -> - inputScaleValue.text = "${value.toInt()}%" - setControlScale(value.toInt()) - }) + addOnChangeListener( + Slider.OnChangeListener { _, value, _ -> + inputScaleValue.text = "${value.toInt()}%" + setControlScale(value.toInt()) + } + ) } inputOpacitySlider.apply { valueTo = 100F value = preferences.getInt(Settings.PREF_CONTROL_OPACITY, 100).toFloat() - addOnChangeListener(Slider.OnChangeListener { _, value, _ -> - inputOpacityValue.text = "${value.toInt()}%" - setControlOpacity(value.toInt()) - }) + addOnChangeListener( + Slider.OnChangeListener { _, value, _ -> + inputOpacityValue.text = "${value.toInt()}%" + setControlOpacity(value.toInt()) + } + ) } inputScaleValue.text = "${inputScaleSlider.value.toInt()}%" inputOpacityValue.text = "${inputOpacitySlider.value.toInt()}%" @@ -457,7 +567,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } private fun setInsets() { - ViewCompat.setOnApplyWindowInsetsListener(binding.inGameMenu) { v: View, windowInsets: WindowInsetsCompat -> + ViewCompat.setOnApplyWindowInsetsListener( + binding.inGameMenu + ) { v: View, windowInsets: WindowInsetsCompat -> val cutInsets: Insets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) var left = 0 var right = 0 @@ -577,8 +689,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { state = State.PAUSED } - State.PAUSED -> Log.warning("[EmulationFragment] Surface cleared while emulation paused.") - else -> Log.warning("[EmulationFragment] Surface cleared while emulation stopped.") + State.PAUSED -> Log.warning( + "[EmulationFragment] Surface cleared while emulation paused." + ) + else -> Log.warning( + "[EmulationFragment] Surface cleared while emulation stopped." + ) } } } 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 536163eb6..6f8adbba5 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 @@ -103,7 +103,9 @@ class HomeSettingsFragment : Fragment() { R.string.select_games_folder, R.string.select_games_folder_description, R.drawable.ic_add - ) { mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) }, + ) { + mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) + }, HomeSetting( R.string.manage_save_data, R.string.import_export_saves_description, @@ -225,7 +227,11 @@ class HomeSettingsFragment : Fragment() { val intent = Intent(action) intent.addCategory(Intent.CATEGORY_DEFAULT) intent.data = DocumentsContract.buildRootUri(authority, DocumentProvider.ROOT_ID) - intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + intent.addFlags( + Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or + Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) return intent } @@ -307,7 +313,9 @@ class HomeSettingsFragment : Fragment() { } private fun setInsets() = - ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view: View, windowInsets: WindowInsetsCompat -> + ViewCompat.setOnApplyWindowInsetsListener( + binding.root + ) { view: View, windowInsets: WindowInsetsCompat -> val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) val spacingNavigation = resources.getDimensionPixelSize(R.dimen.spacing_navigation) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt index 36e63bb9e..e1495ee8c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt @@ -15,6 +15,14 @@ import androidx.appcompat.app.AppCompatActivity import androidx.documentfile.provider.DocumentFile import androidx.fragment.app.DialogFragment import com.google.android.material.dialog.MaterialAlertDialogBuilder +import java.io.BufferedOutputStream +import java.io.File +import java.io.FileOutputStream +import java.io.FilenameFilter +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -24,14 +32,6 @@ import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.features.DocumentProvider import org.yuzu.yuzu_emu.getPublicFilesDir import org.yuzu.yuzu_emu.utils.FileUtil -import java.io.BufferedOutputStream -import java.io.File -import java.io.FileOutputStream -import java.io.FilenameFilter -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter -import java.util.zip.ZipEntry -import java.util.zip.ZipOutputStream class ImportExportSavesFragment : DialogFragment() { private val context = YuzuApplication.appContext @@ -98,7 +98,7 @@ class ImportExportSavesFragment : DialogFragment() { val outputZipFile = File( tempFolder, "yuzu saves - ${ - LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) }.zip" ) outputZipFile.createNewFile() @@ -106,12 +106,14 @@ class ImportExportSavesFragment : DialogFragment() { saveFolder.walkTopDown().forEach { file -> val zipFileName = file.absolutePath.removePrefix(savesFolderRoot).removePrefix("/") - if (zipFileName == "") + if (zipFileName == "") { return@forEach + } val entry = ZipEntry("$zipFileName${(if (file.isDirectory) "/" else "")}") zos.putNextEntry(entry) - if (file.isFile) + if (file.isFile) { file.inputStream().use { fis -> fis.copyTo(zos) } + } } } lastZipCreated = outputZipFile @@ -137,7 +139,8 @@ class ImportExportSavesFragment : DialogFragment() { withContext(Dispatchers.Main) { val file = DocumentFile.fromSingleUri( - context, DocumentsContract.buildDocumentUri( + context, + DocumentsContract.buildDocumentUri( DocumentProvider.AUTHORITY, "${DocumentProvider.ROOT_ID}/temp/${lastZipFile.name}" ) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt index c7880d8cc..739b26f99 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt @@ -14,7 +14,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding import org.yuzu.yuzu_emu.model.TaskViewModel - class IndeterminateProgressDialogFragment : DialogFragment() { private val taskViewModel: TaskViewModel by activityViewModels() diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LicensesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LicensesFragment.kt index 59141e823..b6e9129f7 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LicensesFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LicensesFragment.kt @@ -113,7 +113,9 @@ class LicensesFragment : Fragment() { } private fun setInsets() = - ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _: View, windowInsets: WindowInsetsCompat -> + ViewCompat.setOnApplyWindowInsetsListener( + binding.root + ) { _: View, windowInsets: WindowInsetsCompat -> val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) 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 adbe3696b..dd6c895fd 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 @@ -20,6 +20,7 @@ import androidx.fragment.app.activityViewModels import androidx.preference.PreferenceManager import info.debatty.java.stringsimilarity.Jaccard import info.debatty.java.stringsimilarity.JaroWinkler +import java.util.Locale import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.adapters.GameAdapter @@ -29,8 +30,6 @@ 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.FileUtil -import org.yuzu.yuzu_emu.utils.Log -import java.util.Locale class SearchFragment : Fragment() { private var _binding: FragmentSearchBinding? = null @@ -130,15 +129,15 @@ class SearchFragment : Fragment() { R.id.chip_homebrew -> baseList.filter { it.isHomebrew } R.id.chip_retail -> baseList.filter { - FileUtil.hasExtension(it.path, "xci") - || FileUtil.hasExtension(it.path, "nsp") + FileUtil.hasExtension(it.path, "xci") || + FileUtil.hasExtension(it.path, "nsp") } else -> baseList } - if (binding.searchText.text.toString().isEmpty() - && binding.chipGroup.checkedChipId != View.NO_ID + if (binding.searchText.text.toString().isEmpty() && + binding.chipGroup.checkedChipId != View.NO_ID ) { gamesViewModel.setSearchedGames(filteredList) return @@ -173,14 +172,16 @@ class SearchFragment : Fragment() { private fun focusSearch() { if (_binding != null) { binding.searchText.requestFocus() - val imm = - requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager? + val imm = requireActivity() + .getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager? imm?.showSoftInput(binding.searchText, InputMethodManager.SHOW_IMPLICIT) } } private fun setInsets() = - ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view: View, windowInsets: WindowInsetsCompat -> + ViewCompat.setOnApplyWindowInsetsListener( + binding.root + ) { view: View, windowInsets: WindowInsetsCompat -> val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_med) 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 258773380..6c4ddaf6b 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 @@ -25,6 +25,7 @@ import androidx.navigation.findNavController import androidx.preference.PreferenceManager import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback import com.google.android.material.transition.MaterialFadeThrough +import java.io.File import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.adapters.SetupAdapter @@ -35,7 +36,6 @@ import org.yuzu.yuzu_emu.model.SetupPage import org.yuzu.yuzu_emu.ui.main.MainActivity import org.yuzu.yuzu_emu.utils.DirectoryInitialization import org.yuzu.yuzu_emu.utils.GameHelper -import java.io.File class SetupFragment : Fragment() { private var _binding: FragmentSetupBinding? = null @@ -82,7 +82,8 @@ class SetupFragment : Fragment() { requireActivity().finish() } } - }) + } + ) requireActivity().window.navigationBarColor = ContextCompat.getColor(requireContext(), android.R.color.transparent) @@ -148,14 +149,20 @@ class SetupFragment : Fragment() { R.drawable.ic_add, true, R.string.add_games, - { mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) }, + { + mainActivity.getGamesDirectory.launch( + Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data + ) + }, true, R.string.add_games_warning, R.string.add_games_warning_description, R.string.add_games_warning_help, { val preferences = - PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) + PreferenceManager.getDefaultSharedPreferences( + YuzuApplication.appContext + ) preferences.getString(GameHelper.KEY_GAME_PATH, "")!!.isNotEmpty() } ) @@ -260,7 +267,9 @@ class SetupFragment : Fragment() { @RequiresApi(Build.VERSION_CODES.TIRAMISU) private val permissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { - if (!it && !shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)) { + if (!it && + !shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS) + ) { PermissionDeniedDialogFragment().show( childFragmentManager, PermissionDeniedDialogFragment.TAG @@ -315,7 +324,9 @@ class SetupFragment : Fragment() { } private fun setInsets() = - ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view: View, windowInsets: WindowInsetsCompat -> + ViewCompat.setOnApplyWindowInsetsListener( + binding.root + ) { view: View, windowInsets: WindowInsetsCompat -> val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) view.setPadding( diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/layout/AutofitGridLayoutManager.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/layout/AutofitGridLayoutManager.kt index be5e4c86c..bdd6ea628 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/layout/AutofitGridLayoutManager.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/layout/AutofitGridLayoutManager.kt @@ -44,7 +44,9 @@ class AutofitGridLayoutManager( override fun onLayoutChildren(recycler: Recycler, state: RecyclerView.State) { val width = width val height = height - if (columnWidth > 0 && width > 0 && height > 0 && (isColumnWidthChanged || lastWidth != width || lastHeight != height)) { + if (columnWidth > 0 && width > 0 && height > 0 && + (isColumnWidthChanged || lastWidth != width || lastHeight != height) + ) { val totalSpace: Int = if (orientation == VERTICAL) { width - paddingRight - paddingLeft } else { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt index 35d8000c5..6a048e39f 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt @@ -4,9 +4,9 @@ package org.yuzu.yuzu_emu.model import android.os.Parcelable +import java.util.HashSet import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable -import java.util.HashSet @Parcelize @Serializable @@ -23,8 +23,9 @@ class Game( val keyLastPlayedTime get() = "${gameId}_LastPlayed" override fun equals(other: Any?): Boolean { - if (other !is Game) + if (other !is Game) { return false + } return hashCode() == other.hashCode() } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt index d9b301210..1fe42f922 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt @@ -10,6 +10,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.preference.PreferenceManager +import java.util.Locale import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -20,7 +21,6 @@ import kotlinx.serialization.json.Json import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.utils.GameHelper -import java.util.Locale @OptIn(ExperimentalSerializationApi::class) class GamesViewModel : ViewModel() { @@ -99,8 +99,9 @@ class GamesViewModel : ViewModel() { } fun reloadGames(directoryChanged: Boolean) { - if (isReloading.value == true) + if (isReloading.value == true) { return + } _isReloading.postValue(true) viewModelScope.launch { 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 aa424c768..6251ec783 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 @@ -6,7 +6,6 @@ package org.yuzu.yuzu_emu.overlay import android.app.Activity import android.content.Context import android.content.SharedPreferences -import android.content.res.Configuration import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Point @@ -24,6 +23,8 @@ import android.view.WindowInsets import androidx.core.content.ContextCompat import androidx.preference.PreferenceManager 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 @@ -31,14 +32,13 @@ import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.utils.EmulationMenuSettings -import kotlin.math.max -import kotlin.math.min /** * Draws the interactive input overlay on top of the * [SurfaceView] that is rendering emulation. */ -class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context, attrs), +class InputOverlay(context: Context, attrs: AttributeSet?) : + SurfaceView(context, attrs), OnTouchListener { private val overlayButtons: MutableSet<InputOverlayDrawableButton> = HashSet() private val overlayDpads: MutableSet<InputOverlayDrawableDpad> = HashSet() @@ -51,12 +51,14 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context private lateinit var windowInsets: WindowInsets + var orientation = LANDSCAPE + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { super.onLayout(changed, left, top, right, bottom) windowInsets = rootWindowInsets - if (!preferences.getBoolean(Settings.PREF_OVERLAY_INIT, false)) { + if (!preferences.getBoolean("${Settings.PREF_OVERLAY_INIT}$orientation", false)) { defaultOverlay() } @@ -93,7 +95,11 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context var shouldUpdateView = false val playerIndex = - if (NativeLibrary.isHandheldOnly()) NativeLibrary.ConsoleDevice else NativeLibrary.Player1Device + if (NativeLibrary.isHandheldOnly()) { + NativeLibrary.ConsoleDevice + } else { + NativeLibrary.Player1Device + } for (button in overlayButtons) { if (!button.updateStatus(event)) { @@ -156,8 +162,9 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context shouldUpdateView = true } - if (shouldUpdateView) + if (shouldUpdateView) { invalidate() + } if (!preferences.getBoolean(Settings.PREF_TOUCH_ENABLED, true)) { return true @@ -233,10 +240,6 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context val fingerPositionX = event.getX(pointerIndex).toInt() val fingerPositionY = event.getY(pointerIndex).toInt() - // TODO: Provide support for portrait layout - //val orientation = - // if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) "-Portrait" else "" - for (button in overlayButtons) { // Determine the button state to apply based on the MotionEvent action flag. when (event.action and MotionEvent.ACTION_MASK) { @@ -245,9 +248,9 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context // If no button is being moved now, remember the currently touched button to move. if (buttonBeingConfigured == null && button.bounds.contains( - fingerPositionX, - fingerPositionY - ) + fingerPositionX, + fingerPositionY + ) ) { buttonBeingConfigured = button buttonBeingConfigured!!.onConfigureTouch(event) @@ -266,7 +269,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context buttonBeingConfigured!!.buttonId, buttonBeingConfigured!!.bounds.centerX(), buttonBeingConfigured!!.bounds.centerY(), - "" + orientation ) buttonBeingConfigured = null } @@ -299,7 +302,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context dpadBeingConfigured!!.upId, dpadBeingConfigured!!.bounds.centerX(), dpadBeingConfigured!!.bounds.centerY(), - "" + orientation ) dpadBeingConfigured = null } @@ -311,9 +314,9 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> if (joystickBeingConfigured == null && joystick.bounds.contains( - fingerPositionX, - fingerPositionY - ) + fingerPositionX, + fingerPositionY + ) ) { joystickBeingConfigured = joystick joystickBeingConfigured!!.onConfigureTouch(event) @@ -330,7 +333,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context joystickBeingConfigured!!.buttonId, joystickBeingConfigured!!.bounds.centerX(), joystickBeingConfigured!!.bounds.centerY(), - "" + orientation ) joystickBeingConfigured = null } @@ -533,8 +536,6 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context overlayButtons.clear() overlayDpads.clear() overlayJoysticks.clear() - val orientation = - if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) "-Portrait" else "" // Add all the enabled overlay items back to the HashSet. if (EmulationMenuSettings.showOverlay) { @@ -548,8 +549,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context val min = windowSize.first val max = windowSize.second PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit() - .putFloat("$sharedPrefsId$orientation-X", (x - min.x).toFloat() / max.x) - .putFloat("$sharedPrefsId$orientation-Y", (y - min.y).toFloat() / max.y) + .putFloat("$sharedPrefsId-X$orientation", (x - min.x).toFloat() / max.x) + .putFloat("$sharedPrefsId-Y$orientation", (y - min.y).toFloat() / max.y) .apply() } @@ -558,145 +559,250 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context } private fun defaultOverlay() { - if (!preferences.getBoolean(Settings.PREF_OVERLAY_INIT, false)) { - defaultOverlayLandscape() + if (!preferences.getBoolean("${Settings.PREF_OVERLAY_INIT}$orientation", false)) { + defaultOverlayByLayout(orientation) } resetButtonPlacement() preferences.edit() - .putBoolean(Settings.PREF_OVERLAY_INIT, true) + .putBoolean("${Settings.PREF_OVERLAY_INIT}$orientation", true) .apply() } fun resetButtonPlacement() { - defaultOverlayLandscape() + defaultOverlayByLayout(orientation) refreshControls() } - private fun defaultOverlayLandscape() { + private val landscapeResources = arrayOf( + R.integer.SWITCH_BUTTON_A_X, + R.integer.SWITCH_BUTTON_A_Y, + R.integer.SWITCH_BUTTON_B_X, + R.integer.SWITCH_BUTTON_B_Y, + R.integer.SWITCH_BUTTON_X_X, + R.integer.SWITCH_BUTTON_X_Y, + R.integer.SWITCH_BUTTON_Y_X, + R.integer.SWITCH_BUTTON_Y_Y, + R.integer.SWITCH_TRIGGER_ZL_X, + R.integer.SWITCH_TRIGGER_ZL_Y, + R.integer.SWITCH_TRIGGER_ZR_X, + R.integer.SWITCH_TRIGGER_ZR_Y, + R.integer.SWITCH_BUTTON_DPAD_X, + R.integer.SWITCH_BUTTON_DPAD_Y, + R.integer.SWITCH_TRIGGER_L_X, + R.integer.SWITCH_TRIGGER_L_Y, + R.integer.SWITCH_TRIGGER_R_X, + R.integer.SWITCH_TRIGGER_R_Y, + R.integer.SWITCH_BUTTON_PLUS_X, + R.integer.SWITCH_BUTTON_PLUS_Y, + R.integer.SWITCH_BUTTON_MINUS_X, + R.integer.SWITCH_BUTTON_MINUS_Y, + R.integer.SWITCH_BUTTON_HOME_X, + R.integer.SWITCH_BUTTON_HOME_Y, + R.integer.SWITCH_BUTTON_CAPTURE_X, + R.integer.SWITCH_BUTTON_CAPTURE_Y, + R.integer.SWITCH_STICK_R_X, + R.integer.SWITCH_STICK_R_Y, + R.integer.SWITCH_STICK_L_X, + R.integer.SWITCH_STICK_L_Y + ) + + private val portraitResources = arrayOf( + R.integer.SWITCH_BUTTON_A_X_PORTRAIT, + R.integer.SWITCH_BUTTON_A_Y_PORTRAIT, + R.integer.SWITCH_BUTTON_B_X_PORTRAIT, + R.integer.SWITCH_BUTTON_B_Y_PORTRAIT, + R.integer.SWITCH_BUTTON_X_X_PORTRAIT, + R.integer.SWITCH_BUTTON_X_Y_PORTRAIT, + R.integer.SWITCH_BUTTON_Y_X_PORTRAIT, + R.integer.SWITCH_BUTTON_Y_Y_PORTRAIT, + R.integer.SWITCH_TRIGGER_ZL_X_PORTRAIT, + R.integer.SWITCH_TRIGGER_ZL_Y_PORTRAIT, + R.integer.SWITCH_TRIGGER_ZR_X_PORTRAIT, + R.integer.SWITCH_TRIGGER_ZR_Y_PORTRAIT, + R.integer.SWITCH_BUTTON_DPAD_X_PORTRAIT, + R.integer.SWITCH_BUTTON_DPAD_Y_PORTRAIT, + R.integer.SWITCH_TRIGGER_L_X_PORTRAIT, + R.integer.SWITCH_TRIGGER_L_Y_PORTRAIT, + R.integer.SWITCH_TRIGGER_R_X_PORTRAIT, + R.integer.SWITCH_TRIGGER_R_Y_PORTRAIT, + R.integer.SWITCH_BUTTON_PLUS_X_PORTRAIT, + R.integer.SWITCH_BUTTON_PLUS_Y_PORTRAIT, + R.integer.SWITCH_BUTTON_MINUS_X_PORTRAIT, + R.integer.SWITCH_BUTTON_MINUS_Y_PORTRAIT, + R.integer.SWITCH_BUTTON_HOME_X_PORTRAIT, + R.integer.SWITCH_BUTTON_HOME_Y_PORTRAIT, + R.integer.SWITCH_BUTTON_CAPTURE_X_PORTRAIT, + R.integer.SWITCH_BUTTON_CAPTURE_Y_PORTRAIT, + R.integer.SWITCH_STICK_R_X_PORTRAIT, + R.integer.SWITCH_STICK_R_Y_PORTRAIT, + R.integer.SWITCH_STICK_L_X_PORTRAIT, + R.integer.SWITCH_STICK_L_Y_PORTRAIT + ) + + private val foldableResources = arrayOf( + R.integer.SWITCH_BUTTON_A_X_FOLDABLE, + R.integer.SWITCH_BUTTON_A_Y_FOLDABLE, + R.integer.SWITCH_BUTTON_B_X_FOLDABLE, + R.integer.SWITCH_BUTTON_B_Y_FOLDABLE, + R.integer.SWITCH_BUTTON_X_X_FOLDABLE, + R.integer.SWITCH_BUTTON_X_Y_FOLDABLE, + R.integer.SWITCH_BUTTON_Y_X_FOLDABLE, + R.integer.SWITCH_BUTTON_Y_Y_FOLDABLE, + R.integer.SWITCH_TRIGGER_ZL_X_FOLDABLE, + R.integer.SWITCH_TRIGGER_ZL_Y_FOLDABLE, + R.integer.SWITCH_TRIGGER_ZR_X_FOLDABLE, + R.integer.SWITCH_TRIGGER_ZR_Y_FOLDABLE, + R.integer.SWITCH_BUTTON_DPAD_X_FOLDABLE, + R.integer.SWITCH_BUTTON_DPAD_Y_FOLDABLE, + R.integer.SWITCH_TRIGGER_L_X_FOLDABLE, + R.integer.SWITCH_TRIGGER_L_Y_FOLDABLE, + R.integer.SWITCH_TRIGGER_R_X_FOLDABLE, + R.integer.SWITCH_TRIGGER_R_Y_FOLDABLE, + R.integer.SWITCH_BUTTON_PLUS_X_FOLDABLE, + R.integer.SWITCH_BUTTON_PLUS_Y_FOLDABLE, + R.integer.SWITCH_BUTTON_MINUS_X_FOLDABLE, + R.integer.SWITCH_BUTTON_MINUS_Y_FOLDABLE, + R.integer.SWITCH_BUTTON_HOME_X_FOLDABLE, + R.integer.SWITCH_BUTTON_HOME_Y_FOLDABLE, + R.integer.SWITCH_BUTTON_CAPTURE_X_FOLDABLE, + R.integer.SWITCH_BUTTON_CAPTURE_Y_FOLDABLE, + R.integer.SWITCH_STICK_R_X_FOLDABLE, + R.integer.SWITCH_STICK_R_Y_FOLDABLE, + R.integer.SWITCH_STICK_L_X_FOLDABLE, + R.integer.SWITCH_STICK_L_Y_FOLDABLE + ) + + private fun getResourceValue(orientation: String, position: Int): Float { + return when (orientation) { + PORTRAIT -> resources.getInteger(portraitResources[position]).toFloat() / 1000 + FOLDABLE -> resources.getInteger(foldableResources[position]).toFloat() / 1000 + else -> resources.getInteger(landscapeResources[position]).toFloat() / 1000 + } + } + + private fun defaultOverlayByLayout(orientation: String) { // Each value represents the position of the button in relation to the screen size without insets. preferences.edit() .putFloat( - ButtonType.BUTTON_A.toString() + "-X", - resources.getInteger(R.integer.SWITCH_BUTTON_A_X).toFloat() / 1000 + ButtonType.BUTTON_A.toString() + "-X$orientation", + getResourceValue(orientation, 0) ) .putFloat( - ButtonType.BUTTON_A.toString() + "-Y", - resources.getInteger(R.integer.SWITCH_BUTTON_A_Y).toFloat() / 1000 + ButtonType.BUTTON_A.toString() + "-Y$orientation", + getResourceValue(orientation, 1) ) .putFloat( - ButtonType.BUTTON_B.toString() + "-X", - resources.getInteger(R.integer.SWITCH_BUTTON_B_X).toFloat() / 1000 + ButtonType.BUTTON_B.toString() + "-X$orientation", + getResourceValue(orientation, 2) ) .putFloat( - ButtonType.BUTTON_B.toString() + "-Y", - resources.getInteger(R.integer.SWITCH_BUTTON_B_Y).toFloat() / 1000 + ButtonType.BUTTON_B.toString() + "-Y$orientation", + getResourceValue(orientation, 3) ) .putFloat( - ButtonType.BUTTON_X.toString() + "-X", - resources.getInteger(R.integer.SWITCH_BUTTON_X_X).toFloat() / 1000 + ButtonType.BUTTON_X.toString() + "-X$orientation", + getResourceValue(orientation, 4) ) .putFloat( - ButtonType.BUTTON_X.toString() + "-Y", - resources.getInteger(R.integer.SWITCH_BUTTON_X_Y).toFloat() / 1000 + ButtonType.BUTTON_X.toString() + "-Y$orientation", + getResourceValue(orientation, 5) ) .putFloat( - ButtonType.BUTTON_Y.toString() + "-X", - resources.getInteger(R.integer.SWITCH_BUTTON_Y_X).toFloat() / 1000 + ButtonType.BUTTON_Y.toString() + "-X$orientation", + getResourceValue(orientation, 6) ) .putFloat( - ButtonType.BUTTON_Y.toString() + "-Y", - resources.getInteger(R.integer.SWITCH_BUTTON_Y_Y).toFloat() / 1000 + ButtonType.BUTTON_Y.toString() + "-Y$orientation", + getResourceValue(orientation, 7) ) .putFloat( - ButtonType.TRIGGER_ZL.toString() + "-X", - resources.getInteger(R.integer.SWITCH_TRIGGER_ZL_X).toFloat() / 1000 + ButtonType.TRIGGER_ZL.toString() + "-X$orientation", + getResourceValue(orientation, 8) ) .putFloat( - ButtonType.TRIGGER_ZL.toString() + "-Y", - resources.getInteger(R.integer.SWITCH_TRIGGER_ZL_Y).toFloat() / 1000 + ButtonType.TRIGGER_ZL.toString() + "-Y$orientation", + getResourceValue(orientation, 9) ) .putFloat( - ButtonType.TRIGGER_ZR.toString() + "-X", - resources.getInteger(R.integer.SWITCH_TRIGGER_ZR_X).toFloat() / 1000 + ButtonType.TRIGGER_ZR.toString() + "-X$orientation", + getResourceValue(orientation, 10) ) .putFloat( - ButtonType.TRIGGER_ZR.toString() + "-Y", - resources.getInteger(R.integer.SWITCH_TRIGGER_ZR_Y).toFloat() / 1000 + ButtonType.TRIGGER_ZR.toString() + "-Y$orientation", + getResourceValue(orientation, 11) ) .putFloat( - ButtonType.DPAD_UP.toString() + "-X", - resources.getInteger(R.integer.SWITCH_BUTTON_DPAD_X).toFloat() / 1000 + ButtonType.DPAD_UP.toString() + "-X$orientation", + getResourceValue(orientation, 12) ) .putFloat( - ButtonType.DPAD_UP.toString() + "-Y", - resources.getInteger(R.integer.SWITCH_BUTTON_DPAD_Y).toFloat() / 1000 + ButtonType.DPAD_UP.toString() + "-Y$orientation", + getResourceValue(orientation, 13) ) .putFloat( - ButtonType.TRIGGER_L.toString() + "-X", - resources.getInteger(R.integer.SWITCH_TRIGGER_L_X).toFloat() / 1000 + ButtonType.TRIGGER_L.toString() + "-X$orientation", + getResourceValue(orientation, 14) ) .putFloat( - ButtonType.TRIGGER_L.toString() + "-Y", - resources.getInteger(R.integer.SWITCH_TRIGGER_L_Y).toFloat() / 1000 + ButtonType.TRIGGER_L.toString() + "-Y$orientation", + getResourceValue(orientation, 15) ) .putFloat( - ButtonType.TRIGGER_R.toString() + "-X", - resources.getInteger(R.integer.SWITCH_TRIGGER_R_X).toFloat() / 1000 + ButtonType.TRIGGER_R.toString() + "-X$orientation", + getResourceValue(orientation, 16) ) .putFloat( - ButtonType.TRIGGER_R.toString() + "-Y", - resources.getInteger(R.integer.SWITCH_TRIGGER_R_Y).toFloat() / 1000 + ButtonType.TRIGGER_R.toString() + "-Y$orientation", + getResourceValue(orientation, 17) ) .putFloat( - ButtonType.BUTTON_PLUS.toString() + "-X", - resources.getInteger(R.integer.SWITCH_BUTTON_PLUS_X).toFloat() / 1000 + ButtonType.BUTTON_PLUS.toString() + "-X$orientation", + getResourceValue(orientation, 18) ) .putFloat( - ButtonType.BUTTON_PLUS.toString() + "-Y", - resources.getInteger(R.integer.SWITCH_BUTTON_PLUS_Y).toFloat() / 1000 + ButtonType.BUTTON_PLUS.toString() + "-Y$orientation", + getResourceValue(orientation, 19) ) .putFloat( - ButtonType.BUTTON_MINUS.toString() + "-X", - resources.getInteger(R.integer.SWITCH_BUTTON_MINUS_X).toFloat() / 1000 + ButtonType.BUTTON_MINUS.toString() + "-X$orientation", + getResourceValue(orientation, 20) ) .putFloat( - ButtonType.BUTTON_MINUS.toString() + "-Y", - resources.getInteger(R.integer.SWITCH_BUTTON_MINUS_Y).toFloat() / 1000 + ButtonType.BUTTON_MINUS.toString() + "-Y$orientation", + getResourceValue(orientation, 21) ) .putFloat( - ButtonType.BUTTON_HOME.toString() + "-X", - resources.getInteger(R.integer.SWITCH_BUTTON_HOME_X).toFloat() / 1000 + ButtonType.BUTTON_HOME.toString() + "-X$orientation", + getResourceValue(orientation, 22) ) .putFloat( - ButtonType.BUTTON_HOME.toString() + "-Y", - resources.getInteger(R.integer.SWITCH_BUTTON_HOME_Y).toFloat() / 1000 + ButtonType.BUTTON_HOME.toString() + "-Y$orientation", + getResourceValue(orientation, 23) ) .putFloat( - ButtonType.BUTTON_CAPTURE.toString() + "-X", - resources.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_X) - .toFloat() / 1000 + ButtonType.BUTTON_CAPTURE.toString() + "-X$orientation", + getResourceValue(orientation, 24) ) .putFloat( - ButtonType.BUTTON_CAPTURE.toString() + "-Y", - resources.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_Y) - .toFloat() / 1000 + ButtonType.BUTTON_CAPTURE.toString() + "-Y$orientation", + getResourceValue(orientation, 25) ) .putFloat( - ButtonType.STICK_R.toString() + "-X", - resources.getInteger(R.integer.SWITCH_STICK_R_X).toFloat() / 1000 + ButtonType.STICK_R.toString() + "-X$orientation", + getResourceValue(orientation, 26) ) .putFloat( - ButtonType.STICK_R.toString() + "-Y", - resources.getInteger(R.integer.SWITCH_STICK_R_Y).toFloat() / 1000 + ButtonType.STICK_R.toString() + "-Y$orientation", + getResourceValue(orientation, 27) ) .putFloat( - ButtonType.STICK_L.toString() + "-X", - resources.getInteger(R.integer.SWITCH_STICK_L_X).toFloat() / 1000 + ButtonType.STICK_L.toString() + "-X$orientation", + getResourceValue(orientation, 28) ) .putFloat( - ButtonType.STICK_L.toString() + "-Y", - resources.getInteger(R.integer.SWITCH_STICK_L_Y).toFloat() / 1000 + ButtonType.STICK_L.toString() + "-Y$orientation", + getResourceValue(orientation, 29) ) .apply() } @@ -709,13 +815,17 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context private val preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) + const val LANDSCAPE = "" + const val PORTRAIT = "_Portrait" + const val FOLDABLE = "_Foldable" + /** * Resizes a [Bitmap] by a given scale factor * * @param context Context for getting the vector drawable * @param drawableId The ID of the drawable to scale. * @param scale The scale factor for the bitmap. - * @return The scaled [Bitmap] + * @return The scaled [Bitmap] */ private fun getBitmap(context: Context, drawableId: Int, scale: Float): Bitmap { val vectorDrawable = ContextCompat.getDrawable(context, drawableId) as VectorDrawable @@ -749,14 +859,13 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context * Gets the safe screen size for drawing the overlay * * @param context Context for getting the window metrics - * @return A pair of points, the first being the top left corner of the safe area, + * @return A pair of points, the first being the top left corner of the safe area, * the second being the bottom right corner of the safe area */ private fun getSafeScreenSize(context: Context): Pair<Point, Point> { // Get screen size - val windowMetrics = - WindowMetricsCalculator.getOrCreate() - .computeCurrentWindowMetrics(context as Activity) + val windowMetrics = WindowMetricsCalculator.getOrCreate() + .computeCurrentWindowMetrics(context as Activity) var maxY = windowMetrics.bounds.height().toFloat() var maxX = windowMetrics.bounds.width().toFloat() var minY = 0 @@ -768,10 +877,16 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { val insets = context.windowManager.currentWindowMetrics.windowInsets.displayCutout if (insets != null) { - if (insets.boundingRectTop.bottom != 0 && insets.boundingRectTop.bottom > maxY / 2) - insets.boundingRectTop.bottom.toFloat() else maxY - if (insets.boundingRectRight.left != 0 && insets.boundingRectRight.left > maxX / 2) - insets.boundingRectRight.left.toFloat() else maxX + if (insets.boundingRectTop.bottom != 0 && + insets.boundingRectTop.bottom > maxY / 2 + ) { + maxY = insets.boundingRectTop.bottom.toFloat() + } + if (insets.boundingRectRight.left != 0 && + insets.boundingRectRight.left > maxX / 2 + ) { + maxX = insets.boundingRectRight.left.toFloat() + } minX = insets.boundingRectLeft.right - insets.boundingRectLeft.left minY = insets.boundingRectBottom.top - insets.boundingRectBottom.bottom @@ -878,8 +993,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. // These were set in the input overlay configuration menu. - val xKey = "$buttonId$orientation-X" - val yKey = "$buttonId$orientation-Y" + val xKey = "$buttonId-X$orientation" + val yKey = "$buttonId-Y$orientation" val drawableXPercent = sPrefs.getFloat(xKey, 0f) val drawableYPercent = sPrefs.getFloat(yKey, 0f) val drawableX = (drawableXPercent * max.x + min.x).toInt() @@ -959,8 +1074,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context // The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay. // These were set in the input overlay configuration menu. - val drawableXPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}$orientation-X", 0f) - val drawableYPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}$orientation-Y", 0f) + val drawableXPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}-X$orientation", 0f) + val drawableYPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}-Y$orientation", 0f) val drawableX = (drawableXPercent * max.x + min.x).toInt() val drawableY = (drawableYPercent * max.y + min.y).toInt() val width = overlayDrawable.width @@ -1026,8 +1141,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. // These were set in the input overlay configuration menu. - val drawableXPercent = sPrefs.getFloat("$button$orientation-X", 0f) - val drawableYPercent = sPrefs.getFloat("$button$orientation-Y", 0f) + val drawableXPercent = sPrefs.getFloat("$button-X$orientation", 0f) + val drawableYPercent = sPrefs.getFloat("$button-Y$orientation", 0f) val drawableX = (drawableXPercent * max.x + min.x).toInt() val drawableY = (drawableYPercent * max.y + min.y).toInt() val outerScale = 1.66f 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 43d664d21..8aef6f5a5 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 @@ -133,7 +133,10 @@ class InputOverlayDrawableDpad( downButtonState = axisY > VIRT_AXIS_DEADZONE leftButtonState = axisX < -VIRT_AXIS_DEADZONE rightButtonState = axisX > VIRT_AXIS_DEADZONE - return oldUpState != upButtonState || oldDownState != downButtonState || oldLeftState != leftButtonState || oldRightState != rightButtonState + return oldUpState != upButtonState || + oldDownState != downButtonState || + oldLeftState != leftButtonState || + oldRightState != rightButtonState } return false } 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 f1d32192a..fb48f584d 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 @@ -9,12 +9,12 @@ import android.graphics.Canvas import android.graphics.Rect import android.graphics.drawable.BitmapDrawable import android.view.MotionEvent -import org.yuzu.yuzu_emu.NativeLibrary -import org.yuzu.yuzu_emu.utils.EmulationMenuSettings 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.utils.EmulationMenuSettings /** * Custom [BitmapDrawable] that is capable @@ -241,14 +241,22 @@ class InputOverlayDrawableJoystick( private fun setInnerBounds() { var x = virtBounds.centerX() + (xAxis * (virtBounds.width() / 2)).toInt() var y = virtBounds.centerY() + (yAxis * (virtBounds.height() / 2)).toInt() - if (x > virtBounds.centerX() + virtBounds.width() / 2) x = - virtBounds.centerX() + virtBounds.width() / 2 - if (x < virtBounds.centerX() - virtBounds.width() / 2) x = - virtBounds.centerX() - virtBounds.width() / 2 - if (y > virtBounds.centerY() + virtBounds.height() / 2) y = - virtBounds.centerY() + virtBounds.height() / 2 - if (y < virtBounds.centerY() - virtBounds.height() / 2) y = - virtBounds.centerY() - virtBounds.height() / 2 + if (x > virtBounds.centerX() + virtBounds.width() / 2) { + x = + virtBounds.centerX() + virtBounds.width() / 2 + } + if (x < virtBounds.centerX() - virtBounds.width() / 2) { + x = + virtBounds.centerX() - virtBounds.width() / 2 + } + if (y > virtBounds.centerY() + virtBounds.height() / 2) { + y = + virtBounds.centerY() + virtBounds.height() / 2 + } + if (y < virtBounds.centerY() - virtBounds.height() / 2) { + y = + virtBounds.centerY() - virtBounds.height() / 2 + } val width = pressedStateInnerBitmap.bounds.width() / 2 val height = pressedStateInnerBitmap.bounds.height() / 2 defaultStateInnerBitmap.setBounds( 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 97eef40d2..b0156dca5 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 @@ -99,7 +99,9 @@ class GamesFragment : Fragment() { } shouldSwapData.observe(viewLifecycleOwner) { shouldSwapData -> if (shouldSwapData) { - (binding.gridGames.adapter as GameAdapter).submitList(gamesViewModel.games.value!!) + (binding.gridGames.adapter as GameAdapter).submitList( + gamesViewModel.games.value!! + ) gamesViewModel.setShouldSwapData(false) } } @@ -128,7 +130,9 @@ class GamesFragment : Fragment() { } private fun setInsets() = - ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view: View, windowInsets: WindowInsetsCompat -> + ViewCompat.setOnApplyWindowInsetsListener( + binding.root + ) { view: View, windowInsets: WindowInsetsCompat -> val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_large) 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 041d16f3a..cc1d87f1b 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 @@ -26,6 +26,9 @@ import androidx.preference.PreferenceManager import com.google.android.material.color.MaterialColors import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.navigation.NavigationBarView +import java.io.File +import java.io.FilenameFilter +import java.io.IOException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -43,9 +46,6 @@ import org.yuzu.yuzu_emu.fragments.MessageDialogFragment import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.utils.* -import java.io.File -import java.io.FilenameFilter -import java.io.IOException class MainActivity : AppCompatActivity(), ThemeProvider { private lateinit var binding: ActivityMainBinding @@ -86,7 +86,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider { ThemeHelper.SYSTEM_BAR_ALPHA ) ) - if (InsetsHelper.getSystemGestureType(applicationContext) != InsetsHelper.GESTURE_NAVIGATION) { + if (InsetsHelper.getSystemGestureType(applicationContext) != + InsetsHelper.GESTURE_NAVIGATION + ) { binding.navigationBarShade.setBackgroundColor( ThemeHelper.getColorWithOpacity( MaterialColors.getColor( @@ -172,7 +174,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider { binding.navigationView.height.toFloat() * 2 translationY(0f) } else { - if (ViewCompat.getLayoutDirection(binding.navigationView) == ViewCompat.LAYOUT_DIRECTION_LTR) { + if (ViewCompat.getLayoutDirection(binding.navigationView) == + ViewCompat.LAYOUT_DIRECTION_LTR + ) { binding.navigationView.translationX = binding.navigationView.width.toFloat() * -2 translationX(0f) @@ -189,7 +193,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider { if (smallLayout) { translationY(binding.navigationView.height.toFloat() * 2) } else { - if (ViewCompat.getLayoutDirection(binding.navigationView) == ViewCompat.LAYOUT_DIRECTION_LTR) { + if (ViewCompat.getLayoutDirection(binding.navigationView) == + ViewCompat.LAYOUT_DIRECTION_LTR + ) { translationX(binding.navigationView.width.toFloat() * -2) } else { translationX(binding.navigationView.width.toFloat() * 2) @@ -234,7 +240,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider { } private fun setInsets() = - ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _: View, windowInsets: WindowInsetsCompat -> + ViewCompat.setOnApplyWindowInsetsListener( + binding.root + ) { _: View, windowInsets: WindowInsetsCompat -> val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) val mlpStatusShade = binding.statusBarShade.layoutParams as MarginLayoutParams mlpStatusShade.height = insets.top @@ -256,8 +264,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider { val getGamesDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result -> - if (result == null) + if (result == null) { return@registerForActivityResult + } contentResolver.takePersistableUriPermission( result, @@ -281,8 +290,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider { val getProdKey = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> - if (result == null) + if (result == null) { return@registerForActivityResult + } if (!FileUtil.hasExtension(result, "keys")) { MessageDialogFragment.newInstance( @@ -324,8 +334,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider { val getFirmware = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> - if (result == null) + if (result == null) { return@registerForActivityResult + } val inputZip = contentResolver.openInputStream(result) if (inputZip == null) { @@ -376,8 +387,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider { val getAmiiboKey = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> - if (result == null) + if (result == null) { return@registerForActivityResult + } if (!FileUtil.hasExtension(result, "bin")) { MessageDialogFragment.newInstance( @@ -418,8 +430,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider { val getDriver = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> - if (result == null) + if (result == null) { return@registerForActivityResult + } val takeFlags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION @@ -470,8 +483,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider { val installGameUpdate = registerForActivityResult(ActivityResultContracts.OpenDocument()) { - if (it == null) + if (it == null) { return@registerForActivityResult + } IndeterminateProgressDialogFragment.newInstance( this@MainActivity, diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ControllerMappingHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ControllerMappingHelper.kt index 791cea904..eeefcdf20 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ControllerMappingHelper.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ControllerMappingHelper.kt @@ -19,7 +19,9 @@ class ControllerMappingHelper { // The two analog triggers generate analog motion events as well as a keycode. // We always prefer to use the analog values, so throw away the button press keyCode == KeyEvent.KEYCODE_BUTTON_L2 || keyCode == KeyEvent.KEYCODE_BUTTON_R2 - } else false + } else { + false + } } /** diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt index 36c479e6c..2ee63697e 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt @@ -4,8 +4,8 @@ package org.yuzu.yuzu_emu.utils import android.content.Context -import org.yuzu.yuzu_emu.NativeLibrary import java.io.IOException +import org.yuzu.yuzu_emu.NativeLibrary object DirectoryInitialization { private var userPath: String? = null diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.kt index cc8ea6b9d..cf226ad94 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.kt @@ -5,10 +5,10 @@ package org.yuzu.yuzu_emu.utils import android.net.Uri import androidx.documentfile.provider.DocumentFile -import org.yuzu.yuzu_emu.YuzuApplication -import org.yuzu.yuzu_emu.model.MinimalDocumentFile import java.io.File import java.util.* +import org.yuzu.yuzu_emu.YuzuApplication +import org.yuzu.yuzu_emu.model.MinimalDocumentFile class DocumentsTree { private var root: DocumentsNode? = null @@ -29,13 +29,20 @@ class DocumentsTree { val node = resolvePath(filepath) return if (node == null || node.isDirectory) { 0 - } else FileUtil.getFileSize(YuzuApplication.appContext, node.uri.toString()) + } else { + FileUtil.getFileSize(YuzuApplication.appContext, node.uri.toString()) + } } fun exists(filepath: String): Boolean { return resolvePath(filepath) != null } + fun isDirectory(filepath: String): Boolean { + val node = resolvePath(filepath) + return node != null && node.isDirectory + } + private fun resolvePath(filepath: String): DocumentsNode? { val tokens = StringTokenizer(filepath, File.separator, false) var iterator = root @@ -106,7 +113,9 @@ class DocumentsTree { fun isNativePath(path: String): Boolean { return if (path.isNotEmpty()) { path[0] == '/' - } else false + } else { + false + } } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/EmulationMenuSettings.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/EmulationMenuSettings.kt index e1e7a59d7..7e8f058c1 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/EmulationMenuSettings.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/EmulationMenuSettings.kt @@ -11,14 +11,6 @@ object EmulationMenuSettings { private val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) - // These must match what is defined in src/core/settings.h - const val LayoutOption_Default = 0 - const val LayoutOption_SingleScreen = 1 - const val LayoutOption_LargeScreen = 2 - const val LayoutOption_SideScreen = 3 - const val LayoutOption_MobilePortrait = 4 - const val LayoutOption_MobileLandscape = 5 - var joystickRelCenter: Boolean get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER, true) set(value) { @@ -41,16 +33,6 @@ object EmulationMenuSettings { .apply() } - var landscapeScreenLayout: Int - get() = preferences.getInt( - Settings.PREF_MENU_SETTINGS_LANDSCAPE, - LayoutOption_MobileLandscape - ) - set(value) { - preferences.edit() - .putInt(Settings.PREF_MENU_SETTINGS_LANDSCAPE, value) - .apply() - } var showFps: Boolean get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_SHOW_FPS, false) set(value) { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt index 492b1ad91..9f3bbe56f 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt @@ -9,8 +9,6 @@ import android.net.Uri import android.provider.DocumentsContract import android.provider.OpenableColumns import androidx.documentfile.provider.DocumentFile -import org.yuzu.yuzu_emu.YuzuApplication -import org.yuzu.yuzu_emu.model.MinimalDocumentFile import java.io.BufferedInputStream import java.io.File import java.io.FileOutputStream @@ -19,6 +17,8 @@ import java.io.InputStream import java.net.URLDecoder import java.util.zip.ZipEntry import java.util.zip.ZipInputStream +import org.yuzu.yuzu_emu.YuzuApplication +import org.yuzu.yuzu_emu.model.MinimalDocumentFile object FileUtil { const val PATH_TREE = "tree" diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ForegroundService.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ForegroundService.kt index dc9b7c744..086d17606 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ForegroundService.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ForegroundService.kt @@ -54,7 +54,7 @@ class ForegroundService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (intent == null) { - return START_NOT_STICKY; + return START_NOT_STICKY } if (intent.action == ACTION_STOP) { NotificationManagerCompat.from(this).cancel(EMULATION_RUNNING_NOTIFICATION) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt index 42b207618..ee9f3e570 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt @@ -6,12 +6,12 @@ package org.yuzu.yuzu_emu.utils import android.content.SharedPreferences import android.net.Uri import androidx.preference.PreferenceManager +import java.util.* import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.model.Game -import java.util.* object GameHelper { const val KEY_GAME_PATH = "game_path" diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt index 528011d7f..dad159481 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt @@ -5,14 +5,14 @@ package org.yuzu.yuzu_emu.utils import android.content.Context import android.net.Uri -import org.yuzu.yuzu_emu.NativeLibrary -import org.yuzu.yuzu_emu.utils.FileUtil.copyUriToInternalStorage import java.io.BufferedInputStream import java.io.File import java.io.FileInputStream import java.io.FileOutputStream import java.io.IOException import java.util.zip.ZipInputStream +import org.yuzu.yuzu_emu.NativeLibrary +import org.yuzu.yuzu_emu.utils.FileUtil.copyUriToInternalStorage object GpuDriverHelper { private const val META_JSON_FILENAME = "meta.json" diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt index 70bdb4097..a4e64070a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt @@ -3,12 +3,12 @@ package org.yuzu.yuzu_emu.utils -import org.json.JSONException -import org.json.JSONObject import java.io.IOException import java.nio.charset.StandardCharsets import java.nio.file.Files import java.nio.file.Paths +import org.json.JSONException +import org.json.JSONObject class GpuDriverMetadata(metadataFilePath: String) { var name: String? = null 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 24e999b29..e963dfbc1 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 @@ -5,8 +5,8 @@ package org.yuzu.yuzu_emu.utils import android.view.KeyEvent import android.view.MotionEvent -import org.yuzu.yuzu_emu.NativeLibrary import kotlin.math.sqrt +import org.yuzu.yuzu_emu.NativeLibrary class InputHandler { fun initialize() { @@ -68,7 +68,11 @@ class InputHandler { 6 -> NativeLibrary.Player6Device 7 -> NativeLibrary.Player7Device 8 -> NativeLibrary.Player8Device - else -> if (NativeLibrary.isHandheldOnly()) NativeLibrary.ConsoleDevice else NativeLibrary.Player1Device + else -> if (NativeLibrary.isHandheldOnly()) { + NativeLibrary.ConsoleDevice + } else { + NativeLibrary.Player1Device + } } } @@ -107,7 +111,11 @@ class InputHandler { } private fun getAxisToButton(axis: Float): Int { - return if (axis > 0.5f) NativeLibrary.ButtonState.PRESSED else NativeLibrary.ButtonState.RELEASED + return if (axis > 0.5f) { + NativeLibrary.ButtonState.PRESSED + } else { + NativeLibrary.ButtonState.RELEASED + } } private fun setAxisDpadState(playerNumber: Int, xAxis: Float, yAxis: Float) { @@ -287,7 +295,6 @@ class InputHandler { } } - private fun setJoyconAxisInput(event: MotionEvent, axis: Int) { // Joycon support is half dead. Right joystick doesn't work val playerNumber = getPlayerNumber(event.device.controllerNumber) @@ -355,6 +362,4 @@ class InputHandler { ) } } - - -}
\ No newline at end of file +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InsetsHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InsetsHelper.kt index 19c53c481..595f0d284 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InsetsHelper.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InsetsHelper.kt @@ -4,9 +4,7 @@ package org.yuzu.yuzu_emu.utils import android.annotation.SuppressLint -import android.app.Activity import android.content.Context -import android.graphics.Rect object InsetsHelper { const val THREE_BUTTON_NAVIGATION = 0 @@ -20,12 +18,8 @@ object InsetsHelper { resources.getIdentifier("config_navBarInteractionMode", "integer", "android") return if (resourceId != 0) { resources.getInteger(resourceId) - } else 0 - } - - fun getBottomPaddingRequired(activity: Activity): Int { - val visibleFrame = Rect() - activity.window.decorView.getWindowVisibleDisplayFrame(visibleFrame) - return visibleFrame.bottom - visibleFrame.top - activity.resources.displayMetrics.heightPixels + } else { + 0 + } } } 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 344dd8a0a..68ed66565 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 @@ -13,8 +13,8 @@ import android.nfc.tech.NfcA import android.os.Build import android.os.Handler import android.os.Looper -import org.yuzu.yuzu_emu.NativeLibrary import java.io.IOException +import org.yuzu.yuzu_emu.NativeLibrary class NfcReader(private val activity: Activity) { private var nfcAdapter: NfcAdapter? = null @@ -25,10 +25,13 @@ class NfcReader(private val activity: Activity) { pendingIntent = PendingIntent.getActivity( activity, - 0, Intent(activity, activity.javaClass), - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + 0, + Intent(activity, activity.javaClass), + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE - else PendingIntent.FLAG_UPDATE_CURRENT + } else { + PendingIntent.FLAG_UPDATE_CURRENT + } ) val tagDetected = IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED) @@ -45,9 +48,9 @@ class NfcReader(private val activity: Activity) { fun onNewIntent(intent: Intent) { val action = intent.action - if (NfcAdapter.ACTION_TAG_DISCOVERED != action - && NfcAdapter.ACTION_TECH_DISCOVERED != action - && NfcAdapter.ACTION_NDEF_DISCOVERED != action + if (NfcAdapter.ACTION_TAG_DISCOVERED != action && + NfcAdapter.ACTION_TECH_DISCOVERED != action && + NfcAdapter.ACTION_NDEF_DISCOVERED != action ) { return } @@ -84,7 +87,7 @@ class NfcReader(private val activity: Activity) { } private fun ntag215ReadAll(amiibo: NfcA): ByteArray? { - val bufferSize = amiibo.maxTransceiveLength; + val bufferSize = amiibo.maxTransceiveLength val tagSize = 0x21C val pageSize = 4 val lastPage = tagSize / pageSize - 1 @@ -103,7 +106,7 @@ class NfcReader(private val activity: Activity) { val data = ntag215FastRead(amiibo, dataStart, dataEnd - 1) System.arraycopy(data, 0, tagData, i, (dataEnd - dataStart) * pageSize) } catch (e: IOException) { - return null; + return null } } return tagData diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/SerializableHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/SerializableHelper.kt index 87ee7f2e6..00e58faec 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/SerializableHelper.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/SerializableHelper.kt @@ -11,30 +11,34 @@ import java.io.Serializable object SerializableHelper { inline fun <reified T : Serializable> Bundle.serializable(key: String): T? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { getSerializable(key, T::class.java) - else + } else { getSerializable(key) as? T + } } inline fun <reified T : Serializable> Intent.serializable(key: String): T? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { getSerializableExtra(key, T::class.java) - else + } else { getSerializableExtra(key) as? T + } } inline fun <reified T : Parcelable> Bundle.parcelable(key: String): T? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { getParcelable(key, T::class.java) - else + } else { getParcelable(key) as? T + } } inline fun <reified T : Parcelable> Intent.parcelable(key: String): T? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { getParcelableExtra(key, T::class.java) - else + } else { getParcelableExtra(key) as? T + } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ThemeHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ThemeHelper.kt index e55767c0f..f312e24cf 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ThemeHelper.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ThemeHelper.kt @@ -3,21 +3,19 @@ package org.yuzu.yuzu_emu.utils -import android.app.Activity import android.content.res.Configuration import android.graphics.Color import androidx.annotation.ColorInt import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate -import androidx.core.content.ContextCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsControllerCompat import androidx.preference.PreferenceManager +import kotlin.math.roundToInt import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.ui.main.ThemeProvider -import kotlin.math.roundToInt object ThemeHelper { const val SYSTEM_BAR_ALPHA = 0.9f @@ -36,8 +34,8 @@ object ThemeHelper { // Using a specific night mode check because this could apply incorrectly when using the // light app mode, dark system mode, and black backgrounds. Launching the settings activity // will then show light mode colors/navigation bars but with black backgrounds. - if (preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false) - && isNightMode(activity) + if (preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false) && + isNightMode(activity) ) { activity.setTheme(R.style.ThemeOverlay_Yuzu_Dark) } @@ -46,8 +44,10 @@ object ThemeHelper { @ColorInt fun getColorWithOpacity(@ColorInt color: Int, alphaFactor: Float): Int { return Color.argb( - (alphaFactor * Color.alpha(color)).roundToInt(), Color.red(color), - Color.green(color), Color.blue(color) + (alphaFactor * Color.alpha(color)).roundToInt(), + Color.red(color), + Color.green(color), + Color.blue(color) ) } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/views/FixedRatioSurfaceView.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/views/FixedRatioSurfaceView.kt index d89a89983..685ccaa76 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/views/FixedRatioSurfaceView.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/views/FixedRatioSurfaceView.kt @@ -38,8 +38,8 @@ class FixedRatioSurfaceView @JvmOverloads constructor( newWidth = width newHeight = (width / aspectRatio).roundToInt() } - val left = (width - newWidth) / 2; - val top = (height - newHeight) / 2; + val left = (width - newWidth) / 2 + val top = (height - newHeight) / 2 setLeftTopRightBottom(left, top, left + newWidth, top + newHeight) } else { setLeftTopRightBottom(0, 0, width, height) diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 4091c23d1..f9617202b 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -202,6 +202,11 @@ public: return m_is_running; } + bool IsPaused() const { + std::scoped_lock lock(m_mutex); + return m_is_running && m_is_paused; + } + const Core::PerfStatsResults& PerfStats() const { std::scoped_lock m_perf_stats_lock(m_perf_stats_mutex); return m_perf_stats; @@ -287,11 +292,13 @@ public: void PauseEmulation() { std::scoped_lock lock(m_mutex); m_system.Pause(); + m_is_paused = true; } void UnPauseEmulation() { std::scoped_lock lock(m_mutex); m_system.Run(); + m_is_paused = false; } void HaltEmulation() { @@ -473,6 +480,7 @@ private: std::shared_ptr<FileSys::VfsFilesystem> m_vfs; Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized}; bool m_is_running{}; + bool m_is_paused{}; SoftwareKeyboard::AndroidKeyboard* m_software_keyboard{}; std::unique_ptr<Service::Account::ProfileManager> m_profile_manager; @@ -583,6 +591,11 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isRunning([[maybe_unused]] JNIEnv return static_cast<jboolean>(EmulationSession::GetInstance().IsRunning()); } +jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isPaused([[maybe_unused]] JNIEnv* env, + [[maybe_unused]] jclass clazz) { + return static_cast<jboolean>(EmulationSession::GetInstance().IsPaused()); +} + jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isHandheldOnly([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz) { return EmulationSession::GetInstance().IsHandheldOnly(); diff --git a/src/android/app/src/main/res/drawable/ic_pip_pause.xml b/src/android/app/src/main/res/drawable/ic_pip_pause.xml new file mode 100644 index 000000000..4a7d4ea03 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_pip_pause.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportHeight="24" + android:viewportWidth="24"> + <path + android:fillColor="@android:color/white" + android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z" /> +</vector> diff --git a/src/android/app/src/main/res/drawable/ic_pip_play.xml b/src/android/app/src/main/res/drawable/ic_pip_play.xml new file mode 100644 index 000000000..2303a4623 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_pip_play.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportHeight="24" + android:viewportWidth="24"> + <path + android:fillColor="@android:color/white" + android:pathData="M8,5v14l11,-7z" /> +</vector> diff --git a/src/android/app/src/main/res/layout/fragment_emulation.xml b/src/android/app/src/main/res/layout/fragment_emulation.xml index 09b789b6b..e54a10e8f 100644 --- a/src/android/app/src/main/res/layout/fragment_emulation.xml +++ b/src/android/app/src/main/res/layout/fragment_emulation.xml @@ -12,49 +12,65 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - <!-- This is what everything is rendered to during emulation --> - <org.yuzu.yuzu_emu.views.FixedRatioSurfaceView - android:id="@+id/surface_emulation" + <FrameLayout + android:id="@+id/emulation_container" android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_gravity="center" - android:focusable="false" - android:focusableInTouchMode="false" /> + android:layout_height="match_parent"> + + <!-- This is what everything is rendered to during emulation --> + <org.yuzu.yuzu_emu.views.FixedRatioSurfaceView + android:id="@+id/surface_emulation" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:focusable="false" + android:focusableInTouchMode="false" /> + + </FrameLayout> <FrameLayout - android:id="@+id/overlay_container" + android:id="@+id/input_container" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="bottom"> - <!-- This is the onscreen input overlay --> - <org.yuzu.yuzu_emu.overlay.InputOverlay - android:id="@+id/surface_input_overlay" + <!-- This is the onscreen input overlay --> + <org.yuzu.yuzu_emu.overlay.InputOverlay + android:id="@+id/surface_input_overlay" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:focusable="true" + android:focusableInTouchMode="true" /> + + <Button + style="@style/Widget.Material3.Button.ElevatedButton" + android:id="@+id/done_control_config" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:text="@string/emulation_done" + android:visibility="gone" /> + + </FrameLayout> + + <FrameLayout + android:id="@+id/overlay_container" android:layout_width="match_parent" - android:layout_height="match_parent" - android:focusable="true" - android:focusableInTouchMode="true" /> + android:layout_height="match_parent"> - <TextView - android:id="@+id/show_fps_text" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="left" - android:clickable="false" - android:focusable="false" - android:shadowColor="@android:color/black" - android:textColor="@android:color/white" - android:textSize="12sp" - tools:ignore="RtlHardcoded" /> + <TextView + android:id="@+id/show_fps_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="left" + android:clickable="false" + android:focusable="false" + android:shadowColor="@android:color/black" + android:textColor="@android:color/white" + android:textSize="12sp" + tools:ignore="RtlHardcoded" /> - <Button - style="@style/Widget.Material3.Button.ElevatedButton" - android:id="@+id/done_control_config" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:text="@string/emulation_done" - android:visibility="gone" /> </FrameLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/src/android/app/src/main/res/layout/list_item_setting_switch.xml b/src/android/app/src/main/res/layout/list_item_setting_switch.xml index 599d845ad..a5767adee 100644 --- a/src/android/app/src/main/res/layout/list_item_setting_switch.xml +++ b/src/android/app/src/main/res/layout/list_item_setting_switch.xml @@ -1,16 +1,16 @@ <?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:layout_width="match_parent" android:layout_height="wrap_content" - xmlns:app="http://schemas.android.com/apk/res-auto" android:background="?android:attr/selectableItemBackground" android:clickable="true" android:focusable="true" android:minHeight="72dp" + android:paddingVertical="@dimen/spacing_large" android:paddingStart="@dimen/spacing_large" - android:paddingEnd="24dp" - android:paddingVertical="@dimen/spacing_large"> + android:paddingEnd="24dp"> <com.google.android.material.materialswitch.MaterialSwitch android:id="@+id/switch_widget" @@ -19,32 +19,35 @@ android:layout_alignParentEnd="true" android:layout_centerVertical="true" /> - <com.google.android.material.textview.MaterialTextView - style="@style/TextAppearance.Material3.BodySmall" - android:id="@+id/text_setting_description" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_alignParentStart="true" - android:layout_alignStart="@+id/text_setting_name" - android:layout_below="@+id/text_setting_name" - android:layout_marginEnd="@dimen/spacing_large" - android:layout_marginTop="@dimen/spacing_small" - android:layout_toStartOf="@+id/switch_widget" - android:textAlignment="viewStart" - tools:text="@string/frame_limit_enable_description" /> - - <com.google.android.material.textview.MaterialTextView - style="@style/TextAppearance.Material3.HeadlineMedium" - android:id="@+id/text_setting_name" - android:layout_width="0dp" + <LinearLayout + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_alignParentStart="true" android:layout_alignParentTop="true" + android:layout_centerVertical="true" android:layout_marginEnd="@dimen/spacing_large" android:layout_toStartOf="@+id/switch_widget" - android:textSize="16sp" - android:textAlignment="viewStart" - app:lineHeight="28dp" - tools:text="@string/frame_limit_enable" /> + android:gravity="center_vertical" + android:orientation="vertical"> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/text_setting_name" + style="@style/TextAppearance.Material3.HeadlineMedium" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAlignment="viewStart" + android:textSize="16sp" + app:lineHeight="28dp" + tools:text="@string/frame_limit_enable" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/text_setting_description" + style="@style/TextAppearance.Material3.BodySmall" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_small" + android:textAlignment="viewStart" + tools:text="@string/frame_limit_enable_description" /> + + </LinearLayout> </RelativeLayout> diff --git a/src/android/app/src/main/res/layout/list_item_settings_header.xml b/src/android/app/src/main/res/layout/list_item_settings_header.xml index abd24df6f..cf85bc0da 100644 --- a/src/android/app/src/main/res/layout/list_item_settings_header.xml +++ b/src/android/app/src/main/res/layout/list_item_settings_header.xml @@ -1,20 +1,14 @@ <?xml version="1.0" encoding="utf-8"?> -<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" +<com.google.android.material.textview.MaterialTextView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/text_header_name" + style="@style/TextAppearance.Material3.TitleSmall" android:layout_width="match_parent" - android:layout_height="48dp" - android:paddingVertical="4dp" - android:paddingHorizontal="@dimen/spacing_large"> - - <com.google.android.material.textview.MaterialTextView - style="@style/TextAppearance.Material3.TitleSmall" - android:id="@+id/text_header_name" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_gravity="start|center_vertical" - android:textColor="?attr/colorPrimary" - android:textAlignment="viewStart" - android:textStyle="bold" - tools:text="CPU Settings" /> - -</FrameLayout> + android:layout_height="wrap_content" + android:layout_gravity="start|center_vertical" + android:paddingHorizontal="@dimen/spacing_large" + android:paddingVertical="16dp" + android:textAlignment="viewStart" + android:textColor="?attr/colorPrimary" + android:textStyle="bold" + tools:text="CPU Settings" /> diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml index ea20cb17c..6d092f7a9 100644 --- a/src/android/app/src/main/res/values/arrays.xml +++ b/src/android/app/src/main/res/values/arrays.xml @@ -119,6 +119,18 @@ <item>3</item> </integer-array> + <string-array name="rendererScreenLayoutNames"> + <item>@string/screen_layout_landscape</item> + <item>@string/screen_layout_portrait</item> + <item>@string/screen_layout_auto</item> + </string-array> + + <integer-array name="rendererScreenLayoutValues"> + <item>5</item> + <item>4</item> + <item>0</item> + </integer-array> + <string-array name="rendererAspectRatioNames"> <item>@string/ratio_default</item> <item>@string/ratio_force_four_three</item> @@ -224,4 +236,15 @@ <item>2</item> </integer-array> + <string-array name="outputEngineEntries"> + <item>@string/auto</item> + <item>@string/cubeb</item> + <item>@string/string_null</item> + </string-array> + <string-array name="outputEngineValues"> + <item>auto</item> + <item>cubeb</item> + <item>null</item> + </string-array> + </resources> diff --git a/src/android/app/src/main/res/values/integers.xml b/src/android/app/src/main/res/values/integers.xml index bc614b81d..2e93b408c 100644 --- a/src/android/app/src/main/res/values/integers.xml +++ b/src/android/app/src/main/res/values/integers.xml @@ -34,4 +34,68 @@ <integer name="SWITCH_BUTTON_DPAD_X">260</integer> <integer name="SWITCH_BUTTON_DPAD_Y">790</integer> + <!-- Default SWITCH portrait layout --> + <integer name="SWITCH_BUTTON_A_X_PORTRAIT">840</integer> + <integer name="SWITCH_BUTTON_A_Y_PORTRAIT">840</integer> + <integer name="SWITCH_BUTTON_B_X_PORTRAIT">740</integer> + <integer name="SWITCH_BUTTON_B_Y_PORTRAIT">880</integer> + <integer name="SWITCH_BUTTON_X_X_PORTRAIT">740</integer> + <integer name="SWITCH_BUTTON_X_Y_PORTRAIT">800</integer> + <integer name="SWITCH_BUTTON_Y_X_PORTRAIT">640</integer> + <integer name="SWITCH_BUTTON_Y_Y_PORTRAIT">840</integer> + <integer name="SWITCH_STICK_L_X_PORTRAIT">180</integer> + <integer name="SWITCH_STICK_L_Y_PORTRAIT">660</integer> + <integer name="SWITCH_STICK_R_X_PORTRAIT">820</integer> + <integer name="SWITCH_STICK_R_Y_PORTRAIT">660</integer> + <integer name="SWITCH_TRIGGER_L_X_PORTRAIT">140</integer> + <integer name="SWITCH_TRIGGER_L_Y_PORTRAIT">260</integer> + <integer name="SWITCH_TRIGGER_R_X_PORTRAIT">860</integer> + <integer name="SWITCH_TRIGGER_R_Y_PORTRAIT">260</integer> + <integer name="SWITCH_TRIGGER_ZL_X_PORTRAIT">140</integer> + <integer name="SWITCH_TRIGGER_ZL_Y_PORTRAIT">200</integer> + <integer name="SWITCH_TRIGGER_ZR_X_PORTRAIT">860</integer> + <integer name="SWITCH_TRIGGER_ZR_Y_PORTRAIT">200</integer> + <integer name="SWITCH_BUTTON_MINUS_X_PORTRAIT">440</integer> + <integer name="SWITCH_BUTTON_MINUS_Y_PORTRAIT">950</integer> + <integer name="SWITCH_BUTTON_PLUS_X_PORTRAIT">560</integer> + <integer name="SWITCH_BUTTON_PLUS_Y_PORTRAIT">950</integer> + <integer name="SWITCH_BUTTON_HOME_X_PORTRAIT">680</integer> + <integer name="SWITCH_BUTTON_HOME_Y_PORTRAIT">950</integer> + <integer name="SWITCH_BUTTON_CAPTURE_X_PORTRAIT">320</integer> + <integer name="SWITCH_BUTTON_CAPTURE_Y_PORTRAIT">950</integer> + <integer name="SWITCH_BUTTON_DPAD_X_PORTRAIT">240</integer> + <integer name="SWITCH_BUTTON_DPAD_Y_PORTRAIT">840</integer> + + <!-- Default SWITCH foldable layout --> + <integer name="SWITCH_BUTTON_A_X_FOLDABLE">840</integer> + <integer name="SWITCH_BUTTON_A_Y_FOLDABLE">390</integer> + <integer name="SWITCH_BUTTON_B_X_FOLDABLE">740</integer> + <integer name="SWITCH_BUTTON_B_Y_FOLDABLE">430</integer> + <integer name="SWITCH_BUTTON_X_X_FOLDABLE">740</integer> + <integer name="SWITCH_BUTTON_X_Y_FOLDABLE">350</integer> + <integer name="SWITCH_BUTTON_Y_X_FOLDABLE">640</integer> + <integer name="SWITCH_BUTTON_Y_Y_FOLDABLE">390</integer> + <integer name="SWITCH_STICK_L_X_FOLDABLE">180</integer> + <integer name="SWITCH_STICK_L_Y_FOLDABLE">250</integer> + <integer name="SWITCH_STICK_R_X_FOLDABLE">820</integer> + <integer name="SWITCH_STICK_R_Y_FOLDABLE">250</integer> + <integer name="SWITCH_TRIGGER_L_X_FOLDABLE">140</integer> + <integer name="SWITCH_TRIGGER_L_Y_FOLDABLE">130</integer> + <integer name="SWITCH_TRIGGER_R_X_FOLDABLE">860</integer> + <integer name="SWITCH_TRIGGER_R_Y_FOLDABLE">130</integer> + <integer name="SWITCH_TRIGGER_ZL_X_FOLDABLE">140</integer> + <integer name="SWITCH_TRIGGER_ZL_Y_FOLDABLE">70</integer> + <integer name="SWITCH_TRIGGER_ZR_X_FOLDABLE">860</integer> + <integer name="SWITCH_TRIGGER_ZR_Y_FOLDABLE">70</integer> + <integer name="SWITCH_BUTTON_MINUS_X_FOLDABLE">440</integer> + <integer name="SWITCH_BUTTON_MINUS_Y_FOLDABLE">470</integer> + <integer name="SWITCH_BUTTON_PLUS_X_FOLDABLE">560</integer> + <integer name="SWITCH_BUTTON_PLUS_Y_FOLDABLE">470</integer> + <integer name="SWITCH_BUTTON_HOME_X_FOLDABLE">680</integer> + <integer name="SWITCH_BUTTON_HOME_Y_FOLDABLE">470</integer> + <integer name="SWITCH_BUTTON_CAPTURE_X_FOLDABLE">320</integer> + <integer name="SWITCH_BUTTON_CAPTURE_Y_FOLDABLE">470</integer> + <integer name="SWITCH_BUTTON_DPAD_X_FOLDABLE">240</integer> + <integer name="SWITCH_BUTTON_DPAD_Y_FOLDABLE">390</integer> + </resources> diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index c236811fa..cc1d8c39d 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<resources> +<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"> <!-- General application strings --> <string name="app_name" translatable="false">yuzu</string> @@ -158,10 +158,10 @@ <string name="set_custom_rtc">Set custom RTC</string> <!-- Graphics settings strings --> - <string name="renderer_api">API</string> <string name="renderer_accuracy">Accuracy level</string> <string name="renderer_resolution">Resolution (Handheld/Docked)</string> <string name="renderer_vsync">VSync mode</string> + <string name="renderer_screen_layout">Orientation</string> <string name="renderer_aspect_ratio">Aspect ratio</string> <string name="renderer_scaling_filter">Window adapting filter</string> <string name="renderer_anti_aliasing">Anti-aliasing method</string> @@ -171,12 +171,21 @@ <string name="renderer_asynchronous_shaders_description">Compiles shaders asynchronously, reducing stutter but may introduce glitches.</string> <string name="renderer_reactive_flushing">Use reactive flushing</string> <string name="renderer_reactive_flushing_description">Improves rendering accuracy in some games at the cost of performance.</string> - <string name="renderer_debug">Graphics debugging</string> - <string name="renderer_debug_description">Sets the graphics API to a slow debugging mode.</string> <string name="use_disk_shader_cache">Disk shader cache</string> <string name="use_disk_shader_cache_description">Reduces stuttering by locally storing and loading generated shaders.</string> + <!-- Debug settings strings --> + <string name="cpu">CPU</string> + <string name="cpu_debug_mode">CPU Debugging</string> + <string name="cpu_debug_mode_description">Puts the CPU in a slow debugging mode.</string> + <string name="gpu">GPU</string> + <string name="renderer_api">API</string> + <string name="renderer_debug">Graphics debugging</string> + <string name="renderer_debug_description">Sets the graphics API to a slow debugging mode.</string> + <string name="fastmem">Fastmem</string> + <!-- Audio settings strings --> + <string name="audio_output_engine">Output engine</string> <string name="audio_volume">Volume</string> <string name="audio_volume_description">Specifies the volume of audio output.</string> @@ -195,6 +204,7 @@ <string name="learn_more">Learn more</string> <string name="auto">Auto</string> <string name="submit">Submit</string> + <string name="string_null">Null</string> <!-- GPU driver installation --> <string name="select_gpu_driver">Select GPU driver</string> @@ -326,6 +336,11 @@ <string name="anti_aliasing_fxaa">FXAA</string> <string name="anti_aliasing_smaa">SMAA</string> + <!-- Screen Layouts --> + <string name="screen_layout_landscape">Landscape</string> + <string name="screen_layout_portrait">Portrait</string> + <string name="screen_layout_auto">Auto</string> + <!-- Aspect Ratios --> <string name="ratio_default">Default (16:9)</string> <string name="ratio_force_four_three">Force 4:3</string> @@ -360,10 +375,19 @@ <string name="theme_mode_light">Light</string> <string name="theme_mode_dark">Dark</string> + <!-- Audio output engines --> + <string name="cubeb">cubeb</string> + <!-- Black backgrounds theme --> <string name="use_black_backgrounds">Black backgrounds</string> <string name="use_black_backgrounds_description">When using the dark theme, apply black backgrounds.</string> + <!-- Picture-In-Picture --> + <string name="picture_in_picture">Picture in Picture</string> + <string name="picture_in_picture_description">Minimize window when placed in the background</string> + <string name="pause">Pause</string> + <string name="play">Play</string> + <!-- Licenses screen strings --> <string name="licenses">Licenses</string> <string name="license_fidelityfx_fsr" translatable="false">FidelityFX-FSR</string> diff --git a/src/common/fs/fs.cpp b/src/common/fs/fs.cpp index e1716c62d..6d66c926d 100644 --- a/src/common/fs/fs.cpp +++ b/src/common/fs/fs.cpp @@ -3,6 +3,9 @@ #include "common/fs/file.h" #include "common/fs/fs.h" +#ifdef ANDROID +#include "common/fs/fs_android.h" +#endif #include "common/fs/path_util.h" #include "common/logging/log.h" @@ -525,15 +528,39 @@ void IterateDirEntriesRecursively(const std::filesystem::path& path, // Generic Filesystem Operations bool Exists(const fs::path& path) { +#ifdef ANDROID + if (Android::IsContentUri(path)) { + return Android::Exists(path); + } else { + return fs::exists(path); + } +#else return fs::exists(path); +#endif } bool IsFile(const fs::path& path) { +#ifdef ANDROID + if (Android::IsContentUri(path)) { + return !Android::IsDirectory(path); + } else { + return fs::is_regular_file(path); + } +#else return fs::is_regular_file(path); +#endif } bool IsDir(const fs::path& path) { +#ifdef ANDROID + if (Android::IsContentUri(path)) { + return Android::IsDirectory(path); + } else { + return fs::is_directory(path); + } +#else return fs::is_directory(path); +#endif } fs::path GetCurrentDir() { diff --git a/src/common/fs/fs_android.h b/src/common/fs/fs_android.h index bb8a52648..b441c2a12 100644 --- a/src/common/fs/fs_android.h +++ b/src/common/fs/fs_android.h @@ -12,7 +12,10 @@ "openContentUri", "(Ljava/lang/String;Ljava/lang/String;)I") #define ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(V) \ - V(GetSize, std::uint64_t, get_size, CallStaticLongMethod, "getSize", "(Ljava/lang/String;)J") + V(GetSize, std::uint64_t, get_size, CallStaticLongMethod, "getSize", "(Ljava/lang/String;)J") \ + V(IsDirectory, bool, is_directory, CallStaticBooleanMethod, "isDirectory", \ + "(Ljava/lang/String;)Z") \ + V(Exists, bool, file_exists, CallStaticBooleanMethod, "exists", "(Ljava/lang/String;)Z") namespace Common::FS::Android { diff --git a/src/core/hle/service/nfc/common/amiibo_crypto.cpp b/src/core/hle/service/nfc/common/amiibo_crypto.cpp index b2bcb68c3..bc232c334 100644 --- a/src/core/hle/service/nfc/common/amiibo_crypto.cpp +++ b/src/core/hle/service/nfc/common/amiibo_crypto.cpp @@ -36,12 +36,12 @@ bool IsAmiiboValid(const EncryptedNTAG215File& ntag_file) { // Validate UUID constexpr u8 CT = 0x88; // As defined in `ISO / IEC 14443 - 3` - if ((CT ^ ntag_file.uuid.uid[0] ^ ntag_file.uuid.uid[1] ^ ntag_file.uuid.uid[2]) != - ntag_file.uuid.uid[3]) { + if ((CT ^ ntag_file.uuid.part1[0] ^ ntag_file.uuid.part1[1] ^ ntag_file.uuid.part1[2]) != + ntag_file.uuid.crc_check1) { return false; } - if ((ntag_file.uuid.uid[4] ^ ntag_file.uuid.uid[5] ^ ntag_file.uuid.uid[6] ^ - ntag_file.uuid.nintendo_id) != ntag_file.uuid.lock_bytes[0]) { + if ((ntag_file.uuid.part2[0] ^ ntag_file.uuid.part2[1] ^ ntag_file.uuid.part2[2] ^ + ntag_file.uuid.nintendo_id) != ntag_file.uuid_crc_check2) { return false; } @@ -74,8 +74,9 @@ bool IsAmiiboValid(const NTAG215File& ntag_file) { NTAG215File NfcDataToEncodedData(const EncryptedNTAG215File& nfc_data) { NTAG215File encoded_data{}; - encoded_data.uid = nfc_data.uuid.uid; - encoded_data.nintendo_id = nfc_data.uuid.nintendo_id; + encoded_data.uid = nfc_data.uuid; + encoded_data.uid_crc_check2 = nfc_data.uuid_crc_check2; + encoded_data.internal_number = nfc_data.internal_number; encoded_data.static_lock = nfc_data.static_lock; encoded_data.compability_container = nfc_data.compability_container; encoded_data.hmac_data = nfc_data.user_memory.hmac_data; @@ -94,7 +95,6 @@ NTAG215File NfcDataToEncodedData(const EncryptedNTAG215File& nfc_data) { encoded_data.register_info_crc = nfc_data.user_memory.register_info_crc; encoded_data.application_area = nfc_data.user_memory.application_area; encoded_data.hmac_tag = nfc_data.user_memory.hmac_tag; - encoded_data.lock_bytes = nfc_data.uuid.lock_bytes; encoded_data.model_info = nfc_data.user_memory.model_info; encoded_data.keygen_salt = nfc_data.user_memory.keygen_salt; encoded_data.dynamic_lock = nfc_data.dynamic_lock; @@ -108,9 +108,9 @@ NTAG215File NfcDataToEncodedData(const EncryptedNTAG215File& nfc_data) { EncryptedNTAG215File EncodedDataToNfcData(const NTAG215File& encoded_data) { EncryptedNTAG215File nfc_data{}; - nfc_data.uuid.uid = encoded_data.uid; - nfc_data.uuid.nintendo_id = encoded_data.nintendo_id; - nfc_data.uuid.lock_bytes = encoded_data.lock_bytes; + nfc_data.uuid = encoded_data.uid; + nfc_data.uuid_crc_check2 = encoded_data.uid_crc_check2; + nfc_data.internal_number = encoded_data.internal_number; nfc_data.static_lock = encoded_data.static_lock; nfc_data.compability_container = encoded_data.compability_container; nfc_data.user_memory.hmac_data = encoded_data.hmac_data; @@ -139,23 +139,12 @@ EncryptedNTAG215File EncodedDataToNfcData(const NTAG215File& encoded_data) { return nfc_data; } -u32 GetTagPassword(const TagUuid& uuid) { - // Verify that the generated password is correct - u32 password = 0xAA ^ (uuid.uid[1] ^ uuid.uid[3]); - password &= (0x55 ^ (uuid.uid[2] ^ uuid.uid[4])) << 8; - password &= (0xAA ^ (uuid.uid[3] ^ uuid.uid[5])) << 16; - password &= (0x55 ^ (uuid.uid[4] ^ uuid.uid[6])) << 24; - return password; -} - HashSeed GetSeed(const NTAG215File& data) { HashSeed seed{ .magic = data.write_counter, .padding = {}, .uid_1 = data.uid, - .nintendo_id_1 = data.nintendo_id, .uid_2 = data.uid, - .nintendo_id_2 = data.nintendo_id, .keygen_salt = data.keygen_salt, }; @@ -177,10 +166,11 @@ std::vector<u8> GenerateInternalKey(const InternalKey& key, const HashSeed& seed output.insert(output.end(), key.magic_bytes.begin(), key.magic_bytes.begin() + key.magic_length); - output.insert(output.end(), seed.uid_1.begin(), seed.uid_1.end()); - output.emplace_back(seed.nintendo_id_1); - output.insert(output.end(), seed.uid_2.begin(), seed.uid_2.end()); - output.emplace_back(seed.nintendo_id_2); + std::array<u8, sizeof(NFP::TagUuid)> seed_uuid{}; + memcpy(seed_uuid.data(), &seed.uid_1, sizeof(NFP::TagUuid)); + output.insert(output.end(), seed_uuid.begin(), seed_uuid.end()); + memcpy(seed_uuid.data(), &seed.uid_2, sizeof(NFP::TagUuid)); + output.insert(output.end(), seed_uuid.begin(), seed_uuid.end()); for (std::size_t i = 0; i < sizeof(seed.keygen_salt); i++) { output.emplace_back(static_cast<u8>(seed.keygen_salt[i] ^ key.xor_pad[i])); @@ -264,8 +254,8 @@ void Cipher(const DerivedKeys& keys, const NTAG215File& in_data, NTAG215File& ou // Copy the rest of the data directly out_data.uid = in_data.uid; - out_data.nintendo_id = in_data.nintendo_id; - out_data.lock_bytes = in_data.lock_bytes; + out_data.uid_crc_check2 = in_data.uid_crc_check2; + out_data.internal_number = in_data.internal_number; out_data.static_lock = in_data.static_lock; out_data.compability_container = in_data.compability_container; diff --git a/src/core/hle/service/nfc/common/amiibo_crypto.h b/src/core/hle/service/nfc/common/amiibo_crypto.h index bf3044ed9..6a3e0841e 100644 --- a/src/core/hle/service/nfc/common/amiibo_crypto.h +++ b/src/core/hle/service/nfc/common/amiibo_crypto.h @@ -24,10 +24,8 @@ using DrgbOutput = std::array<u8, 0x20>; struct HashSeed { u16_be magic; std::array<u8, 0xE> padding; - NFC::UniqueSerialNumber uid_1; - u8 nintendo_id_1; - NFC::UniqueSerialNumber uid_2; - u8 nintendo_id_2; + TagUuid uid_1; + TagUuid uid_2; std::array<u8, 0x20> keygen_salt; }; static_assert(sizeof(HashSeed) == 0x40, "HashSeed is an invalid size"); @@ -69,9 +67,6 @@ NTAG215File NfcDataToEncodedData(const EncryptedNTAG215File& nfc_data); /// Converts from encoded file format to encrypted file format EncryptedNTAG215File EncodedDataToNfcData(const NTAG215File& encoded_data); -/// Returns password needed to allow write access to protected memory -u32 GetTagPassword(const TagUuid& uuid); - // Generates Seed needed for key derivation HashSeed GetSeed(const NTAG215File& data); diff --git a/src/core/hle/service/nfc/common/device.cpp b/src/core/hle/service/nfc/common/device.cpp index b14f682b5..f4b180b06 100644 --- a/src/core/hle/service/nfc/common/device.cpp +++ b/src/core/hle/service/nfc/common/device.cpp @@ -242,34 +242,39 @@ Result NfcDevice::GetTagInfo(NFP::TagInfo& tag_info, bool is_mifare) const { return ResultWrongDeviceState; } - UniqueSerialNumber uuid = encrypted_tag_data.uuid.uid; - - // Generate random UUID to bypass amiibo load limits - if (Settings::values.random_amiibo_id) { - Common::TinyMT rng{}; - rng.Initialize(static_cast<u32>(GetCurrentPosixTime())); - rng.GenerateRandomBytes(uuid.data(), sizeof(UniqueSerialNumber)); - uuid[3] = 0x88 ^ uuid[0] ^ uuid[1] ^ uuid[2]; - } + UniqueSerialNumber uuid{}; + u8 uuid_length{}; + NfcProtocol protocol{NfcProtocol::TypeA}; + TagType tag_type{TagType::Type2}; if (is_mifare) { - tag_info = { - .uuid = uuid, - .uuid_extension = {}, - .uuid_length = static_cast<u8>(uuid.size()), - .protocol = NfcProtocol::TypeA, - .tag_type = TagType::Type4, + tag_type = TagType::Mifare; + uuid_length = sizeof(NFP::NtagTagUuid); + memcpy(uuid.data(), mifare_data.data(), uuid_length); + } else { + tag_type = TagType::Type2; + uuid_length = sizeof(NFP::NtagTagUuid); + NFP::NtagTagUuid nUuid{ + .part1 = encrypted_tag_data.uuid.part1, + .part2 = encrypted_tag_data.uuid.part2, + .nintendo_id = encrypted_tag_data.uuid.nintendo_id, }; - return ResultSuccess; + memcpy(uuid.data(), &nUuid, uuid_length); + + // Generate random UUID to bypass amiibo load limits + if (Settings::values.random_amiibo_id) { + Common::TinyMT rng{}; + rng.Initialize(static_cast<u32>(GetCurrentPosixTime())); + rng.GenerateRandomBytes(uuid.data(), uuid_length); + } } // Protocol and tag type may change here tag_info = { .uuid = uuid, - .uuid_extension = {}, - .uuid_length = static_cast<u8>(uuid.size()), - .protocol = NfcProtocol::TypeA, - .tag_type = TagType::Type2, + .uuid_length = uuid_length, + .protocol = protocol, + .tag_type = tag_type, }; return ResultSuccess; @@ -277,8 +282,38 @@ Result NfcDevice::GetTagInfo(NFP::TagInfo& tag_info, bool is_mifare) const { Result NfcDevice::ReadMifare(std::span<const MifareReadBlockParameter> parameters, std::span<MifareReadBlockData> read_block_data) const { + if (device_state != DeviceState::TagFound && device_state != DeviceState::TagMounted) { + LOG_ERROR(Service_NFC, "Wrong device state {}", device_state); + if (device_state == DeviceState::TagRemoved) { + return ResultTagRemoved; + } + return ResultWrongDeviceState; + } + Result result = ResultSuccess; + TagInfo tag_info{}; + result = GetTagInfo(tag_info, true); + + if (result.IsError()) { + return result; + } + + if (tag_info.protocol != NfcProtocol::TypeA || tag_info.tag_type != TagType::Mifare) { + return ResultInvalidTagType; + } + + if (parameters.size() == 0) { + return ResultInvalidArgument; + } + + const auto unknown = parameters[0].sector_key.unknown; + for (std::size_t i = 0; i < parameters.size(); i++) { + if (unknown != parameters[i].sector_key.unknown) { + return ResultInvalidArgument; + } + } + for (std::size_t i = 0; i < parameters.size(); i++) { result = ReadMifare(parameters[i], read_block_data[i]); if (result.IsError()) { @@ -293,17 +328,8 @@ Result NfcDevice::ReadMifare(const MifareReadBlockParameter& parameter, MifareReadBlockData& read_block_data) const { const std::size_t sector_index = parameter.sector_number * sizeof(DataBlock); read_block_data.sector_number = parameter.sector_number; - - if (device_state != DeviceState::TagFound && device_state != DeviceState::TagMounted) { - LOG_ERROR(Service_NFC, "Wrong device state {}", device_state); - if (device_state == DeviceState::TagRemoved) { - return ResultTagRemoved; - } - return ResultWrongDeviceState; - } - if (mifare_data.size() < sector_index + sizeof(DataBlock)) { - return Mifare::ResultReadError; + return ResultMifareError288; } // TODO: Use parameter.sector_key to read encrypted data @@ -315,6 +341,28 @@ Result NfcDevice::ReadMifare(const MifareReadBlockParameter& parameter, Result NfcDevice::WriteMifare(std::span<const MifareWriteBlockParameter> parameters) { Result result = ResultSuccess; + TagInfo tag_info{}; + result = GetTagInfo(tag_info, true); + + if (result.IsError()) { + return result; + } + + if (tag_info.protocol != NfcProtocol::TypeA || tag_info.tag_type != TagType::Mifare) { + return ResultInvalidTagType; + } + + if (parameters.size() == 0) { + return ResultInvalidArgument; + } + + const auto unknown = parameters[0].sector_key.unknown; + for (std::size_t i = 0; i < parameters.size(); i++) { + if (unknown != parameters[i].sector_key.unknown) { + return ResultInvalidArgument; + } + } + for (std::size_t i = 0; i < parameters.size(); i++) { result = WriteMifare(parameters[i]); if (result.IsError()) { @@ -324,7 +372,7 @@ Result NfcDevice::WriteMifare(std::span<const MifareWriteBlockParameter> paramet if (!npad_device->WriteNfc(mifare_data)) { LOG_ERROR(Service_NFP, "Error writing to file"); - return Mifare::ResultReadError; + return ResultMifareError288; } return result; @@ -342,7 +390,7 @@ Result NfcDevice::WriteMifare(const MifareWriteBlockParameter& parameter) { } if (mifare_data.size() < sector_index + sizeof(DataBlock)) { - return Mifare::ResultReadError; + return ResultMifareError288; } // TODO: Use parameter.sector_key to encrypt the data @@ -366,7 +414,7 @@ Result NfcDevice::Mount(NFP::ModelType model_type, NFP::MountTarget mount_target if (!NFP::AmiiboCrypto::IsAmiiboValid(encrypted_tag_data)) { LOG_ERROR(Service_NFP, "Not an amiibo"); - return ResultNotAnAmiibo; + return ResultInvalidTagType; } // The loaded amiibo is not encrypted @@ -381,14 +429,14 @@ Result NfcDevice::Mount(NFP::ModelType model_type, NFP::MountTarget mount_target } if (!NFP::AmiiboCrypto::DecodeAmiibo(encrypted_tag_data, tag_data)) { - bool has_backup = HasBackup(encrypted_tag_data.uuid.uid).IsSuccess(); + bool has_backup = HasBackup(encrypted_tag_data.uuid).IsSuccess(); LOG_ERROR(Service_NFP, "Can't decode amiibo, has_backup= {}", has_backup); return has_backup ? ResultCorruptedDataWithBackup : ResultCorruptedData; } std::vector<u8> data(sizeof(NFP::EncryptedNTAG215File)); memcpy(data.data(), &encrypted_tag_data, sizeof(encrypted_tag_data)); - WriteBackupData(encrypted_tag_data.uuid.uid, data); + WriteBackupData(encrypted_tag_data.uuid, data); device_state = DeviceState::TagMounted; mount_target = mount_target_; @@ -492,7 +540,7 @@ Result NfcDevice::FlushWithBreak(NFP::BreakType break_type) { } memcpy(data.data(), &encrypted_tag_data, sizeof(encrypted_tag_data)); - WriteBackupData(encrypted_tag_data.uuid.uid, data); + WriteBackupData(encrypted_tag_data.uuid, data); } if (!npad_device->WriteNfc(data)) { @@ -520,7 +568,7 @@ Result NfcDevice::Restore() { return result; } - result = ReadBackupData(tag_info.uuid, data); + result = ReadBackupData(tag_info.uuid, tag_info.uuid_length, data); if (result.IsError()) { return result; @@ -548,7 +596,7 @@ Result NfcDevice::Restore() { } if (!NFP::AmiiboCrypto::IsAmiiboValid(temporary_encrypted_tag_data)) { - return ResultNotAnAmiibo; + return ResultInvalidTagType; } if (!is_plain_amiibo) { @@ -1194,10 +1242,12 @@ Result NfcDevice::BreakTag(NFP::BreakType break_type) { return FlushWithBreak(break_type); } -Result NfcDevice::HasBackup(const NFC::UniqueSerialNumber& uid) const { +Result NfcDevice::HasBackup(const UniqueSerialNumber& uid, std::size_t uuid_size) const { + ASSERT_MSG(uuid_size < sizeof(UniqueSerialNumber), "Invalid UUID size"); constexpr auto backup_dir = "backup"; const auto yuzu_amiibo_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::AmiiboDir); - const auto file_name = fmt::format("{0:02x}.bin", fmt::join(uid, "")); + const auto file_name = + fmt::format("{0:02x}.bin", fmt::join(uid.begin(), uid.begin() + uuid_size, "")); if (!Common::FS::Exists(yuzu_amiibo_dir / backup_dir / file_name)) { return ResultUnableToAccessBackupFile; @@ -1206,10 +1256,19 @@ Result NfcDevice::HasBackup(const NFC::UniqueSerialNumber& uid) const { return ResultSuccess; } -Result NfcDevice::ReadBackupData(const NFC::UniqueSerialNumber& uid, std::span<u8> data) const { +Result NfcDevice::HasBackup(const NFP::TagUuid& tag_uid) const { + UniqueSerialNumber uuid{}; + memcpy(uuid.data(), &tag_uid, sizeof(NFP::TagUuid)); + return HasBackup(uuid, sizeof(NFP::TagUuid)); +} + +Result NfcDevice::ReadBackupData(const UniqueSerialNumber& uid, std::size_t uuid_size, + std::span<u8> data) const { + ASSERT_MSG(uuid_size < sizeof(UniqueSerialNumber), "Invalid UUID size"); constexpr auto backup_dir = "backup"; const auto yuzu_amiibo_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::AmiiboDir); - const auto file_name = fmt::format("{0:02x}.bin", fmt::join(uid, "")); + const auto file_name = + fmt::format("{0:02x}.bin", fmt::join(uid.begin(), uid.begin() + uuid_size, "")); const Common::FS::IOFile keys_file{yuzu_amiibo_dir / backup_dir / file_name, Common::FS::FileAccessMode::Read, @@ -1228,12 +1287,21 @@ Result NfcDevice::ReadBackupData(const NFC::UniqueSerialNumber& uid, std::span<u return ResultSuccess; } -Result NfcDevice::WriteBackupData(const NFC::UniqueSerialNumber& uid, std::span<const u8> data) { +Result NfcDevice::ReadBackupData(const NFP::TagUuid& tag_uid, std::span<u8> data) const { + UniqueSerialNumber uuid{}; + memcpy(uuid.data(), &tag_uid, sizeof(NFP::TagUuid)); + return ReadBackupData(uuid, sizeof(NFP::TagUuid), data); +} + +Result NfcDevice::WriteBackupData(const UniqueSerialNumber& uid, std::size_t uuid_size, + std::span<const u8> data) { + ASSERT_MSG(uuid_size < sizeof(UniqueSerialNumber), "Invalid UUID size"); constexpr auto backup_dir = "backup"; const auto yuzu_amiibo_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::AmiiboDir); - const auto file_name = fmt::format("{0:02x}.bin", fmt::join(uid, "")); + const auto file_name = + fmt::format("{0:02x}.bin", fmt::join(uid.begin(), uid.begin() + uuid_size, "")); - if (HasBackup(uid).IsError()) { + if (HasBackup(uid, uuid_size).IsError()) { if (!Common::FS::CreateDir(yuzu_amiibo_dir / backup_dir)) { return ResultBackupPathAlreadyExist; } @@ -1260,6 +1328,12 @@ Result NfcDevice::WriteBackupData(const NFC::UniqueSerialNumber& uid, std::span< return ResultSuccess; } +Result NfcDevice::WriteBackupData(const NFP::TagUuid& tag_uid, std::span<const u8> data) { + UniqueSerialNumber uuid{}; + memcpy(uuid.data(), &tag_uid, sizeof(NFP::TagUuid)); + return WriteBackupData(uuid, sizeof(NFP::TagUuid), data); +} + Result NfcDevice::WriteNtf(std::span<const u8> data) { if (device_state != DeviceState::TagMounted) { LOG_ERROR(Service_NFC, "Wrong device state {}", device_state); diff --git a/src/core/hle/service/nfc/common/device.h b/src/core/hle/service/nfc/common/device.h index 6f049b687..7560210d6 100644 --- a/src/core/hle/service/nfc/common/device.h +++ b/src/core/hle/service/nfc/common/device.h @@ -86,9 +86,14 @@ public: Result GetAll(NFP::NfpData& data) const; Result SetAll(const NFP::NfpData& data); Result BreakTag(NFP::BreakType break_type); - Result HasBackup(const NFC::UniqueSerialNumber& uid) const; - Result ReadBackupData(const NFC::UniqueSerialNumber& uid, std::span<u8> data) const; - Result WriteBackupData(const NFC::UniqueSerialNumber& uid, std::span<const u8> data); + Result HasBackup(const UniqueSerialNumber& uid, std::size_t uuid_size) const; + Result HasBackup(const NFP::TagUuid& tag_uid) const; + Result ReadBackupData(const UniqueSerialNumber& uid, std::size_t uuid_size, + std::span<u8> data) const; + Result ReadBackupData(const NFP::TagUuid& tag_uid, std::span<u8> data) const; + Result WriteBackupData(const UniqueSerialNumber& uid, std::size_t uuid_size, + std::span<const u8> data); + Result WriteBackupData(const NFP::TagUuid& tag_uid, std::span<const u8> data); Result WriteNtf(std::span<const u8> data); u64 GetHandle() const; diff --git a/src/core/hle/service/nfc/common/device_manager.cpp b/src/core/hle/service/nfc/common/device_manager.cpp index cffd602df..b0456508e 100644 --- a/src/core/hle/service/nfc/common/device_manager.cpp +++ b/src/core/hle/service/nfc/common/device_manager.cpp @@ -550,7 +550,7 @@ Result DeviceManager::ReadBackupData(u64 device_handle, std::span<u8> data) cons } if (result.IsSuccess()) { - result = device->ReadBackupData(tag_info.uuid, data); + result = device->ReadBackupData(tag_info.uuid, tag_info.uuid_length, data); result = VerifyDeviceResult(device, result); } @@ -569,7 +569,7 @@ Result DeviceManager::WriteBackupData(u64 device_handle, std::span<const u8> dat } if (result.IsSuccess()) { - result = device->WriteBackupData(tag_info.uuid, data); + result = device->WriteBackupData(tag_info.uuid, tag_info.uuid_length, data); result = VerifyDeviceResult(device, result); } diff --git a/src/core/hle/service/nfc/mifare_result.h b/src/core/hle/service/nfc/mifare_result.h index 4b60048a5..16a9171e6 100644 --- a/src/core/hle/service/nfc/mifare_result.h +++ b/src/core/hle/service/nfc/mifare_result.h @@ -12,6 +12,6 @@ constexpr Result ResultInvalidArgument(ErrorModule::NFCMifare, 65); constexpr Result ResultWrongDeviceState(ErrorModule::NFCMifare, 73); constexpr Result ResultNfcDisabled(ErrorModule::NFCMifare, 80); constexpr Result ResultTagRemoved(ErrorModule::NFCMifare, 97); -constexpr Result ResultReadError(ErrorModule::NFCMifare, 288); +constexpr Result ResultNotAMifare(ErrorModule::NFCMifare, 288); } // namespace Service::NFC::Mifare diff --git a/src/core/hle/service/nfc/nfc_interface.cpp b/src/core/hle/service/nfc/nfc_interface.cpp index 198d0f2b9..130fb7f78 100644 --- a/src/core/hle/service/nfc/nfc_interface.cpp +++ b/src/core/hle/service/nfc/nfc_interface.cpp @@ -142,9 +142,13 @@ void NfcInterface::AttachAvailabilityChangeEvent(HLERequestContext& ctx) { void NfcInterface::StartDetection(HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; const auto device_handle{rp.Pop<u64>()}; - const auto tag_protocol{rp.PopEnum<NfcProtocol>()}; - LOG_INFO(Service_NFC, "called, device_handle={}, nfp_protocol={}", device_handle, tag_protocol); + auto tag_protocol{NfcProtocol::All}; + + if (backend_type == BackendType::Nfc) { + tag_protocol = rp.PopEnum<NfcProtocol>(); + } + LOG_INFO(Service_NFC, "called, device_handle={}, nfp_protocol={}", device_handle, tag_protocol); auto result = GetManager()->StartDetection(device_handle, tag_protocol); result = TranslateResultToServiceError(result); @@ -355,7 +359,7 @@ Result NfcInterface::TranslateResultToNfp(Result result) const { if (result == ResultApplicationAreaExist) { return NFP::ResultApplicationAreaExist; } - if (result == ResultNotAnAmiibo) { + if (result == ResultInvalidTagType) { return NFP::ResultNotAnAmiibo; } if (result == ResultUnableToAccessBackupFile) { @@ -381,6 +385,9 @@ Result NfcInterface::TranslateResultToMifare(Result result) const { if (result == ResultTagRemoved) { return Mifare::ResultTagRemoved; } + if (result == ResultInvalidTagType) { + return Mifare::ResultNotAMifare; + } LOG_WARNING(Service_NFC, "Result conversion not handled"); return result; } diff --git a/src/core/hle/service/nfc/nfc_result.h b/src/core/hle/service/nfc/nfc_result.h index 59a808740..715c0e80c 100644 --- a/src/core/hle/service/nfc/nfc_result.h +++ b/src/core/hle/service/nfc/nfc_result.h @@ -24,7 +24,8 @@ constexpr Result ResultCorruptedDataWithBackup(ErrorModule::NFC, 136); constexpr Result ResultCorruptedData(ErrorModule::NFC, 144); constexpr Result ResultWrongApplicationAreaId(ErrorModule::NFC, 152); constexpr Result ResultApplicationAreaExist(ErrorModule::NFC, 168); -constexpr Result ResultNotAnAmiibo(ErrorModule::NFC, 178); +constexpr Result ResultInvalidTagType(ErrorModule::NFC, 178); constexpr Result ResultBackupPathAlreadyExist(ErrorModule::NFC, 216); +constexpr Result ResultMifareError288(ErrorModule::NFC, 288); } // namespace Service::NFC diff --git a/src/core/hle/service/nfc/nfc_types.h b/src/core/hle/service/nfc/nfc_types.h index c7ebd1fdb..68e724442 100644 --- a/src/core/hle/service/nfc/nfc_types.h +++ b/src/core/hle/service/nfc/nfc_types.h @@ -35,32 +35,35 @@ enum class State : u32 { // This is nn::nfc::TagType enum class TagType : u32 { - None, - Type1, // ISO14443A RW 96-2k bytes 106kbit/s - Type2, // ISO14443A RW/RO 540 bytes 106kbit/s - Type3, // Sony FeliCa RW/RO 2k bytes 212kbit/s - Type4, // ISO14443A RW/RO 4k-32k bytes 424kbit/s - Type5, // ISO15693 RW/RO 540 bytes 106kbit/s + None = 0, + Type1 = 1U << 0, // ISO14443A RW. Topaz + Type2 = 1U << 1, // ISO14443A RW. Ultralight, NTAGX, ST25TN + Type3 = 1U << 2, // ISO14443A RW/RO. Sony FeliCa + Type4A = 1U << 3, // ISO14443A RW/RO. DESFire + Type4B = 1U << 4, // ISO14443B RW/RO. DESFire + Type5 = 1U << 5, // ISO15693 RW/RO. SLI, SLIX, ST25TV + Mifare = 1U << 6, // Mifare classic. Skylanders + All = 0xFFFFFFFF, }; enum class PackedTagType : u8 { - None, - Type1, // ISO14443A RW 96-2k bytes 106kbit/s - Type2, // ISO14443A RW/RO 540 bytes 106kbit/s - Type3, // Sony FeliCa RW/RO 2k bytes 212kbit/s - Type4, // ISO14443A RW/RO 4k-32k bytes 424kbit/s - Type5, // ISO15693 RW/RO 540 bytes 106kbit/s + None = 0, + Type1 = 1U << 0, // ISO14443A RW. Topaz + Type2 = 1U << 1, // ISO14443A RW. Ultralight, NTAGX, ST25TN + Type3 = 1U << 2, // ISO14443A RW/RO. Sony FeliCa + Type4A = 1U << 3, // ISO14443A RW/RO. DESFire + Type4B = 1U << 4, // ISO14443B RW/RO. DESFire + Type5 = 1U << 5, // ISO15693 RW/RO. SLI, SLIX, ST25TV + Mifare = 1U << 6, // Mifare classic. Skylanders + All = 0xFF, }; // This is nn::nfc::NfcProtocol -// Verify this enum. It might be completely wrong default protocol is 0x48 enum class NfcProtocol : u32 { None, TypeA = 1U << 0, // ISO14443A TypeB = 1U << 1, // ISO14443B TypeF = 1U << 2, // Sony FeliCa - Unknown1 = 1U << 3, - Unknown2 = 1U << 5, All = 0xFFFFFFFFU, }; @@ -69,8 +72,7 @@ enum class TestWaveType : u32 { Unknown, }; -using UniqueSerialNumber = std::array<u8, 7>; -using UniqueSerialNumberExtension = std::array<u8, 3>; +using UniqueSerialNumber = std::array<u8, 10>; // This is nn::nfc::DeviceHandle using DeviceHandle = u64; @@ -78,7 +80,6 @@ using DeviceHandle = u64; // This is nn::nfc::TagInfo struct TagInfo { UniqueSerialNumber uuid; - UniqueSerialNumberExtension uuid_extension; u8 uuid_length; INSERT_PADDING_BYTES(0x15); NfcProtocol protocol; diff --git a/src/core/hle/service/nfp/nfp_types.h b/src/core/hle/service/nfp/nfp_types.h index 7d36d5ee6..aed12a7f8 100644 --- a/src/core/hle/service/nfp/nfp_types.h +++ b/src/core/hle/service/nfp/nfp_types.h @@ -85,7 +85,7 @@ enum class CabinetMode : u8 { StartFormatter, }; -using LockBytes = std::array<u8, 2>; +using UuidPart = std::array<u8, 3>; using HashData = std::array<u8, 0x20>; using ApplicationArea = std::array<u8, 0xD8>; using AmiiboName = std::array<char, (amiibo_name_length * 4) + 1>; @@ -93,12 +93,20 @@ using AmiiboName = std::array<char, (amiibo_name_length * 4) + 1>; // This is nn::nfp::TagInfo using TagInfo = NFC::TagInfo; +struct NtagTagUuid { + UuidPart part1; + UuidPart part2; + u8 nintendo_id; +}; +static_assert(sizeof(NtagTagUuid) == 7, "NtagTagUuid is an invalid size"); + struct TagUuid { - NFC::UniqueSerialNumber uid; + UuidPart part1; + u8 crc_check1; + UuidPart part2; u8 nintendo_id; - LockBytes lock_bytes; }; -static_assert(sizeof(TagUuid) == 10, "TagUuid is an invalid size"); +static_assert(sizeof(TagUuid) == 8, "TagUuid is an invalid size"); struct WriteDate { u16 year; @@ -231,7 +239,8 @@ struct EncryptedAmiiboFile { static_assert(sizeof(EncryptedAmiiboFile) == 0x1F8, "AmiiboFile is an invalid size"); struct NTAG215File { - LockBytes lock_bytes; // Tag UUID + u8 uid_crc_check2; + u8 internal_number; u16 static_lock; // Set defined pages as read only u32 compability_container; // Defines available memory HashData hmac_data; // Hash @@ -250,8 +259,7 @@ struct NTAG215File { u32_be register_info_crc; ApplicationArea application_area; // Encrypted Game data HashData hmac_tag; // Hash - NFC::UniqueSerialNumber uid; // Unique serial number - u8 nintendo_id; // Tag UUID + TagUuid uid; AmiiboModelInfo model_info; HashData keygen_salt; // Salt u32 dynamic_lock; // Dynamic lock @@ -264,7 +272,9 @@ static_assert(std::is_trivially_copyable_v<NTAG215File>, "NTAG215File must be tr #pragma pack() struct EncryptedNTAG215File { - TagUuid uuid; // Unique serial number + TagUuid uuid; + u8 uuid_crc_check2; + u8 internal_number; u16 static_lock; // Set defined pages as read only u32 compability_container; // Defines available memory EncryptedAmiiboFile user_memory; // Writable data diff --git a/src/input_common/drivers/virtual_amiibo.cpp b/src/input_common/drivers/virtual_amiibo.cpp index f8bafe553..6435b8af8 100644 --- a/src/input_common/drivers/virtual_amiibo.cpp +++ b/src/input_common/drivers/virtual_amiibo.cpp @@ -82,6 +82,7 @@ VirtualAmiibo::Info VirtualAmiibo::LoadAmiibo(const std::string& filename) { switch (nfc_file.GetSize()) { case AmiiboSize: case AmiiboSizeWithoutPassword: + case AmiiboSizeWithSignature: data.resize(AmiiboSize); if (nfc_file.Read(data) < AmiiboSizeWithoutPassword) { return Info::NotAnAmiibo; @@ -109,6 +110,7 @@ VirtualAmiibo::Info VirtualAmiibo::LoadAmiibo(std::span<u8> data) { switch (data.size_bytes()) { case AmiiboSize: case AmiiboSizeWithoutPassword: + case AmiiboSizeWithSignature: nfc_data.resize(AmiiboSize); break; case MifareSize: diff --git a/src/input_common/drivers/virtual_amiibo.h b/src/input_common/drivers/virtual_amiibo.h index 34e97cd91..09ca09e68 100644 --- a/src/input_common/drivers/virtual_amiibo.h +++ b/src/input_common/drivers/virtual_amiibo.h @@ -57,6 +57,7 @@ public: private: static constexpr std::size_t AmiiboSize = 0x21C; static constexpr std::size_t AmiiboSizeWithoutPassword = AmiiboSize - 0x8; + static constexpr std::size_t AmiiboSizeWithSignature = AmiiboSize + 0x20; static constexpr std::size_t MifareSize = 0x400; std::string file_path{}; diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index 5734f51e5..18e040a1b 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -358,6 +358,7 @@ PipelineCache::PipelineCache(RasterizerVulkan& rasterizer_, const Device& device .support_snorm_render_buffer = true, .support_viewport_index_layer = device.IsExtShaderViewportIndexLayerSupported(), .support_geometry_shader_passthrough = device.IsNvGeometryShaderPassthroughSupported(), + .support_conditional_barrier = device.SupportsConditionalBarriers(), }; if (device.GetMaxVertexInputAttributes() < Maxwell::NumVertexAttributes) { diff --git a/src/video_core/vulkan_common/vulkan_device.cpp b/src/video_core/vulkan_common/vulkan_device.cpp index a46f9beed..3d2e9a16a 100644 --- a/src/video_core/vulkan_common/vulkan_device.cpp +++ b/src/video_core/vulkan_common/vulkan_device.cpp @@ -344,6 +344,7 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR const bool is_qualcomm = driver_id == VK_DRIVER_ID_QUALCOMM_PROPRIETARY; const bool is_turnip = driver_id == VK_DRIVER_ID_MESA_TURNIP; const bool is_s8gen2 = device_id == 0x43050a01; + const bool is_arm = driver_id == VK_DRIVER_ID_ARM_PROPRIETARY; if ((is_mvk || is_qualcomm || is_turnip) && !is_suitable) { LOG_WARNING(Render_Vulkan, "Unsuitable driver, continuing anyway"); @@ -391,7 +392,6 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR CollectPhysicalMemoryInfo(); CollectToolingInfo(); -#ifdef ANDROID if (is_qualcomm || is_turnip) { LOG_WARNING(Render_Vulkan, "Qualcomm and Turnip drivers have broken VK_EXT_custom_border_color"); @@ -411,7 +411,7 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR extensions.push_descriptor = false; loaded_extensions.erase(VK_KHR_PUSH_DESCRIPTOR_EXTENSION_NAME); -#ifdef ARCHITECTURE_arm64 +#if defined(ANDROID) && defined(ARCHITECTURE_arm64) // Patch the driver to enable BCn textures. const auto major = (properties.properties.driverVersion >> 24) << 2; const auto minor = (properties.properties.driverVersion >> 12) & 0xFFFU; @@ -431,18 +431,23 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR } else { LOG_WARNING(Render_Vulkan, "Adreno driver can't be patched to enable BCn textures"); } -#endif // ARCHITECTURE_arm64 +#endif } - const bool is_arm = driver_id == VK_DRIVER_ID_ARM_PROPRIETARY; if (is_arm) { must_emulate_scaled_formats = true; LOG_WARNING(Render_Vulkan, "ARM drivers have broken VK_EXT_extended_dynamic_state"); extensions.extended_dynamic_state = false; loaded_extensions.erase(VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME); + + LOG_WARNING(Render_Vulkan, "ARM drivers have broken VK_EXT_extended_dynamic_state2"); + features.extended_dynamic_state2.extendedDynamicState2 = false; + features.extended_dynamic_state2.extendedDynamicState2LogicOp = false; + features.extended_dynamic_state2.extendedDynamicState2PatchControlPoints = false; + extensions.extended_dynamic_state2 = false; + loaded_extensions.erase(VK_EXT_EXTENDED_DYNAMIC_STATE_2_EXTENSION_NAME); } -#endif // ANDROID if (is_nvidia) { const u32 nv_major_version = (properties.properties.driverVersion >> 22) & 0x3ff; |