From 14d6e280f716768ff436cf48d8dbbfa79fe44e89 Mon Sep 17 00:00:00 2001
From: Emmanuel Hansen <emmausssss@gmail.com>
Date: Sat, 28 Oct 2023 14:45:23 +0000
Subject: [PATCH] android - improve game update selection

---
 src/LibRyujinx/Android/JniExportedMethods.cs  |   7 +-
 src/LibRyujinx/LibRyujinx.cs                  |   1 +
 src/RyujinxAndroid/app/src/main/cpp/ryuijnx.h |   1 +
 .../app/src/main/cpp/ryujinx.cpp              |  22 ++
 .../java/org/ryujinx/android/GameActivity.kt  |   1 +
 .../main/java/org/ryujinx/android/Helpers.kt  |  61 ++++++
 .../java/org/ryujinx/android/NativeHelpers.kt |   2 +
 .../java/org/ryujinx/android/RyujinxNative.kt |   2 +-
 .../android/viewmodels/HomeViewModel.kt       |  47 ++--
 .../android/viewmodels/MainViewModel.kt       |  41 ++++
 .../viewmodels/TitleUpdateViewModel.kt        | 200 +++++-------------
 .../org/ryujinx/android/views/HomeViews.kt    |  45 ++--
 .../org/ryujinx/android/views/SettingViews.kt |   2 +
 .../ryujinx/android/views/TitleUpdateViews.kt |  39 ++--
 .../org/ryujinx/android/views/UserViews.kt    |   4 +-
 15 files changed, 274 insertions(+), 201 deletions(-)

diff --git a/src/LibRyujinx/Android/JniExportedMethods.cs b/src/LibRyujinx/Android/JniExportedMethods.cs
index 76ee4039e..255adb456 100644
--- a/src/LibRyujinx/Android/JniExportedMethods.cs
+++ b/src/LibRyujinx/Android/JniExportedMethods.cs
@@ -41,6 +41,9 @@ namespace LibRyujinx
         [DllImport("libryujinxjni")]
         private extern static JStringLocalRef createString(JEnvRef jEnv, IntPtr ch);
 
+        [DllImport("libryujinxjni")]
+        private extern static void pushString(string ch);
+
         [DllImport("libryujinxjni")]
         internal extern static void setRenderingThread();
 
