From ba745514a1bbec1bc0457ee9345b032b8385497f Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Sun, 22 Oct 2023 21:30:05 +0000 Subject: [PATCH] android - add basic user management --- src/LibRyujinx/Android/JniExportedMethods.cs | 90 +++++++++ src/LibRyujinx/LibRyujinx.Device.cs | 6 + src/LibRyujinx/LibRyujinx.User.cs | 82 ++++++++ src/LibRyujinx/LibRyujinx.cs | 6 + .../java/org/ryujinx/android/RyujinxNative.kt | 10 + .../org/ryujinx/android/views/HomeViews.kt | 26 ++- .../org/ryujinx/android/views/MainView.kt | 1 + .../org/ryujinx/android/views/UserViews.kt | 182 ++++++++++++++++++ 8 files changed, 399 insertions(+), 4 deletions(-) create mode 100644 src/LibRyujinx/LibRyujinx.User.cs create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/UserViews.kt diff --git a/src/LibRyujinx/Android/JniExportedMethods.cs b/src/LibRyujinx/Android/JniExportedMethods.cs index e3a9ab860..76ee4039e 100644 --- a/src/LibRyujinx/Android/JniExportedMethods.cs +++ b/src/LibRyujinx/Android/JniExportedMethods.cs @@ -18,6 +18,7 @@ using Silk.NET.Vulkan.Extensions.KHR; using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Numerics; using System.Runtime.InteropServices; using System.Security.Cryptography; @@ -95,6 +96,12 @@ namespace LibRyujinx return s; } + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceReloadFilesystem")] + public static void JniReloadFileSystem() + { + SwitchDevice?.ReloadFileSystem(); + } + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceInitialize")] public static JBoolean JniInitializeDeviceNative(JEnvRef jEnv, JObjectLocalRef jObj, @@ -503,6 +510,89 @@ namespace LibRyujinx return ConnectGamepad(index); } + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userGetOpenedUser")] + public static JStringLocalRef JniGetOpenedUser(JEnvRef jEnv, JObjectLocalRef jObj) + { + var userId = GetOpenedUser(); + + return CreateString(jEnv, userId); + } + + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userGetUserPicture")] + public static JStringLocalRef JniGetUserPicture(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr) + { + var userId = GetString(jEnv, userIdPtr) ?? ""; + + return CreateString(jEnv, GetUserPicture(userId)); + } + + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userSetUserPicture")] + public static void JniGetUserPicture(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr, JStringLocalRef picturePtr) + { + var userId = GetString(jEnv, userIdPtr) ?? ""; + var picture = GetString(jEnv, picturePtr) ?? ""; + + SetUserPicture(userId, picture); + } + + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userGetUserName")] + public static JStringLocalRef JniGetUserName(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr) + { + var userId = GetString(jEnv, userIdPtr) ?? ""; + + return CreateString(jEnv, GetUserName(userId)); + } + + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userSetUserName")] + public static void JniSetUserName(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr, JStringLocalRef userNamePtr) + { + var userId = GetString(jEnv, userIdPtr) ?? ""; + var userName = GetString(jEnv, userNamePtr) ?? ""; + + SetUserName(userId, userName); + } + + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userGetAllUsers")] + public static JArrayLocalRef JniGetAllUsers(JEnvRef jEnv, JObjectLocalRef jObj) + { + var users = GetAllUsers(); + + return CreateStringArray(jEnv, users.ToList()); + } + + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userAddUser")] + public static void JniAddUser(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userNamePtr, JStringLocalRef picturePtr) + { + var userName = GetString(jEnv, userNamePtr) ?? ""; + var picture = GetString(jEnv, picturePtr) ?? ""; + + AddUser(userName, picture); + } + + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userDeleteUser")] + public static void JniDeleteUser(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr) + { + var userId = GetString(jEnv, userIdPtr) ?? ""; + + DeleteUser(userId); + } + + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userOpenUser")] + public static void JniOpenUser(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr) + { + var userId = GetString(jEnv, userIdPtr) ?? ""; + + OpenUser(userId); + } + + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userCloseUser")] + public static void JniCloseUser(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr) + { + var userId = GetString(jEnv, userIdPtr) ?? ""; + + CloseUser(userId); + } + private static FileStream OpenFile(int descriptor) { var safeHandle = new SafeFileHandle(descriptor, false); diff --git a/src/LibRyujinx/LibRyujinx.Device.cs b/src/LibRyujinx/LibRyujinx.Device.cs index 376b7a352..724630bbb 100644 --- a/src/LibRyujinx/LibRyujinx.Device.cs +++ b/src/LibRyujinx/LibRyujinx.Device.cs @@ -17,6 +17,12 @@ namespace LibRyujinx return InitializeDevice(true, false, SystemLanguage.AmericanEnglish, RegionCode.USA, true, true, true, false, "UTC", false); } + [UnmanagedCallersOnly(EntryPoint = "device_reloadFilesystem")] + public static void ReloadFileSystem() + { + SwitchDevice?.ReloadFileSystem(); + } + public static bool InitializeDevice(bool isHostMapped, bool useNce, SystemLanguage systemLanguage, diff --git a/src/LibRyujinx/LibRyujinx.User.cs b/src/LibRyujinx/LibRyujinx.User.cs new file mode 100644 index 000000000..f78ee22be --- /dev/null +++ b/src/LibRyujinx/LibRyujinx.User.cs @@ -0,0 +1,82 @@ +using Ryujinx.HLE.HOS.Services.Account.Acc; +using System; +using System.Linq; + +namespace LibRyujinx +{ + public static partial class LibRyujinx + { + public static string GetOpenedUser() + { + var lastProfile = SwitchDevice?.AccountManager.LastOpenedUser; + + return lastProfile?.UserId.ToString() ?? ""; + } + + public static string GetUserPicture(string userId) + { + var uid = new UserId(userId); + + var user = SwitchDevice?.AccountManager.GetAllUsers().FirstOrDefault(x => x.UserId == uid); + + if (user == null) + return ""; + + var pic = user.Image; + + return pic != null ? Convert.ToBase64String(pic) : ""; + } + + public static void SetUserPicture(string userId, string picture) + { + var uid = new UserId(userId); + + SwitchDevice?.AccountManager.SetUserImage(uid, Convert.FromBase64String(picture)); + } + + public static string GetUserName(string userId) + { + var uid = new UserId(userId); + + var user = SwitchDevice?.AccountManager.GetAllUsers().FirstOrDefault(x => x.UserId == uid); + + return user?.Name ?? ""; + } + + public static void SetUserName(string userId, string name) + { + var uid = new UserId(userId); + + SwitchDevice?.AccountManager.SetUserName(uid, name); + } + + public static string[] GetAllUsers() + { + return SwitchDevice?.AccountManager.GetAllUsers().Select(x => x.UserId.ToString()).ToArray() ?? + Array.Empty(); + } + + public static void AddUser(string userName, string picture) + { + SwitchDevice?.AccountManager.AddUser(userName, Convert.FromBase64String(picture)); + } + + public static void DeleteUser(string userId) + { + var uid = new UserId(userId); + SwitchDevice?.AccountManager.DeleteUser(uid); + } + + public static void OpenUser(string userId) + { + var uid = new UserId(userId); + SwitchDevice?.AccountManager.OpenUser(uid); + } + + public static void CloseUser(string userId) + { + var uid = new UserId(userId); + SwitchDevice?.AccountManager.CloseUser(uid); + } + } +} diff --git a/src/LibRyujinx/LibRyujinx.cs b/src/LibRyujinx/LibRyujinx.cs index 359b4e395..52318260f 100644 --- a/src/LibRyujinx/LibRyujinx.cs +++ b/src/LibRyujinx/LibRyujinx.cs @@ -724,6 +724,12 @@ namespace LibRyujinx return true; } + internal void ReloadFileSystem() + { + VirtualFileSystem.ReloadKeySet(); + ContentManager = new ContentManager(VirtualFileSystem); + } + internal void DisposeContext() { EmulationContext?.Dispose(); 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 b308785f7..88b266fed 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,4 +53,14 @@ class RyujinxNative { external fun deviceSignalEmulationClose() external fun deviceGetDlcTitleId(path: String, ncaPath: String) : String external fun deviceGetDlcContentList(path: String, titleId: Long) : Array + external fun userGetOpenedUser() : String + external fun userGetUserPicture(userId: String) : String + external fun userSetUserPicture(userId: String, picture: String) + external fun userGetUserName(userId: String) : String + external fun userSetUserName(userId: String, userName: String) + external fun userGetAllUsers() : Array + external fun userAddUser(username: String, picture: String) + external fun userDeleteUser(userId: String) + external fun userOpenUser(userId: String) + external fun userCloseUser(userId: String) } 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 a4ca7d1b9..8646432b4 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 @@ -1,7 +1,9 @@ package org.ryujinx.android.views import android.content.res.Resources +import android.graphics.BitmapFactory import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -11,15 +13,16 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Menu -import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.AlertDialog @@ -47,7 +50,10 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview @@ -57,9 +63,11 @@ import coil.compose.AsyncImage import com.anggrayudi.storage.extension.launchOnUiThread import org.ryujinx.android.MainActivity import org.ryujinx.android.R +import org.ryujinx.android.RyujinxNative import org.ryujinx.android.viewmodels.GameModel import org.ryujinx.android.viewmodels.HomeViewModel import java.io.File +import java.util.Base64 import java.util.Locale import kotlin.concurrent.thread import kotlin.math.roundToInt @@ -84,6 +92,10 @@ class HomeViews { val refresh = remember { mutableStateOf(true) } + val native = RyujinxNative() + val user = native.userGetOpenedUser() + val decoder = Base64.getDecoder() + val pic = decoder.decode(native.userGetUserPicture(user)) Scaffold( modifier = Modifier.fillMaxSize(), topBar = { @@ -115,10 +127,16 @@ class HomeViews { }, actions = { IconButton(onClick = { + navController?.navigate("user") }) { - Icon( - Icons.Filled.Person, - contentDescription = "Run" + Image( + bitmap = BitmapFactory.decodeByteArray(pic, 0, pic.size).asImageBitmap(), + contentDescription = "user image", + contentScale = ContentScale.Crop, + modifier = Modifier + .padding(4.dp) + .size(52.dp) + .clip(CircleShape) ) } IconButton( diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt index 7c5c988e1..48df07647 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt @@ -16,6 +16,7 @@ class MainView { NavHost(navController = navController, startDestination = "home") { composable("home") { HomeViews.Home(mainViewModel.homeViewModel, navController) } + composable("user") { UserViews.Main(mainViewModel, navController) } composable("settings") { SettingViews.Main( SettingsViewModel( 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 new file mode 100644 index 000000000..2b54a7738 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/UserViews.kt @@ -0,0 +1,182 @@ +package org.ryujinx.android.views + +import android.graphics.BitmapFactory +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.asImageBitmap +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.RyujinxNative +import org.ryujinx.android.viewmodels.MainViewModel +import java.util.Base64 + +class UserViews { + companion object { + @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) + @Composable + fun Main(viewModel: MainViewModel? = null, navController: NavHostController? = null) { + val ryujinxNative = RyujinxNative() + val decoder = Base64.getDecoder() + val openedUser = remember { + mutableStateOf(ryujinxNative.userGetOpenedUser()) + } + + val openedUserPic = remember { + mutableStateOf(decoder.decode(ryujinxNative.userGetUserPicture(openedUser.value))) + } + val openedUserName = remember { + mutableStateOf(ryujinxNative.userGetUserName(openedUser.value)) + } + + val userList = remember { + mutableListOf("") + } + + fun refresh() { + userList.clear() + userList.addAll(ryujinxNative.userGetAllUsers()) + } + + refresh() + + Scaffold(modifier = Modifier.fillMaxSize(), + topBar = { + TopAppBar(title = { + Text(text = "Users") + }, + navigationIcon = { + IconButton(onClick = { + viewModel?.navController?.popBackStack() + }) { + Icon(Icons.Filled.ArrowBack, contentDescription = "Back") + } + }) + }) { contentPadding -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(contentPadding) + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text(text = "Selected user") + Row( + modifier = Modifier + .fillMaxWidth() + .padding(4.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Image( + bitmap = BitmapFactory.decodeByteArray( + openedUserPic.value, + 0, + openedUserPic.value.size + ).asImageBitmap(), + contentDescription = "selected image", + contentScale = ContentScale.Crop, + modifier = Modifier + .padding(4.dp) + .size(96.dp) + .clip(CircleShape) + ) + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text(text = openedUserName.value) + Text(text = openedUser.value) + } + } + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(text = "Available Users") + IconButton(onClick = { + refresh() + }) { + Icon( + imageVector = Icons.Filled.Refresh, + contentDescription = "refresh users" + ) + } + } + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 96.dp), + modifier = Modifier + .fillMaxSize() + .padding(4.dp) + ) { + items(userList) { user -> + val pic = decoder.decode(ryujinxNative.userGetUserPicture(user)) + val name = ryujinxNative.userGetUserName(user) + Image( + bitmap = BitmapFactory.decodeByteArray(pic, 0, pic.size) + .asImageBitmap(), + contentDescription = "selected image", + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxSize() + .padding(4.dp) + .clip(CircleShape) + .align(Alignment.CenterHorizontally) + .combinedClickable( + onClick = { + ryujinxNative.userOpenUser(user) + openedUser.value = user + openedUserPic.value = pic + openedUserName.value = name + }) + ) + } + } + } + + } + } + } + + } + + @Preview + @Composable + fun Preview() { + UserViews.Main() + } +}