android - improve game update selection

This commit is contained in:
Emmanuel Hansen 2023-10-28 14:45:23 +00:00
parent 999fae7699
commit 0ed7220bb2
15 changed files with 274 additions and 201 deletions

View File

@ -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")]

View File

@ -728,6 +728,7 @@ namespace LibRyujinx
{
VirtualFileSystem.ReloadKeySet();
ContentManager = new ContentManager(VirtualFileSystem);
AccountManager = new AccountManager(LibHacHorizonManager.RyujinxClient);
}
internal void DisposeContext()

View File

@ -45,5 +45,6 @@ long _currentRenderingThreadId = 0;
JavaVM* _vm = nullptr;
jobject _mainActivity = nullptr;
jclass _mainActivityClass = nullptr;
std::string _currentString = "";
#endif //RYUJINXNATIVE_RYUIJNX_H

View File

@ -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();
}

View File

@ -354,6 +354,7 @@ class GameActivity : BaseActivity() {
.padding(16.dp)
) {
Button(onClick = {
showBackNotice.value = false
mainViewModel.closeGame()
setFullScreen(false)
finishActivity(0)

View File

@ -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,

View File

@ -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
}

View File

@ -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

View File

@ -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()
}
}

View File

@ -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>,

View File

@ -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!!

View File

@ -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)
}
}

View File

@ -325,6 +325,8 @@ class SettingViews {
AlertDialog(onDismissRequest = {
showImportCompletion.value = false
importFile.value = null
mainViewModel.requestUserRefresh()
mainViewModel.homeViewModel.clearLoadedCache()
}) {
Card(
modifier = Modifier,

View File

@ -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")

View File

@ -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 {