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() 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 @Keep
@JvmStatic @JvmStatic
fun exitEmulationActivity(resultCode: Int) { fun exitEmulationActivity(resultCode: Int) {
@ -454,12 +423,21 @@ object NativeLibrary {
external fun verifyFirmware(): Int 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 path The path to install keys from.
* @param ext What extension the keys should have.
* @return The result code. * @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 * Checks the PatchManager for any addons that are available

View file

@ -3,11 +3,16 @@
package org.yuzu.yuzu_emu.adapters package org.yuzu.yuzu_emu.adapters
import android.content.DialogInterface
import android.net.Uri import android.net.Uri
import android.text.Html
import android.text.method.LinkMovementMethod
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.pm.ShortcutInfoCompat 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.utils.ViewUtils.marquee
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
import androidx.recyclerview.widget.RecyclerView 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) : class GameAdapter(private val activity: AppCompatActivity) :
AbstractDiffAdapter<Game, GameAdapter.GameViewHolder>(exact = false) { AbstractDiffAdapter<Game, GameAdapter.GameViewHolder>(exact = false) {
@ -171,8 +180,9 @@ class GameAdapter(private val activity: AppCompatActivity) :
fun onClick(game: Game) { fun onClick(game: Game) {
val gameExists = DocumentFile.fromSingleUri( val gameExists = DocumentFile.fromSingleUri(
YuzuApplication.appContext, YuzuApplication.appContext,
Uri.parse(game.path) game.path.toUri()
)?.exists() == true )?.exists() == true
if (!gameExists) { if (!gameExists) {
Toast.makeText( Toast.makeText(
YuzuApplication.appContext, YuzuApplication.appContext,
@ -184,29 +194,49 @@ class GameAdapter(private val activity: AppCompatActivity) :
return return
} }
val preferences = val launch: () -> Unit = {
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) val preferences =
preferences.edit() PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
.putLong( preferences.edit {
game.keyLastPlayedTime, putLong(
System.currentTimeMillis() 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)
} }
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) if (NativeLibrary.gameRequiresFirmware(game.programId) && !NativeLibrary.isFirmwareAvailable()) {
binding.root.findNavController().navigate(action) 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 { fun onLongClick(game: Game): Boolean {

View file

@ -478,6 +478,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
} }
private fun startEmulation(programIndex: Int = 0) { private fun startEmulation(programIndex: Int = 0) {
println("PROGRAM INDEX: $programIndex")
if (!NativeLibrary.isRunning() && !NativeLibrary.isPaused()) { if (!NativeLibrary.isRunning() && !NativeLibrary.isPaused()) {
if (!DirectoryInitialization.areDirectoriesReady) { if (!DirectoryInitialization.areDirectoriesReady) {
DirectoryInitialization.start() DirectoryInitialization.start()

View file

@ -352,7 +352,7 @@ class SetupFragment : Fragment() {
val getProdKey = val getProdKey =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
if (result != null) { if (result != null) {
mainActivity.processKey(result) mainActivity.processKey(result, "keys")
if (NativeLibrary.areKeysPresent()) { if (NativeLibrary.areKeysPresent()) {
keyCallback.onStepCompleted() keyCallback.onStepCompleted()
} }

View file

@ -47,9 +47,6 @@ import info.debatty.java.stringsimilarity.Jaccard
import info.debatty.java.stringsimilarity.JaroWinkler import info.debatty.java.stringsimilarity.JaroWinkler
import java.util.Locale import java.util.Locale
import androidx.core.content.edit 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 import androidx.core.view.doOnNextLayout
class GamesFragment : Fragment() { class GamesFragment : Fragment() {
@ -151,7 +148,7 @@ class GamesFragment : Fragment() {
) )
} }
gamesViewModel.games.collect(viewLifecycleOwner) { gamesViewModel.games.collect(viewLifecycleOwner) {
if (it.size > 0) { if (it.isNotEmpty()) {
setAdapter(it) setAdapter(it)
} }
} }
@ -361,7 +358,7 @@ class GamesFragment : Fragment() {
popup.setOnMenuItemClickListener { item -> popup.setOnMenuItemClickListener { item ->
currentFilter = item.itemId currentFilter = item.itemId
preferences.edit().putInt(PREF_SORT_TYPE, currentFilter).apply() preferences.edit { putInt(PREF_SORT_TYPE, currentFilter) }
filterAndSearch() filterAndSearch()
true true
} }

View file

@ -6,6 +6,8 @@ package org.yuzu.yuzu_emu.ui.main
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.ParcelFileDescriptor
import android.provider.OpenableColumns
import android.view.View import android.view.View
import android.view.ViewGroup.MarginLayoutParams import android.view.ViewGroup.MarginLayoutParams
import android.view.WindowManager import android.view.WindowManager
@ -47,6 +49,7 @@ import java.io.BufferedOutputStream
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.net.toFile
class MainActivity : AppCompatActivity(), ThemeProvider { class MainActivity : AppCompatActivity(), ThemeProvider {
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
@ -328,12 +331,18 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
val getProdKey = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> val getProdKey = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
if (result != null) { if (result != null) {
processKey(result) processKey(result, "keys")
} }
} }
fun processKey(result: Uri): Boolean { val getAmiiboKey = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
if (FileUtil.getExtension(result) != "keys") { 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( MessageDialogFragment.newInstance(
this, this,
titleId = R.string.keys_failed, titleId = R.string.keys_failed,
@ -348,23 +357,26 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
val dstPath = DirectoryInitialization.userDirectory + "/keys/" val dstPath = DirectoryInitialization.userDirectory + "/keys/"
if (FileUtil.copyUriToInternalStorage( if (FileUtil.copyUriToInternalStorage(
result, dstPath, "prod.keys" result, dstPath, ""
) != null ) != null
) { ) {
if (NativeLibrary.reloadKeys()) { if (NativeLibrary.reloadKeys()) {
Toast.makeText( Toast.makeText(
applicationContext, R.string.keys_install_success, Toast.LENGTH_SHORT applicationContext, R.string.keys_install_success, Toast.LENGTH_SHORT
).show() ).show()
homeViewModel.setCheckKeys(true)
val firstTimeSetup = if (check) {
PreferenceManager.getDefaultSharedPreferences(applicationContext) homeViewModel.setCheckKeys(true)
.getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true)
if (!firstTimeSetup) { val firstTimeSetup =
homeViewModel.setCheckFirmware(true) PreferenceManager.getDefaultSharedPreferences(applicationContext)
.getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true)
if (!firstTimeSetup) {
homeViewModel.setCheckFirmware(true)
}
gamesViewModel.reloadGames(true)
} }
gamesViewModel.reloadGames(true)
return true return true
} else { } else {
MessageDialogFragment.newInstance( MessageDialogFragment.newInstance(
@ -381,11 +393,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
} }
val getFirmware = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> val getFirmware = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
if (result == null) { if (result != null) {
return@registerForActivityResult processFirmware(result)
} }
processFirmware(result)
} }
fun processFirmware(result: Uri, onComplete: (() -> Unit)? = null) { fun processFirmware(result: Uri, onComplete: (() -> Unit)? = null) {
@ -458,44 +468,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
}.show(supportFragmentManager, ProgressDialogFragment.TAG) }.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( val installGameUpdate = registerForActivityResult(
ActivityResultContracts.OpenMultipleDocuments() ActivityResultContracts.OpenMultipleDocuments()
) { documents: List<Uri> -> ) { documents: List<Uri> ->

View file

@ -285,7 +285,6 @@ Core::SystemResultStatus EmulationSession::InitializeEmulation(const std::string
.program_index = static_cast<s32>(program_index), .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); m_load_result = m_system.Load(EmulationSession::GetInstance().Window(), filepath, params);
if (m_load_result != Core::SystemResultStatus::Success) { if (m_load_result != Core::SystemResultStatus::Success) {
return m_load_result; 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())); return static_cast<int>(FirmwareManager::VerifyFirmware(EmulationSession::GetInstance().System()));
} }
jint Java_org_yuzu_yuzu_1emu_NativeLibrary_installDecryptionKeys(JNIEnv* env, jclass clazz, jstring jpath) { jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_gameRequiresFirmware(JNIEnv* env, jclass clazz, jstring jprogramId) {
const auto path = Common::Android::GetJString(env, jpath); 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, 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_corrupted</item>
<item>@string/error_firmware_too_new</item> <item>@string/error_firmware_too_new</item>
</string-array> </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> </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 "firmware_manager.h"
#include <filesystem> #include <filesystem>
@ -9,12 +12,15 @@
#include "core/crypto/key_manager.h" #include "core/crypto/key_manager.h"
#include "frontend_common/content_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); 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 prod_key_path = location;
const std::filesystem::path key_source_path = prod_key_path.parent_path(); const std::filesystem::path key_source_path = prod_key_path.parent_path();
if (!Common::FS::IsDir(key_source_path)) { if (!Common::FS::IsDir(key_source_path)) {
return InvalidDir; return InvalidDir;
} }
@ -39,9 +45,8 @@ FirmwareManager::KeyInstallResult FirmwareManager::InstallDecryptionKeys(std::st
return ErrorWrongFilename; return ErrorWrongFilename;
} }
const auto yuzu_keys_dir = Common::FS::GetEdenPath(Common::FS::EdenPath::KeysDir);
for (const auto &key_file : source_key_files) { 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, if (!std::filesystem::copy_file(key_file,
destination_key_file, destination_key_file,
std::filesystem::copy_options::overwrite_existing)) { std::filesystem::copy_options::overwrite_existing)) {
@ -54,7 +59,6 @@ FirmwareManager::KeyInstallResult FirmwareManager::InstallDecryptionKeys(std::st
} }
// Reinitialize the key manager // Reinitialize the key manager
Core::Crypto::KeyManager::Instance().ReloadKeys(); Core::Crypto::KeyManager::Instance().ReloadKeys();
if (ContentManager::AreKeysPresent()) { 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 #ifndef FIRMWARE_MANAGER_H
#define FIRMWARE_MANAGER_H #define FIRMWARE_MANAGER_H
@ -15,7 +18,6 @@
#include "core/hle/service/set/system_settings_server.h" #include "core/hle/service/set/system_settings_server.h"
#include "core/hle/result.h" #include "core/hle/result.h"
namespace FirmwareManager { namespace FirmwareManager {
static constexpr std::array<const char *, 5> KEY_INSTALL_RESULT_STRINGS = { 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 location Where the keys are located.
* @param expected_extension What extension the file should have.
* @return A result code for the operation. * @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. * \param result The result code.
* \return A string representation of the passed 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( 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); QFileDialog::ReadOnly);
if (key_source_location.isEmpty()) { if (key_source_location.isEmpty()) {
return; return;
} }
FirmwareManager::KeyInstallResult result = FirmwareManager::InstallDecryptionKeys(key_source_location.toStdString()); FirmwareManager::KeyInstallResult result = FirmwareManager::InstallKeys(key_source_location.toStdString(), "keys");
system->GetFileSystemController().CreateFactories(*vfs); system->GetFileSystemController().CreateFactories(*vfs);
game_list->PopulateAsync(UISettings::values.game_dirs); game_list->PopulateAsync(UISettings::values.game_dirs);