Implement firmware game checker on Android
All checks were successful
eden-license / license-header (pull_request) Successful in 15s
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:
parent
d702d6d7bf
commit
67edb897bf
11 changed files with 126 additions and 128 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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> ->
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue