From a338de785097752633eb67dacacc3d8761b9c577 Mon Sep 17 00:00:00 2001
From: Narr the Reg <juangerman-13@hotmail.com>
Date: Thu, 8 Jun 2023 17:26:24 -0600
Subject: [PATCH] android: Add update support

---
 .../java/org/yuzu/yuzu_emu/NativeLibrary.kt   | 13 +++
 .../fragments/HomeSettingsFragment.kt         | 12 ++-
 .../org/yuzu/yuzu_emu/ui/main/MainActivity.kt | 58 +++++++++++++
 src/android/app/src/main/jni/native.cpp       | 86 ++++++++++++++++++-
 .../res/drawable/ic_system_update_alt.xml     |  9 ++
 .../app/src/main/res/values/strings.xml       |  9 ++
 src/core/file_sys/submission_package.h        |  1 +
 7 files changed, 183 insertions(+), 5 deletions(-)
 create mode 100644 src/android/app/src/main/res/drawable/ic_system_update_alt.xml

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 22af9e435a..4be9ade142 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
@@ -227,6 +227,8 @@ object NativeLibrary {
 
     external fun setAppDirectory(directory: String)
 
+    external fun installFileToNand(filename: String): Int
+
     external fun initializeGpuDriver(
         hookLibDir: String?,
         customDriverDir: String?,
@@ -507,4 +509,15 @@ 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/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
index bdc3375016..536163eb6e 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
@@ -94,6 +94,11 @@ class HomeSettingsFragment : Fragment() {
                 R.string.install_amiibo_keys_description,
                 R.drawable.ic_nfc
             ) { mainActivity.getAmiiboKey.launch(arrayOf("*/*")) },
+            HomeSetting(
+                R.string.install_game_content,
+                R.string.install_game_content_description,
+                R.drawable.ic_system_update_alt
+            ) { mainActivity.installGameUpdate.launch(arrayOf("*/*")) },
             HomeSetting(
                 R.string.select_games_folder,
                 R.string.select_games_folder_description,
@@ -103,7 +108,12 @@ class HomeSettingsFragment : Fragment() {
                 R.string.manage_save_data,
                 R.string.import_export_saves_description,
                 R.drawable.ic_save
-            ) { ImportExportSavesFragment().show(parentFragmentManager, ImportExportSavesFragment.TAG) },
+            ) {
+                ImportExportSavesFragment().show(
+                    parentFragmentManager,
+                    ImportExportSavesFragment.TAG
+                )
+            },
             HomeSetting(
                 R.string.install_prod_keys,
                 R.string.install_prod_keys_description,
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
index 124f62f08f..82fc9e04e3 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
@@ -467,4 +467,62 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
                 }
             }
         }
