From ccd3dd842f2bf7cf16c7b93e3b83a2afc8af4a69 Mon Sep 17 00:00:00 2001
From: t895 <clombardo169@gmail.com>
Date: Fri, 19 Jan 2024 00:56:43 -0500
Subject: [PATCH] frontend_common: Add content manager utility functions

Creates utility functions to remove/install DLC, updates, and base game content
---
 .../java/org/yuzu/yuzu_emu/NativeLibrary.kt   |  19 +-
 .../org/yuzu/yuzu_emu/model/InstallResult.kt  |  15 ++
 .../jni/android_common/android_common.cpp     |  16 ++
 .../main/jni/android_common/android_common.h  |   7 +
 src/android/app/src/main/jni/id_cache.cpp     |  46 +++++
 src/android/app/src/main/jni/id_cache.h       |   8 +
 src/android/app/src/main/jni/native.cpp       |  80 ++-------
 src/android/app/src/main/jni/native.h         |   2 +-
 src/frontend_common/CMakeLists.txt            |   1 +
 src/frontend_common/content_manager.h         | 168 ++++++++++++++++++
 src/yuzu/main.cpp                             | 166 ++++-------------
 src/yuzu/main.h                               |  11 +-
 12 files changed, 318 insertions(+), 221 deletions(-)
 create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/model/InstallResult.kt
 create mode 100644 src/frontend_common/content_manager.h

diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
index b7556e3530..8cb98d6d7b 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
@@ -21,6 +21,7 @@ import org.yuzu.yuzu_emu.utils.DocumentsTree
 import org.yuzu.yuzu_emu.utils.FileUtil
 import org.yuzu.yuzu_emu.utils.Log
 import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable
+import org.yuzu.yuzu_emu.model.InstallResult
 
 /**
  * Class which contains methods that interact
@@ -235,9 +236,12 @@ object NativeLibrary {
     /**
      * Installs a nsp or xci file to nand
      * @param filename String representation of file uri
-     * @param extension Lowercase string representation of file extension without "."
+     * @return int representation of [InstallResult]
      */
-    external fun installFileToNand(filename: String, extension: String): Int
+    external fun installFileToNand(
+        filename: String,
+        callback: (max: Long, progress: Long) -> Boolean
+    ): Int
 
     external fun doesUpdateMatchProgram(programId: String, updatePath: String): Boolean
 
@@ -609,15 +613,4 @@ object NativeLibrary {
         const val RELEASED = 0
         const val PRESSED = 1
     }
