From f403484276c050008098ada9b2974cf08a9ad6b6 Mon Sep 17 00:00:00 2001
From: Emmanuel Hansen <emmausssss@gmail.com>
Date: Sat, 30 Dec 2023 12:23:07 +0000
Subject: [PATCH] android - move title updates support to SAF

---
 src/LibRyujinx/Android/JniExportedMethods.cs  |  5 +-
 src/LibRyujinx/LibRyujinx.Device.cs           |  6 +-
 .../PartitionFileSystemExtensions.cs          | 79 ++++++++++--------
 .../Loaders/Processes/ProcessLoader.cs        |  8 +-
 src/Ryujinx.HLE/Switch.cs                     |  6 +-
 .../java/org/ryujinx/android/RyujinxNative.kt |  2 +-
 .../ryujinx/android/viewmodels/GameModel.kt   | 22 +++++
 .../android/viewmodels/MainViewModel.kt       |  4 +-
 .../viewmodels/TitleUpdateViewModel.kt        | 83 ++++++++++++-------
 .../ryujinx/android/views/TitleUpdateViews.kt | 78 +++++++----------
 10 files changed, 165 insertions(+), 128 deletions(-)

diff --git a/src/LibRyujinx/Android/JniExportedMethods.cs b/src/LibRyujinx/Android/JniExportedMethods.cs
index 9cc8e848c..7be533091 100644
--- a/src/LibRyujinx/Android/JniExportedMethods.cs
+++ b/src/LibRyujinx/Android/JniExportedMethods.cs
@@ -262,7 +262,7 @@ namespace LibRyujinx
         }
 
         [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceLoadDescriptor")]
-        public static JBoolean JniLoadApplicationNative(JEnvRef jEnv, JObjectLocalRef jObj, JInt descriptor, JInt type)
+        public static JBoolean JniLoadApplicationNative(JEnvRef jEnv, JObjectLocalRef jObj, JInt descriptor, JInt type, JInt updateDescriptor)
         {
             Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
             if (SwitchDevice?.EmulationContext == null)
@@ -271,8 +271,9 @@ namespace LibRyujinx
             }
 
             var stream = OpenFile(descriptor);
+            var update = updateDescriptor == -1 ? null : OpenFile(updateDescriptor);
 
-            return LoadApplication(stream, (FileType)(int)type);
+            return LoadApplication(stream, (FileType)(int)type, update);
         }
 
         [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceVerifyFirmware")]
diff --git a/src/LibRyujinx/LibRyujinx.Device.cs b/src/LibRyujinx/LibRyujinx.Device.cs
index d46d03f43..839852933 100644
--- a/src/LibRyujinx/LibRyujinx.Device.cs
+++ b/src/LibRyujinx/LibRyujinx.Device.cs
@@ -77,14 +77,14 @@ namespace LibRyujinx
             return SwitchDevice?.ContentManager?.VerifyFirmwarePackage(stream, isXci) ?? null;
         }
 
-        public static bool LoadApplication(Stream stream, FileType type)
+        public static bool LoadApplication(Stream stream, FileType type, Stream? updateStream = null)
         {
             var emulationContext = SwitchDevice.EmulationContext;
             return type switch
             {
                 FileType.None => false,
-                FileType.Nsp => emulationContext?.LoadNsp(stream) ?? false,
-                FileType.Xci => emulationContext?.LoadXci(stream) ?? false,
+                FileType.Nsp => emulationContext?.LoadNsp(stream, updateStream) ?? false,
+                FileType.Xci => emulationContext?.LoadXci(stream, updateStream) ?? false,
                 FileType.Nro => emulationContext?.LoadProgram(stream, true, "") ?? false,
             };
         }
diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs
index 6a01d2f41..68f4305b2 100644
--- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs
+++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs
@@ -20,7 +20,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
         private static readonly DownloadableContentJsonSerializerContext _contentSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
         private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
 