+
+    val installGameUpdate =
+        registerForActivityResult(ActivityResultContracts.OpenDocument()) {
+            if (it == null)
+                return@registerForActivityResult
+
+            IndeterminateProgressDialogFragment.newInstance(
+                this@MainActivity,
+                R.string.install_game_content
+            ) {
+                val result = NativeLibrary.installFileToNand(it.toString())
+                lifecycleScope.launch {
+                    withContext(Dispatchers.Main) {
+                        when (result) {
+                            NativeLibrary.InstallFileToNandResult.Success -> {
+                                Toast.makeText(
+                                    applicationContext,
+                                    R.string.install_game_content_success,
+                                    Toast.LENGTH_SHORT
+                                ).show()
+                            }
+
+                            NativeLibrary.InstallFileToNandResult.SuccessFileOverwritten -> {
+                                Toast.makeText(
+                                    applicationContext,
+                                    R.string.install_game_content_success_overwrite,
+                                    Toast.LENGTH_SHORT
+                                ).show()
+                            }
+
+                            NativeLibrary.InstallFileToNandResult.ErrorBaseGame -> {
+                                MessageDialogFragment.newInstance(
+                                    R.string.install_game_content_failure,
+                                    R.string.install_game_content_failure_base
+                                ).show(supportFragmentManager, MessageDialogFragment.TAG)
+                            }
+
+                            NativeLibrary.InstallFileToNandResult.ErrorFilenameExtension -> {
+                                MessageDialogFragment.newInstance(
+                                    R.string.install_game_content_failure,
+                                    R.string.install_game_content_failure_file_extension,
+                                    R.string.install_game_content_help_link
+                                ).show(supportFragmentManager, MessageDialogFragment.TAG)
+                            }
+
+                            else -> {
+                                MessageDialogFragment.newInstance(
+                                    R.string.install_game_content_failure,
+                                    R.string.install_game_content_failure_description,
+                                    R.string.install_game_content_help_link
+                                ).show(supportFragmentManager, MessageDialogFragment.TAG)
+                            }
+                        }
+                    }
+                }
+                return@newInstance result
+            }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
+        }
 }
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp
index 7ebed5e6aa..4091c23d18 100644
--- a/src/android/app/src/main/jni/native.cpp
+++ b/src/android/app/src/main/jni/native.cpp
@@ -28,7 +28,10 @@
 #include "core/core.h"
 #include "core/cpu_manager.h"
 #include "core/crypto/key_manager.h"
+#include "core/file_sys/card_image.h"
 #include "core/file_sys/registered_cache.h"
+#include "core/file_sys/submission_package.h"
+#include "core/file_sys/vfs.h"
 #include "core/file_sys/vfs_real.h"
 #include "core/frontend/applets/cabinet.h"
 #include "core/frontend/applets/controller.h"
@@ -94,6 +97,74 @@ public:
         m_native_window = native_window;
     }
 
+    int InstallFileToNand(std::string filename) {
+        const auto 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;
+            std::vector<u8> buffer(1_MiB);
+
+            for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
+                const auto 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,
+        };
+
+        m_system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>());
+        m_system.GetFileSystemController().CreateFactories(*m_vfs);
+
+        std::shared_ptr<FileSys::NSP> nsp;
+        if (filename.ends_with("nsp")) {
+            nsp = std::make_shared<FileSys::NSP>(m_vfs->OpenFile(filename, FileSys::Mode::Read));
+            if (nsp->IsExtractedType()) {
+                return InstallError;
+            }
+        } else if (filename.ends_with("xci")) {
+            const auto xci =
+                std::make_shared<FileSys::XCI>(m_vfs->OpenFile(filename, FileSys::Mode::Read));
+            nsp = xci->GetSecurePartitionNSP();
+        } else {
+            return ErrorFilenameExtension;
+        }
+
+        if (!nsp) {
+            return InstallError;
+        }
+
+        if (nsp->GetStatus() != Loader::ResultStatus::Success) {
+            return InstallError;
+        }
+
+        const auto 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 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) {
@@ -154,14 +225,14 @@ public:
         m_window = std::make_unique<EmuWindow_Android>(&m_input_subsystem, m_native_window,
                                                        m_vulkan_library);
 
+        m_system.SetFilesystem(m_vfs);
+
         // Initialize system.
         auto android_keyboard = std::make_unique<SoftwareKeyboard::AndroidKeyboard>();
         m_software_keyboard = android_keyboard.get();
         m_system.SetShuttingDown(false);
         m_system.ApplySettings();
         m_system.HIDCore().ReloadInputDevices();
-        m_system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>());
-        m_system.SetFilesystem(std::make_shared<FileSys::RealVfsFilesystem>());
         m_system.SetAppletFrontendSet({
             nullptr,                     // Amiibo Settings
             nullptr,                     // Controller Selector
@@ -173,7 +244,8 @@ public:
             std::move(android_keyboard), // Software Keyboard
             nullptr,                     // Web Browser
         });