@@ -511,11 +514,11 @@ namespace LibRyujinx
         }
 
         [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userGetOpenedUser")]
-        public static JStringLocalRef JniGetOpenedUser(JEnvRef jEnv, JObjectLocalRef jObj)
+        public static void JniGetOpenedUser(JEnvRef jEnv, JObjectLocalRef jObj)
         {
             var userId = GetOpenedUser();
 
-            return CreateString(jEnv, userId);
+            pushString(userId);
         }
 
         [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userGetUserPicture")]
diff --git a/src/LibRyujinx/LibRyujinx.cs b/src/LibRyujinx/LibRyujinx.cs
index 52318260f..51dd76156 100644
--- a/src/LibRyujinx/LibRyujinx.cs
+++ b/src/LibRyujinx/LibRyujinx.cs
@@ -728,6 +728,7 @@ namespace LibRyujinx
         {
             VirtualFileSystem.ReloadKeySet();
             ContentManager = new ContentManager(VirtualFileSystem);
+            AccountManager = new AccountManager(LibHacHorizonManager.RyujinxClient);
         }
 
         internal void DisposeContext()
diff --git a/src/RyujinxAndroid/app/src/main/cpp/ryuijnx.h b/src/RyujinxAndroid/app/src/main/cpp/ryuijnx.h
index b26687b44..87e7c3124 100644
--- a/src/RyujinxAndroid/app/src/main/cpp/ryuijnx.h
+++ b/src/RyujinxAndroid/app/src/main/cpp/ryuijnx.h
@@ -45,5 +45,6 @@ long _currentRenderingThreadId = 0;
 JavaVM* _vm = nullptr;
 jobject _mainActivity = nullptr;
 jclass _mainActivityClass = nullptr;
+std::string _currentString = "";
 
 #endif //RYUJINXNATIVE_RYUIJNX_H
diff --git a/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp b/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp
index 278c176b5..095f462db 100644
--- a/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp
+++ b/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp
@@ -311,3 +311,25 @@ JNIEXPORT jstring JNICALL
 Java_org_ryujinx_android_NativeHelpers_getProgressInfo(JNIEnv *env, jobject thiz) {
     return createStringFromStdString(env, progressInfo);
 }
+
+extern "C"
+JNIEXPORT jstring JNICALL
+Java_org_ryujinx_android_NativeHelpers_popStringJava(JNIEnv *env, jobject thiz) {
+    return createStringFromStdString(env, _currentString);
+}
+extern "C"
+JNIEXPORT void JNICALL
+Java_org_ryujinx_android_NativeHelpers_pushStringJava(JNIEnv *env, jobject thiz, jstring string) {
+    _currentString = getStringPointer(env, string);
+}
+
+
+extern "C"
+void pushString(char* str){
+    _currentString = str;
+}
+
+extern "C"
+const char* popString(){
+    return _currentString.c_str();
+}
diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameActivity.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameActivity.kt
index 8dc39bfeb..1670e879a 100644
--- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameActivity.kt
+++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameActivity.kt
@@ -354,6 +354,7 @@ class GameActivity : BaseActivity() {
                                         .padding(16.dp)
                                 ) {
                                     Button(onClick = {
+                                        showBackNotice.value = false
                                         mainViewModel.closeGame()
                                         setFullScreen(false)
                                         finishActivity(0)
diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Helpers.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Helpers.kt
index 764d6edab..aef544c7a 100644
--- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Helpers.kt
+++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Helpers.kt
@@ -9,7 +9,13 @@ import android.provider.DocumentsContract
 import android.provider.MediaStore
 import androidx.compose.runtime.MutableState
 import androidx.documentfile.provider.DocumentFile
+import com.anggrayudi.storage.SimpleStorageHelper
+import com.anggrayudi.storage.callback.FileCallback
+import com.anggrayudi.storage.file.copyFileTo
 import com.anggrayudi.storage.file.openInputStream
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
 import net.lingala.zip4j.io.inputstream.ZipInputStream
 import java.io.BufferedOutputStream
 import java.io.File
@@ -66,6 +72,61 @@ class Helpers {
             }
             return null
         }
+        fun copyToData(
+            file: DocumentFile, path: String, storageHelper: SimpleStorageHelper,
+            isCopying: MutableState<Boolean>,
+            copyProgress: MutableState<Float>,
+            currentProgressName: MutableState<String>,
+            finish: () -> Unit
+        ) {
+            var callback: FileCallback? = object : FileCallback() {
+                override fun onFailed(errorCode: FileCallback.ErrorCode) {
+                    super.onFailed(errorCode)
+                    File(path).delete()
+                    finish()
+                }
+
+                override fun onStart(file: Any, workerThread: Thread): Long {
+                    copyProgress.value = 0f
+
+                    (file as DocumentFile)?.apply {
+                        currentProgressName.value = "Copying ${file.name}"
+                    }
+                    return super.onStart(file, workerThread)
+                }
+
+                override fun onReport(report: Report) {
+                    super.onReport(report)
+
+                    if(!isCopying.value) {
+                        Thread.currentThread().interrupt()
+                    }
+
+                    copyProgress.value = report.progress / 100f
+                }
+
+                override fun onCompleted(result: Any) {
+                    super.onCompleted(result)
+                    isCopying.value = false
+                    finish()
+                }
+            }
+            val ioScope = CoroutineScope(Dispatchers.IO)
+            isCopying.value = true
+            file.apply {
+                if (!File(path + "/${file.name}").exists()) {
+                    val f = this
+                    ioScope.launch {
+                        f.copyFileTo(
+                            storageHelper.storage.context,
+                            File(path),
+                            callback = callback!!
+                        )
+
+                    }
+                }
+            }
+        }
 
         private fun getDataColumn(
             context: Context,
diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt
index eead88176..229b61a90 100644
--- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt
+++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt
@@ -28,4 +28,6 @@ class NativeHelpers {
     external fun setSwapInterval(nativeWindow: Long, swapInterval: Int): Int
     external fun getProgressInfo() : String
     external fun getProgressValue() : Float
+    external fun pushStringJava(string: String)
+    external fun popStringJava() : String
 }
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 88b266fed..4c990f094 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
@@ -53,7 +53,7 @@ class RyujinxNative {
     external fun deviceSignalEmulationClose()
     external fun deviceGetDlcTitleId(path: String, ncaPath: String) : String
     external fun deviceGetDlcContentList(path: String, titleId: Long) : Array<String>
-    external fun userGetOpenedUser() : String
+    external fun userGetOpenedUser()
     external fun userGetUserPicture(userId: String) : String
     external fun userSetUserPicture(userId: String, picture: String)
     external fun userGetUserName(userId: String) : String
diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/HomeViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/HomeViewModel.kt
index dc34f6882..bbfeb9e8a 100644
--- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/HomeViewModel.kt
+++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/HomeViewModel.kt
@@ -68,7 +68,7 @@ class HomeViewModel(
             )
     }
 
-    fun reloadGameList() {
+    fun reloadGameList(ignoreCache: Boolean = false) {
         var storage = activity?.storageHelper ?: return
         
         if(isLoading)
@@ -77,27 +77,32 @@ class HomeViewModel(
         
         isLoading = true
 
-        val files = mutableListOf<GameModel>()
+        if(!ignoreCache) {
+            val files = mutableListOf<GameModel>()
 
-        thread {
-            try {
-                for (file in folder.search(false, DocumentFileType.FILE)) {
-                    if (file.extension == "xci" || file.extension == "nsp")
-                        activity.let {
-                            files.add(GameModel(file, it))
-                        }
+            thread {
+                try {
+                    for (file in folder.search(false, DocumentFileType.FILE)) {
+                        if (file.extension == "xci" || file.extension == "nsp")
+                            activity.let {
+                                files.add(GameModel(file, it))
+                            }
+                    }
+
+                    loadedCache = files.toList()
+
+                    isLoading = false
+
+                    applyFilter()
+                } finally {
+                    isLoading = false
                 }
-
-                loadedCache = files.toList()
-
-                isLoading = false
-
-                applyFilter()
-            }
-            finally {
-                isLoading = false
             }
         }
+        else{
+            isLoading = false
+            applyFilter()
+        }
     }
 
     private fun applyFilter() {
@@ -109,6 +114,10 @@ class HomeViewModel(
 
     fun setViewList(list: SnapshotStateList<GameModel>) {
         gameList = list
-        reloadGameList()
+        reloadGameList(loadedCache.isNotEmpty())
+    }
+
+    fun clearLoadedCache(){
+        loadedCache = listOf()
     }
 }
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 734267720..0d3850425 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
@@ -169,6 +169,47 @@ class MainViewModel(val activity: MainActivity) {
         return true
     }
 
+    fun clearPptcCache(titleId :String){
+        if(titleId.isNotEmpty()){
+            val basePath = MainActivity.AppPath + "/games/$titleId/cache/cpu"
+            if(File(basePath).exists()){
+                var caches = mutableListOf<String>()
+
+                val mainCache = basePath + "${File.separator}0"
+                File(mainCache).listFiles()?.forEach {
+                    if(it.isFile && it.name.endsWith(".cache"))
+                        caches.add(it.absolutePath)
+                }
+                val backupCache = basePath + "${File.separator}1"
+                File(backupCache).listFiles()?.forEach {
+                    if(it.isFile && it.name.endsWith(".cache"))
+                        caches.add(it.absolutePath)
+                }
+                for(path in caches)
+                    File(path).delete()
+            }
+        }
+    }
+
+    fun purgeShaderCache(titleId :String) {
+        if(titleId.isNotEmpty()){
+            val basePath = MainActivity.AppPath + "/games/$titleId/cache/shader"
+            if(File(basePath).exists()){
+                var caches = mutableListOf<String>()
+                File(basePath).listFiles()?.forEach {
+                    if(!it.isFile)
+                        it.delete()
+                    else{
+                        if(it.name.endsWith(".toc") || it.name.endsWith(".data"))
+                            caches.add(it.absolutePath)
+                    }
+                }
+                for(path in caches)
+                    File(path).delete()
+            }
+        }
+    }
+
     fun setStatStates(
         fifo: MutableState<Double>,
         gameFps: MutableState<Double>,
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 8d3a53456..e42108220 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
@@ -4,27 +4,17 @@ 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.callback.FileCallback
-import com.anggrayudi.storage.file.DocumentFileCompat
-import com.anggrayudi.storage.file.DocumentFileType
-import com.anggrayudi.storage.file.copyFileTo
-import com.anggrayudi.storage.file.getAbsolutePath
 import com.google.gson.Gson
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
+import org.ryujinx.android.Helpers
 import org.ryujinx.android.MainActivity
 import java.io.File
-import java.util.LinkedList
-import java.util.Queue
 import kotlin.math.max
 
 class TitleUpdateViewModel(val titleId: String) {
+    private var canClose: MutableState<Boolean>? = null
     private var basePath: String
     private var updateJsonName = "updates.json"
-    private var stagingUpdateJsonName = "staging_updates.json"
     private var storageHelper: SimpleStorageHelper
     var pathsState: SnapshotStateList<String>? = null
 
@@ -37,32 +27,37 @@ class TitleUpdateViewModel(val titleId: String) {
             return
 
         data?.paths?.apply {
-            removeAt(index - 1)
+            val removed = removeAt(index - 1)
+            File(removed).deleteRecursively()
             pathsState?.clear()
             pathsState?.addAll(this)
         }
     }
 
-    fun Add() {
+    fun Add(
+        isCopying: MutableState<Boolean>,
+        copyProgress: MutableState<Float>,
+        currentProgressName: MutableState<String>
+    ) {
         val callBack = storageHelper.onFileSelected
 
         storageHelper.onFileSelected = { requestCode, files ->
             run {
                 storageHelper.onFileSelected = callBack
-                if(requestCode == UpdateRequestCode)
-                {
+                if (requestCode == UpdateRequestCode) {
                     val file = files.firstOrNull()
                     file?.apply {
-                        val path = file.getAbsolutePath(storageHelper.storage.context)
-                        if(path.isNotEmpty()){
-                            data?.apply {
-                                if(!paths.contains(path)) {
-                                    paths.add(path)
-                                    pathsState?.clear()
-                                    pathsState?.addAll(paths)
-                                }
-                            }
-                        }
+                        // Copy updates to internal data folder
+                        val updatePath = "$basePath/update"
+                        File(updatePath).mkdirs()
+                        Helpers.copyToData(
+                            this,
+                            updatePath,
+                            storageHelper,
+                            isCopying,
+                            copyProgress,
+                            currentProgressName, ::refreshPaths
+                        )
                     }
                 }
             }
@@ -70,128 +65,60 @@ class TitleUpdateViewModel(val titleId: String) {
         storageHelper.openFilePicker(UpdateRequestCode)
     }
 
+    fun refreshPaths() {
+        data?.apply {
+            val updatePath = "$basePath/update"
+            val existingPaths = mutableListOf<String>()
+            File(updatePath).listFiles()?.forEach { existingPaths.add(it.absolutePath) }
+
+            if (!existingPaths.contains(selected)) {
+                selected = ""
+            }
+            pathsState?.clear()
+            pathsState?.addAll(existingPaths)
+            paths = existingPaths
+            canClose?.apply {
+                value = true
+            }
+        }
+    }
+
     fun save(
         index: Int,
-        isCopying: MutableState<Boolean>,
-        openDialog: MutableState<Boolean>,
-        copyProgress: MutableState<Float>,
-        currentProgressName: MutableState<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)
                 this.selected = paths[ind]
             }
             val gson = Gson()
-            var json = gson.toJson(this)
             File(basePath).mkdirs()
-            File("$basePath/$stagingUpdateJsonName").writeText(json)
 
-            // Copy updates to internal data folder
-            val updatePath = "$basePath/update"
-            File(updatePath).mkdirs()
-
-            val ioScope = CoroutineScope(Dispatchers.IO)
 
             var metadata = TitleUpdateMetadata()
-            var queue: Queue<String> = LinkedList()
+            val savedUpdates = mutableListOf<String>()
+            File(updatePath).listFiles()?.forEach { savedUpdates.add(it.absolutePath) }
+            metadata.paths = savedUpdates
 
-            var callback: FileCallback? = null
-
-            fun copy(path: String) {
-                isCopying.value = true
-                val documentFile = DocumentFileCompat.fromFullPath(
-                    storageHelper.storage.context,
-                    path,
-                    DocumentFileType.FILE
-                )
-                documentFile?.apply {
-                    val stagedPath = "$basePath/${name}"
-                    if (!File(stagedPath).exists()) {
-                        var file = this
-                        ioScope.launch {
-                            file.copyFileTo(
-                                storageHelper.storage.context,
-                                File(updatePath),
-                                callback = callback!!
-                            )
-
-                        }
-
-                        metadata.paths.add(stagedPath)
-                    }
-                }
+            val selectedName = File(selected).name
+            val newSelectedPath = "$updatePath/$selectedName"
+            if (File(newSelectedPath).exists()) {
+                metadata.selected = newSelectedPath
             }
 
-            fun finish() {
-                val savedUpdates = mutableListOf<String>()
-                File(updatePath).listFiles()?.forEach { savedUpdates.add(it.absolutePath) }
-                var missingFiles =
-                    savedUpdates.filter { i -> paths.find { it.endsWith(File(i).name) } == null }
-                for (path in missingFiles) {
-                    File(path).delete()
-                }
+            var json = gson.toJson(metadata)
+            File("$basePath/$updateJsonName").writeText(json)
 
-                val selectedName = File(selected).name
-                val newSelectedPath = "$updatePath/$selectedName"
-                if (File(newSelectedPath).exists()) {
-                    metadata.selected = newSelectedPath
-                }
-
-                json = gson.toJson(metadata)
-                File("$basePath/$updateJsonName").writeText(json)
-
-                openDialog.value = false
-                isCopying.value = false
-            }
-            callback = object : FileCallback() {
-                override fun onFailed(errorCode: FileCallback.ErrorCode) {
-                    super.onFailed(errorCode)
-                }
-
-                override fun onStart(file: Any, workerThread: Thread): Long {
-                    copyProgress.value = 0f
-
-                    (file as DocumentFile)?.apply {
-                        currentProgressName.value = "Copying ${file.name}"
-                    }
-                    return super.onStart(file, workerThread)
-                }
-
-                override fun onReport(report: Report) {
-                    super.onReport(report)
-
-                    copyProgress.value = report.progress / 100f
-                }
-
-                override fun onCompleted(result: Any) {
-                    super.onCompleted(result)
-
-                    if (queue.isNotEmpty())
-                        copy(queue.remove())
-                    else {
-                        finish()
-                    }
-                }
-            }
-            for (path in paths) {
-                queue.add(path)
-            }
-
-            ioScope.launch {
-                if (queue.isNotEmpty()) {
-                    copy(queue.remove())
-                } else {
-                    finish()
-                }
-
-            }
+            openDialog.value = false
         }
     }
 
-    fun setPaths(paths: SnapshotStateList<String>) {
+    fun setPaths(paths: SnapshotStateList<String>, canClose: MutableState<Boolean>) {
         pathsState = paths
+        this.canClose = canClose
         data?.apply {
             pathsState?.clear()
             pathsState?.addAll(this.paths)
@@ -203,29 +130,14 @@ class TitleUpdateViewModel(val titleId: String) {
 
     init {
         basePath = MainActivity.AppPath + "/games/" + titleId.toLowerCase(Locale.current)
-        val stagingJson = "${basePath}/${stagingUpdateJsonName}"
         jsonPath = "${basePath}/${updateJsonName}"
 
         data = TitleUpdateMetadata()
-        if (File(stagingJson).exists()) {
+        if (File(jsonPath).exists()) {
             val gson = Gson()
-            data = gson.fromJson(File(stagingJson).readText(), TitleUpdateMetadata::class.java)
+            data = gson.fromJson(File(jsonPath).readText(), TitleUpdateMetadata::class.java)
 
-            data?.apply {
-                val existingPaths = mutableListOf<String>()
-                for (path in paths) {
-                    if (File(path).exists()) {
-                        existingPaths.add(path)
-                    }
-                }
-
-                if(!existingPaths.contains(selected)){
-                    selected = ""
-                }
-                pathsState?.clear()
-                pathsState?.addAll(existingPaths)
-                paths = existingPaths
-            }
+            refreshPaths()
         }
 
         storageHelper = MainActivity.StorageHelper!!
diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt
index bd1911ad0..f567410f1 100644
--- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt
+++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt
@@ -61,6 +61,7 @@ import androidx.navigation.NavHostController
 import coil.compose.AsyncImage
 import com.anggrayudi.storage.extension.launchOnUiThread
 import org.ryujinx.android.MainActivity
+import org.ryujinx.android.NativeHelpers
 import org.ryujinx.android.RyujinxNative
 import org.ryujinx.android.viewmodels.GameModel
 import org.ryujinx.android.viewmodels.HomeViewModel
@@ -84,6 +85,7 @@ class HomeViews {
             val showAppActions = remember { mutableStateOf(false) }
             val showLoading = remember { mutableStateOf(false) }
             val openTitleUpdateDialog = remember { mutableStateOf(false) }
+            val canClose = remember { mutableStateOf(true) }
             val openDlcDialog = remember { mutableStateOf(false) }
             val query = remember {
                 mutableStateOf("")
@@ -102,10 +104,11 @@ class HomeViews {
             val pic = remember {
                 mutableStateOf(ByteArray(0))
             }
-            
-            if(refreshUser.value){
-                user.value = native.userGetOpenedUser()
-                if(user.value.isNotEmpty()) {
+
+            if (refreshUser.value) {
+                native.userGetOpenedUser()
+                user.value = NativeHelpers().popStringJava()
+                if (user.value.isNotEmpty()) {
                     val decoder = Base64.getDecoder()
                     pic.value = decoder.decode(native.userGetUserPicture(user.value))
                 }
@@ -145,7 +148,7 @@ class HomeViews {
                             IconButton(onClick = {
                                 navController?.navigate("user")
                             }) {
-                                if(pic.value.isNotEmpty()) {
+                                if (pic.value.isNotEmpty()) {
                                     Image(
                                         bitmap = BitmapFactory.decodeByteArray(
                                             pic.value,
@@ -160,8 +163,7 @@ class HomeViews {
                                             .size(52.dp)
                                             .clip(CircleShape)
                                     )
-                                }
-                                else{
+                                } else {
                                     Icon(
                                         Icons.Filled.Person,
                                         contentDescription = "user"
@@ -204,29 +206,29 @@ class HomeViews {
                                 DropdownMenu(
                                     expanded = showAppMenu.value,
                                     onDismissRequest = { showAppMenu.value = false }) {
+                                    DropdownMenuItem(text = {
+                                        Text(text = "Clear PPTC Cache")
+                                    }, onClick = {
+                                        showAppMenu.value = false
+                                        viewModel.mainViewModel?.clearPptcCache(viewModel.mainViewModel?.selected?.titleId ?: "")
+                                    })
+                                    DropdownMenuItem(text = {
+                                        Text(text = "Purge Shader Cache")
+                                    }, onClick = {
+                                        showAppMenu.value = false
+                                        viewModel.mainViewModel?.purgeShaderCache(viewModel.mainViewModel?.selected?.titleId ?: "")
+                                    })
                                     DropdownMenuItem(text = {
                                         Text(text = "Manage Updates")
                                     }, onClick = {
                                         showAppMenu.value = false
                                         openTitleUpdateDialog.value = true
-                                    }, leadingIcon = {
-                                        Icon(
-                                            imageVector = org.ryujinx.android.Icons.gameUpdate(),
-                                            contentDescription = "Updates",
-                                            tint = MaterialTheme.colorScheme.onSurface
-                                        )
                                     })
                                     DropdownMenuItem(text = {
                                         Text(text = "Manage DLC")
                                     }, onClick = {
                                         showAppMenu.value = false
                                         openDlcDialog.value = true
-                                    }, leadingIcon = {
-                                        Icon(
-                                            imageVector = org.ryujinx.android.Icons.download(),
-                                            contentDescription = "Dlc",
-                                            tint = MaterialTheme.colorScheme.onSurface
-                                        )
                                     })
                                 }
                             }
@@ -255,11 +257,10 @@ class HomeViews {
                     val list = remember {
                         mutableStateListOf<GameModel>()
                     }
-
-
                     if (refresh.value) {
                         viewModel.setViewList(list)
                         refresh.value = false
+                        showAppActions.value = false
                     }
                     val selectedModel = remember {
                         mutableStateOf(viewModel.mainViewModel?.selected)
@@ -323,7 +324,7 @@ class HomeViews {
                         ) {
                             val titleId = viewModel.mainViewModel?.selected?.titleId ?: ""
                             val name = viewModel.mainViewModel?.selected?.titleName ?: ""
-                            TitleUpdateViews.Main(titleId, name, openTitleUpdateDialog)
+                            TitleUpdateViews.Main(titleId, name, openTitleUpdateDialog, canClose)
                         }
 
                     }
diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt
index 5b28aad91..1870b4261 100644
--- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt
+++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt
@@ -325,6 +325,8 @@ class SettingViews {
                                 AlertDialog(onDismissRequest = {
                                     showImportCompletion.value = false
                                     importFile.value = null
+                                    mainViewModel.requestUserRefresh()
+                                    mainViewModel.homeViewModel.clearLoadedCache()
                                 }) {
                                     Card(
                                         modifier = Modifier,
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 558bc559b..96347f259 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
@@ -27,11 +27,12 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.dp
 import org.ryujinx.android.viewmodels.TitleUpdateViewModel
+import java.io.File
 
 class TitleUpdateViews {
     companion object {
         @Composable
-        fun Main(titleId: String, name: String, openDialog: MutableState<Boolean>) {
+        fun Main(titleId: String, name: String, openDialog: MutableState<Boolean>, canClose: MutableState<Boolean>) {
             val viewModel = TitleUpdateViewModel(titleId)
 
             val selected = remember { mutableStateOf(0) }
@@ -46,6 +47,9 @@ class TitleUpdateViews {
                 val copyProgress = remember {
                     mutableStateOf(0.0f)
                 }
+                var currentProgressName = remember {
+                    mutableStateOf("Starting Copy")
+                }
                 Column {
                     Text(text = "Updates for ${name}", textAlign = TextAlign.Center)
                     Surface(
@@ -77,7 +81,7 @@ class TitleUpdateViews {
                                 mutableStateListOf<String>()
                             }
 
-                            viewModel.setPaths(paths)
+                            viewModel.setPaths(paths, canClose)
                             var index = 1
                             for (path in paths) {
                                 val i = index
@@ -86,7 +90,7 @@ class TitleUpdateViews {
                                         selected = (selected.value == i),
                                         onClick = { selected.value = i })
                                     Text(
-                                        text = path,
+                                        text = File(path).name,
                                         modifier = Modifier
                                             .fillMaxWidth()
                                             .align(Alignment.CenterVertically)
@@ -111,7 +115,7 @@ class TitleUpdateViews {
 
                         IconButton(
                             onClick = {
-                                viewModel.Add()
+                                viewModel.Add(isCopying, copyProgress, currentProgressName)
                             }
                         ) {
                             Icon(
@@ -122,22 +126,33 @@ class TitleUpdateViews {
                     }
 
                 }
-                var currentProgressName = remember {
-                    mutableStateOf("Starting Copy")
-                }
                 if (isCopying.value) {
                     Text(text = "Copying updates to local storage")
                     Text(text = currentProgressName.value)
-                    LinearProgressIndicator(
-                        modifier = Modifier.fillMaxWidth(),
-                        progress = copyProgress.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 = {
-                        viewModel.save(selected.value, isCopying, openDialog, copyProgress, currentProgressName)
+                        if (!isCopying.value) {
+                            canClose.value = true
+                            viewModel.save(selected.value, openDialog)
+                        }
                     },
                 ) {
                     Text("Save")
diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/UserViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/UserViews.kt
index b41d17ea9..63f0f2379 100644
--- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/UserViews.kt
+++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/UserViews.kt
@@ -36,6 +36,7 @@ import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.dp
 import androidx.navigation.NavHostController
+import org.ryujinx.android.NativeHelpers
 import org.ryujinx.android.RyujinxNative
 import org.ryujinx.android.viewmodels.MainViewModel
 import java.util.Base64
@@ -47,8 +48,9 @@ class UserViews {
         fun Main(viewModel: MainViewModel? = null, navController: NavHostController? = null) {
             val ryujinxNative = RyujinxNative()
             val decoder = Base64.getDecoder()
+            ryujinxNative.userGetOpenedUser()
             val openedUser = remember {
-                mutableStateOf(ryujinxNative.userGetOpenedUser())
+                mutableStateOf(NativeHelpers().popStringJava())
             }
 
             val openedUserPic = remember {