-
-    /**
-     * Result from installFileToNand
-     */
-    object InstallFileToNandResult {
-        const val Success = 0
-        const val SuccessFileOverwritten = 1
-        const val Error = 2
-        const val ErrorBaseGame = 3
-        const val ErrorFilenameExtension = 4
-    }
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/InstallResult.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/InstallResult.kt
new file mode 100644
index 0000000000..0c3cd0521e
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/InstallResult.kt
@@ -0,0 +1,15 @@
+// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.model
+
+enum class InstallResult(val int: Int) {
+    Success(0),
+    Overwrite(1),
+    Failure(2),
+    BaseInstallAttempted(3);
+
+    companion object {
+        fun from(int: Int): InstallResult = entries.firstOrNull { it.int == int } ?: Success
+    }
+}
diff --git a/src/android/app/src/main/jni/android_common/android_common.cpp b/src/android/app/src/main/jni/android_common/android_common.cpp
index 1e884ffdd2..7018a52af3 100644
--- a/src/android/app/src/main/jni/android_common/android_common.cpp
+++ b/src/android/app/src/main/jni/android_common/android_common.cpp
@@ -42,3 +42,19 @@ double GetJDouble(JNIEnv* env, jobject jdouble) {
 jobject ToJDouble(JNIEnv* env, double value) {
     return env->NewObject(IDCache::GetDoubleClass(), IDCache::GetDoubleConstructor(), value);
 }
+
+s32 GetJInteger(JNIEnv* env, jobject jinteger) {
+    return env->GetIntField(jinteger, IDCache::GetIntegerValueField());
+}
+
+jobject ToJInteger(JNIEnv* env, s32 value) {
+    return env->NewObject(IDCache::GetIntegerClass(), IDCache::GetIntegerConstructor(), value);
+}
+
+bool GetJBoolean(JNIEnv* env, jobject jboolean) {
+    return env->GetBooleanField(jboolean, IDCache::GetBooleanValueField());
+}
+
+jobject ToJBoolean(JNIEnv* env, bool value) {
+    return env->NewObject(IDCache::GetBooleanClass(), IDCache::GetBooleanConstructor(), value);
+}
diff --git a/src/android/app/src/main/jni/android_common/android_common.h b/src/android/app/src/main/jni/android_common/android_common.h
index 8eb803e1b0..29a338c0ae 100644
--- a/src/android/app/src/main/jni/android_common/android_common.h
+++ b/src/android/app/src/main/jni/android_common/android_common.h
@@ -6,6 +6,7 @@
 #include <string>
 
 #include <jni.h>
+#include "common/common_types.h"
 
 std::string GetJString(JNIEnv* env, jstring jstr);
 jstring ToJString(JNIEnv* env, std::string_view str);
@@ -13,3 +14,9 @@ jstring ToJString(JNIEnv* env, std::u16string_view str);
 
 double GetJDouble(JNIEnv* env, jobject jdouble);
 jobject ToJDouble(JNIEnv* env, double value);
+
+s32 GetJInteger(JNIEnv* env, jobject jinteger);
+jobject ToJInteger(JNIEnv* env, s32 value);
+
+bool GetJBoolean(JNIEnv* env, jobject jboolean);
+jobject ToJBoolean(JNIEnv* env, bool value);
diff --git a/src/android/app/src/main/jni/id_cache.cpp b/src/android/app/src/main/jni/id_cache.cpp
index c79ad7d76b..19ced175fa 100644
--- a/src/android/app/src/main/jni/id_cache.cpp
+++ b/src/android/app/src/main/jni/id_cache.cpp
@@ -47,6 +47,14 @@ static jclass s_double_class;
 static jmethodID s_double_constructor;
 static jfieldID s_double_value_field;
 
+static jclass s_integer_class;
+static jmethodID s_integer_constructor;
+static jfieldID s_integer_value_field;
+
+static jclass s_boolean_class;
+static jmethodID s_boolean_constructor;
+static jfieldID s_boolean_value_field;
+
 static constexpr jint JNI_VERSION = JNI_VERSION_1_6;
 
 namespace IDCache {
@@ -198,6 +206,30 @@ jfieldID GetDoubleValueField() {
     return s_double_value_field;
 }
 
+jclass GetIntegerClass() {
+    return s_integer_class;
+}
+
+jmethodID GetIntegerConstructor() {
+    return s_integer_constructor;
+}
+
+jfieldID GetIntegerValueField() {
+    return s_integer_value_field;
+}
+
+jclass GetBooleanClass() {
+    return s_boolean_class;
+}
+
+jmethodID GetBooleanConstructor() {
+    return s_boolean_constructor;
+}
+
+jfieldID GetBooleanValueField() {
+    return s_boolean_value_field;
+}
+
 } // namespace IDCache
 
 #ifdef __cplusplus
@@ -284,6 +316,18 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
     s_double_value_field = env->GetFieldID(double_class, "value", "D");
     env->DeleteLocalRef(double_class);
 
+    const jclass int_class = env->FindClass("java/lang/Integer");
+    s_integer_class = reinterpret_cast<jclass>(env->NewGlobalRef(int_class));
+    s_integer_constructor = env->GetMethodID(int_class, "<init>", "(I)V");
+    s_integer_value_field = env->GetFieldID(int_class, "value", "I");
+    env->DeleteLocalRef(int_class);
+
+    const jclass boolean_class = env->FindClass("java/lang/Boolean");
+    s_boolean_class = reinterpret_cast<jclass>(env->NewGlobalRef(boolean_class));
+    s_boolean_constructor = env->GetMethodID(boolean_class, "<init>", "(Z)V");
+    s_boolean_value_field = env->GetFieldID(boolean_class, "value", "Z");
+    env->DeleteLocalRef(boolean_class);
+
     // Initialize Android Storage
     Common::FS::Android::RegisterCallbacks(env, s_native_library_class);
 
@@ -310,6 +354,8 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) {
     env->DeleteGlobalRef(s_pair_class);
     env->DeleteGlobalRef(s_overlay_control_data_class);
     env->DeleteGlobalRef(s_double_class);
+    env->DeleteGlobalRef(s_integer_class);
+    env->DeleteGlobalRef(s_boolean_class);
 
     // UnInitialize applets
     SoftwareKeyboard::CleanupJNI(env);
diff --git a/src/android/app/src/main/jni/id_cache.h b/src/android/app/src/main/jni/id_cache.h
index 784d1412f6..0e5267b738 100644
--- a/src/android/app/src/main/jni/id_cache.h
+++ b/src/android/app/src/main/jni/id_cache.h
@@ -47,4 +47,12 @@ jclass GetDoubleClass();
 jmethodID GetDoubleConstructor();
 jfieldID GetDoubleValueField();
 
+jclass GetIntegerClass();
+jmethodID GetIntegerConstructor();
+jfieldID GetIntegerValueField();
+
+jclass GetBooleanClass();
+jmethodID GetBooleanConstructor();
+jfieldID GetBooleanValueField();
+
 } // namespace IDCache
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp
index ed3b1353a0..b8fef5c6fd 100644
--- a/src/android/app/src/main/jni/native.cpp
+++ b/src/android/app/src/main/jni/native.cpp
@@ -17,6 +17,7 @@
 #include <core/file_sys/patch_manager.h>
 #include <core/file_sys/savedata_factory.h>
 #include <core/loader/nro.h>