-        m_system.GetFileSystemController().CreateFactories(*m_system.GetFilesystem());
+        m_system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>());
+        m_system.GetFileSystemController().CreateFactories(*m_vfs);
 
         // Initialize account manager
         m_profile_manager = std::make_unique<Service::Account::ProfileManager>();
@@ -398,7 +470,7 @@ private:
     InputCommon::InputSubsystem m_input_subsystem;
     Common::DetachedTasks m_detached_tasks;
     Core::PerfStatsResults m_perf_stats{};
-    std::shared_ptr<FileSys::RealVfsFilesystem> m_vfs;
+    std::shared_ptr<FileSys::VfsFilesystem> m_vfs;
     Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized};
     bool m_is_running{};
     SoftwareKeyboard::AndroidKeyboard* m_software_keyboard{};
@@ -466,6 +538,12 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_setAppDirectory(JNIEnv* env,
     Common::FS::SetAppDirectory(GetJString(env, j_directory));
 }
 
+int Java_org_yuzu_yuzu_1emu_NativeLibrary_installFileToNand(JNIEnv* env,
+                                                            [[maybe_unused]] jclass clazz,
+                                                            jstring j_file) {
+    return EmulationSession::GetInstance().InstallFileToNand(GetJString(env, j_file));
+}
+
 void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeGpuDriver(
     JNIEnv* env, [[maybe_unused]] jclass clazz, jstring hook_lib_dir, jstring custom_driver_dir,
     jstring custom_driver_name, jstring file_redirect_dir) {
diff --git a/src/android/app/src/main/res/drawable/ic_system_update_alt.xml b/src/android/app/src/main/res/drawable/ic_system_update_alt.xml
new file mode 100644
index 0000000000..0f6adfdb86
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_system_update_alt.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="48dp"
+    android:height="48dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960">
+  <path
+      android:fillColor="#FF000000"
+      android:pathData="M140,800q-24,0 -42,-18t-18,-42v-520q0,-24 18,-42t42,-18h250v60L140,220v520h680v-520L570,220v-60h250q24,0 42,18t18,42v520q0,24 -18,42t-42,18L140,800ZM480,615L280,415l43,-43 127,127v-339h60v339l127,-127 43,43 -200,200Z"/>
+</vector>
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index 0ae69afb47..5d4636d1ae 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -105,6 +105,15 @@
     <string name="share_log">Share debug logs</string>
     <string name="share_log_description">Share yuzu\'s log file to debug issues</string>
     <string name="share_log_missing">No log file found</string>
+    <string name="install_game_content">Install game content</string>
+    <string name="install_game_content_description">Install game updates or DLC</string>
+    <string name="install_game_content_failure">Error installing file to NAND</string>
+    <string name="install_game_content_failure_description">Game content installation failed. Please ensure content is valid and that the prod.keys file is installed.</string>
+    <string name="install_game_content_failure_base">Installation of base games isn\'t permitted in order to avoid possible conflicts. Please select an update or DLC instead.</string>
+    <string name="install_game_content_failure_file_extension">The selected file type is not supported. Only NSP and XCI content is supported for this action. Please verify the game content is valid.</string>
+    <string name="install_game_content_success">Game content installed successfully</string>
+    <string name="install_game_content_success_overwrite">Game content was overwritten successfully</string>
+    <string name="install_game_content_help_link">https://yuzu-emu.org/help/quickstart/#dumping-installed-updates</string>
 
     <!-- About screen strings -->
     <string name="gaia_is_not_real">Gaia isn\'t real</string>
diff --git a/src/core/file_sys/submission_package.h b/src/core/file_sys/submission_package.h
index 3226b884a0..27f97c7251 100644
--- a/src/core/file_sys/submission_package.h
+++ b/src/core/file_sys/submission_package.h
@@ -8,6 +8,7 @@
 #include <set>
 #include <vector>
 #include "common/common_types.h"
+#include "core/file_sys/nca_metadata.h"
 #include "core/file_sys/vfs.h"
 
 namespace Core::Crypto {