diff --git a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java
index 3aa73f99e..08041f790 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java
+++ b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java
@@ -128,30 +128,6 @@ public final class NativeLibrary {
 
     public static native void InitGameIni(String gameID);
 
-    /**
-     * Gets the embedded icon within the given ROM.
-     *
-     * @param filename the file path to the ROM.
-     * @return an integer array containing the color data for the icon.
-     */
-    public static native int[] GetIcon(String filename);
-
-    /**
-     * 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.
-     */
-    public static native String GetTitle(String filename);
-
-    public static native String GetDescription(String filename);
-
-    public static native String GetGameId(String filename);
-
-    public static native String GetRegions(String filename);
-
-    public static native String GetCompany(String filename);
-
     public static native String GetGitRevision();
 
     /**
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/model/GameDatabase.java b/src/android/app/src/main/java/org/citra/citra_emu/model/GameDatabase.java
index 4a67c9a1a..cbbd8e32e 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/model/GameDatabase.java
+++ b/src/android/app/src/main/java/org/citra/citra_emu/model/GameDatabase.java
@@ -12,6 +12,7 @@ import org.citra.citra_emu.utils.FileUtil;
 import org.citra.citra_emu.utils.Log;
 
 import java.io.File;
+import java.io.IOException;
 import java.lang.reflect.Array;
 import java.util.Arrays;
 import java.util.HashSet;
@@ -206,27 +207,26 @@ public final class GameDatabase extends SQLiteOpenHelper {
     }
 
     private static void attemptToAddGame(SQLiteDatabase database, String filePath) {
-        String name = NativeLibrary.GetTitle(filePath);
+        GameInfo gameInfo;
+        try {
+            gameInfo = new GameInfo(filePath);
+        } catch (IOException e) {
+            gameInfo = null;
+        }
+
+        String name = gameInfo != null ? gameInfo.getTitle() : "";
 
         // If the game's title field is empty, use the filename.
         if (name.isEmpty()) {
             name = filePath.substring(filePath.lastIndexOf("/") + 1);
         }
 
-        String gameId = NativeLibrary.GetGameId(filePath);
-
-        // If the game's ID field is empty, use the filename without extension.
-        if (gameId.isEmpty()) {
-            gameId = filePath.substring(filePath.lastIndexOf("/") + 1,
-                    filePath.lastIndexOf("."));
-        }
-
         ContentValues game = Game.asContentValues(name,
-                NativeLibrary.GetDescription(filePath).replace("\n", " "),
-                NativeLibrary.GetRegions(filePath),
+                filePath.replace("\n", " "),
+                gameInfo != null ? gameInfo.getRegions() : "Invalid region",
                 filePath,
-                gameId,
-                NativeLibrary.GetCompany(filePath));
+                filePath,
+                gameInfo != null ? gameInfo.getCompany() : "");
 
         // Try to update an existing game first.
         int rowsMatched = database.update(TABLE_NAME_GAMES,    // Which table to update.
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/model/GameInfo.java b/src/android/app/src/main/java/org/citra/citra_emu/model/GameInfo.java
new file mode 100644
index 000000000..35ce9947c
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/model/GameInfo.java
@@ -0,0 +1,37 @@
+package org.citra.citra_emu.model;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.IOException;
+
+public class GameInfo {
+    @Keep
+    private final long mPointer;
+
+    @Keep
+    public GameInfo(String path) throws IOException {
+        mPointer = initialize(path);
+        if (mPointer == 0L) {
+            throw new IOException();
+        }
+    }
+
+    private static native long initialize(String path);
+
+    @Override
+    protected native void finalize();
+
+    @NonNull
+    public native String getTitle();
+
+    @NonNull
+    public native String getRegions();
+
+    @NonNull
+    public native String getCompany();
+
+    @Nullable
+    public native int[] getIcon();
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/GameIconRequestHandler.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/GameIconRequestHandler.java
index 6ebe70161..7057c07ad 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/utils/GameIconRequestHandler.java
+++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/GameIconRequestHandler.java
@@ -7,7 +7,9 @@ import com.squareup.picasso.Request;
 import com.squareup.picasso.RequestHandler;
 
 import org.citra.citra_emu.NativeLibrary;
+import org.citra.citra_emu.model.GameInfo;
 
+import java.io.IOException;
 import java.nio.IntBuffer;
 
 public class GameIconRequestHandler extends RequestHandler {
@@ -18,8 +20,14 @@ public class GameIconRequestHandler extends RequestHandler {
 
     @Override
     public Result load(Request request, int networkPolicy) {
-        String url = request.uri.toString();
-        int[] vector = NativeLibrary.GetIcon(url);
+        int[] vector;
+        try {
+            String url = request.uri.toString();
+            vector = new GameInfo(url).getIcon();
+        } catch (IOException e) {
+            vector = null;
+        }
+
         Bitmap bitmap = Bitmap.createBitmap(48, 48, Bitmap.Config.RGB_565);
         bitmap.copyPixelsFromBuffer(IntBuffer.wrap(vector));
         return new Result(bitmap, Picasso.LoadedFrom.DISK);
diff --git a/src/android/app/src/main/jni/CMakeLists.txt b/src/android/app/src/main/jni/CMakeLists.txt
index 5a1abc35d..531704490 100644
--- a/src/android/app/src/main/jni/CMakeLists.txt
+++ b/src/android/app/src/main/jni/CMakeLists.txt
@@ -20,7 +20,6 @@ add_library(citra-android SHARED
     emu_window/emu_window.cpp
     emu_window/emu_window.h
     game_info.cpp
-    game_info.h
     game_settings.cpp
     game_settings.h
     id_cache.cpp
diff --git a/src/android/app/src/main/jni/game_info.cpp b/src/android/app/src/main/jni/game_info.cpp
index 80c0379d6..ca0ff355c 100644
--- a/src/android/app/src/main/jni/game_info.cpp
+++ b/src/android/app/src/main/jni/game_info.cpp
@@ -12,12 +12,13 @@
 #include "core/hle/service/fs/archive.h"
 #include "core/loader/loader.h"
 #include "core/loader/smdh.h"
-#include "jni/game_info.h"
+#include "jni/android_common/android_common.h"
+#include "jni/id_cache.h"
 
-namespace GameInfo {
+namespace {
 
-std::vector<u8> GetSMDHData(std::string physical_name) {
-    std::unique_ptr<Loader::AppLoader> loader = Loader::GetLoader(physical_name);
+std::vector<u8> GetSMDHData(const std::string& path) {
+    std::unique_ptr<Loader::AppLoader> loader = Loader::GetLoader(path);
     if (!loader) {
         return {};
     }
@@ -51,55 +52,55 @@ std::vector<u8> GetSMDHData(std::string physical_name) {
     return smdh;
 }
 
-std::u16string GetTitle(std::string physical_name) {
-    Loader::SMDH::TitleLanguage language = Loader::SMDH::TitleLanguage::English;
-    std::vector<u8> smdh_data = GetSMDHData(physical_name);
+} // namespace
 
-    if (!Loader::IsValidSMDH(smdh_data)) {
-        // SMDH is not valid, return null
-        return {};
+extern "C" {
+
+static Loader::SMDH* GetPointer(JNIEnv* env, jobject obj) {
+    return reinterpret_cast<Loader::SMDH*>(env->GetLongField(obj, IDCache::GetGameInfoPointer()));
+}
+
+JNIEXPORT jlong JNICALL Java_org_citra_citra_1emu_model_GameInfo_initialize(JNIEnv* env, jclass,
+                                                                            jstring j_path) {
+    std::vector<u8> smdh_data = GetSMDHData(GetJString(env, j_path));
+
+    Loader::SMDH* smdh = nullptr;
+    if (Loader::IsValidSMDH(smdh_data)) {
+        smdh = new Loader::SMDH;
+        memcpy(smdh, smdh_data.data(), sizeof(Loader::SMDH));
     }
+    return reinterpret_cast<jlong>(smdh);
+}
 
-    Loader::SMDH smdh;
-    memcpy(&smdh, smdh_data.data(), sizeof(Loader::SMDH));
+JNIEXPORT void JNICALL Java_org_citra_citra_1emu_model_GameInfo_finalize(JNIEnv* env, jobject obj) {
+    delete GetPointer(env, obj);
+}
+
+jstring Java_org_citra_citra_1emu_model_GameInfo_getTitle(JNIEnv* env, jobject obj) {
+    Loader::SMDH* smdh = GetPointer(env, obj);
+    Loader::SMDH::TitleLanguage language = Loader::SMDH::TitleLanguage::English;
 
     // Get the title from SMDH in UTF-16 format
     std::u16string title{
-        reinterpret_cast<char16_t*>(smdh.titles[static_cast<int>(language)].long_title.data())};
+        reinterpret_cast<char16_t*>(smdh->titles[static_cast<size_t>(language)].long_title.data())};
 
-    return title;
+    return ToJString(env, Common::UTF16ToUTF8(title).data());
 }
 
-std::u16string GetPublisher(std::string physical_name) {
+jstring Java_org_citra_citra_1emu_model_GameInfo_getCompany(JNIEnv* env, jobject obj) {
+    Loader::SMDH* smdh = GetPointer(env, obj);
     Loader::SMDH::TitleLanguage language = Loader::SMDH::TitleLanguage::English;
-    std::vector<u8> smdh_data = GetSMDHData(physical_name);
-
-    if (!Loader::IsValidSMDH(smdh_data)) {
-        // SMDH is not valid, return null
-        return {};
-    }
-
-    Loader::SMDH smdh;
-    memcpy(&smdh, smdh_data.data(), sizeof(Loader::SMDH));
 
     // Get the Publisher's name from SMDH in UTF-16 format
     char16_t* publisher;
     publisher =
-        reinterpret_cast<char16_t*>(smdh.titles[static_cast<int>(language)].publisher.data());
+        reinterpret_cast<char16_t*>(smdh->titles[static_cast<size_t>(language)].publisher.data());
 
-    return publisher;
+    return ToJString(env, Common::UTF16ToUTF8(publisher).data());
 }
 
-std::string GetRegions(std::string physical_name) {
-    std::vector<u8> smdh_data = GetSMDHData(physical_name);
-
-    if (!Loader::IsValidSMDH(smdh_data)) {
-        // SMDH is not valid, return "Invalid region"
-        return "Invalid region";
-    }
-
-    Loader::SMDH smdh;
-    memcpy(&smdh, smdh_data.data(), sizeof(Loader::SMDH));
+jstring Java_org_citra_citra_1emu_model_GameInfo_getRegions(JNIEnv* env, jobject obj) {
+    Loader::SMDH* smdh = GetPointer(env, obj);
 
     using GameRegion = Loader::SMDH::GameRegion;
     static const std::map<GameRegion, const char*> regions_map = {
@@ -107,10 +108,10 @@ std::string GetRegions(std::string physical_name) {
         {GameRegion::Europe, "Europe"}, {GameRegion::Australia, "Australia"},
         {GameRegion::China, "China"},   {GameRegion::Korea, "Korea"},
         {GameRegion::Taiwan, "Taiwan"}};
-    std::vector<GameRegion> regions = smdh.GetRegions();
+    std::vector<GameRegion> regions = smdh->GetRegions();
 
     if (regions.empty()) {
-        return "Invalid region";
+        return ToJString(env, "Invalid region");
     }
 
     const bool region_free =
@@ -119,7 +120,7 @@ std::string GetRegions(std::string physical_name) {
         });
 
     if (region_free) {
-        return "Region free";
+        return ToJString(env, "Region free");
     }
 
     const std::string separator = ", ";
@@ -128,23 +129,22 @@ std::string GetRegions(std::string physical_name) {
         result += separator + regions_map.at(*region);
     }
 
-    return result;
+    return ToJString(env, result);
 }
 
-std::vector<u16> GetIcon(std::string physical_name) {
-    std::vector<u8> smdh_data = GetSMDHData(physical_name);
-
-    if (!Loader::IsValidSMDH(smdh_data)) {
-        // SMDH is not valid, return null
-        return std::vector<u16>(0, 0);
-    }
-
-    Loader::SMDH smdh;
-    memcpy(&smdh, smdh_data.data(), sizeof(Loader::SMDH));
+jintArray Java_org_citra_citra_1emu_model_GameInfo_getIcon(JNIEnv* env, jobject obj) {
+    Loader::SMDH* smdh = GetPointer(env, obj);
 
     // Always get a 48x48(large) icon
-    std::vector<u16> icon_data = smdh.GetIcon(true);
-    return icon_data;
-}
+    std::vector<u16> icon_data = smdh->GetIcon(true);
+    if (icon_data.empty()) {
+        return nullptr;
+    }
 
-} // namespace GameInfo
+    jintArray icon = env->NewIntArray(static_cast<jsize>(icon_data.size() / 2));
+    env->SetIntArrayRegion(icon, 0, env->GetArrayLength(icon),
+                           reinterpret_cast<jint*>(icon_data.data()));
+
+    return icon;
+}
+}
diff --git a/src/android/app/src/main/jni/game_info.h b/src/android/app/src/main/jni/game_info.h
deleted file mode 100644
index 7b9750fe2..000000000
--- a/src/android/app/src/main/jni/game_info.h
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright 2017 Citra Emulator Project
-// Licensed under GPLv2 or any later version
-// Refer to the license.txt file included.
-
-#include <string>
-
-#include "common/common_types.h"
-
-namespace GameInfo {
-std::vector<u8> GetSMDHData(std::string physical_name);
-
-std::u16string GetTitle(std::string physical_name);
-
-std::u16string GetPublisher(std::string physical_name);
-
-std::string GetRegions(std::string physical_name);
-
-std::vector<u16> GetIcon(std::string physical_name);
-} // namespace GameInfo
diff --git a/src/android/app/src/main/jni/id_cache.cpp b/src/android/app/src/main/jni/id_cache.cpp
index 255e1ae0a..f90c046c2 100644
--- a/src/android/app/src/main/jni/id_cache.cpp
+++ b/src/android/app/src/main/jni/id_cache.cpp
@@ -40,6 +40,8 @@ static jclass s_cheat_class;
 static jfieldID s_cheat_pointer;
 static jmethodID s_cheat_constructor;
 
+static jfieldID s_game_info_pointer;
+
 static std::unordered_map<VideoCore::LoadCallbackStage, jobject> s_java_load_callback_stages;
 
 namespace IDCache {
@@ -135,6 +137,10 @@ jmethodID GetCheatConstructor() {
     return s_cheat_constructor;
 }
 
+jfieldID GetGameInfoPointer() {
+    return s_game_info_pointer;
+}
+
 jobject GetJavaLoadCallbackStage(VideoCore::LoadCallbackStage stage) {
     const auto it = s_java_load_callback_stages.find(stage);
     ASSERT_MSG(it != s_java_load_callback_stages.end(), "Invalid LoadCallbackStage: {}", stage);
@@ -205,6 +211,11 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
     s_cheat_constructor = env->GetMethodID(cheat_class, "<init>", "(J)V");
     env->DeleteLocalRef(cheat_class);
 
+    // Initialize GameInfo
+    const jclass game_info_class = env->FindClass("org/citra/citra_emu/model/GameInfo");
+    s_game_info_pointer = env->GetFieldID(game_info_class, "mPointer", "J");
+    env->DeleteLocalRef(game_info_class);
+
     // Initialize LoadCallbackStage map
     const auto to_java_load_callback_stage = [env](const std::string& stage) {
         jclass load_callback_stage_class = IDCache::GetDiskCacheLoadCallbackStageClass();
diff --git a/src/android/app/src/main/jni/id_cache.h b/src/android/app/src/main/jni/id_cache.h
index 87bebed0e..0fd687666 100644
--- a/src/android/app/src/main/jni/id_cache.h
+++ b/src/android/app/src/main/jni/id_cache.h
@@ -34,6 +34,8 @@ jclass GetCheatClass();
 jfieldID GetCheatPointer();
 jmethodID GetCheatConstructor();
 
+jfieldID GetGameInfoPointer();
+
 jobject GetJavaLoadCallbackStage(VideoCore::LoadCallbackStage stage);
 
 } // namespace IDCache
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp
index 9c0e022fb..48d37666a 100644
--- a/src/android/app/src/main/jni/native.cpp
+++ b/src/android/app/src/main/jni/native.cpp
@@ -32,7 +32,6 @@
 #include "jni/camera/still_image_camera.h"
 #include "jni/config.h"
 #include "jni/emu_window/emu_window.h"
-#include "jni/game_info.h"
 #include "jni/game_settings.h"
 #include "jni/id_cache.h"
 #include "jni/input_manager.h"
@@ -436,60 +435,6 @@ void Java_org_citra_citra_1emu_NativeLibrary_onTouchMoved(JNIEnv* env,
     window->OnTouchMoved((int)x, (int)y);
 }
 
-jintArray Java_org_citra_citra_1emu_NativeLibrary_GetIcon(JNIEnv* env,
-                                                          [[maybe_unused]] jclass clazz,
-                                                          jstring j_file) {
-    std::string filepath = GetJString(env, j_file);
-
-    std::vector<u16> icon_data = GameInfo::GetIcon(filepath);
-    if (icon_data.size() == 0) {
-        return 0;
-    }
-
-    jintArray icon = env->NewIntArray(static_cast<jsize>(icon_data.size() / 2));
-    env->SetIntArrayRegion(icon, 0, env->GetArrayLength(icon),
-                           reinterpret_cast<jint*>(icon_data.data()));
-
-    return icon;
-}
-
-jstring Java_org_citra_citra_1emu_NativeLibrary_GetTitle(JNIEnv* env, [[maybe_unused]] jclass clazz,
-                                                         jstring j_filename) {
-    std::string filepath = GetJString(env, j_filename);
-    auto Title = GameInfo::GetTitle(filepath);
-    return env->NewStringUTF(Common::UTF16ToUTF8(Title).data());
-}
-
-jstring Java_org_citra_citra_1emu_NativeLibrary_GetDescription(JNIEnv* env,
-                                                               [[maybe_unused]] jclass clazz,
-                                                               jstring j_filename) {
-    return j_filename;
-}
-
-jstring Java_org_citra_citra_1emu_NativeLibrary_GetGameId(JNIEnv* env,
-                                                          [[maybe_unused]] jclass clazz,
-                                                          jstring j_filename) {
-    return j_filename;
-}
-
-jstring Java_org_citra_citra_1emu_NativeLibrary_GetRegions(JNIEnv* env,
-                                                           [[maybe_unused]] jclass clazz,
-                                                           jstring j_filename) {
-    std::string filepath = GetJString(env, j_filename);
-
-    std::string regions = GameInfo::GetRegions(filepath);
-
-    return env->NewStringUTF(regions.c_str());
-}
-
-jstring Java_org_citra_citra_1emu_NativeLibrary_GetCompany(JNIEnv* env,
-                                                           [[maybe_unused]] jclass clazz,
-                                                           jstring j_filename) {
-    std::string filepath = GetJString(env, j_filename);
-    auto publisher = GameInfo::GetPublisher(filepath);
-    return env->NewStringUTF(Common::UTF16ToUTF8(publisher).data());
-}
-
 jstring Java_org_citra_citra_1emu_NativeLibrary_GetGitRevision(JNIEnv* env,
                                                                [[maybe_unused]] jclass clazz) {
     return nullptr;