+#include <frontend_common/content_manager.h>
 #include <jni.h>
 
 #include "common/detached_tasks.h"
@@ -100,67 +101,6 @@ void EmulationSession::SetNativeWindow(ANativeWindow* native_window) {
     m_native_window = native_window;
 }
 
-int EmulationSession::InstallFileToNand(std::string filename, std::string file_extension) {
-    jconst copy_func = [](const FileSys::VirtualFile& src, const FileSys::VirtualFile& dest,
-                          std::size_t block_size) {
-        if (src == nullptr || dest == nullptr) {
-            return false;
-        }
-        if (!dest->Resize(src->GetSize())) {
-            return false;
-        }
-
-        using namespace Common::Literals;
-        [[maybe_unused]] std::vector<u8> buffer(1_MiB);
-
-        for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
-            jconst read = src->Read(buffer.data(), buffer.size(), i);
-            dest->Write(buffer.data(), read, i);
-        }
-        return true;
-    };
-
-    enum InstallResult {
-        Success = 0,
-        SuccessFileOverwritten = 1,
-        InstallError = 2,
-        ErrorBaseGame = 3,
-        ErrorFilenameExtension = 4,
-    };
-
-    [[maybe_unused]] std::shared_ptr<FileSys::NSP> nsp;
-    if (file_extension == "nsp") {
-        nsp = std::make_shared<FileSys::NSP>(m_vfs->OpenFile(filename, FileSys::Mode::Read));
-        if (nsp->IsExtractedType()) {
-            return InstallError;
-        }
-    } else {
-        return ErrorFilenameExtension;
-    }
-
-    if (!nsp) {
-        return InstallError;
-    }
-
-    if (nsp->GetStatus() != Loader::ResultStatus::Success) {
-        return InstallError;
-    }
-
-    jconst res = m_system.GetFileSystemController().GetUserNANDContents()->InstallEntry(*nsp, true,
-                                                                                        copy_func);
-
-    switch (res) {
-    case FileSys::InstallResult::Success:
-        return Success;
-    case FileSys::InstallResult::OverwriteExisting:
-        return SuccessFileOverwritten;
-    case FileSys::InstallResult::ErrorBaseInstall:
-        return ErrorBaseGame;
-    default:
-        return InstallError;
-    }
-}
-
 void EmulationSession::InitializeGpuDriver(const std::string& hook_lib_dir,
                                            const std::string& custom_driver_dir,
                                            const std::string& custom_driver_name,
@@ -512,10 +452,20 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_setAppDirectory(JNIEnv* env, jobject
 }
 
 int Java_org_yuzu_yuzu_1emu_NativeLibrary_installFileToNand(JNIEnv* env, jobject instance,
-                                                            jstring j_file,
-                                                            jstring j_file_extension) {
-    return EmulationSession::GetInstance().InstallFileToNand(GetJString(env, j_file),
-                                                             GetJString(env, j_file_extension));
+                                                            jstring j_file, jobject jcallback) {
+    auto jlambdaClass = env->GetObjectClass(jcallback);
+    auto jlambdaInvokeMethod = env->GetMethodID(
+        jlambdaClass, "invoke", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
+    const auto callback = [env, jcallback, jlambdaInvokeMethod](size_t max, size_t progress) {
+        auto jwasCancelled = env->CallObjectMethod(jcallback, jlambdaInvokeMethod,
+                                                   ToJDouble(env, max), ToJDouble(env, progress));
+        return GetJBoolean(env, jwasCancelled);
+    };
+
+    return static_cast<int>(
+        ContentManager::InstallNSP(&EmulationSession::GetInstance().System(),
+                                   EmulationSession::GetInstance().System().GetFilesystem().get(),
+                                   GetJString(env, j_file), callback));
 }
 
 jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_doesUpdateMatchProgram(JNIEnv* env, jobject jobj,
diff --git a/src/android/app/src/main/jni/native.h b/src/android/app/src/main/jni/native.h
index 4a80495784..dadb138ad6 100644
--- a/src/android/app/src/main/jni/native.h
+++ b/src/android/app/src/main/jni/native.h
@@ -7,6 +7,7 @@
 #include "core/file_sys/registered_cache.h"
 #include "core/hle/service/acc/profile_manager.h"
 #include "core/perf_stats.h"
+#include "frontend_common/content_manager.h"
 #include "jni/applets/software_keyboard.h"
 #include "jni/emu_window/emu_window.h"
 #include "video_core/rasterizer_interface.h"
@@ -29,7 +30,6 @@ public:
     void SetNativeWindow(ANativeWindow* native_window);
     void SurfaceChanged();
 
-    int InstallFileToNand(std::string filename, std::string file_extension);
     void InitializeGpuDriver(const std::string& hook_lib_dir, const std::string& custom_driver_dir,
                              const std::string& custom_driver_name,
                              const std::string& file_redirect_dir);
diff --git a/src/frontend_common/CMakeLists.txt b/src/frontend_common/CMakeLists.txt
index 22e9337c4c..94d8cc4c3b 100644
--- a/src/frontend_common/CMakeLists.txt
+++ b/src/frontend_common/CMakeLists.txt
@@ -4,6 +4,7 @@
 add_library(frontend_common STATIC
     config.cpp
     config.h
+    content_manager.h
 )
 
 create_target_directory_groups(frontend_common)
diff --git a/src/frontend_common/content_manager.h b/src/frontend_common/content_manager.h
new file mode 100644
index 0000000000..8e55f4ca09
--- /dev/null
+++ b/src/frontend_common/content_manager.h
@@ -0,0 +1,168 @@
+// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <boost/algorithm/string.hpp>
+#include "common/common_types.h"
+#include "common/literals.h"
+#include "core/core.h"
+#include "core/file_sys/common_funcs.h"
+#include "core/file_sys/content_archive.h"
+#include "core/file_sys/mode.h"
+#include "core/file_sys/nca_metadata.h"
+#include "core/file_sys/registered_cache.h"
+#include "core/file_sys/submission_package.h"
+#include "core/hle/service/filesystem/filesystem.h"
+#include "core/loader/loader.h"
+
+namespace ContentManager {
+
+enum class InstallResult {
+    Success,
+    Overwrite,
+    Failure,
+    BaseInstallAttempted,
+};
+
+inline bool RemoveDLC(const Service::FileSystem::FileSystemController& fs_controller,
+                      const u64 title_id) {
+    return fs_controller.GetUserNANDContents()->RemoveExistingEntry(title_id) ||
+           fs_controller.GetSDMCContents()->RemoveExistingEntry(title_id);
+}
+
+inline size_t RemoveAllDLC(Core::System* system, const u64 program_id) {
+    size_t count{};
+    const auto& fs_controller = system->GetFileSystemController();
+    const auto dlc_entries = system->GetContentProvider().ListEntriesFilter(
+        FileSys::TitleType::AOC, FileSys::ContentRecordType::Data);
+    std::vector<u64> program_dlc_entries;
+
+    for (const auto& entry : dlc_entries) {
+        if (FileSys::GetBaseTitleID(entry.title_id) == program_id) {
+            program_dlc_entries.push_back(entry.title_id);
+        }
+    }
+
+    for (const auto& entry : program_dlc_entries) {
+        if (RemoveDLC(fs_controller, entry)) {
+            ++count;
+        }
+    }
+    return count;
+}
+
+inline bool RemoveUpdate(const Service::FileSystem::FileSystemController& fs_controller,
+                         const u64 program_id) {
+    const auto update_id = program_id | 0x800;
+    return fs_controller.GetUserNANDContents()->RemoveExistingEntry(update_id) ||
+           fs_controller.GetSDMCContents()->RemoveExistingEntry(update_id);
+}
+
+inline bool RemoveBaseContent(const Service::FileSystem::FileSystemController& fs_controller,
+                              const u64 program_id) {
+    return fs_controller.GetUserNANDContents()->RemoveExistingEntry(program_id) ||
+           fs_controller.GetSDMCContents()->RemoveExistingEntry(program_id);
+}
+
+inline InstallResult InstallNSP(
+    Core::System* system, FileSys::VfsFilesystem* vfs, const std::string& filename,
+    const std::function<bool(size_t, size_t)>& callback = std::function<bool(size_t, size_t)>()) {
+    const auto copy = [callback](const FileSys::VirtualFile& src, const FileSys::VirtualFile& dest,
+                                 std::size_t block_size) {
+        if (src == nullptr || dest == nullptr) {
+            return false;
+        }
+        if (!dest->Resize(src->GetSize())) {
+            return false;
+        }
+
+        using namespace Common::Literals;
+        std::vector<u8> buffer(1_MiB);
+
+        for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
+            if (callback(src->GetSize(), i)) {
+                dest->Resize(0);
+                return false;
+            }
+            const auto read = src->Read(buffer.data(), buffer.size(), i);
+            dest->Write(buffer.data(), read, i);
+        }
+        return true;
+    };
+
+    std::shared_ptr<FileSys::NSP> nsp;
+    FileSys::VirtualFile file = vfs->OpenFile(filename, FileSys::Mode::Read);
+    if (boost::to_lower_copy(file->GetName()).ends_with(std::string("nsp"))) {
+        nsp = std::make_shared<FileSys::NSP>(file);
+        if (nsp->IsExtractedType()) {
+            return InstallResult::Failure;
+        }
+    } else {
+        return InstallResult::Failure;
+    }
+
+    if (nsp->GetStatus() != Loader::ResultStatus::Success) {
+        return InstallResult::Failure;
+    }
+    const auto res =
+        system->GetFileSystemController().GetUserNANDContents()->InstallEntry(*nsp, true, copy);
+    switch (res) {
+    case FileSys::InstallResult::Success:
+        return InstallResult::Success;
+    case FileSys::InstallResult::OverwriteExisting:
+        return InstallResult::Overwrite;
+    case FileSys::InstallResult::ErrorBaseInstall:
+        return InstallResult::BaseInstallAttempted;
+    default:
+        return InstallResult::Failure;
+    }
+}
+
+inline InstallResult InstallNCA(
+    FileSys::VfsFilesystem* vfs, const std::string& filename,
+    FileSys::RegisteredCache* registered_cache, const FileSys::TitleType title_type,
+    const std::function<bool(size_t, size_t)>& callback = std::function<bool(size_t, size_t)>()) {
+    const auto copy = [callback](const FileSys::VirtualFile& src, const FileSys::VirtualFile& dest,
+                                 std::size_t block_size) {
+        if (src == nullptr || dest == nullptr) {
+            return false;
+        }
+        if (!dest->Resize(src->GetSize())) {
+            return false;
+        }
+
+        using namespace Common::Literals;
+        std::vector<u8> buffer(1_MiB);
+
+        for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
+            if (callback(src->GetSize(), i)) {
+                dest->Resize(0);
+                return false;
+            }
+            const auto read = src->Read(buffer.data(), buffer.size(), i);
+            dest->Write(buffer.data(), read, i);
+        }
+        return true;
+    };
+
+    const auto nca = std::make_shared<FileSys::NCA>(vfs->OpenFile(filename, FileSys::Mode::Read));
+    const auto id = nca->GetStatus();
+
+    // Game updates necessary are missing base RomFS
+    if (id != Loader::ResultStatus::Success &&
+        id != Loader::ResultStatus::ErrorMissingBKTRBaseRomFS) {
+        return InstallResult::Failure;
+    }
+
+    const auto res = registered_cache->InstallEntry(*nca, title_type, true, copy);
+    if (res == FileSys::InstallResult::Success) {
+        return InstallResult::Success;
+    } else if (res == FileSys::InstallResult::OverwriteExisting) {
+        return InstallResult::Overwrite;
+    } else {
+        return InstallResult::Failure;
+    }
+}
+
+} // namespace ContentManager
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp
index 3c562e3b28..05bd4174c7 100644
--- a/src/yuzu/main.cpp
+++ b/src/yuzu/main.cpp
@@ -47,6 +47,7 @@
 #include "core/hle/service/am/applet_oe.h"
 #include "core/hle/service/am/applets/applets.h"
 #include "core/hle/service/set/system_settings_server.h"
+#include "frontend_common/content_manager.h"
 #include "hid_core/frontend/emulated_controller.h"
 #include "hid_core/hid_core.h"
 #include "yuzu/multiplayer/state.h"
@@ -2476,10 +2477,8 @@ void GMainWindow::OnGameListRemoveInstalledEntry(u64 program_id, InstalledEntryT
 }
 
 void GMainWindow::RemoveBaseContent(u64 program_id, InstalledEntryType type) {
-    const auto& fs_controller = system->GetFileSystemController();
-    const auto res = fs_controller.GetUserNANDContents()->RemoveExistingEntry(program_id) ||
-                     fs_controller.GetSDMCContents()->RemoveExistingEntry(program_id);
-
+    const auto res =
+        ContentManager::RemoveBaseContent(system->GetFileSystemController(), program_id);
     if (res) {
         QMessageBox::information(this, tr("Successfully Removed"),
                                  tr("Successfully removed the installed base game."));
@@ -2491,11 +2490,7 @@ void GMainWindow::RemoveBaseContent(u64 program_id, InstalledEntryType type) {
 }
 
 void GMainWindow::RemoveUpdateContent(u64 program_id, InstalledEntryType type) {
-    const auto update_id = program_id | 0x800;
-    const auto& fs_controller = system->GetFileSystemController();
-    const auto res = fs_controller.GetUserNANDContents()->RemoveExistingEntry(update_id) ||
-                     fs_controller.GetSDMCContents()->RemoveExistingEntry(update_id);
-
+    const auto res = ContentManager::RemoveUpdate(system->GetFileSystemController(), program_id);
     if (res) {
         QMessageBox::information(this, tr("Successfully Removed"),
                                  tr("Successfully removed the installed update."));
@@ -2506,22 +2501,7 @@ void GMainWindow::RemoveUpdateContent(u64 program_id, InstalledEntryType type) {
 }
 
 void GMainWindow::RemoveAddOnContent(u64 program_id, InstalledEntryType type) {
-    u32 count{};
-    const auto& fs_controller = system->GetFileSystemController();
-    const auto dlc_entries = system->GetContentProvider().ListEntriesFilter(
-        FileSys::TitleType::AOC, FileSys::ContentRecordType::Data);
-
-    for (const auto& entry : dlc_entries) {
-        if (FileSys::GetBaseTitleID(entry.title_id) == program_id) {
-            const auto res =
-                fs_controller.GetUserNANDContents()->RemoveExistingEntry(entry.title_id) ||
-                fs_controller.GetSDMCContents()->RemoveExistingEntry(entry.title_id);
-            if (res) {
-                ++count;
-            }
-        }
-    }
-
+    const size_t count = ContentManager::RemoveAllDLC(system.get(), program_id);
     if (count == 0) {
         QMessageBox::warning(this, GetGameListErrorRemoving(type),
                              tr("There are no DLC installed for this title."));
@@ -3290,12 +3270,21 @@ void GMainWindow::OnMenuInstallToNAND() {
         install_progress->setLabelText(
             tr("Installing file \"%1\"...").arg(QFileInfo(file).fileName()));
 
-        QFuture<InstallResult> future;
-        InstallResult result;
+        QFuture<ContentManager::InstallResult> future;
+        ContentManager::InstallResult result;
 
         if (file.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) {
-
-            future = QtConcurrent::run([this, &file] { return InstallNSP(file); });
+            const auto progress_callback = [this](size_t size, size_t progress) {
+                emit UpdateInstallProgress();
+                if (install_progress->wasCanceled()) {
+                    return true;
+                }
+                return false;
+            };
+            future = QtConcurrent::run([this, &file, progress_callback] {
+                return ContentManager::InstallNSP(system.get(), vfs.get(), file.toStdString(),
+                                                  progress_callback);
+            });
 
             while (!future.isFinished()) {
                 QCoreApplication::processEvents();
@@ -3311,16 +3300,16 @@ void GMainWindow::OnMenuInstallToNAND() {
         std::this_thread::sleep_for(std::chrono::milliseconds(10));
 
         switch (result) {
-        case InstallResult::Success:
+        case ContentManager::InstallResult::Success:
             new_files.append(QFileInfo(file).fileName());
             break;
-        case InstallResult::Overwrite:
+        case ContentManager::InstallResult::Overwrite:
             overwritten_files.append(QFileInfo(file).fileName());
             break;
-        case InstallResult::Failure:
+        case ContentManager::InstallResult::Failure:
             failed_files.append(QFileInfo(file).fileName());
             break;
-        case InstallResult::BaseInstallAttempted:
+        case ContentManager::InstallResult::BaseInstallAttempted:
             failed_files.append(QFileInfo(file).fileName());
             detected_base_install = true;
             break;
@@ -3354,96 +3343,7 @@ void GMainWindow::OnMenuInstallToNAND() {
     ui->action_Install_File_NAND->setEnabled(true);
 }
 
-InstallResult GMainWindow::InstallNSP(const QString& filename) {
-    const auto qt_raw_copy = [this](const FileSys::VirtualFile& src,
-                                    const FileSys::VirtualFile& dest, std::size_t block_size) {
-        if (src == nullptr || dest == nullptr) {
-            return false;
-        }
-        if (!dest->Resize(src->GetSize())) {
-            return false;
-        }
-
-        std::vector<u8> buffer(CopyBufferSize);
-
-        for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
-            if (install_progress->wasCanceled()) {
-                dest->Resize(0);
-                return false;
-            }
-
-            emit UpdateInstallProgress();
-
-            const auto read = src->Read(buffer.data(), buffer.size(), i);
-            dest->Write(buffer.data(), read, i);
-        }
-        return true;
-    };
-
-    std::shared_ptr<FileSys::NSP> nsp;
-    if (filename.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) {
-        nsp = std::make_shared<FileSys::NSP>(
-            vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
-        if (nsp->IsExtractedType()) {
-            return InstallResult::Failure;
-        }
-    } else {
-        return InstallResult::Failure;
-    }
-
-    if (nsp->GetStatus() != Loader::ResultStatus::Success) {
-        return InstallResult::Failure;
-    }
-    const auto res = system->GetFileSystemController().GetUserNANDContents()->InstallEntry(
-        *nsp, true, qt_raw_copy);
-    switch (res) {
-    case FileSys::InstallResult::Success:
-        return InstallResult::Success;
-    case FileSys::InstallResult::OverwriteExisting:
-        return InstallResult::Overwrite;
-    case FileSys::InstallResult::ErrorBaseInstall:
-        return InstallResult::BaseInstallAttempted;
-    default:
-        return InstallResult::Failure;
-    }
-}
-
-InstallResult GMainWindow::InstallNCA(const QString& filename) {
-    const auto qt_raw_copy = [this](const FileSys::VirtualFile& src,
-                                    const FileSys::VirtualFile& dest, std::size_t block_size) {
-        if (src == nullptr || dest == nullptr) {
-            return false;
-        }
-        if (!dest->Resize(src->GetSize())) {
-            return false;
-        }
-
-        std::vector<u8> buffer(CopyBufferSize);
-
-        for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
-            if (install_progress->wasCanceled()) {
-                dest->Resize(0);
-                return false;
-            }
-
-            emit UpdateInstallProgress();
-
-            const auto read = src->Read(buffer.data(), buffer.size(), i);
-            dest->Write(buffer.data(), read, i);
-        }
-        return true;
-    };
-
-    const auto nca =
-        std::make_shared<FileSys::NCA>(vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
-    const auto id = nca->GetStatus();
-
-    // Game updates necessary are missing base RomFS
-    if (id != Loader::ResultStatus::Success &&
-        id != Loader::ResultStatus::ErrorMissingBKTRBaseRomFS) {
-        return InstallResult::Failure;
-    }
-
+ContentManager::InstallResult GMainWindow::InstallNCA(const QString& filename) {
     const QStringList tt_options{tr("System Application"),
                                  tr("System Archive"),
                                  tr("System Application Update"),
@@ -3464,7 +3364,7 @@ InstallResult GMainWindow::InstallNCA(const QString& filename) {
     if (!ok || index == -1) {
         QMessageBox::warning(this, tr("Failed to Install"),
                              tr("The title type you selected for the NCA is invalid."));
-        return InstallResult::Failure;
+        return ContentManager::InstallResult::Failure;
     }
 
     // If index is equal to or past Game, add the jump in TitleType.
@@ -3478,15 +3378,15 @@ InstallResult GMainWindow::InstallNCA(const QString& filename) {
     auto* registered_cache = is_application ? fs_controller.GetUserNANDContents()
                                             : fs_controller.GetSystemNANDContents();
 
-    const auto res = registered_cache->InstallEntry(*nca, static_cast<FileSys::TitleType>(index),
-                                                    true, qt_raw_copy);
-    if (res == FileSys::InstallResult::Success) {
-        return InstallResult::Success;
-    } else if (res == FileSys::InstallResult::OverwriteExisting) {
-        return InstallResult::Overwrite;
-    } else {
-        return InstallResult::Failure;
-    }
+    const auto progress_callback = [this](size_t size, size_t progress) {
+        emit UpdateInstallProgress();
+        if (install_progress->wasCanceled()) {
+            return true;
+        }
+        return false;
+    };
+    return ContentManager::InstallNCA(vfs.get(), filename.toStdString(), registered_cache,
+                                      static_cast<FileSys::TitleType>(index), progress_callback);
 }
 
 void GMainWindow::OnMenuRecentFile() {
diff --git a/src/yuzu/main.h b/src/yuzu/main.h
index f3276da642..280fae5c35 100644
--- a/src/yuzu/main.h
+++ b/src/yuzu/main.h
@@ -16,6 +16,7 @@
 #include "common/announce_multiplayer_room.h"
 #include "common/common_types.h"
 #include "configuration/qt_config.h"
+#include "frontend_common/content_manager.h"
 #include "input_common/drivers/tas_input.h"
 #include "yuzu/compatibility_list.h"
 #include "yuzu/hotkeys.h"
@@ -124,13 +125,6 @@ enum class EmulatedDirectoryTarget {
     SDMC,
 };
 
-enum class InstallResult {
-    Success,
-    Overwrite,
-    Failure,
-    BaseInstallAttempted,
-};
-
 enum class ReinitializeKeyBehavior {
     NoWarning,
     Warning,
@@ -427,8 +421,7 @@ private:
     void RemoveCacheStorage(u64 program_id);
     bool SelectRomFSDumpTarget(const FileSys::ContentProvider&, u64 program_id,
                                u64* selected_title_id, u8* selected_content_record_type);
-    InstallResult InstallNSP(const QString& filename);
-    InstallResult InstallNCA(const QString& filename);
+    ContentManager::InstallResult InstallNCA(const QString& filename);
     void MigrateConfigFiles();
     void UpdateWindowTitle(std::string_view title_name = {}, std::string_view title_version = {},
                            std::string_view gpu_vendor = {});