From 2ef28525be31085d87638128572b65dd68f01769 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Sun, 17 Dec 2023 19:09:52 +0000 Subject: [PATCH] android - implement firmware installation --- src/LibRyujinx/Android/JniExportedMethods.cs | 53 ++ src/LibRyujinx/LibRyujinx.Device.cs | 11 + src/Ryujinx.HLE/FileSystem/ContentManager.cs | 477 ++++++++++-------- src/RyujinxAndroid/app/build.gradle | 5 +- .../java/org/ryujinx/android/MainActivity.kt | 2 + .../java/org/ryujinx/android/RyujinxNative.kt | 39 +- .../android/viewmodels/MainViewModel.kt | 10 +- .../android/viewmodels/SettingsViewModel.kt | 143 +++++- .../org/ryujinx/android/views/SettingViews.kt | 212 +++++++- 9 files changed, 669 insertions(+), 283 deletions(-) diff --git a/src/LibRyujinx/Android/JniExportedMethods.cs b/src/LibRyujinx/Android/JniExportedMethods.cs index 6a110ba7c..c155e29d2 100644 --- a/src/LibRyujinx/Android/JniExportedMethods.cs +++ b/src/LibRyujinx/Android/JniExportedMethods.cs @@ -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) { diff --git a/src/LibRyujinx/LibRyujinx.Device.cs b/src/LibRyujinx/LibRyujinx.Device.cs index 675206720..d46d03f43 100644 --- a/src/LibRyujinx/LibRyujinx.Device.cs +++ b/src/LibRyujinx/LibRyujinx.Device.cs @@ -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; diff --git a/src/Ryujinx.HLE/FileSystem/ContentManager.cs b/src/Ryujinx.HLE/FileSystem/ContentManager.cs index eaa94dc0b..1cd8664ca 100644 --- a/src/Ryujinx.HLE/FileSystem/ContentManager.cs +++ b/src/Ryujinx.HLE/FileSystem/ContentManager.cs @@ -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> 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(); - - 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(); - - 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(); - - 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 content = new(contentData); - - contentStorage.Read(0, content); - - Span 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> 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(); - - 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(); - - 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(); + + 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(); + + 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> 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(); + + 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(); + + 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(); + + 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 content = new(contentData); + + contentStorage.Read(0, content); + + Span 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() diff --git a/src/RyujinxAndroid/app/build.gradle b/src/RyujinxAndroid/app/build.gradle index 7aae8293c..2740c03c7 100644 --- a/src/RyujinxAndroid/app/build.gradle +++ b/src/RyujinxAndroid/app/build.gradle @@ -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' diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt index b00cf8ed1..70d1eb2b6 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt @@ -102,6 +102,8 @@ class MainActivity : BaseActivity() { mainViewModel!!.physicalControllerManager = physicalControllerManager mainViewModel!!.motionSensorManager = motionSensorManager + mainViewModel!!.refreshFirmwareVersion() + mainViewModel?.apply { setContent { RyujinxAndroidTheme { 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 d09362e11..25ba5d562 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 @@ -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, @@ -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 - 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 + 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 + external fun userGetAllUsers(): Array 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 } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt index 7daad2393..16e399c4a 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt @@ -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? = null private var gameFpsState: MutableState? = null private var fifoState: MutableState? = 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 diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/SettingsViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/SettingsViewModel.kt index e295df5e8..1dd73fd54 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/SettingsViewModel.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/SettingsViewModel.kt @@ -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) -> 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, enableAccessLogs: MutableState, enableTraceLogs: MutableState - ) - { + ) { 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, enableAccessLogs: MutableState, enableTraceLogs: MutableState - ){ + ) { 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) { + 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) { + 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){ + selectedFirmwareFile = null + selectedFirmwareVersion = "" + installState.value = FirmwareInstallState.None + } +} + + +enum class FirmwareInstallState{ + None, + Cancelled, + Verifying, + Query, + Install, + Done } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt index a0f476e2a..96acd52f8 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt @@ -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") } } }