-        internal static (bool, ProcessResult) TryLoad<TMetaData, TFormat, THeader, TEntry>(this PartitionFileSystemCore<TMetaData, TFormat, THeader, TEntry> partitionFileSystem, Switch device, Stream stream, out string errorMessage, string extension)
+        internal static (bool, ProcessResult) TryLoad<TMetaData, TFormat, THeader, TEntry>(this PartitionFileSystemCore<TMetaData, TFormat, THeader, TEntry> partitionFileSystem, Switch device, Stream stream, out string errorMessage, string extension, Stream updateStream = null)
             where TMetaData : PartitionFileSystemMetaCore<TFormat, THeader, TEntry>, new()
             where TFormat : IPartitionFileSystemFormat
             where THeader : unmanaged, IPartitionFileSystemHeader
@@ -87,42 +87,21 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
                 {
                     // Clear the program index part.
                     titleIdBase &= ~0xFUL;
-
-                    // Load update information if exists.
-                    string titleUpdateMetadataPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json");
-                    if (File.Exists(titleUpdateMetadataPath))
+                    PartitionFileSystem updatePartitionFileSystem = new();
+                    if (updateStream != null)
                     {
-                        string updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected;
-                        if (File.Exists(updatePath))
+                        LoadUpdate(device, updateStream, ref updatePatchNca, ref updateControlNca, titleIdBase, updatePartitionFileSystem);
+                    }
+                    else
+                    {
+                        // Load update information if exists.
+                        string titleUpdateMetadataPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json");
+                        if (File.Exists(titleUpdateMetadataPath))
                         {
-                            PartitionFileSystem updatePartitionFileSystem = new();
-                            updatePartitionFileSystem.Initialize(new FileStream(updatePath, FileMode.Open, FileAccess.Read).AsStorage()).ThrowIfFailure();
-
-                            device.Configuration.VirtualFileSystem.ImportTickets(updatePartitionFileSystem);
-
-                            // TODO: This should use CNMT NCA instead.
-                            foreach (DirectoryEntryEx fileEntry in updatePartitionFileSystem.EnumerateEntries("/", "*.nca"))
+                            string updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected;
+                            if (File.Exists(updatePath))
                             {
-                                Nca nca = updatePartitionFileSystem.GetNca(device, fileEntry.FullPath);
-
-                                if (nca.GetProgramIndex() != device.Configuration.UserChannelPersistence.Index)
-                                {
-                                    continue;
-                                }
-
-                                if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != titleIdBase.ToString("x16"))
-                                {
-                                    break;
-                                }
-
-                                if (nca.IsProgram())
-                                {
-                                    updatePatchNca = nca;
-                                }
-                                else if (nca.IsControl())
-                                {
-                                    updateControlNca = nca;
-                                }
+                                LoadUpdate(device, new FileStream(updatePath, FileMode.Open, FileAccess.Read), ref updatePatchNca, ref updateControlNca, titleIdBase, updatePartitionFileSystem);
                             }
                         }
                     }
@@ -171,6 +150,38 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
             errorMessage = "Unable to load: Could not find Main NCA";
 
             return (false, ProcessResult.Failed);
+
+            static void LoadUpdate(Switch device, Stream updateStream, ref Nca updatePatchNca, ref Nca updateControlNca, ulong titleIdBase, PartitionFileSystem updatePartitionFileSystem)
+            {
+                updatePartitionFileSystem.Initialize(updateStream.AsStorage()).ThrowIfFailure();
+
+                device.Configuration.VirtualFileSystem.ImportTickets(updatePartitionFileSystem);
+
+                // TODO: This should use CNMT NCA instead.
+                foreach (DirectoryEntryEx fileEntry in updatePartitionFileSystem.EnumerateEntries("/", "*.nca"))
+                {
+                    Nca nca = updatePartitionFileSystem.GetNca(device, fileEntry.FullPath);
+
+                    if (nca.GetProgramIndex() != device.Configuration.UserChannelPersistence.Index)
+                    {
+                        continue;
+                    }
+
+                    if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != titleIdBase.ToString("x16"))
+                    {
+                        break;
+                    }
+
+                    if (nca.IsProgram())
+                    {
+                        updatePatchNca = nca;
+                    }
+                    else if (nca.IsControl())
+                    {
+                        updateControlNca = nca;
+                    }
+                }
+            }
         }
 
         public static Nca GetNca(this IFileSystem fileSystem, Switch device, string path)
diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs
index 2246b9994..966d692ef 100644
--- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs
+++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs
@@ -39,7 +39,7 @@ namespace Ryujinx.HLE.Loaders.Processes
             return LoadXci(stream);
         }
 
-        public bool LoadXci(Stream stream)
+        public bool LoadXci(Stream stream, Stream updateStream = null)
         {
             Xci xci = new(_device.Configuration.VirtualFileSystem.KeySet, stream.AsStorage());
 
@@ -50,7 +50,7 @@ namespace Ryujinx.HLE.Loaders.Processes
                 return false;
             }
 
-            (bool success, ProcessResult processResult) = xci.OpenPartition(XciPartitionType.Secure).TryLoad(_device, stream, out string errorMessage, "xci");
+            (bool success, ProcessResult processResult) = xci.OpenPartition(XciPartitionType.Secure).TryLoad(_device, stream, out string errorMessage, "xci", updateStream);
 
             if (!success)
             {
@@ -79,12 +79,12 @@ namespace Ryujinx.HLE.Loaders.Processes
             return LoadNsp(file);
         }
 
-        public bool LoadNsp(Stream stream)
+        public bool LoadNsp(Stream stream, Stream updateStream = null)
         {
             PartitionFileSystem partitionFileSystem = new();
             partitionFileSystem.Initialize(stream.AsStorage()).ThrowIfFailure();
 
-            (bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, stream, out string errorMessage, "nsp");
+            (bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, stream, out string errorMessage, "nsp", updateStream);
 
             if (processResult.ProcessId == 0)
             {
diff --git a/src/Ryujinx.HLE/Switch.cs b/src/Ryujinx.HLE/Switch.cs
index bcf66db9d..391f842c2 100644
--- a/src/Ryujinx.HLE/Switch.cs
+++ b/src/Ryujinx.HLE/Switch.cs
@@ -93,7 +93,7 @@ namespace Ryujinx.HLE
             return Processes.LoadNxo(fileName);
         }
 
-        public bool LoadXci(Stream xciStream)
+        public bool LoadXci(Stream xciStream, Stream updateStream = null)
         {
             return Processes.LoadXci(xciStream);
         }
@@ -103,9 +103,9 @@ namespace Ryujinx.HLE
             return Processes.LoadNca(ncaStream);
         }
 
-        public bool LoadNsp(Stream nspStream)
+        public bool LoadNsp(Stream nspStream, Stream updateStream = null)
         {
-            return Processes.LoadNsp(nspStream);
+            return Processes.LoadNsp(nspStream, updateStream);
         }
 
         public bool LoadProgram(Stream stream, bool isNro, string name)
diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt
index fc3447d49..a01d75332 100644
--- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt
+++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt
@@ -39,7 +39,7 @@ class RyujinxNative {
     external fun deviceGetGameFifo(): Double
     external fun deviceGetGameInfo(fileDescriptor: Int, extension: Long): GameInfo
     external fun deviceGetGameInfoFromPath(path: String): GameInfo
-    external fun deviceLoadDescriptor(fileDescriptor: Int, gameType: Int): Boolean
+    external fun deviceLoadDescriptor(fileDescriptor: Int, gameType: Int, updateDescriptor: Int): Boolean
     external fun graphicsRendererSetSize(width: Int, height: Int)
     external fun graphicsRendererSetVsync(enabled: Boolean)
     external fun graphicsRendererRunLoop()
diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/GameModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/GameModel.kt
index 47aaa7f34..017809ea0 100644
--- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/GameModel.kt
+++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/GameModel.kt
@@ -1,6 +1,7 @@
 package org.ryujinx.android.viewmodels
 
 import android.content.Context
+import android.net.Uri
 import android.os.ParcelFileDescriptor
 import androidx.documentfile.provider.DocumentFile
 import com.anggrayudi.storage.file.extension
@@ -9,6 +10,7 @@ import org.ryujinx.android.RyujinxNative
 
 
 class GameModel(var file: DocumentFile, val context: Context) {
+    private var updateDescriptor: ParcelFileDescriptor? = null
     var type: FileType
     var descriptor: ParcelFileDescriptor? = null
     var fileName: String?
@@ -50,9 +52,29 @@ class GameModel(var file: DocumentFile, val context: Context) {
         return descriptor?.fd ?: 0
     }
 
+    fun openUpdate() : Int {
+        if(titleId?.isNotEmpty() == true) {
+            val vm = TitleUpdateViewModel(titleId ?: "")
+
+            if(vm.data?.selected?.isNotEmpty() == true){
+                val uri = Uri.parse(vm.data?.selected)
+                val file = DocumentFile.fromSingleUri(context, uri)
+                if(file?.exists() == true){
+                    updateDescriptor = context.contentResolver.openFileDescriptor(file.uri, "rw")
+
+                    return updateDescriptor ?.fd ?: -1;
+                }
+            }
+        }
+
+        return -1;
+    }
+
     fun close() {
         descriptor?.close()
         descriptor = null
+        updateDescriptor?.close()
+        updateDescriptor = null
     }
 }
 
diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt
index e7728360c..bf538287c 100644
--- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt
+++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt
@@ -85,6 +85,8 @@ class MainViewModel(val activity: MainActivity) {
         if (descriptor == 0)
             return false
 
+        val update = game.openUpdate()
+
         gameModel = game
         isMiiEditorLaunched = false
 
@@ -178,7 +180,7 @@ class MainViewModel(val activity: MainActivity) {
         if (!success)
             return false
 
-        success = nativeRyujinx.deviceLoadDescriptor(descriptor, game.type.ordinal)
+        success = nativeRyujinx.deviceLoadDescriptor(descriptor, game.type.ordinal, update)
 
         if (!success)
             return false
diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/TitleUpdateViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/TitleUpdateViewModel.kt
index 6a515f404..2f3df1b08 100644
--- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/TitleUpdateViewModel.kt
+++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/TitleUpdateViewModel.kt
@@ -1,12 +1,15 @@
 package org.ryujinx.android.viewmodels
 
+import android.content.Intent
+import android.net.Uri
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.snapshots.SnapshotStateList
 import androidx.compose.ui.text.intl.Locale
 import androidx.compose.ui.text.toLowerCase
+import androidx.documentfile.provider.DocumentFile
 import com.anggrayudi.storage.SimpleStorageHelper
+import com.anggrayudi.storage.file.extension
 import com.google.gson.Gson
-import org.ryujinx.android.Helpers
 import org.ryujinx.android.MainActivity
 import java.io.File
 import kotlin.math.max
@@ -16,29 +19,32 @@ class TitleUpdateViewModel(val titleId: String) {
     private var basePath: String
     private var updateJsonName = "updates.json"
     private var storageHelper: SimpleStorageHelper
+    var currentPaths: MutableList<String> = mutableListOf()
     var pathsState: SnapshotStateList<String>? = null
 
     companion object {
         const val UpdateRequestCode = 1002
     }
 
-    fun Remove(index: Int) {
+    fun remove(index: Int) {
         if (index <= 0)
             return
 
         data?.paths?.apply {
-            val removed = removeAt(index - 1)
-            File(removed).deleteRecursively()
+            val str = removeAt(index - 1)
+            Uri.parse(str)?.apply {
+                storageHelper.storage.context.contentResolver.releasePersistableUriPermission(
+                    this,
+                    Intent.FLAG_GRANT_READ_URI_PERMISSION
+                )
+            }
             pathsState?.clear()
             pathsState?.addAll(this)
+            currentPaths = this
         }
     }
 
-    fun Add(
-        isCopying: MutableState<Boolean>,
-        copyProgress: MutableState<Float>,
-        currentProgressName: MutableState<String>
-    ) {
+    fun add() {
         val callBack = storageHelper.onFileSelected
 
         storageHelper.onFileSelected = { requestCode, files ->
@@ -47,29 +53,29 @@ class TitleUpdateViewModel(val titleId: String) {
                 if (requestCode == UpdateRequestCode) {
                     val file = files.firstOrNull()
                     file?.apply {
-                        // Copy updates to internal data folder
-                        val updatePath = "$basePath/update"
-                        File(updatePath).mkdirs()
-                        Helpers.copyToData(
-                            this,
-                            updatePath,
-                            storageHelper,
-                            isCopying,
-                            copyProgress,
-                            currentProgressName, ::refreshPaths
-                        )
+                        if(file.extension == "nsp"){
+                            storageHelper.storage.context.contentResolver.takePersistableUriPermission(file.uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
+                            currentPaths.add(file.uri.toString())
+                        }
                     }
+
+                    refreshPaths()
                 }
             }
         }
         storageHelper.openFilePicker(UpdateRequestCode)
     }
 
-    fun refreshPaths() {
+    private fun refreshPaths() {
         data?.apply {
-            val updatePath = "$basePath/update"
             val existingPaths = mutableListOf<String>()
-            File(updatePath).listFiles()?.forEach { existingPaths.add(it.absolutePath) }
+            currentPaths.forEach {
+                val uri = Uri.parse(it)
+                val file = DocumentFile.fromSingleUri(storageHelper.storage.context, uri)
+                if(file?.exists() == true){
+                    existingPaths.add(it)
+                }
+            }
 
             if (!existingPaths.contains(selected)) {
                 selected = ""
@@ -88,7 +94,6 @@ class TitleUpdateViewModel(val titleId: String) {
         openDialog: MutableState<Boolean>
     ) {
         data?.apply {
-            val updatePath = "$basePath/update"
             this.selected = ""
             if (paths.isNotEmpty() && index > 0) {
                 val ind = max(index - 1, paths.count() - 1)
@@ -98,18 +103,29 @@ class TitleUpdateViewModel(val titleId: String) {
             File(basePath).mkdirs()
 
 
-            var metadata = TitleUpdateMetadata()
+            val metadata = TitleUpdateMetadata()
             val savedUpdates = mutableListOf<String>()
-            File(updatePath).listFiles()?.forEach { savedUpdates.add(it.absolutePath) }
+            currentPaths.forEach {
+                val uri = Uri.parse(it)
+                val file = DocumentFile.fromSingleUri(storageHelper.storage.context, uri)
+                if(file?.exists() == true){
+                    savedUpdates.add(it)
+                }
+            }
             metadata.paths = savedUpdates
 
-            val selectedName = File(selected).name
-            val newSelectedPath = "$updatePath/$selectedName"
-            if (File(newSelectedPath).exists()) {
-                metadata.selected = newSelectedPath
+            if(selected.isNotEmpty()){
+                val uri = Uri.parse(selected)
+                val file = DocumentFile.fromSingleUri(storageHelper.storage.context, uri)
+                if(file?.exists() == true){
+                    metadata.selected = selected
+                }
+            }
+            else {
+                metadata.selected = selected
             }
 
-            var json = gson.toJson(metadata)
+            val json = gson.toJson(metadata)
             File("$basePath/$updateJsonName").writeText(json)
 
             openDialog.value = false
@@ -138,9 +154,12 @@ class TitleUpdateViewModel(val titleId: String) {
             data = gson.fromJson(File(jsonPath).readText(), TitleUpdateMetadata::class.java)
 
         }
+        currentPaths = data?.paths ?: mutableListOf()
+        storageHelper = MainActivity.StorageHelper!!
         refreshPaths()
 
-        storageHelper = MainActivity.StorageHelper!!
+        File("$basePath/update").deleteRecursively()
+
     }
 }
 
diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/TitleUpdateViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/TitleUpdateViews.kt
index 719911db1..7eb8d62cf 100644
--- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/TitleUpdateViews.kt
+++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/TitleUpdateViews.kt
@@ -1,5 +1,6 @@
 package org.ryujinx.android.views
 
+import android.net.Uri
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
@@ -13,7 +14,6 @@ import androidx.compose.material.icons.filled.Add
 import androidx.compose.material.icons.filled.Delete
 import androidx.compose.material3.Icon
 import androidx.compose.material3.IconButton
-import androidx.compose.material3.LinearProgressIndicator
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.RadioButton
 import androidx.compose.material3.Surface
@@ -28,13 +28,19 @@ import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.dp
+import androidx.documentfile.provider.DocumentFile
+import org.ryujinx.android.MainActivity
 import org.ryujinx.android.viewmodels.TitleUpdateViewModel
-import java.io.File
 
 class TitleUpdateViews {
     companion object {
         @Composable
-        fun Main(titleId: String, name: String, openDialog: MutableState<Boolean>, canClose: MutableState<Boolean>) {
+        fun Main(
+            titleId: String,
+            name: String,
+            openDialog: MutableState<Boolean>,
+            canClose: MutableState<Boolean>
+        ) {
             val viewModel = TitleUpdateViewModel(titleId)
 
             val selected = remember { mutableStateOf(0) }
@@ -43,15 +49,6 @@ class TitleUpdateViews {
             }
 
             Column(modifier = Modifier.padding(16.dp)) {
-                val isCopying = remember {
-                    mutableStateOf(false)
-                }
-                val copyProgress = remember {
-                    mutableStateOf(0.0f)
-                }
-                var currentProgressName = remember {
-                    mutableStateOf("Starting Copy")
-                }
                 Column {
                     Text(text = "Updates for ${name}", textAlign = TextAlign.Center)
                     Surface(
@@ -88,18 +85,24 @@ class TitleUpdateViews {
                             var index = 1
                             for (path in paths) {
                                 val i = index
-                                Row(modifier = Modifier.padding(8.dp)) {
-                                    RadioButton(
-                                        selected = (selected.value == i),
-                                        onClick = { selected.value = i })
-                                    Text(
-                                        text = File(path).name,
-                                        modifier = Modifier
-                                            .fillMaxWidth()
-                                            .align(Alignment.CenterVertically)
-                                    )
+                                val uri = Uri.parse(path)
+                                val file = DocumentFile.fromSingleUri(
+                                    MainActivity.mainViewModel!!.activity,
+                                    uri
+                                )
+                                file?.apply {
+                                    Row(modifier = Modifier.padding(8.dp)) {
+                                        RadioButton(
+                                            selected = (selected.value == i),
+                                            onClick = { selected.value = i })
+                                        Text(
+                                            text = file.name ?: "",
+                                            modifier = Modifier
+                                                .fillMaxWidth()
+                                                .align(Alignment.CenterVertically)
+                                        )
+                                    }
                                 }
-
                                 index++
                             }
                         }
@@ -107,7 +110,7 @@ class TitleUpdateViews {
                     Row(modifier = Modifier.align(Alignment.End)) {
                         IconButton(
                             onClick = {
-                                viewModel.Remove(selected.value)
+                                viewModel.remove(selected.value)
                             }
                         ) {
                             Icon(
@@ -118,7 +121,7 @@ class TitleUpdateViews {
 
                         IconButton(
                             onClick = {
-                                viewModel.Add(isCopying, copyProgress, currentProgressName)
+                                viewModel.add()
                             }
                         ) {
                             Icon(
@@ -129,33 +132,12 @@ class TitleUpdateViews {
                     }
 
                 }
-                if (isCopying.value) {
-                    Text(text = "Copying updates to local storage")
-                    Text(text = currentProgressName.value)
-                    Row {
-                        LinearProgressIndicator(
-                            modifier = Modifier.fillMaxWidth(),
-                            progress = copyProgress.value
-                        )
-                        TextButton(
-                            onClick = {
-                                isCopying.value = false
-                                canClose.value = true
-                                viewModel.refreshPaths()
-                            },
-                        ) {
-                            Text("Cancel")
-                        }
-                    }
-                }
                 Spacer(modifier = Modifier.height(18.dp))
                 TextButton(
                     modifier = Modifier.align(Alignment.End),
                     onClick = {
-                        if (!isCopying.value) {
-                            canClose.value = true
-                            viewModel.save(selected.value, openDialog)
-                        }
+                        canClose.value = true
+                        viewModel.save(selected.value, openDialog)
                     },
                 ) {
                     Text("Save")