Implement firmware game checker on Android
All checks were successful
eden-license / license-header (pull_request) Successful in 15s

Signed-off-by: crueter <crueter@eden-emu.dev>
This commit is contained in:
crueter 2025-07-10 23:52:14 -04:00
parent d702d6d7bf
commit 67edb897bf
Signed by: crueter
GPG key ID: 425ACD2D4830EBC6
11 changed files with 126 additions and 128 deletions

View file

@ -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<View>(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

View file

@ -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<Game, GameAdapter.GameViewHolder>(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,14 +194,15 @@ class GameAdapter(private val activity: AppCompatActivity) :
return
}
val launch: () -> Unit = {
val preferences =
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
preferences.edit()
.putLong(
preferences.edit {
putLong(
game.keyLastPlayedTime,
System.currentTimeMillis()
)
.apply()
}
activity.lifecycleScope.launch {
withContext(Dispatchers.IO) {
@ -209,6 +220,25 @@ class GameAdapter(private val activity: AppCompatActivity) :
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 {
val action = HomeNavigationDirections.actionGlobalPerGamePropertiesFragment(game)
binding.root.findNavController().navigate(action)

View file

@ -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()

View file

@ -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()
}

View file

@ -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
}

View file

@ -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,13 +357,15 @@ 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()
if (check) {
homeViewModel.setCheckKeys(true)
val firstTimeSetup =
@ -365,6 +376,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
}
gamesViewModel.reloadGames(true)
}
return true
} else {
MessageDialogFragment.newInstance(
@ -381,12 +393,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
}
val getFirmware = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
if (result == null) {
return@registerForActivityResult
}
if (result != null) {
processFirmware(result)
}
}
fun processFirmware(result: Uri, onComplete: (() -> Unit)? = null) {
val filterNCA = FilenameFilter { _, dirName -> dirName.endsWith(".nca") }
@ -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<Uri> ->

View file

@ -285,7 +285,6 @@ Core::SystemResultStatus EmulationSession::InitializeEmulation(const std::string
.program_index = static_cast<s32>(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<int>(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<int>(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<int>(FirmwareManager::InstallKeys(path, ext));
}
jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPatchesForFile(JNIEnv* env, jobject jobj,

View file

@ -502,4 +502,11 @@
<item>@string/error_firmware_corrupted</item>
<item>@string/error_firmware_too_new</item>
</string-array>
<string-array name="installKeysResults">
<item>""</item>
<item>@string/error_keys_copy_failed</item>
<item>@string/error_keys_invalid_filename</item>
<item>@string/error_keys_failed_init</item>
</string-array>
</resources>

View file

@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include "firmware_manager.h"
#include <filesystem>
@ -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()) {

View file

@ -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<const char *, 5> 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.
*/

View file

@ -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);