diff options
Diffstat (limited to 'src/android')
14 files changed, 98 insertions, 112 deletions
diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts index 13bb227ff..d4698ae1c 100644 --- a/src/android/app/build.gradle.kts +++ b/src/android/app/build.gradle.kts @@ -74,16 +74,7 @@ android { // Signed by release key, allowing for upload to Play Store. release { - signingConfig = signingConfigs.getByName("debug") - isMinifyEnabled = true - isDebuggable = false - proguardFiles( - getDefaultProguardFile("proguard-android.txt"), - "proguard-rules.pro" - ) - } - - register("relWithVersionCode") { + resValue("string", "app_name_suffixed", "yuzu") signingConfig = signingConfigs.getByName("debug") isMinifyEnabled = true isDebuggable = false @@ -96,6 +87,7 @@ android { // builds a release build that doesn't need signing // Attaches 'debug' suffix to version and package name, allowing installation alongside the release build. register("relWithDebInfo") { + resValue("string", "app_name_suffixed", "yuzu Debug Release") signingConfig = signingConfigs.getByName("debug") isMinifyEnabled = true isDebuggable = true @@ -103,16 +95,19 @@ android { getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" ) - versionNameSuffix = "-debug" + versionNameSuffix = "-relWithDebInfo" + applicationIdSuffix = ".relWithDebInfo" isJniDebuggable = true } // Signed by debug key disallowing distribution on Play Store. // Attaches 'debug' suffix to version and package name, allowing installation alongside the release build. debug { + resValue("string", "app_name_suffixed", "yuzu Debug") isDebuggable = true isJniDebuggable = true versionNameSuffix = "-debug" + applicationIdSuffix = ".debug" } } @@ -162,19 +157,19 @@ dependencies { implementation("androidx.appcompat:appcompat:1.6.1") implementation("androidx.recyclerview:recyclerview:1.3.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") - implementation("androidx.fragment:fragment-ktx:1.5.7") + implementation("androidx.fragment:fragment-ktx:1.6.0") implementation("androidx.documentfile:documentfile:1.0.1") implementation("com.google.android.material:material:1.9.0") implementation("androidx.preference:preference:1.2.0") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1") implementation("io.coil-kt:coil:2.2.2") implementation("androidx.core:core-splashscreen:1.0.1") - implementation("androidx.window:window:1.0.0") + implementation("androidx.window:window:1.1.0") implementation("org.ini4j:ini4j:0.5.4") implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") - implementation("androidx.navigation:navigation-fragment-ktx:2.5.3") - implementation("androidx.navigation:navigation-ui-ktx:2.5.3") + implementation("androidx.navigation:navigation-fragment-ktx:2.6.0") + implementation("androidx.navigation:navigation-ui-ktx:2.6.0") implementation("info.debatty:java-string-similarity:2.0.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0") } diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml index 43087f2c0..1e92098ec 100644 --- a/src/android/app/src/main/AndroidManifest.xml +++ b/src/android/app/src/main/AndroidManifest.xml @@ -6,17 +6,10 @@ SPDX-License-Identifier: GPL-3.0-or-later --> <manifest xmlns:android="http://schemas.android.com/apk/res/android"> - <uses-feature - android:name="android.hardware.touchscreen" - android:required="false"/> - <uses-feature - android:name="android.hardware.gamepad" - android:required="false"/> - - <uses-feature - android:name="android.hardware.vulkan.version" - android:version="0x401000" - android:required="true" /> + <uses-feature android:name="android.hardware.touchscreen" android:required="false" /> + <uses-feature android:name="android.hardware.gamepad" android:required="false" /> + <uses-feature android:name="android.software.leanback" android:required="false" /> + <uses-feature android:name="android.hardware.vulkan.version" android:version="0x401000" android:required="true" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> @@ -25,13 +18,13 @@ SPDX-License-Identifier: GPL-3.0-or-later <application android:name="org.yuzu.yuzu_emu.YuzuApplication" - android:label="@string/app_name" + android:label="@string/app_name_suffixed" android:icon="@drawable/ic_launcher" android:allowBackup="true" android:hasFragileUserData="true" android:supportsRtl="true" android:isGame="true" - android:banner="@drawable/ic_launcher" + android:banner="@drawable/tv_banner" android:extractNativeLibs="true" android:fullBackupContent="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules_api_31" @@ -44,9 +37,10 @@ SPDX-License-Identifier: GPL-3.0-or-later <!-- This intentfilter marks this Activity as the one that gets launched from Home screen. --> <intent-filter> - <action android:name="android.intent.action.MAIN"/> + <action android:name="android.intent.action.MAIN" /> - <category android:name="android.intent.category.LAUNCHER"/> + <category android:name="android.intent.category.LAUNCHER" /> + <category android:name="android.intent.category.LEANBACK_LAUNCHER" /> </intent-filter> </activity> 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 c11b6bc16..22af9e435 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 @@ -223,6 +223,8 @@ object NativeLibrary { external fun getCompany(filename: String): String + external fun isHomebrew(filename: String): Boolean + external fun setAppDirectory(directory: String) external fun initializeGpuDriver( 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 94d5156cf..20a0394f5 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 @@ -6,15 +6,12 @@ package org.yuzu.yuzu_emu.activities import android.app.Activity import android.content.Context import android.content.Intent -import android.content.res.Configuration import android.graphics.Rect import android.hardware.Sensor import android.hardware.SensorEvent import android.hardware.SensorEventListener import android.hardware.SensorManager -import android.hardware.display.DisplayManager import android.os.Bundle -import android.view.Display import android.view.InputDevice import android.view.KeyEvent import android.view.MotionEvent @@ -23,7 +20,6 @@ import android.view.View import android.view.inputmethod.InputMethodManager import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.getSystemService import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat @@ -39,7 +35,6 @@ import org.yuzu.yuzu_emu.features.settings.model.SettingsViewModel import org.yuzu.yuzu_emu.fragments.EmulationFragment import org.yuzu.yuzu_emu.model.Game import org.yuzu.yuzu_emu.utils.ControllerMappingHelper -import org.yuzu.yuzu_emu.utils.EmulationMenuSettings import org.yuzu.yuzu_emu.utils.ForegroundService import org.yuzu.yuzu_emu.utils.InputHandler import org.yuzu.yuzu_emu.utils.NfcReader @@ -148,11 +143,6 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { super.onResume() nfcReader.startScanning() startMotionSensorListener() - - NativeLibrary.notifyOrientationChange( - EmulationMenuSettings.landscapeScreenLayout, - getAdjustedRotation() - ) } override fun onPause() { @@ -258,23 +248,6 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { override fun onAccuracyChanged(sensor: Sensor, i: Int) {} - private fun getAdjustedRotation():Int { - val rotation = getSystemService<DisplayManager>()!!.getDisplay(Display.DEFAULT_DISPLAY).rotation - val config: Configuration = resources.configuration - - if ((config.screenLayout and Configuration.SCREENLAYOUT_LONG_YES) != 0 || - (config.screenLayout and Configuration.SCREENLAYOUT_LONG_NO) == 0) { - return rotation - } - when (rotation) { - Surface.ROTATION_0 -> return Surface.ROTATION_90 - Surface.ROTATION_90 -> return Surface.ROTATION_0 - Surface.ROTATION_180 -> return Surface.ROTATION_270 - Surface.ROTATION_270 -> return Surface.ROTATION_180 - } - return rotation - } - private fun restoreState(savedInstanceState: Bundle) { game = savedInstanceState.parcelable(EXTRA_SELECTED_GAME)!! } 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 ebc0f164a..adbe3696b 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 @@ -127,13 +127,7 @@ class SearchFragment : Fragment() { } } - R.id.chip_homebrew -> { - baseList.filter { - Log.error("Guh - ${it.path}") - FileUtil.hasExtension(it.path, "nro") - || FileUtil.hasExtension(it.path, "nso") - } - } + R.id.chip_homebrew -> baseList.filter { it.isHomebrew } R.id.chip_retail -> baseList.filter { FileUtil.hasExtension(it.path, "xci") 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 2a17653b2..3d6782c49 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 @@ -16,7 +16,8 @@ class Game( val regions: String, val path: String, val gameId: String, - val company: String + val company: String, + val isHomebrew: Boolean ) : Parcelable { val keyAddedToLibraryTime get() = "${gameId}_AddedToLibraryTime" val keyLastPlayedTime get() = "${gameId}_LastPlayed" @@ -31,6 +32,7 @@ class Game( && path == other.path && gameId == other.gameId && company == other.company + && isHomebrew == other.isHomebrew } companion object { 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 7059856f1..d9b301210 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 @@ -13,6 +13,8 @@ import androidx.preference.PreferenceManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.MissingFieldException import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import org.yuzu.yuzu_emu.NativeLibrary @@ -20,6 +22,7 @@ import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.utils.GameHelper import java.util.Locale +@OptIn(ExperimentalSerializationApi::class) class GamesViewModel : ViewModel() { private val _games = MutableLiveData<List<Game>>(emptyList()) val games: LiveData<List<Game>> get() = _games @@ -49,7 +52,13 @@ class GamesViewModel : ViewModel() { if (storedGames!!.isNotEmpty()) { val deserializedGames = mutableSetOf<Game>() storedGames.forEach { - val game: Game = Json.decodeFromString(it) + val game: Game + try { + game = Json.decodeFromString(it) + } catch (e: MissingFieldException) { + return@forEach + } + val gameExists = DocumentFile.fromSingleUri(YuzuApplication.appContext, Uri.parse(game.path)) ?.exists() 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 c9f5797ac..aa424c768 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 @@ -765,18 +765,20 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context // If we have API access, calculate the safe area to draw the overlay var cutoutLeft = 0 var cutoutBottom = 0 - val insets = context.windowManager.currentWindowMetrics.windowInsets.displayCutout - if (insets != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - 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 - - minX = insets.boundingRectLeft.right - insets.boundingRectLeft.left - minY = insets.boundingRectBottom.top - insets.boundingRectBottom.bottom - - cutoutLeft = insets.boundingRectRight.right - insets.boundingRectRight.left - cutoutBottom = insets.boundingRectTop.top - insets.boundingRectTop.bottom + 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 + + minX = insets.boundingRectLeft.right - insets.boundingRectLeft.left + minY = insets.boundingRectBottom.top - insets.boundingRectBottom.bottom + + cutoutLeft = insets.boundingRectRight.right - insets.boundingRectRight.left + cutoutBottom = insets.boundingRectTop.top - insets.boundingRectTop.bottom + } } // This makes sure that if we have an inset on one side of the screen, we mirror it on 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 124f62f08..3fca0a7e6 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 @@ -284,10 +284,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider { if (result == null) return@registerForActivityResult - if (!FileUtil.hasExtension(result.toString(), "keys")) { + if (!FileUtil.hasExtension(result, "keys")) { MessageDialogFragment.newInstance( R.string.reading_keys_failure, - R.string.install_keys_failure_extension_description + R.string.install_prod_keys_failure_extension_description ).show(supportFragmentManager, MessageDialogFragment.TAG) return@registerForActivityResult } @@ -379,10 +379,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider { if (result == null) return@registerForActivityResult - if (!FileUtil.hasExtension(result.toString(), "bin")) { + if (!FileUtil.hasExtension(result, "bin")) { MessageDialogFragment.newInstance( R.string.reading_keys_failure, - R.string.install_keys_failure_extension_description + R.string.install_amiibo_keys_failure_extension_description ).show(supportFragmentManager, MessageDialogFragment.TAG) return@registerForActivityResult } 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 593dad8d3..492b1ad91 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 @@ -7,7 +7,9 @@ import android.content.Context import android.database.Cursor 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 @@ -324,7 +326,25 @@ object FileUtil { } } - fun hasExtension(path: String, extension: String): Boolean { - return path.substring(path.lastIndexOf(".") + 1).contains(extension) + fun hasExtension(path: String, extension: String): Boolean = + path.substring(path.lastIndexOf(".") + 1).contains(extension) + + fun hasExtension(uri: Uri, extension: String): Boolean { + val fileName: String? + val cursor = YuzuApplication.appContext.contentResolver.query(uri, null, null, null, null) + val nameIndex = cursor?.getColumnIndex(OpenableColumns.DISPLAY_NAME) + cursor?.moveToFirst() + + if (nameIndex == null) { + return false + } + + fileName = cursor.getString(nameIndex) + cursor.close() + + if (fileName == null) { + return false + } + return fileName.substring(fileName.lastIndexOf(".") + 1).contains(extension) } } 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 ba6b5783e..42b207618 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,7 +6,6 @@ package org.yuzu.yuzu_emu.utils import android.content.SharedPreferences import android.net.Uri import androidx.preference.PreferenceManager -import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import org.yuzu.yuzu_emu.NativeLibrary @@ -83,7 +82,8 @@ object GameHelper { NativeLibrary.getRegions(filePath), filePath, gameId, - NativeLibrary.getCompany(filePath) + NativeLibrary.getCompany(filePath), + NativeLibrary.isHomebrew(filePath) ) val addedTime = preferences.getLong(newGame.keyAddedToLibraryTime, 0L) diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index b87e04b3d..7ebed5e6a 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -13,6 +13,7 @@ #include <android/api-level.h> #include <android/native_window_jni.h> +#include <core/loader/nro.h> #include "common/detached_tasks.h" #include "common/dynamic_library.h" @@ -93,14 +94,6 @@ public: m_native_window = native_window; } - u32 ScreenRotation() const { - return m_screen_rotation; - } - - void SetScreenRotation(u32 screen_rotation) { - m_screen_rotation = screen_rotation; - } - void InitializeGpuDriver(const std::string& hook_lib_dir, const std::string& custom_driver_dir, const std::string& custom_driver_name, const std::string& file_redirect_dir) { @@ -281,6 +274,10 @@ public: return GetRomMetadata(path).icon; } + bool GetIsHomebrew(const std::string& path) { + return GetRomMetadata(path).isHomebrew; + } + void ResetRomMetadata() { m_rom_metadata_cache.clear(); } @@ -348,6 +345,7 @@ private: struct RomMetadata { std::string title; std::vector<u8> icon; + bool isHomebrew; }; RomMetadata GetRomMetadata(const std::string& path) { @@ -360,11 +358,17 @@ private: RomMetadata CacheRomMetadata(const std::string& path) { const auto file = Core::GetGameFileFromPath(m_vfs, path); - const auto loader = Loader::GetLoader(EmulationSession::GetInstance().System(), file, 0, 0); + auto loader = Loader::GetLoader(EmulationSession::GetInstance().System(), file, 0, 0); RomMetadata entry; loader->ReadTitle(entry.title); loader->ReadIcon(entry.icon); + if (loader->GetFileType() == Loader::FileType::NRO) { + auto loader_nro = dynamic_cast<Loader::AppLoader_NRO*>(loader.get()); + entry.isHomebrew = loader_nro->IsHomebrew(); + } else { + entry.isHomebrew = false; + } m_rom_metadata_cache[path] = entry; @@ -388,7 +392,6 @@ private: // Window management std::unique_ptr<EmuWindow_Android> m_window; ANativeWindow* m_native_window{}; - u32 m_screen_rotation{}; // Core emulation Core::System m_system; @@ -414,10 +417,6 @@ private: } // Anonymous namespace -u32 GetAndroidScreenRotation() { - return EmulationSession::GetInstance().ScreenRotation(); -} - static Core::SystemResultStatus RunEmulation(const std::string& filepath) { Common::Log::Initialize(); Common::Log::SetColorConsoleBackendEnabled(true); @@ -461,13 +460,6 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_surfaceDestroyed(JNIEnv* env, EmulationSession::GetInstance().SurfaceChanged(); } -void Java_org_yuzu_yuzu_1emu_NativeLibrary_notifyOrientationChange(JNIEnv* env, - [[maybe_unused]] jclass clazz, - jint layout_option, - jint rotation) { - return EmulationSession::GetInstance().SetScreenRotation(static_cast<u32>(rotation)); -} - void Java_org_yuzu_yuzu_1emu_NativeLibrary_setAppDirectory(JNIEnv* env, [[maybe_unused]] jclass clazz, jstring j_directory) { @@ -662,6 +654,12 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getCompany([[maybe_unused]] JNIEnv return env->NewStringUTF(""); } +jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isHomebrew([[maybe_unused]] JNIEnv* env, + [[maybe_unused]] jclass clazz, + [[maybe_unused]] jstring j_filename) { + return EmulationSession::GetInstance().GetIsHomebrew(GetJString(env, j_filename)); +} + void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeEmulation [[maybe_unused]] (JNIEnv* env, [[maybe_unused]] jclass clazz) { // Create the default config.ini. diff --git a/src/android/app/src/main/res/drawable-xhdpi/tv_banner.png b/src/android/app/src/main/res/drawable-xhdpi/tv_banner.png Binary files differnew file mode 100644 index 000000000..20c770591 --- /dev/null +++ b/src/android/app/src/main/res/drawable-xhdpi/tv_banner.png diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 0ae69afb4..6e9d47557 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -65,11 +65,8 @@ <string name="invalid_keys_file">Invalid keys file selected</string> <string name="install_keys_success">Keys successfully installed</string> <string name="reading_keys_failure">Error reading encryption keys</string> - <string name="install_keys_failure_extension_description"> - 1. Verify your keys have the .keys extension.\n\n - 2. Keys must not be stored in the Downloads folder.\n\n - Resolve the issue(s) and try again. - </string> + <string name="install_prod_keys_failure_extension_description">Verify your keys file has a .keys extension and try again.</string> + <string name="install_amiibo_keys_failure_extension_description">Verify your keys file has a .bin extension and try again.</string> <string name="invalid_keys_error">Invalid encryption keys</string> <string name="dumping_keys_quickstart_link">https://yuzu-emu.org/help/quickstart/#dumping-decryption-keys</string> <string name="install_keys_failure_description">The selected file is incorrect or corrupt. Please redump your keys.</string> |