android - add basic user management

This commit is contained in:
Emmanuel Hansen 2023-10-22 21:30:05 +00:00
parent 8e2eb5fd26
commit ba745514a1
8 changed files with 399 additions and 4 deletions

View File

@ -18,6 +18,7 @@ using Silk.NET.Vulkan.Extensions.KHR;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Numerics; using System.Numerics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Security.Cryptography; using System.Security.Cryptography;
@ -95,6 +96,12 @@ namespace LibRyujinx
return s; return s;
} }
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceReloadFilesystem")]
public static void JniReloadFileSystem()
{
SwitchDevice?.ReloadFileSystem();
}
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceInitialize")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceInitialize")]
public static JBoolean JniInitializeDeviceNative(JEnvRef jEnv, public static JBoolean JniInitializeDeviceNative(JEnvRef jEnv,
JObjectLocalRef jObj, JObjectLocalRef jObj,
@ -503,6 +510,89 @@ namespace LibRyujinx
return ConnectGamepad(index); 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) private static FileStream OpenFile(int descriptor)
{ {
var safeHandle = new SafeFileHandle(descriptor, false); var safeHandle = new SafeFileHandle(descriptor, false);

View File

@ -17,6 +17,12 @@ namespace LibRyujinx
return InitializeDevice(true, false, SystemLanguage.AmericanEnglish, RegionCode.USA, true, true, true, false, "UTC", false); 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, public static bool InitializeDevice(bool isHostMapped,
bool useNce, bool useNce,
SystemLanguage systemLanguage, SystemLanguage systemLanguage,

View File

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

View File

@ -724,6 +724,12 @@ namespace LibRyujinx
return true; return true;
} }
internal void ReloadFileSystem()
{
VirtualFileSystem.ReloadKeySet();
ContentManager = new ContentManager(VirtualFileSystem);
}
internal void DisposeContext() internal void DisposeContext()
{ {
EmulationContext?.Dispose(); EmulationContext?.Dispose();

View File

@ -53,4 +53,14 @@ class RyujinxNative {
external fun deviceSignalEmulationClose() external fun deviceSignalEmulationClose()
external fun deviceGetDlcTitleId(path: String, ncaPath: String) : String external fun deviceGetDlcTitleId(path: String, ncaPath: String) : String
external fun deviceGetDlcContentList(path: String, titleId: Long) : Array<String> external fun deviceGetDlcContentList(path: String, titleId: Long) : Array<String>
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<String>
external fun userAddUser(username: String, picture: String)
external fun userDeleteUser(userId: String)
external fun userOpenUser(userId: String)
external fun userCloseUser(userId: String)
} }

View File

@ -1,7 +1,9 @@
package org.ryujinx.android.views package org.ryujinx.android.views
import android.content.res.Resources import android.content.res.Resources
import android.graphics.BitmapFactory
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box 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.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Menu 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.Search
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
@ -47,7 +50,10 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color 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.res.painterResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@ -57,9 +63,11 @@ import coil.compose.AsyncImage
import com.anggrayudi.storage.extension.launchOnUiThread import com.anggrayudi.storage.extension.launchOnUiThread
import org.ryujinx.android.MainActivity import org.ryujinx.android.MainActivity
import org.ryujinx.android.R import org.ryujinx.android.R
import org.ryujinx.android.RyujinxNative
import org.ryujinx.android.viewmodels.GameModel import org.ryujinx.android.viewmodels.GameModel
import org.ryujinx.android.viewmodels.HomeViewModel import org.ryujinx.android.viewmodels.HomeViewModel
import java.io.File import java.io.File
import java.util.Base64
import java.util.Locale import java.util.Locale
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -84,6 +92,10 @@ class HomeViews {
val refresh = remember { val refresh = remember {
mutableStateOf(true) mutableStateOf(true)
} }
val native = RyujinxNative()
val user = native.userGetOpenedUser()
val decoder = Base64.getDecoder()
val pic = decoder.decode(native.userGetUserPicture(user))
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
topBar = { topBar = {
@ -115,10 +127,16 @@ class HomeViews {
}, },
actions = { actions = {
IconButton(onClick = { IconButton(onClick = {
navController?.navigate("user")
}) { }) {
Icon( Image(
Icons.Filled.Person, bitmap = BitmapFactory.decodeByteArray(pic, 0, pic.size).asImageBitmap(),
contentDescription = "Run" contentDescription = "user image",
contentScale = ContentScale.Crop,
modifier = Modifier
.padding(4.dp)
.size(52.dp)
.clip(CircleShape)
) )
} }
IconButton( IconButton(

View File

@ -16,6 +16,7 @@ class MainView {
NavHost(navController = navController, startDestination = "home") { NavHost(navController = navController, startDestination = "home") {
composable("home") { HomeViews.Home(mainViewModel.homeViewModel, navController) } composable("home") { HomeViews.Home(mainViewModel.homeViewModel, navController) }
composable("user") { UserViews.Main(mainViewModel, navController) }
composable("settings") { composable("settings") {
SettingViews.Main( SettingViews.Main(
SettingsViewModel( SettingsViewModel(

View File

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