From 67edb897bf143f07cfe251cd97d435d3e58cb3d1 Mon Sep 17 00:00:00 2001 From: crueter Date: Thu, 10 Jul 2025 23:52:14 -0400 Subject: [PATCH] Implement firmware game checker on Android Signed-off-by: crueter --- .../java/org/yuzu/yuzu_emu/NativeLibrary.kt | 44 +++------- .../org/yuzu/yuzu_emu/adapters/GameAdapter.kt | 72 ++++++++++++----- .../yuzu_emu/fragments/EmulationFragment.kt | 1 + .../yuzu/yuzu_emu/fragments/SetupFragment.kt | 2 +- .../org/yuzu/yuzu_emu/ui/GamesFragment.kt | 7 +- .../org/yuzu/yuzu_emu/ui/main/MainActivity.kt | 80 ++++++------------- src/android/app/src/main/jni/native.cpp | 14 +++- .../app/src/main/res/values/arrays.xml | 7 ++ src/frontend_common/firmware_manager.cpp | 12 ++- src/frontend_common/firmware_manager.h | 11 ++- src/yuzu/main.cpp | 4 +- 11 files changed, 126 insertions(+), 128 deletions(-) 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 920b4d757b..a207b7997d 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 @@ -273,37 +273,6 @@ object NativeLibrary { external fun initMultiplayer() - // TODO(crueter): Implement this--may need to implant it into the loader - @Keep - @JvmStatic - fun gameRequiresFirmware() { - val emulationActivity = sEmulationActivity.get() - if (emulationActivity == null) { - Log.warning("[NativeLibrary] EmulationActivity is null, can't exit.") - return - } - - val builder = MaterialAlertDialogBuilder(emulationActivity) - .setTitle(R.string.loader_requires_firmware) - .setMessage( - Html.fromHtml( - emulationActivity.getString(R.string.loader_requires_firmware_description), - Html.FROM_HTML_MODE_LEGACY - ) - ) - .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> - emulationActivity.finish() - } - .setOnDismissListener { emulationActivity.finish() } - - emulationActivity.runOnUiThread { - val alert = builder.create() - alert.show() - (alert.findViewById(android.R.id.message) as TextView).movementMethod = - LinkMovementMethod.getInstance() - } - } - @Keep @JvmStatic fun exitEmulationActivity(resultCode: Int) { @@ -454,12 +423,21 @@ object NativeLibrary { external fun verifyFirmware(): Int /** - * Installs decryption keys from the specified path. + * Check if a game requires firmware to be playable. + * + * @param programId The game's Program ID. + * @return Whether or not the game requires firmware to be playable. + */ + external fun gameRequiresFirmware(programId: String): Boolean + + /** + * Installs keys from the specified path. * * @param path The path to install keys from. + * @param ext What extension the keys should have. * @return The result code. */ - external fun installDecryptionKeys(path: String): Int + external fun installKeys(path: String, ext: String): Int /** * Checks the PatchManager for any addons that are available 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 750e8f4729..c4652f55e1 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 @@ -3,11 +3,16 @@ package org.yuzu.yuzu_emu.adapters +import android.content.DialogInterface import android.net.Uri +import android.text.Html +import android.text.method.LinkMovementMethod import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import android.widget.LinearLayout import android.widget.ImageView +import android.widget.TextView import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.core.content.pm.ShortcutInfoCompat @@ -33,6 +38,10 @@ import org.yuzu.yuzu_emu.utils.GameIconUtils import org.yuzu.yuzu_emu.utils.ViewUtils.marquee import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder import androidx.recyclerview.widget.RecyclerView +import androidx.core.net.toUri +import androidx.core.content.edit +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.yuzu.yuzu_emu.NativeLibrary class GameAdapter(private val activity: AppCompatActivity) : AbstractDiffAdapter(exact = false) { @@ -171,8 +180,9 @@ class GameAdapter(private val activity: AppCompatActivity) : fun onClick(game: Game) { val gameExists = DocumentFile.fromSingleUri( YuzuApplication.appContext, - Uri.parse(game.path) + game.path.toUri() )?.exists() == true + if (!gameExists) { Toast.makeText( YuzuApplication.appContext, @@ -184,29 +194,49 @@ class GameAdapter(private val activity: AppCompatActivity) : return } - val preferences = - PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) - preferences.edit() - .putLong( - game.keyLastPlayedTime, - System.currentTimeMillis() - ) - .apply() - - activity.lifecycleScope.launch { - withContext(Dispatchers.IO) { - val shortcut = - ShortcutInfoCompat.Builder(YuzuApplication.appContext, game.path) - .setShortLabel(game.title) - .setIcon(GameIconUtils.getShortcutIcon(activity, game)) - .setIntent(game.launchIntent) - .build() - ShortcutManagerCompat.pushDynamicShortcut(YuzuApplication.appContext, shortcut) + val launch: () -> Unit = { + val preferences = + PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) + preferences.edit { + putLong( + game.keyLastPlayedTime, + System.currentTimeMillis() + ) } + + activity.lifecycleScope.launch { + withContext(Dispatchers.IO) { + val shortcut = + ShortcutInfoCompat.Builder(YuzuApplication.appContext, game.path) + .setShortLabel(game.title) + .setIcon(GameIconUtils.getShortcutIcon(activity, game)) + .setIntent(game.launchIntent) + .build() + ShortcutManagerCompat.pushDynamicShortcut(YuzuApplication.appContext, shortcut) + } + } + + val action = HomeNavigationDirections.actionGlobalEmulationActivity(game, true) + binding.root.findNavController().navigate(action) } - val action = HomeNavigationDirections.actionGlobalEmulationActivity(game, true) - binding.root.findNavController().navigate(action) + if (NativeLibrary.gameRequiresFirmware(game.programId) && !NativeLibrary.isFirmwareAvailable()) { + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.loader_requires_firmware) + .setMessage( + Html.fromHtml( + activity.getString(R.string.loader_requires_firmware_description), + Html.FROM_HTML_MODE_LEGACY + ) + ) + .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> + launch() + } + .setNegativeButton(android.R.string.cancel) { _,_ -> } + .show() + } else { + launch() + } } fun onLongClick(game: Game): Boolean { 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 05c14e278d..4efc822e21 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 @@ -478,6 +478,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } private fun startEmulation(programIndex: Int = 0) { + println("PROGRAM INDEX: $programIndex") if (!NativeLibrary.isRunning() && !NativeLibrary.isPaused()) { if (!DirectoryInitialization.areDirectoriesReady) { DirectoryInitialization.start() 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 6443067885..61797f75f5 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 @@ -352,7 +352,7 @@ class SetupFragment : Fragment() { val getProdKey = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> if (result != null) { - mainActivity.processKey(result) + mainActivity.processKey(result, "keys") if (NativeLibrary.areKeysPresent()) { keyCallback.onStepCompleted() } 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 1a74057569..43b9085f50 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 @@ -47,9 +47,6 @@ import info.debatty.java.stringsimilarity.Jaccard import info.debatty.java.stringsimilarity.JaroWinkler import java.util.Locale import androidx.core.content.edit -import androidx.core.view.updateLayoutParams -import org.yuzu.yuzu_emu.features.settings.model.Settings -import android.view.ViewParent import androidx.core.view.doOnNextLayout class GamesFragment : Fragment() { @@ -151,7 +148,7 @@ class GamesFragment : Fragment() { ) } gamesViewModel.games.collect(viewLifecycleOwner) { - if (it.size > 0) { + if (it.isNotEmpty()) { setAdapter(it) } } @@ -361,7 +358,7 @@ class GamesFragment : Fragment() { popup.setOnMenuItemClickListener { item -> currentFilter = item.itemId - preferences.edit().putInt(PREF_SORT_TYPE, currentFilter).apply() + preferences.edit { putInt(PREF_SORT_TYPE, currentFilter) } filterAndSearch() true } 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 0b2a38ac3d..f95081b318 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 @@ -6,6 +6,8 @@ package org.yuzu.yuzu_emu.ui.main import android.content.Intent import android.net.Uri import android.os.Bundle +import android.os.ParcelFileDescriptor +import android.provider.OpenableColumns import android.view.View import android.view.ViewGroup.MarginLayoutParams import android.view.WindowManager @@ -47,6 +49,7 @@ import java.io.BufferedOutputStream import java.util.zip.ZipEntry import java.util.zip.ZipInputStream import androidx.core.content.edit +import androidx.core.net.toFile class MainActivity : AppCompatActivity(), ThemeProvider { private lateinit var binding: ActivityMainBinding @@ -328,12 +331,18 @@ class MainActivity : AppCompatActivity(), ThemeProvider { val getProdKey = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> if (result != null) { - processKey(result) + processKey(result, "keys") } } - fun processKey(result: Uri): Boolean { - if (FileUtil.getExtension(result) != "keys") { + val getAmiiboKey = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> + if (result != null) { + processKey(result, "bin", false) + } + } + + fun processKey(result: Uri, extension: String = "keys", check: Boolean = true): Boolean { + if (FileUtil.getExtension(result) != extension) { MessageDialogFragment.newInstance( this, titleId = R.string.keys_failed, @@ -348,23 +357,26 @@ class MainActivity : AppCompatActivity(), ThemeProvider { val dstPath = DirectoryInitialization.userDirectory + "/keys/" if (FileUtil.copyUriToInternalStorage( - result, dstPath, "prod.keys" + result, dstPath, "" ) != null ) { if (NativeLibrary.reloadKeys()) { Toast.makeText( applicationContext, R.string.keys_install_success, Toast.LENGTH_SHORT ).show() - homeViewModel.setCheckKeys(true) - val firstTimeSetup = - PreferenceManager.getDefaultSharedPreferences(applicationContext) - .getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true) - if (!firstTimeSetup) { - homeViewModel.setCheckFirmware(true) + if (check) { + homeViewModel.setCheckKeys(true) + + val firstTimeSetup = + PreferenceManager.getDefaultSharedPreferences(applicationContext) + .getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true) + if (!firstTimeSetup) { + homeViewModel.setCheckFirmware(true) + } + + gamesViewModel.reloadGames(true) } - - gamesViewModel.reloadGames(true) return true } else { MessageDialogFragment.newInstance( @@ -381,11 +393,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider { } val getFirmware = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> - if (result == null) { - return@registerForActivityResult + if (result != null) { + processFirmware(result) } - - processFirmware(result) } fun processFirmware(result: Uri, onComplete: (() -> Unit)? = null) { @@ -458,44 +468,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider { }.show(supportFragmentManager, ProgressDialogFragment.TAG) } - val getAmiiboKey = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> - if (result == null) { - return@registerForActivityResult - } - - if (FileUtil.getExtension(result) != "bin") { - MessageDialogFragment.newInstance( - this, - titleId = R.string.keys_failed, - descriptionId = R.string.install_amiibo_keys_failure_extension_description - ).show(supportFragmentManager, MessageDialogFragment.TAG) - return@registerForActivityResult - } - - contentResolver.takePersistableUriPermission( - result, Intent.FLAG_GRANT_READ_URI_PERMISSION - ) - - val dstPath = DirectoryInitialization.userDirectory + "/keys/" - if (FileUtil.copyUriToInternalStorage( - result, dstPath, "key_retail.bin" - ) != null - ) { - if (NativeLibrary.reloadKeys()) { - Toast.makeText( - applicationContext, R.string.keys_install_success, Toast.LENGTH_SHORT - ).show() - } else { - MessageDialogFragment.newInstance( - this, - titleId = R.string.keys_failed, - descriptionId = R.string.error_keys_failed_init, - helpLinkId = R.string.dumping_keys_quickstart_link - ).show(supportFragmentManager, MessageDialogFragment.TAG) - } - } - } - val installGameUpdate = registerForActivityResult( ActivityResultContracts.OpenMultipleDocuments() ) { documents: List -> diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 55ba79b7e8..9fed0b1449 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -285,7 +285,6 @@ Core::SystemResultStatus EmulationSession::InitializeEmulation(const std::string .program_index = static_cast(program_index), }; - // TODO(crueter): Place checks somewhere around here m_load_result = m_system.Load(EmulationSession::GetInstance().Window(), filepath, params); if (m_load_result != Core::SystemResultStatus::Success) { return m_load_result; @@ -795,10 +794,17 @@ jint Java_org_yuzu_yuzu_1emu_NativeLibrary_verifyFirmware(JNIEnv* env, jclass cl return static_cast(FirmwareManager::VerifyFirmware(EmulationSession::GetInstance().System())); } -jint Java_org_yuzu_yuzu_1emu_NativeLibrary_installDecryptionKeys(JNIEnv* env, jclass clazz, jstring jpath) { - const auto path = Common::Android::GetJString(env, jpath); +jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_gameRequiresFirmware(JNIEnv* env, jclass clazz, jstring jprogramId) { + auto program_id = EmulationSession::GetProgramId(env, jprogramId); - return static_cast(FirmwareManager::InstallDecryptionKeys(path)); + return FirmwareManager::GameRequiresFirmware(program_id); +} + +jint Java_org_yuzu_yuzu_1emu_NativeLibrary_installKeys(JNIEnv* env, jclass clazz, jstring jpath, jstring jext) { + const auto path = Common::Android::GetJString(env, jpath); + const auto ext = Common::Android::GetJString(env, jext); + + return static_cast(FirmwareManager::InstallKeys(path, ext)); } jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPatchesForFile(JNIEnv* env, jobject jobj, diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml index affb2a45f4..028bef3fd9 100644 --- a/src/android/app/src/main/res/values/arrays.xml +++ b/src/android/app/src/main/res/values/arrays.xml @@ -502,4 +502,11 @@ @string/error_firmware_corrupted @string/error_firmware_too_new + + + "" + @string/error_keys_copy_failed + @string/error_keys_invalid_filename + @string/error_keys_failed_init + diff --git a/src/frontend_common/firmware_manager.cpp b/src/frontend_common/firmware_manager.cpp index 8425ffbd51..dfdf61d321 100644 --- a/src/frontend_common/firmware_manager.cpp +++ b/src/frontend_common/firmware_manager.cpp @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + #include "firmware_manager.h" #include @@ -9,12 +12,15 @@ #include "core/crypto/key_manager.h" #include "frontend_common/content_manager.h" -FirmwareManager::KeyInstallResult FirmwareManager::InstallDecryptionKeys(std::string location) +FirmwareManager::KeyInstallResult FirmwareManager::InstallKeys(std::string location, std::string extension) { LOG_INFO(Frontend, "Installing key files from {}", location); + const auto keys_dir = Common::FS::GetEdenPath(Common::FS::EdenPath::KeysDir); + const std::filesystem::path prod_key_path = location; const std::filesystem::path key_source_path = prod_key_path.parent_path(); + if (!Common::FS::IsDir(key_source_path)) { return InvalidDir; } @@ -39,9 +45,8 @@ FirmwareManager::KeyInstallResult FirmwareManager::InstallDecryptionKeys(std::st return ErrorWrongFilename; } - const auto yuzu_keys_dir = Common::FS::GetEdenPath(Common::FS::EdenPath::KeysDir); for (const auto &key_file : source_key_files) { - std::filesystem::path destination_key_file = yuzu_keys_dir / key_file.filename(); + std::filesystem::path destination_key_file = keys_dir / key_file.filename(); if (!std::filesystem::copy_file(key_file, destination_key_file, std::filesystem::copy_options::overwrite_existing)) { @@ -54,7 +59,6 @@ FirmwareManager::KeyInstallResult FirmwareManager::InstallDecryptionKeys(std::st } // Reinitialize the key manager - Core::Crypto::KeyManager::Instance().ReloadKeys(); if (ContentManager::AreKeysPresent()) { diff --git a/src/frontend_common/firmware_manager.h b/src/frontend_common/firmware_manager.h index a33cf5a8b4..20f3b41478 100644 --- a/src/frontend_common/firmware_manager.h +++ b/src/frontend_common/firmware_manager.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + #ifndef FIRMWARE_MANAGER_H #define FIRMWARE_MANAGER_H @@ -15,7 +18,6 @@ #include "core/hle/service/set/system_settings_server.h" #include "core/hle/result.h" - namespace FirmwareManager { static constexpr std::array KEY_INSTALL_RESULT_STRINGS = { @@ -40,14 +42,15 @@ enum KeyInstallResult { }; /** - * @brief Installs decryption keys for the emulator. + * @brief Installs any arbitrary set of keys for the emulator. * @param location Where the keys are located. + * @param expected_extension What extension the file should have. * @return A result code for the operation. */ -KeyInstallResult InstallDecryptionKeys(std::string location); +KeyInstallResult InstallKeys(std::string location, std::string expected_extension); /** - * \brief Get a string representation of a result from InstallDecryptionKeys. + * \brief Get a string representation of a result from InstallKeys. * \param result The result code. * \return A string representation of the passed result code. */ diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index aaedd8df1e..d04a597ed2 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -4355,13 +4355,13 @@ void GMainWindow::OnInstallDecryptionKeys() { } const QString key_source_location = QFileDialog::getOpenFileName( - this, tr("Select Dumped Keys Location"), {}, QStringLiteral("prod.keys (prod.keys)"), {}, + this, tr("Select Dumped Keys Location"), {}, QStringLiteral("Decryption Keys (*.keys)"), {}, QFileDialog::ReadOnly); if (key_source_location.isEmpty()) { return; } - FirmwareManager::KeyInstallResult result = FirmwareManager::InstallDecryptionKeys(key_source_location.toStdString()); + FirmwareManager::KeyInstallResult result = FirmwareManager::InstallKeys(key_source_location.toStdString(), "keys"); system->GetFileSystemController().CreateFactories(*vfs); game_list->PopulateAsync(UISettings::values.game_dirs);