android - implement firmware installation

This commit is contained in:
Emmanuel Hansen 2023-12-17 19:09:52 +00:00
parent 60f320bc07
commit aa87b4abd8
9 changed files with 669 additions and 283 deletions

View File

@ -250,6 +250,59 @@ namespace LibRyujinx
return LoadApplication(stream, (FileType)(int)type);
}
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceVerifyFirmware")]
public static JLong JniVerifyFirmware(JEnvRef jEnv, JObjectLocalRef jObj, JInt descriptor, JBoolean isXci)
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
var stream = OpenFile(descriptor);
long stringHandle = -1;
try
{
var version = VerifyFirmware(stream, isXci);
if (version != null)
{
stringHandle = storeString(version.VersionString);
}
}
catch(Exception _)
{
}
return stringHandle;
}
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceInstallFirmware")]
public static void JniInstallFirmware(JEnvRef jEnv, JObjectLocalRef jObj, JInt descriptor, JBoolean isXci)
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
var stream = OpenFile(descriptor);
InstallFirmware(stream, isXci);
}
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetInstalledFirmwareVersion")]
public static JLong JniGetInstalledFirmwareVersion(JEnvRef jEnv, JObjectLocalRef jObj)
{
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
var version = SwitchDevice?.ContentManager.GetCurrentFirmwareVersion();
long stringHandle = -1;
if (version != null)
{
stringHandle = storeString(version.VersionString);
}
return stringHandle;
}
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsInitialize")]
public static JBoolean JniInitializeGraphicsNative(JEnvRef jEnv, JObjectLocalRef jObj, JObjectLocalRef graphicObject)
{

View File

@ -2,6 +2,7 @@ using ARMeilleure.Translation;
using LibHac.Ncm;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS.SystemState;
using Ryujinx.Input.HLE;
using Silk.NET.Vulkan;
@ -66,6 +67,16 @@ namespace LibRyujinx
return LoadApplication(path);
}
public static void InstallFirmware(Stream stream, bool isXci)
{
SwitchDevice?.ContentManager.InstallFirmware(stream, isXci);
}
public static SystemVersion? VerifyFirmware(Stream stream, bool isXci)
{
return SwitchDevice?.ContentManager?.VerifyFirmwarePackage(stream, isXci) ?? null;
}
public static bool LoadApplication(Stream stream, FileType type)
{
var emulationContext = SwitchDevice.EmulationContext;

View File

@ -525,6 +525,27 @@ namespace Ryujinx.HLE.FileSystem
FinishInstallation(temporaryDirectory, registeredDirectory);
}
public void InstallFirmware(Stream stream, bool isXci)
{
string contentPathString = ContentPath.GetContentPath(StorageId.BuiltInSystem);
string contentDirectory = ContentPath.GetRealPath(contentPathString);
string registeredDirectory = Path.Combine(contentDirectory, "registered");
string temporaryDirectory = Path.Combine(contentDirectory, "temp");
if (!isXci)
{
using ZipArchive archive = new ZipArchive(stream);
InstallFromZip(archive, temporaryDirectory);
}
else
{
Xci xci = new(_virtualFileSystem.KeySet, stream.AsStorage());
InstallFromCart(xci, temporaryDirectory);
}
FinishInstallation(temporaryDirectory, registeredDirectory);
}
private void FinishInstallation(string temporaryDirectory, string registeredDirectory)
{
if (Directory.Exists(registeredDirectory))
@ -643,13 +664,16 @@ namespace Ryujinx.HLE.FileSystem
throw new MissingKeyException("HeaderKey is empty. Cannot decrypt NCA headers.");
}
Dictionary<ulong, List<(NcaContentType type, string path)>> updateNcas = new();
if (Directory.Exists(firmwarePackage))
{
return VerifyAndGetVersionDirectory(firmwarePackage);
}
SystemVersion VerifyAndGetVersionDirectory(string firmwareDirectory)
{
return VerifyAndGetVersion(new LocalFileSystem(firmwareDirectory));
}
if (!File.Exists(firmwarePackage))
{
throw new FileNotFoundException("Firmware file does not exist.");
@ -657,249 +681,99 @@ namespace Ryujinx.HLE.FileSystem
FileInfo info = new(firmwarePackage);
using FileStream file = File.OpenRead(firmwarePackage);
switch (info.Extension)
if (info.Extension == ".zip" || info.Extension == ".xci")
{
case ".zip":
using (ZipArchive archive = ZipFile.OpenRead(firmwarePackage))
{
return VerifyAndGetVersionZip(archive);
}
case ".xci":
Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage());
using FileStream file = File.OpenRead(firmwarePackage);
if (xci.HasPartition(XciPartitionType.Update))
{
XciPartition partition = xci.OpenPartition(XciPartitionType.Update);
var isXci = info.Extension == ".xci";
return VerifyAndGetVersion(partition);
}
else
{
throw new InvalidFirmwarePackageException("Update not found in xci file.");
}
default:
break;
return VerifyFirmwarePackage(file, isXci);
}
SystemVersion VerifyAndGetVersionDirectory(string firmwareDirectory)
return null;
}
public SystemVersion VerifyFirmwarePackage(Stream file, bool isXci)
{
if (!isXci)
{
return VerifyAndGetVersion(new LocalFileSystem(firmwareDirectory));
using ZipArchive archive = new ZipArchive(file, ZipArchiveMode.Read);
return VerifyAndGetVersionZip(archive);
}
SystemVersion VerifyAndGetVersionZip(ZipArchive archive)
else
{
SystemVersion systemVersion = null;
Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage());
foreach (var entry in archive.Entries)
if (xci.HasPartition(XciPartitionType.Update))
{
if (entry.FullName.EndsWith(".nca") || entry.FullName.EndsWith(".nca/00"))
{
using Stream ncaStream = GetZipStream(entry);
IStorage storage = ncaStream.AsStorage();
XciPartition partition = xci.OpenPartition(XciPartitionType.Update);
Nca nca = new(_virtualFileSystem.KeySet, storage);
if (updateNcas.TryGetValue(nca.Header.TitleId, out var updateNcasItem))
{
updateNcasItem.Add((nca.Header.ContentType, entry.FullName));
}
else
{
updateNcas.Add(nca.Header.TitleId, new List<(NcaContentType, string)>());
updateNcas[nca.Header.TitleId].Add((nca.Header.ContentType, entry.FullName));
}
}
}
if (updateNcas.TryGetValue(SystemUpdateTitleId, out var ncaEntry))
{
string metaPath = ncaEntry.Find(x => x.type == NcaContentType.Meta).path;
CnmtContentMetaEntry[] metaEntries = null;
var fileEntry = archive.GetEntry(metaPath);
using (Stream ncaStream = GetZipStream(fileEntry))
{
Nca metaNca = new(_virtualFileSystem.KeySet, ncaStream.AsStorage());
IFileSystem fs = metaNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath;
using var metaFile = new UniqueRef<IFile>();
if (fs.OpenFile(ref metaFile.Ref, cnmtPath.ToU8Span(), OpenMode.Read).IsSuccess())
{
var meta = new Cnmt(metaFile.Get.AsStream());
if (meta.Type == ContentMetaType.SystemUpdate)
{
metaEntries = meta.MetaEntries;
updateNcas.Remove(SystemUpdateTitleId);
}
}
}
if (metaEntries == null)
{
throw new FileNotFoundException("System update title was not found in the firmware package.");
}
if (updateNcas.TryGetValue(SystemVersionTitleId, out var updateNcasItem))
{
string versionEntry = updateNcasItem.Find(x => x.type != NcaContentType.Meta).path;
using Stream ncaStream = GetZipStream(archive.GetEntry(versionEntry));
Nca nca = new(_virtualFileSystem.KeySet, ncaStream.AsStorage());
var romfs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
using var systemVersionFile = new UniqueRef<IFile>();
if (romfs.OpenFile(ref systemVersionFile.Ref, "/file".ToU8Span(), OpenMode.Read).IsSuccess())
{
systemVersion = new SystemVersion(systemVersionFile.Get.AsStream());
}
}
foreach (CnmtContentMetaEntry metaEntry in metaEntries)
{
if (updateNcas.TryGetValue(metaEntry.TitleId, out ncaEntry))
{
metaPath = ncaEntry.Find(x => x.type == NcaContentType.Meta).path;
string contentPath = ncaEntry.Find(x => x.type != NcaContentType.Meta).path;
// Nintendo in 9.0.0, removed PPC and only kept the meta nca of it.
// This is a perfect valid case, so we should just ignore the missing content nca and continue.
if (contentPath == null)
{
updateNcas.Remove(metaEntry.TitleId);
continue;
}
ZipArchiveEntry metaZipEntry = archive.GetEntry(metaPath);
ZipArchiveEntry contentZipEntry = archive.GetEntry(contentPath);
using Stream metaNcaStream = GetZipStream(metaZipEntry);
using Stream contentNcaStream = GetZipStream(contentZipEntry);
Nca metaNca = new(_virtualFileSystem.KeySet, metaNcaStream.AsStorage());
IFileSystem fs = metaNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath;
using var metaFile = new UniqueRef<IFile>();
if (fs.OpenFile(ref metaFile.Ref, cnmtPath.ToU8Span(), OpenMode.Read).IsSuccess())
{
var meta = new Cnmt(metaFile.Get.AsStream());
IStorage contentStorage = contentNcaStream.AsStorage();
if (contentStorage.GetSize(out long size).IsSuccess())
{
byte[] contentData = new byte[size];
Span<byte> content = new(contentData);
contentStorage.Read(0, content);
Span<byte> hash = new(new byte[32]);
LibHac.Crypto.Sha256.GenerateSha256Hash(content, hash);
if (LibHac.Common.Utilities.ArraysEqual(hash.ToArray(), meta.ContentEntries[0].Hash))
{
updateNcas.Remove(metaEntry.TitleId);
}
}
}
}
}
if (updateNcas.Count > 0)
{
StringBuilder extraNcas = new();
foreach (var entry in updateNcas)
{
foreach (var (type, path) in entry.Value)
{
extraNcas.AppendLine(path);
}
}
throw new InvalidFirmwarePackageException($"Firmware package contains unrelated archives. Please remove these paths: {Environment.NewLine}{extraNcas}");
}
return VerifyAndGetVersion(partition);
}
else
{
throw new FileNotFoundException("System update title was not found in the firmware package.");
throw new InvalidFirmwarePackageException("Update not found in xci file.");
}
return systemVersion;
}
}
SystemVersion VerifyAndGetVersion(IFileSystem filesystem)
private SystemVersion VerifyAndGetVersionZip(ZipArchive archive)
{
Dictionary<ulong, List<(NcaContentType type, string path)>> updateNcas = new();
SystemVersion systemVersion = null;
foreach (var entry in archive.Entries)
{
SystemVersion systemVersion = null;
CnmtContentMetaEntry[] metaEntries = null;
foreach (var entry in filesystem.EnumerateEntries("/", "*.nca"))
if (entry.FullName.EndsWith(".nca") || entry.FullName.EndsWith(".nca/00"))
{
IStorage ncaStorage = OpenPossibleFragmentedFile(filesystem, entry.FullPath, OpenMode.Read).AsStorage();
using Stream ncaStream = GetZipStream(entry);
IStorage storage = ncaStream.AsStorage();
Nca nca = new(_virtualFileSystem.KeySet, ncaStorage);
if (nca.Header.TitleId == SystemUpdateTitleId && nca.Header.ContentType == NcaContentType.Meta)
{
IFileSystem fs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath;
using var metaFile = new UniqueRef<IFile>();
if (fs.OpenFile(ref metaFile.Ref, cnmtPath.ToU8Span(), OpenMode.Read).IsSuccess())
{
var meta = new Cnmt(metaFile.Get.AsStream());
if (meta.Type == ContentMetaType.SystemUpdate)
{
metaEntries = meta.MetaEntries;
}
}
continue;
}
else if (nca.Header.TitleId == SystemVersionTitleId && nca.Header.ContentType == NcaContentType.Data)
{
var romfs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
using var systemVersionFile = new UniqueRef<IFile>();
if (romfs.OpenFile(ref systemVersionFile.Ref, "/file".ToU8Span(), OpenMode.Read).IsSuccess())
{
systemVersion = new SystemVersion(systemVersionFile.Get.AsStream());
}
}
Nca nca = new(_virtualFileSystem.KeySet, storage);
if (updateNcas.TryGetValue(nca.Header.TitleId, out var updateNcasItem))
{
updateNcasItem.Add((nca.Header.ContentType, entry.FullPath));
updateNcasItem.Add((nca.Header.ContentType, entry.FullName));
}
else
{
updateNcas.Add(nca.Header.TitleId, new List<(NcaContentType, string)>());
updateNcas[nca.Header.TitleId].Add((nca.Header.ContentType, entry.FullPath));
updateNcas[nca.Header.TitleId].Add((nca.Header.ContentType, entry.FullName));
}
}
}
ncaStorage.Dispose();
if (updateNcas.TryGetValue(SystemUpdateTitleId, out var ncaEntry))
{
string metaPath = ncaEntry.Find(x => x.type == NcaContentType.Meta).path;
CnmtContentMetaEntry[] metaEntries = null;
var fileEntry = archive.GetEntry(metaPath);
using (Stream ncaStream = GetZipStream(fileEntry))
{
Nca metaNca = new(_virtualFileSystem.KeySet, ncaStream.AsStorage());
IFileSystem fs = metaNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath;
using var metaFile = new UniqueRef<IFile>();
if (fs.OpenFile(ref metaFile.Ref, cnmtPath.ToU8Span(), OpenMode.Read).IsSuccess())
{
var meta = new Cnmt(metaFile.Get.AsStream());
if (meta.Type == ContentMetaType.SystemUpdate)
{
metaEntries = meta.MetaEntries;
updateNcas.Remove(SystemUpdateTitleId);
}
}
}
if (metaEntries == null)
@ -907,11 +781,29 @@ namespace Ryujinx.HLE.FileSystem
throw new FileNotFoundException("System update title was not found in the firmware package.");
}
if (updateNcas.TryGetValue(SystemVersionTitleId, out var updateNcasItem))
{
string versionEntry = updateNcasItem.Find(x => x.type != NcaContentType.Meta).path;
using Stream ncaStream = GetZipStream(archive.GetEntry(versionEntry));
Nca nca = new(_virtualFileSystem.KeySet, ncaStream.AsStorage());
var romfs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
using var systemVersionFile = new UniqueRef<IFile>();
if (romfs.OpenFile(ref systemVersionFile.Ref, "/file".ToU8Span(), OpenMode.Read).IsSuccess())
{
systemVersion = new SystemVersion(systemVersionFile.Get.AsStream());
}
}
foreach (CnmtContentMetaEntry metaEntry in metaEntries)
{
if (updateNcas.TryGetValue(metaEntry.TitleId, out var ncaEntry))
if (updateNcas.TryGetValue(metaEntry.TitleId, out ncaEntry))
{
string metaNcaPath = ncaEntry.Find(x => x.type == NcaContentType.Meta).path;
metaPath = ncaEntry.Find(x => x.type == NcaContentType.Meta).path;
string contentPath = ncaEntry.Find(x => x.type != NcaContentType.Meta).path;
// Nintendo in 9.0.0, removed PPC and only kept the meta nca of it.
@ -923,10 +815,12 @@ namespace Ryujinx.HLE.FileSystem
continue;
}
IStorage metaStorage = OpenPossibleFragmentedFile(filesystem, metaNcaPath, OpenMode.Read).AsStorage();
IStorage contentStorage = OpenPossibleFragmentedFile(filesystem, contentPath, OpenMode.Read).AsStorage();
ZipArchiveEntry metaZipEntry = archive.GetEntry(metaPath);
ZipArchiveEntry contentZipEntry = archive.GetEntry(contentPath);
Nca metaNca = new(_virtualFileSystem.KeySet, metaStorage);
using Stream metaNcaStream = GetZipStream(metaZipEntry);
using Stream contentNcaStream = GetZipStream(contentZipEntry);
Nca metaNca = new(_virtualFileSystem.KeySet, metaNcaStream.AsStorage());
IFileSystem fs = metaNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
@ -938,6 +832,7 @@ namespace Ryujinx.HLE.FileSystem
{
var meta = new Cnmt(metaFile.Get.AsStream());
IStorage contentStorage = contentNcaStream.AsStorage();
if (contentStorage.GetSize(out long size).IsSuccess())
{
byte[] contentData = new byte[size];
@ -973,11 +868,147 @@ namespace Ryujinx.HLE.FileSystem
throw new InvalidFirmwarePackageException($"Firmware package contains unrelated archives. Please remove these paths: {Environment.NewLine}{extraNcas}");
}
return systemVersion;
}
else
{
throw new FileNotFoundException("System update title was not found in the firmware package.");
}
return null;
return systemVersion;
}
private SystemVersion VerifyAndGetVersion(IFileSystem filesystem)
{
Dictionary<ulong, List<(NcaContentType type, string path)>> updateNcas = new();
SystemVersion systemVersion = null;
CnmtContentMetaEntry[] metaEntries = null;
foreach (var entry in filesystem.EnumerateEntries("/", "*.nca"))
{
IStorage ncaStorage = OpenPossibleFragmentedFile(filesystem, entry.FullPath, OpenMode.Read).AsStorage();
Nca nca = new(_virtualFileSystem.KeySet, ncaStorage);
if (nca.Header.TitleId == SystemUpdateTitleId && nca.Header.ContentType == NcaContentType.Meta)
{
IFileSystem fs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath;
using var metaFile = new UniqueRef<IFile>();
if (fs.OpenFile(ref metaFile.Ref, cnmtPath.ToU8Span(), OpenMode.Read).IsSuccess())
{
var meta = new Cnmt(metaFile.Get.AsStream());
if (meta.Type == ContentMetaType.SystemUpdate)
{
metaEntries = meta.MetaEntries;
}
}
continue;
}
else if (nca.Header.TitleId == SystemVersionTitleId && nca.Header.ContentType == NcaContentType.Data)
{
var romfs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
using var systemVersionFile = new UniqueRef<IFile>();
if (romfs.OpenFile(ref systemVersionFile.Ref, "/file".ToU8Span(), OpenMode.Read).IsSuccess())
{
systemVersion = new SystemVersion(systemVersionFile.Get.AsStream());
}
}
if (updateNcas.TryGetValue(nca.Header.TitleId, out var updateNcasItem))
{
updateNcasItem.Add((nca.Header.ContentType, entry.FullPath));
}
else
{
updateNcas.Add(nca.Header.TitleId, new List<(NcaContentType, string)>());
updateNcas[nca.Header.TitleId].Add((nca.Header.ContentType, entry.FullPath));
}
ncaStorage.Dispose();
}
if (metaEntries == null)
{
throw new FileNotFoundException("System update title was not found in the firmware package.");
}
foreach (CnmtContentMetaEntry metaEntry in metaEntries)
{
if (updateNcas.TryGetValue(metaEntry.TitleId, out var ncaEntry))
{
string metaNcaPath = ncaEntry.Find(x => x.type == NcaContentType.Meta).path;
string contentPath = ncaEntry.Find(x => x.type != NcaContentType.Meta).path;
// Nintendo in 9.0.0, removed PPC and only kept the meta nca of it.
// This is a perfect valid case, so we should just ignore the missing content nca and continue.
if (contentPath == null)
{
updateNcas.Remove(metaEntry.TitleId);
continue;
}
IStorage metaStorage = OpenPossibleFragmentedFile(filesystem, metaNcaPath, OpenMode.Read).AsStorage();
IStorage contentStorage = OpenPossibleFragmentedFile(filesystem, contentPath, OpenMode.Read).AsStorage();
Nca metaNca = new(_virtualFileSystem.KeySet, metaStorage);
IFileSystem fs = metaNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath;
using var metaFile = new UniqueRef<IFile>();
if (fs.OpenFile(ref metaFile.Ref, cnmtPath.ToU8Span(), OpenMode.Read).IsSuccess())
{
var meta = new Cnmt(metaFile.Get.AsStream());
if (contentStorage.GetSize(out long size).IsSuccess())
{
byte[] contentData = new byte[size];
Span<byte> content = new(contentData);
contentStorage.Read(0, content);
Span<byte> hash = new(new byte[32]);
LibHac.Crypto.Sha256.GenerateSha256Hash(content, hash);
if (LibHac.Common.Utilities.ArraysEqual(hash.ToArray(), meta.ContentEntries[0].Hash))
{
updateNcas.Remove(metaEntry.TitleId);
}
}
}
}
}
if (updateNcas.Count > 0)
{
StringBuilder extraNcas = new();
foreach (var entry in updateNcas)
{
foreach (var (type, path) in entry.Value)
{
extraNcas.AppendLine(path);
}
}
throw new InvalidFirmwarePackageException($"Firmware package contains unrelated archives. Please remove these paths: {Environment.NewLine}{extraNcas}");
}
return systemVersion;
}
public SystemVersion GetCurrentFirmwareVersion()

View File

@ -11,8 +11,8 @@ android {
applicationId "org.ryujinx.android"
minSdk 30
targetSdk 33
versionCode 10008
versionName '1.0.8'
versionCode 10010
versionName '1.0.10'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@ -49,6 +49,7 @@ android {
buildFeatures {
compose true
prefab true
buildConfig true
}
composeOptions {
kotlinCompilerExtensionVersion '1.3.2'

View File

@ -102,6 +102,8 @@ class MainActivity : BaseActivity() {
mainViewModel!!.physicalControllerManager = physicalControllerManager
mainViewModel!!.motionSensorManager = motionSensorManager
mainViewModel!!.refreshFirmwareVersion()
mainViewModel?.apply {
setContent {
RyujinxAndroidTheme {

View File

@ -8,20 +8,24 @@ class RyujinxNative {
companion object {
val instance: RyujinxNative = RyujinxNative()
init {
System.loadLibrary("ryujinx")
}
}
external fun deviceInitialize(isHostMapped: Boolean, useNce: Boolean,
systemLanguage : Int,
regionCode : Int,
enableVsync : Boolean,
enableDockedMode : Boolean,
enablePtc : Boolean,
enableInternetAccess : Boolean,
timeZone : Long,
ignoreMissingServices : Boolean): Boolean
external fun deviceInitialize(
isHostMapped: Boolean, useNce: Boolean,
systemLanguage: Int,
regionCode: Int,
enableVsync: Boolean,
enableDockedMode: Boolean,
enablePtc: Boolean,
enableInternetAccess: Boolean,
timeZone: Long,
ignoreMissingServices: Boolean
): Boolean
external fun graphicsInitialize(configuration: GraphicsConfiguration): Boolean
external fun graphicsInitializeRenderer(
extensions: Array<String>,
@ -35,7 +39,7 @@ class RyujinxNative {
external fun deviceGetGameFifo(): Double
external fun deviceGetGameInfo(fileDescriptor: Int, extension: Long): GameInfo
external fun deviceGetGameInfoFromPath(path: String): GameInfo
external fun deviceLoadDescriptor(fileDescriptor: Int, gameType: Int): Boolean
external fun deviceLoadDescriptor(fileDescriptor: Int, gameType: Int): Boolean
external fun graphicsRendererSetSize(width: Int, height: Int)
external fun graphicsRendererSetVsync(enabled: Boolean)
external fun graphicsRendererRunLoop()
@ -54,17 +58,20 @@ class RyujinxNative {
external fun graphicsSetSurface(surface: Long, window: Long)
external fun deviceCloseEmulation()
external fun deviceSignalEmulationClose()
external fun deviceGetDlcTitleId(path: Long, ncaPath: Long) : Long
external fun deviceGetDlcContentList(path: Long, titleId: Long) : Array<String>
external fun userGetOpenedUser() : Long
external fun userGetUserPicture(userId: Long) : Long
external fun deviceGetDlcTitleId(path: Long, ncaPath: Long): Long
external fun deviceGetDlcContentList(path: Long, titleId: Long): Array<String>
external fun userGetOpenedUser(): Long
external fun userGetUserPicture(userId: Long): Long
external fun userSetUserPicture(userId: String, picture: String)
external fun userGetUserName(userId: Long) : Long
external fun userGetUserName(userId: Long): Long
external fun userSetUserName(userId: String, userName: String)
external fun userGetAllUsers() : Array<String>
external fun userGetAllUsers(): Array<String>
external fun userAddUser(username: String, picture: String)
external fun userDeleteUser(userId: String)
external fun userOpenUser(userId: Long)
external fun userCloseUser(userId: String)
external fun loggingSetEnabled(logLevel: Int, enabled: Boolean)
external fun deviceVerifyFirmware(fileDescriptor: Int, isXci: Boolean): Long
external fun deviceInstallFirmware(fileDescriptor: Int, isXci: Boolean)
external fun deviceGetInstalledFirmwareVersion() : Long
}

View File

@ -35,6 +35,7 @@ class MainViewModel(val activity: MainActivity) {
var isMiiEditorLaunched = false
val userViewModel = UserViewModel()
val logging = Logging(this)
var firmwareVersion = ""
private var gameTimeState: MutableState<Double>? = null
private var gameFpsState: MutableState<Double>? = null
private var fifoState: MutableState<Double>? = null
@ -69,6 +70,13 @@ class MainViewModel(val activity: MainActivity) {
motionSensorManager?.setControllerId(-1)
}
fun refreshFirmwareVersion(){
var handle = RyujinxNative.instance.deviceGetInstalledFirmwareVersion()
if(handle != -1L) {
firmwareVersion = NativeHelpers.instance.getStringJava(handle)
}
}
fun loadGame(game:GameModel) : Boolean {
val nativeRyujinx = RyujinxNative.instance
@ -178,8 +186,6 @@ class MainViewModel(val activity: MainActivity) {
return true
}
fun loadMiiEditor() : Boolean {
val nativeRyujinx = RyujinxNative.instance

View File

@ -5,30 +5,40 @@ import androidx.compose.runtime.MutableState
import androidx.documentfile.provider.DocumentFile
import androidx.navigation.NavHostController
import androidx.preference.PreferenceManager
import com.anggrayudi.storage.callback.FileCallback
import com.anggrayudi.storage.file.FileFullPath
import com.anggrayudi.storage.file.copyFileTo
import com.anggrayudi.storage.file.extension
import com.anggrayudi.storage.file.getAbsolutePath
import org.ryujinx.android.LogLevel
import org.ryujinx.android.MainActivity
import org.ryujinx.android.NativeHelpers
import org.ryujinx.android.RyujinxNative
import java.io.File
import kotlin.concurrent.thread
class SettingsViewModel(var navController: NavHostController, val activity: MainActivity) {
private var previousCallback: ((requestCode: Int, folder: DocumentFile) -> Unit)?
var selectedFirmwareVersion: String = ""
private var previousFileCallback: ((requestCode: Int, files: List<DocumentFile>) -> Unit)?
private var previousFolderCallback: ((requestCode: Int, folder: DocumentFile) -> Unit)?
private var sharedPref: SharedPreferences
var selectedFirmwareFile: DocumentFile? = null
init {
sharedPref = getPreferences()
previousCallback = activity.storageHelper!!.onFolderSelected
previousFolderCallback = activity.storageHelper!!.onFolderSelected
previousFileCallback = activity.storageHelper!!.onFileSelected
activity.storageHelper!!.onFolderSelected = { requestCode, folder ->
run {
val p = folder.getAbsolutePath(activity!!)
val editor = sharedPref?.edit()
val p = folder.getAbsolutePath(activity)
val editor = sharedPref.edit()
editor?.putString("gameFolder", p)
editor?.apply()
}
}
}
private fun getPreferences() : SharedPreferences {
private fun getPreferences(): SharedPreferences {
return PreferenceManager.getDefaultSharedPreferences(activity)
}
@ -52,8 +62,7 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
enableGuestLogs: MutableState<Boolean>,
enableAccessLogs: MutableState<Boolean>,
enableTraceLogs: MutableState<Boolean>
)
{
) {
isHostMapped.value = sharedPref.getBoolean("isHostMapped", true)
useNce.value = sharedPref.getBoolean("useNce", true)
@ -62,7 +71,8 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
enablePtc.value = sharedPref.getBoolean("enablePtc", true)
ignoreMissingServices.value = sharedPref.getBoolean("ignoreMissingServices", false)
enableShaderCache.value = sharedPref.getBoolean("enableShaderCache", true)
enableTextureRecompression.value = sharedPref.getBoolean("enableTextureRecompression", false)
enableTextureRecompression.value =
sharedPref.getBoolean("enableTextureRecompression", false)
resScale.value = sharedPref.getFloat("resScale", 1f)
useVirtualController.value = sharedPref.getBoolean("useVirtualController", true)
isGrid.value = sharedPref.getBoolean("isGrid", true)
@ -97,7 +107,7 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
enableGuestLogs: MutableState<Boolean>,
enableAccessLogs: MutableState<Boolean>,
enableTraceLogs: MutableState<Boolean>
){
) {
val editor = sharedPref.edit()
editor.putBoolean("isHostMapped", isHostMapped.value)
@ -123,7 +133,7 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
editor.putBoolean("enableTraceLogs", enableTraceLogs.value)
editor.apply()
activity.storageHelper!!.onFolderSelected = previousCallback
activity.storageHelper!!.onFolderSelected = previousFolderCallback
RyujinxNative.instance.loggingSetEnabled(LogLevel.Debug.ordinal, enableDebugLogs.value)
RyujinxNative.instance.loggingSetEnabled(LogLevel.Info.ordinal, enableInfoLogs.value)
@ -135,17 +145,122 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
RyujinxNative.instance.loggingSetEnabled(LogLevel.Trace.ordinal, enableTraceLogs.value)
}
fun openGameFolder() {
val path = sharedPref?.getString("gameFolder", "") ?: ""
if (path.isEmpty())
activity?.storageHelper?.storage?.openFolderPicker()
activity.storageHelper?.storage?.openFolderPicker()
else
activity?.storageHelper?.storage?.openFolderPicker(
activity.storageHelper?.storage?.openFolderPicker(
activity.storageHelper!!.storage.requestCodeFolderPicker,
FileFullPath(activity, path)
)
}
fun importProdKeys() {
activity.storageHelper!!.onFileSelected = { requestCode, files ->
run {
activity.storageHelper!!.onFileSelected = previousFileCallback
val file = files.firstOrNull()
file?.apply {
if (name == "prod.keys") {
val outputFile = File(MainActivity.AppPath + "/system");
outputFile.delete()
thread {
file.copyFileTo(
activity,
outputFile,
callback = object : FileCallback() {
override fun onCompleted(result: Any) {
super.onCompleted(result)
}
})
}
}
}
}
}
activity.storageHelper?.storage?.openFilePicker()
}
fun selectFirmware(installState: MutableState<FirmwareInstallState>) {
if (installState.value != FirmwareInstallState.None)
return
activity.storageHelper!!.onFileSelected = { _, files ->
run {
activity.storageHelper!!.onFileSelected = previousFileCallback
val file = files.firstOrNull()
file?.apply {
if (extension == "xci" || extension == "zip") {
installState.value = FirmwareInstallState.Verifying
thread {
val descriptor =
activity.contentResolver.openFileDescriptor(file.uri, "rw")
descriptor?.use { d ->
val version = RyujinxNative.instance.deviceVerifyFirmware(
d.fd,
extension == "xci"
)
selectedFirmwareFile = file
if (version != -1L) {
selectedFirmwareVersion =
NativeHelpers.instance.getStringJava(version)
installState.value = FirmwareInstallState.Query
} else {
installState.value = FirmwareInstallState.Cancelled
}
}
}
} else {
installState.value = FirmwareInstallState.Cancelled
}
}
}
}
activity.storageHelper?.storage?.openFilePicker()
}
fun installFirmware(installState: MutableState<FirmwareInstallState>) {
if (installState.value != FirmwareInstallState.Query)
return
if (selectedFirmwareFile == null) {
installState.value = FirmwareInstallState.None
return
}
selectedFirmwareFile?.apply {
val descriptor =
activity.contentResolver.openFileDescriptor(uri, "rw")
descriptor?.use { d ->
installState.value = FirmwareInstallState.Install
thread {
try {
RyujinxNative.instance.deviceInstallFirmware(
d.fd,
extension == "xci"
)
} finally {
MainActivity.mainViewModel?.refreshFirmwareVersion()
installState.value = FirmwareInstallState.Done
}
}
}
}
}
fun clearFirmwareSelection(installState: MutableState<FirmwareInstallState>){
selectedFirmwareFile = null
selectedFirmwareVersion = ""
installState.value = FirmwareInstallState.None
}
}
enum class FirmwareInstallState{
None,
Cancelled,
Verifying,
Query,
Install,
Done
}

View File

@ -60,6 +60,7 @@ import com.anggrayudi.storage.file.extension
import org.ryujinx.android.Helpers
import org.ryujinx.android.MainActivity
import org.ryujinx.android.providers.DocumentProvider
import org.ryujinx.android.viewmodels.FirmwareInstallState
import org.ryujinx.android.viewmodels.MainViewModel
import org.ryujinx.android.viewmodels.SettingsViewModel
import org.ryujinx.android.viewmodels.VulkanDriverViewModel
@ -107,6 +108,15 @@ class SettingViews {
val useVirtualController = remember {
mutableStateOf(true)
}
val showFirwmareDialog = remember {
mutableStateOf(false)
}
val firmwareInstallState = remember {
mutableStateOf(FirmwareInstallState.None)
}
val firmwareVersion = remember {
mutableStateOf(mainViewModel.firmwareVersion)
}
val isGrid = remember { mutableStateOf(true) }
val enableDebugLogs = remember { mutableStateOf(true) }
@ -211,36 +221,186 @@ class SettingViews {
Text(text = "Choose Folder")
}
}
Button(onClick = {
fun createIntent(action: String) : Intent{
val intent = Intent(action)
intent.addCategory(Intent.CATEGORY_DEFAULT)
intent.data = DocumentsContract.buildRootUri(DocumentProvider.AUTHORITY, DocumentProvider.ROOT_ID)
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
return intent
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "System Firmware",
modifier = Modifier.align(Alignment.CenterVertically)
)
Text(
text = firmwareVersion.value,
modifier = Modifier.align(Alignment.CenterVertically)
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Button(onClick = {
fun createIntent(action: String): Intent {
val intent = Intent(action)
intent.addCategory(Intent.CATEGORY_DEFAULT)
intent.data = DocumentsContract.buildRootUri(
DocumentProvider.AUTHORITY,
DocumentProvider.ROOT_ID
)
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
return intent
}
try {
mainViewModel.activity.startActivity(createIntent(Intent.ACTION_VIEW))
return@Button
} catch (_: ActivityNotFoundException) {
}
try {
mainViewModel.activity.startActivity(createIntent("android.provider.action.BROWSE"))
return@Button
} catch (_: ActivityNotFoundException) {
}
try {
mainViewModel.activity.startActivity(createIntent("com.google.android.documentsui"))
return@Button
} catch (_: ActivityNotFoundException) {
}
try {
mainViewModel.activity.startActivity(createIntent("com.android.documentsui"))
return@Button
} catch (_: ActivityNotFoundException) {
}
}) {
Text(text = "Open App Folder")
}
try {
mainViewModel.activity.startActivity(createIntent(Intent.ACTION_VIEW))
return@Button
Button(onClick = {
settingsViewModel.importProdKeys()
}) {
Text(text = "Import prod Keys")
}
catch (_: ActivityNotFoundException){}
try {
mainViewModel.activity.startActivity(createIntent("android.provider.action.BROWSE"))
return@Button
Button(onClick = {
showFirwmareDialog.value = true
}) {
Text(text = "Install Firmware")
}
catch (_: ActivityNotFoundException){}
try {
mainViewModel.activity.startActivity(createIntent("com.google.android.documentsui"))
return@Button
}
}
}
if(showFirwmareDialog.value) {
AlertDialog(onDismissRequest = {
if(firmwareInstallState.value != FirmwareInstallState.Install) {
showFirwmareDialog.value = false
settingsViewModel.clearFirmwareSelection(firmwareInstallState)
}
}) {
Card(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
shape = MaterialTheme.shapes.medium
) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.align(Alignment.CenterHorizontally),
verticalArrangement = Arrangement.SpaceBetween
) {
if (firmwareInstallState.value == FirmwareInstallState.None) {
Text(text = "Select a zip or XCI file to install from.")
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier.fillMaxWidth()
.padding(top = 4.dp)
) {
Button(onClick = {
settingsViewModel.selectFirmware(
firmwareInstallState
)
}, modifier = Modifier.padding(horizontal = 8.dp)) {
Text(text = "Select File")
}
Button(onClick = {
showFirwmareDialog.value = false
settingsViewModel.clearFirmwareSelection(
firmwareInstallState
)
}, modifier = Modifier.padding(horizontal = 8.dp)) {
Text(text = "Cancel")
}
}
} else if (firmwareInstallState.value == FirmwareInstallState.Query) {
Text(text = "Firmware ${settingsViewModel.selectedFirmwareVersion} will be installed. Do you want to continue?")
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier.fillMaxWidth()
.padding(top = 4.dp)
) {
Button(onClick = {
settingsViewModel.installFirmware(
firmwareInstallState
)
if(firmwareInstallState.value == FirmwareInstallState.None){
showFirwmareDialog.value = false
settingsViewModel.clearFirmwareSelection(firmwareInstallState)
}
}, modifier = Modifier.padding(horizontal = 8.dp)) {
Text(text = "Yes")
}
Button(onClick = {
showFirwmareDialog.value = false
settingsViewModel.clearFirmwareSelection(
firmwareInstallState
)
}, modifier = Modifier.padding(horizontal = 8.dp)) {
Text(text = "No")
}
}
} else if (firmwareInstallState.value == FirmwareInstallState.Install) {
Text(text = "Installing Firmware ${settingsViewModel.selectedFirmwareVersion}...")
LinearProgressIndicator(modifier = Modifier
.padding(top = 4.dp))
} else if (firmwareInstallState.value == FirmwareInstallState.Verifying) {
Text(text = "Verifying selected file...")
LinearProgressIndicator(modifier = Modifier
.fillMaxWidth()
)
}
else if (firmwareInstallState.value == FirmwareInstallState.Done) {
Text(text = "Installed Firmware ${settingsViewModel.selectedFirmwareVersion}")
firmwareVersion.value = mainViewModel.firmwareVersion
}
else if(firmwareInstallState.value == FirmwareInstallState.Cancelled){
val file = settingsViewModel.selectedFirmwareFile
if(file != null){
if(file.extension == "xci" || file.extension == "zip"){
if(settingsViewModel.selectedFirmwareVersion.isEmpty()) {
Text(text = "Unable to find version in selected file")
}
else {
Text(text = "Unknown Error has occurred. Please check logs")
}
}
else {
Text(text = "File type is not supported")
}
}
else {
Text(text = "File type is not supported")
}
}
}
catch (_: ActivityNotFoundException){}
try {
mainViewModel.activity.startActivity(createIntent("com.android.documentsui"))
return@Button
}
catch (_: ActivityNotFoundException){}
}) {
Text(text = "Open App Folder")
}
}
}