android: Refactor game metadata collection to new file
This also removes irrelevant data and adds new information from/to the Game data class and RomMetadata struct
This commit is contained in:
		@@ -215,32 +215,6 @@ object NativeLibrary {
 | 
			
		||||
 | 
			
		||||
    external fun initGameIni(gameID: String?)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets the embedded icon within the given ROM.
 | 
			
		||||
     *
 | 
			
		||||
     * @param filename the file path to the ROM.
 | 
			
		||||
     * @return a byte array containing the JPEG data for the icon.
 | 
			
		||||
     */
 | 
			
		||||
    external fun getIcon(filename: String): ByteArray
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets the embedded title of the given ISO/ROM.
 | 
			
		||||
     *
 | 
			
		||||
     * @param filename The file path to the ISO/ROM.
 | 
			
		||||
     * @return the embedded title of the ISO/ROM.
 | 
			
		||||
     */
 | 
			
		||||
    external fun getTitle(filename: String): String
 | 
			
		||||
 | 
			
		||||
    external fun getDescription(filename: String): String
 | 
			
		||||
 | 
			
		||||
    external fun getGameId(filename: String): String
 | 
			
		||||
 | 
			
		||||
    external fun getRegions(filename: String): String
 | 
			
		||||
 | 
			
		||||
    external fun getCompany(filename: String): String
 | 
			
		||||
 | 
			
		||||
    external fun isHomebrew(filename: String): Boolean
 | 
			
		||||
 | 
			
		||||
    external fun setAppDirectory(directory: String)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -293,11 +267,6 @@ object NativeLibrary {
 | 
			
		||||
     */
 | 
			
		||||
    external fun stopEmulation()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Resets the in-memory ROM metadata cache.
 | 
			
		||||
     */
 | 
			
		||||
    external fun resetRomMetadata()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns true if emulation is running (or is paused).
 | 
			
		||||
     */
 | 
			
		||||
 
 | 
			
		||||
@@ -147,7 +147,7 @@ class GameAdapter(private val activity: AppCompatActivity) :
 | 
			
		||||
 | 
			
		||||
    private class DiffCallback : DiffUtil.ItemCallback<Game>() {
 | 
			
		||||
        override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean {
 | 
			
		||||
            return oldItem.gameId == newItem.gameId
 | 
			
		||||
            return oldItem.programId == newItem.programId
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean {
 | 
			
		||||
 
 | 
			
		||||
@@ -12,15 +12,14 @@ import kotlinx.serialization.Serializable
 | 
			
		||||
@Serializable
 | 
			
		||||
class Game(
 | 
			
		||||
    val title: String,
 | 
			
		||||
    val description: String,
 | 
			
		||||
    val regions: String,
 | 
			
		||||
    val path: String,
 | 
			
		||||
    val gameId: String,
 | 
			
		||||
    val company: String,
 | 
			
		||||
    val programId: String,
 | 
			
		||||
    val developer: String,
 | 
			
		||||
    val version: String,
 | 
			
		||||
    val isHomebrew: Boolean
 | 
			
		||||
) : Parcelable {
 | 
			
		||||
    val keyAddedToLibraryTime get() = "${gameId}_AddedToLibraryTime"
 | 
			
		||||
    val keyLastPlayedTime get() = "${gameId}_LastPlayed"
 | 
			
		||||
    val keyAddedToLibraryTime get() = "${programId}_AddedToLibraryTime"
 | 
			
		||||
    val keyLastPlayedTime get() = "${programId}_LastPlayed"
 | 
			
		||||
 | 
			
		||||
    override fun equals(other: Any?): Boolean {
 | 
			
		||||
        if (other !is Game) {
 | 
			
		||||
@@ -32,11 +31,9 @@ class Game(
 | 
			
		||||
 | 
			
		||||
    override fun hashCode(): Int {
 | 
			
		||||
        var result = title.hashCode()
 | 
			
		||||
        result = 31 * result + description.hashCode()
 | 
			
		||||
        result = 31 * result + regions.hashCode()
 | 
			
		||||
        result = 31 * result + path.hashCode()
 | 
			
		||||
        result = 31 * result + gameId.hashCode()
 | 
			
		||||
        result = 31 * result + company.hashCode()
 | 
			
		||||
        result = 31 * result + programId.hashCode()
 | 
			
		||||
        result = 31 * result + developer.hashCode()
 | 
			
		||||
        result = 31 * result + isHomebrew.hashCode()
 | 
			
		||||
        return result
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -14,15 +14,13 @@ import kotlinx.coroutines.flow.MutableStateFlow
 | 
			
		||||
import kotlinx.coroutines.flow.StateFlow
 | 
			
		||||
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
 | 
			
		||||
import org.yuzu.yuzu_emu.YuzuApplication
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.GameHelper
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.GameMetadata
 | 
			
		||||
 | 
			
		||||
@OptIn(ExperimentalSerializationApi::class)
 | 
			
		||||
class GamesViewModel : ViewModel() {
 | 
			
		||||
    val games: StateFlow<List<Game>> get() = _games
 | 
			
		||||
    private val _games = MutableStateFlow(emptyList<Game>())
 | 
			
		||||
@@ -58,7 +56,8 @@ class GamesViewModel : ViewModel() {
 | 
			
		||||
                        val game: Game
 | 
			
		||||
                        try {
 | 
			
		||||
                            game = Json.decodeFromString(it)
 | 
			
		||||
                        } catch (e: MissingFieldException) {
 | 
			
		||||
                        } catch (e: Exception) {
 | 
			
		||||
                            // We don't care about any errors related to parsing the game cache
 | 
			
		||||
                            return@forEach
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
@@ -113,7 +112,7 @@ class GamesViewModel : ViewModel() {
 | 
			
		||||
 | 
			
		||||
        viewModelScope.launch {
 | 
			
		||||
            withContext(Dispatchers.IO) {
 | 
			
		||||
                NativeLibrary.resetRomMetadata()
 | 
			
		||||
                GameMetadata.resetMetadata()
 | 
			
		||||
                setGames(GameHelper.getGames())
 | 
			
		||||
                _isReloading.value = false
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -71,27 +71,26 @@ object GameHelper {
 | 
			
		||||
 | 
			
		||||
    fun getGame(uri: Uri, addedToLibrary: Boolean): Game {
 | 
			
		||||
        val filePath = uri.toString()
 | 
			
		||||
        var name = NativeLibrary.getTitle(filePath)
 | 
			
		||||
        var name = GameMetadata.getTitle(filePath)
 | 
			
		||||
 | 
			
		||||
        // If the game's title field is empty, use the filename.
 | 
			
		||||
        if (name.isEmpty()) {
 | 
			
		||||
            name = FileUtil.getFilename(uri)
 | 
			
		||||
        }
 | 
			
		||||
        var gameId = NativeLibrary.getGameId(filePath)
 | 
			
		||||
        var programId = GameMetadata.getProgramId(filePath)
 | 
			
		||||
 | 
			
		||||
        // If the game's ID field is empty, use the filename without extension.
 | 
			
		||||
        if (gameId.isEmpty()) {
 | 
			
		||||
            gameId = name.substring(0, name.lastIndexOf("."))
 | 
			
		||||
        if (programId.isEmpty()) {
 | 
			
		||||
            programId = name.substring(0, name.lastIndexOf("."))
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val newGame = Game(
 | 
			
		||||
            name,
 | 
			
		||||
            NativeLibrary.getDescription(filePath).replace("\n", " "),
 | 
			
		||||
            NativeLibrary.getRegions(filePath),
 | 
			
		||||
            filePath,
 | 
			
		||||
            gameId,
 | 
			
		||||
            NativeLibrary.getCompany(filePath),
 | 
			
		||||
            NativeLibrary.isHomebrew(filePath)
 | 
			
		||||
            programId,
 | 
			
		||||
            GameMetadata.getDeveloper(filePath),
 | 
			
		||||
            GameMetadata.getVersion(filePath),
 | 
			
		||||
            GameMetadata.getIsHomebrew(filePath)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if (addedToLibrary) {
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,6 @@ import coil.key.Keyer
 | 
			
		||||
import coil.memory.MemoryCache
 | 
			
		||||
import coil.request.ImageRequest
 | 
			
		||||
import coil.request.Options
 | 
			
		||||
import org.yuzu.yuzu_emu.NativeLibrary
 | 
			
		||||
import org.yuzu.yuzu_emu.R
 | 
			
		||||
import org.yuzu.yuzu_emu.YuzuApplication
 | 
			
		||||
import org.yuzu.yuzu_emu.model.Game
 | 
			
		||||
@@ -36,7 +35,7 @@ class GameIconFetcher(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun decodeGameIcon(uri: String): Bitmap? {
 | 
			
		||||
        val data = NativeLibrary.getIcon(uri)
 | 
			
		||||
        val data = GameMetadata.getIcon(uri)
 | 
			
		||||
        return BitmapFactory.decodeByteArray(
 | 
			
		||||
            data,
 | 
			
		||||
            0,
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,20 @@
 | 
			
		||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.utils
 | 
			
		||||
 | 
			
		||||
object GameMetadata {
 | 
			
		||||
    external fun getTitle(path: String): String
 | 
			
		||||
 | 
			
		||||
    external fun getProgramId(path: String): String
 | 
			
		||||
 | 
			
		||||
    external fun getDeveloper(path: String): String
 | 
			
		||||
 | 
			
		||||
    external fun getVersion(path: String): String
 | 
			
		||||
 | 
			
		||||
    external fun getIcon(path: String): ByteArray
 | 
			
		||||
 | 
			
		||||
    external fun getIsHomebrew(path: String): Boolean
 | 
			
		||||
 | 
			
		||||
    external fun resetMetadata()
 | 
			
		||||
}
 | 
			
		||||
@@ -17,6 +17,7 @@ add_library(yuzu-android SHARED
 | 
			
		||||
    native.h
 | 
			
		||||
    native_config.cpp
 | 
			
		||||
    uisettings.cpp
 | 
			
		||||
    game_metadata.cpp
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
set_property(TARGET yuzu-android PROPERTY IMPORTED_LOCATION ${FFmpeg_LIBRARY_DIR})
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										112
									
								
								src/android/app/src/main/jni/game_metadata.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								src/android/app/src/main/jni/game_metadata.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,112 @@
 | 
			
		||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
#include <core/core.h>
 | 
			
		||||
#include <core/file_sys/patch_manager.h>
 | 
			
		||||
#include <core/loader/nro.h>
 | 
			
		||||
#include <jni.h>
 | 
			
		||||
#include "core/loader/loader.h"
 | 
			
		||||
#include "jni/android_common/android_common.h"
 | 
			
		||||
#include "native.h"
 | 
			
		||||
 | 
			
		||||
struct RomMetadata {
 | 
			
		||||
    std::string title;
 | 
			
		||||
    u64 programId;
 | 
			
		||||
    std::string developer;
 | 
			
		||||
    std::string version;
 | 
			
		||||
    std::vector<u8> icon;
 | 
			
		||||
    bool isHomebrew;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
std::unordered_map<std::string, RomMetadata> m_rom_metadata_cache;
 | 
			
		||||
 | 
			
		||||
RomMetadata CacheRomMetadata(const std::string& path) {
 | 
			
		||||
    const auto file =
 | 
			
		||||
        Core::GetGameFileFromPath(EmulationSession::GetInstance().System().GetFilesystem(), path);
 | 
			
		||||
    auto loader = Loader::GetLoader(EmulationSession::GetInstance().System(), file, 0, 0);
 | 
			
		||||
 | 
			
		||||
    RomMetadata entry;
 | 
			
		||||
    loader->ReadTitle(entry.title);
 | 
			
		||||
    loader->ReadProgramId(entry.programId);
 | 
			
		||||
    loader->ReadIcon(entry.icon);
 | 
			
		||||
 | 
			
		||||
    const FileSys::PatchManager pm{
 | 
			
		||||
        entry.programId, EmulationSession::GetInstance().System().GetFileSystemController(),
 | 
			
		||||
        EmulationSession::GetInstance().System().GetContentProvider()};
 | 
			
		||||
    const auto control = pm.GetControlMetadata();
 | 
			
		||||
 | 
			
		||||
    if (control.first != nullptr) {
 | 
			
		||||
        entry.developer = control.first->GetDeveloperName();
 | 
			
		||||
        entry.version = control.first->GetVersionString();
 | 
			
		||||
    } else {
 | 
			
		||||
        FileSys::NACP nacp;
 | 
			
		||||
        if (loader->ReadControlData(nacp) == Loader::ResultStatus::Success) {
 | 
			
		||||
            entry.developer = nacp.GetDeveloperName();
 | 
			
		||||
        } else {
 | 
			
		||||
            entry.developer = "";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        entry.version = "1.0.0";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (loader->GetFileType() == Loader::FileType::NRO) {
 | 
			
		||||
        auto loader_nro = reinterpret_cast<Loader::AppLoader_NRO*>(loader.get());
 | 
			
		||||
        entry.isHomebrew = loader_nro->IsHomebrew();
 | 
			
		||||
    } else {
 | 
			
		||||
        entry.isHomebrew = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    m_rom_metadata_cache[path] = entry;
 | 
			
		||||
 | 
			
		||||
    return entry;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
RomMetadata GetRomMetadata(const std::string& path) {
 | 
			
		||||
    if (auto search = m_rom_metadata_cache.find(path); search != m_rom_metadata_cache.end()) {
 | 
			
		||||
        return search->second;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return CacheRomMetadata(path);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extern "C" {
 | 
			
		||||
 | 
			
		||||
jstring Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getTitle(JNIEnv* env, jobject obj,
 | 
			
		||||
                                                            jstring jpath) {
 | 
			
		||||
    return ToJString(env, GetRomMetadata(GetJString(env, jpath)).title);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jstring Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getProgramId(JNIEnv* env, jobject obj,
 | 
			
		||||
                                                                jstring jpath) {
 | 
			
		||||
    return ToJString(env, std::to_string(GetRomMetadata(GetJString(env, jpath)).programId));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jstring Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getDeveloper(JNIEnv* env, jobject obj,
 | 
			
		||||
                                                                jstring jpath) {
 | 
			
		||||
    return ToJString(env, GetRomMetadata(GetJString(env, jpath)).developer);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jstring Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getVersion(JNIEnv* env, jobject obj,
 | 
			
		||||
                                                              jstring jpath) {
 | 
			
		||||
    return ToJString(env, GetRomMetadata(GetJString(env, jpath)).version);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jbyteArray Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getIcon(JNIEnv* env, jobject obj,
 | 
			
		||||
                                                              jstring jpath) {
 | 
			
		||||
    auto icon_data = GetRomMetadata(GetJString(env, jpath)).icon;
 | 
			
		||||
    jbyteArray icon = env->NewByteArray(static_cast<jsize>(icon_data.size()));
 | 
			
		||||
    env->SetByteArrayRegion(icon, 0, env->GetArrayLength(icon),
 | 
			
		||||
                            reinterpret_cast<jbyte*>(icon_data.data()));
 | 
			
		||||
    return icon;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jboolean Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getIsHomebrew(JNIEnv* env, jobject obj,
 | 
			
		||||
                                                                  jstring jpath) {
 | 
			
		||||
    return static_cast<jboolean>(GetRomMetadata(GetJString(env, jpath)).isHomebrew);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_utils_GameMetadata_resetMetadata(JNIEnv* env, jobject obj) {
 | 
			
		||||
    return m_rom_metadata_cache.clear();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
} // extern "C"
 | 
			
		||||
@@ -558,10 +558,6 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_stopEmulation(JNIEnv* env, jclass cla
 | 
			
		||||
    EmulationSession::GetInstance().HaltEmulation();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_NativeLibrary_resetRomMetadata(JNIEnv* env, jclass clazz) {
 | 
			
		||||
    EmulationSession::GetInstance().ResetRomMetadata();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isRunning(JNIEnv* env, jclass clazz) {
 | 
			
		||||
    return static_cast<jboolean>(EmulationSession::GetInstance().IsRunning());
 | 
			
		||||
}
 | 
			
		||||
@@ -667,46 +663,6 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchReleased(JNIEnv* env, jclass c
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jbyteArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getIcon(JNIEnv* env, jclass clazz,
 | 
			
		||||
                                                         jstring j_filename) {
 | 
			
		||||
    jauto icon_data = EmulationSession::GetInstance().GetRomIcon(GetJString(env, j_filename));
 | 
			
		||||
    jbyteArray icon = env->NewByteArray(static_cast<jsize>(icon_data.size()));
 | 
			
		||||
    env->SetByteArrayRegion(icon, 0, env->GetArrayLength(icon),
 | 
			
		||||
                            reinterpret_cast<jbyte*>(icon_data.data()));
 | 
			
		||||
    return icon;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getTitle(JNIEnv* env, jclass clazz,
 | 
			
		||||
                                                       jstring j_filename) {
 | 
			
		||||
    jauto title = EmulationSession::GetInstance().GetRomTitle(GetJString(env, j_filename));
 | 
			
		||||
    return env->NewStringUTF(title.c_str());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getDescription(JNIEnv* env, jclass clazz,
 | 
			
		||||
                                                             jstring j_filename) {
 | 
			
		||||
    return j_filename;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getGameId(JNIEnv* env, jclass clazz,
 | 
			
		||||
                                                        jstring j_filename) {
 | 
			
		||||
    return j_filename;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getRegions(JNIEnv* env, jclass clazz,
 | 
			
		||||
                                                         jstring j_filename) {
 | 
			
		||||
    return env->NewStringUTF("");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getCompany(JNIEnv* env, jclass clazz,
 | 
			
		||||
                                                         jstring j_filename) {
 | 
			
		||||
    return env->NewStringUTF("");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isHomebrew(JNIEnv* env, jclass clazz,
 | 
			
		||||
                                                          jstring j_filename) {
 | 
			
		||||
    return EmulationSession::GetInstance().GetIsHomebrew(GetJString(env, j_filename));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeEmulation(JNIEnv* env, jclass clazz) {
 | 
			
		||||
    // Create the default config.ini.
 | 
			
		||||
    Config{};
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user