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); 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")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsInitialize")]
public static JBoolean JniInitializeGraphicsNative(JEnvRef jEnv, JObjectLocalRef jObj, JObjectLocalRef graphicObject) public static JBoolean JniInitializeGraphicsNative(JEnvRef jEnv, JObjectLocalRef jObj, JObjectLocalRef graphicObject)
{ {

View File

@ -2,6 +2,7 @@ using ARMeilleure.Translation;
using LibHac.Ncm; using LibHac.Ncm;
using LibHac.Tools.FsSystem.NcaUtils; using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS.SystemState; using Ryujinx.HLE.HOS.SystemState;
using Ryujinx.Input.HLE; using Ryujinx.Input.HLE;
using Silk.NET.Vulkan; using Silk.NET.Vulkan;
@ -66,6 +67,16 @@ namespace LibRyujinx
return LoadApplication(path); 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) public static bool LoadApplication(Stream stream, FileType type)
{ {
var emulationContext = SwitchDevice.EmulationContext; var emulationContext = SwitchDevice.EmulationContext;

View File

@ -525,6 +525,27 @@ namespace Ryujinx.HLE.FileSystem
FinishInstallation(temporaryDirectory, registeredDirectory); 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) private void FinishInstallation(string temporaryDirectory, string registeredDirectory)
{ {
if (Directory.Exists(registeredDirectory)) if (Directory.Exists(registeredDirectory))
@ -643,13 +664,16 @@ namespace Ryujinx.HLE.FileSystem
throw new MissingKeyException("HeaderKey is empty. Cannot decrypt NCA headers."); throw new MissingKeyException("HeaderKey is empty. Cannot decrypt NCA headers.");
} }
Dictionary<ulong, List<(NcaContentType type, string path)>> updateNcas = new();
if (Directory.Exists(firmwarePackage)) if (Directory.Exists(firmwarePackage))
{ {
return VerifyAndGetVersionDirectory(firmwarePackage); return VerifyAndGetVersionDirectory(firmwarePackage);
} }
SystemVersion VerifyAndGetVersionDirectory(string firmwareDirectory)
{
return VerifyAndGetVersion(new LocalFileSystem(firmwareDirectory));
}
if (!File.Exists(firmwarePackage)) if (!File.Exists(firmwarePackage))
{ {
throw new FileNotFoundException("Firmware file does not exist."); throw new FileNotFoundException("Firmware file does not exist.");
@ -657,249 +681,99 @@ namespace Ryujinx.HLE.FileSystem
FileInfo info = new(firmwarePackage); FileInfo info = new(firmwarePackage);
using FileStream file = File.OpenRead(firmwarePackage);
switch (info.Extension) if (info.Extension == ".zip" || info.Extension == ".xci")
{ {
case ".zip": using FileStream file = File.OpenRead(firmwarePackage);
using (ZipArchive archive = ZipFile.OpenRead(firmwarePackage))
{
return VerifyAndGetVersionZip(archive);
}
case ".xci":
Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage());
if (xci.HasPartition(XciPartitionType.Update)) var isXci = info.Extension == ".xci";
{
XciPartition partition = xci.OpenPartition(XciPartitionType.Update);
return VerifyAndGetVersion(partition); return VerifyFirmwarePackage(file, isXci);
}
else
{
throw new InvalidFirmwarePackageException("Update not found in xci file.");
}
default:
break;
} }
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);
} }
else
SystemVersion VerifyAndGetVersionZip(ZipArchive archive)
{ {
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")) XciPartition partition = xci.OpenPartition(XciPartitionType.Update);
{
using Stream ncaStream = GetZipStream(entry);
IStorage storage = ncaStream.AsStorage();
Nca nca = new(_virtualFileSystem.KeySet, storage); return VerifyAndGetVersion(partition);
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}");
}
} }
else 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; if (entry.FullName.EndsWith(".nca") || entry.FullName.EndsWith(".nca/00"))
CnmtContentMetaEntry[] metaEntries = null;
foreach (var entry in filesystem.EnumerateEntries("/", "*.nca"))
{ {
IStorage ncaStorage = OpenPossibleFragmentedFile(filesystem, entry.FullPath, OpenMode.Read).AsStorage(); using Stream ncaStream = GetZipStream(entry);
IStorage storage = ncaStream.AsStorage();
Nca nca = new(_virtualFileSystem.KeySet, ncaStorage); Nca nca = new(_virtualFileSystem.KeySet, storage);
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)) if (updateNcas.TryGetValue(nca.Header.TitleId, out var updateNcasItem))
{ {
updateNcasItem.Add((nca.Header.ContentType, entry.FullPath)); updateNcasItem.Add((nca.Header.ContentType, entry.FullName));
} }
else else
{ {
updateNcas.Add(nca.Header.TitleId, new List<(NcaContentType, string)>()); 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) if (metaEntries == null)
@ -907,11 +781,29 @@ namespace Ryujinx.HLE.FileSystem
throw new FileNotFoundException("System update title was not found in the firmware package."); 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) 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; 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. // Nintendo in 9.0.0, removed PPC and only kept the meta nca of it.
@ -923,10 +815,12 @@ namespace Ryujinx.HLE.FileSystem
continue; continue;
} }
IStorage metaStorage = OpenPossibleFragmentedFile(filesystem, metaNcaPath, OpenMode.Read).AsStorage(); ZipArchiveEntry metaZipEntry = archive.GetEntry(metaPath);
IStorage contentStorage = OpenPossibleFragmentedFile(filesystem, contentPath, OpenMode.Read).AsStorage(); 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); IFileSystem fs = metaNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
@ -938,6 +832,7 @@ namespace Ryujinx.HLE.FileSystem
{ {
var meta = new Cnmt(metaFile.Get.AsStream()); var meta = new Cnmt(metaFile.Get.AsStream());
IStorage contentStorage = contentNcaStream.AsStorage();
if (contentStorage.GetSize(out long size).IsSuccess()) if (contentStorage.GetSize(out long size).IsSuccess())
{ {
byte[] contentData = new byte[size]; 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}"); 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() public SystemVersion GetCurrentFirmwareVersion()

View File

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

View File

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

View File

@ -8,20 +8,24 @@ class RyujinxNative {
companion object { companion object {
val instance: RyujinxNative = RyujinxNative() val instance: RyujinxNative = RyujinxNative()
init { init {
System.loadLibrary("ryujinx") System.loadLibrary("ryujinx")
} }
} }
external fun deviceInitialize(isHostMapped: Boolean, useNce: Boolean, external fun deviceInitialize(
systemLanguage : Int, isHostMapped: Boolean, useNce: Boolean,
regionCode : Int, systemLanguage: Int,
enableVsync : Boolean, regionCode: Int,
enableDockedMode : Boolean, enableVsync: Boolean,
enablePtc : Boolean, enableDockedMode: Boolean,
enableInternetAccess : Boolean, enablePtc: Boolean,
timeZone : Long, enableInternetAccess: Boolean,
ignoreMissingServices : Boolean): Boolean timeZone: Long,
ignoreMissingServices: Boolean
): Boolean
external fun graphicsInitialize(configuration: GraphicsConfiguration): Boolean external fun graphicsInitialize(configuration: GraphicsConfiguration): Boolean
external fun graphicsInitializeRenderer( external fun graphicsInitializeRenderer(
extensions: Array<String>, extensions: Array<String>,
@ -35,7 +39,7 @@ class RyujinxNative {
external fun deviceGetGameFifo(): Double external fun deviceGetGameFifo(): Double
external fun deviceGetGameInfo(fileDescriptor: Int, extension: Long): GameInfo external fun deviceGetGameInfo(fileDescriptor: Int, extension: Long): GameInfo
external fun deviceGetGameInfoFromPath(path: String): 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 graphicsRendererSetSize(width: Int, height: Int)
external fun graphicsRendererSetVsync(enabled: Boolean) external fun graphicsRendererSetVsync(enabled: Boolean)
external fun graphicsRendererRunLoop() external fun graphicsRendererRunLoop()
@ -54,17 +58,20 @@ class RyujinxNative {
external fun graphicsSetSurface(surface: Long, window: Long) external fun graphicsSetSurface(surface: Long, window: Long)
external fun deviceCloseEmulation() external fun deviceCloseEmulation()
external fun deviceSignalEmulationClose() external fun deviceSignalEmulationClose()
external fun deviceGetDlcTitleId(path: Long, ncaPath: Long) : Long external fun deviceGetDlcTitleId(path: Long, ncaPath: Long): Long
external fun deviceGetDlcContentList(path: Long, titleId: Long) : Array<String> external fun deviceGetDlcContentList(path: Long, titleId: Long): Array<String>
external fun userGetOpenedUser() : Long external fun userGetOpenedUser(): Long
external fun userGetUserPicture(userId: Long) : Long external fun userGetUserPicture(userId: Long): Long
external fun userSetUserPicture(userId: String, picture: String) 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 userSetUserName(userId: String, userName: String)
external fun userGetAllUsers() : Array<String> external fun userGetAllUsers(): Array<String>
external fun userAddUser(username: String, picture: String) external fun userAddUser(username: String, picture: String)
external fun userDeleteUser(userId: String) external fun userDeleteUser(userId: String)
external fun userOpenUser(userId: Long) external fun userOpenUser(userId: Long)
external fun userCloseUser(userId: String) external fun userCloseUser(userId: String)
external fun loggingSetEnabled(logLevel: Int, enabled: Boolean) 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 var isMiiEditorLaunched = false
val userViewModel = UserViewModel() val userViewModel = UserViewModel()
val logging = Logging(this) val logging = Logging(this)
var firmwareVersion = ""
private var gameTimeState: MutableState<Double>? = null private var gameTimeState: MutableState<Double>? = null
private var gameFpsState: MutableState<Double>? = null private var gameFpsState: MutableState<Double>? = null
private var fifoState: MutableState<Double>? = null private var fifoState: MutableState<Double>? = null
@ -69,6 +70,13 @@ class MainViewModel(val activity: MainActivity) {
motionSensorManager?.setControllerId(-1) motionSensorManager?.setControllerId(-1)
} }
fun refreshFirmwareVersion(){
var handle = RyujinxNative.instance.deviceGetInstalledFirmwareVersion()
if(handle != -1L) {
firmwareVersion = NativeHelpers.instance.getStringJava(handle)
}
}
fun loadGame(game:GameModel) : Boolean { fun loadGame(game:GameModel) : Boolean {
val nativeRyujinx = RyujinxNative.instance val nativeRyujinx = RyujinxNative.instance
@ -178,8 +186,6 @@ class MainViewModel(val activity: MainActivity) {
return true return true
} }
fun loadMiiEditor() : Boolean { fun loadMiiEditor() : Boolean {
val nativeRyujinx = RyujinxNative.instance val nativeRyujinx = RyujinxNative.instance

View File

@ -5,30 +5,40 @@ import androidx.compose.runtime.MutableState
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.anggrayudi.storage.callback.FileCallback
import com.anggrayudi.storage.file.FileFullPath 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 com.anggrayudi.storage.file.getAbsolutePath
import org.ryujinx.android.LogLevel import org.ryujinx.android.LogLevel
import org.ryujinx.android.MainActivity import org.ryujinx.android.MainActivity
import org.ryujinx.android.NativeHelpers
import org.ryujinx.android.RyujinxNative import org.ryujinx.android.RyujinxNative
import java.io.File
import kotlin.concurrent.thread
class SettingsViewModel(var navController: NavHostController, val activity: MainActivity) { 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 private var sharedPref: SharedPreferences
var selectedFirmwareFile: DocumentFile? = null
init { init {
sharedPref = getPreferences() sharedPref = getPreferences()
previousCallback = activity.storageHelper!!.onFolderSelected previousFolderCallback = activity.storageHelper!!.onFolderSelected
previousFileCallback = activity.storageHelper!!.onFileSelected
activity.storageHelper!!.onFolderSelected = { requestCode, folder -> activity.storageHelper!!.onFolderSelected = { requestCode, folder ->
run { run {
val p = folder.getAbsolutePath(activity!!) val p = folder.getAbsolutePath(activity)
val editor = sharedPref?.edit() val editor = sharedPref.edit()
editor?.putString("gameFolder", p) editor?.putString("gameFolder", p)
editor?.apply() editor?.apply()
} }
} }
} }
private fun getPreferences() : SharedPreferences { private fun getPreferences(): SharedPreferences {
return PreferenceManager.getDefaultSharedPreferences(activity) return PreferenceManager.getDefaultSharedPreferences(activity)
} }
@ -52,8 +62,7 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
enableGuestLogs: MutableState<Boolean>, enableGuestLogs: MutableState<Boolean>,
enableAccessLogs: MutableState<Boolean>, enableAccessLogs: MutableState<Boolean>,
enableTraceLogs: MutableState<Boolean> enableTraceLogs: MutableState<Boolean>
) ) {
{
isHostMapped.value = sharedPref.getBoolean("isHostMapped", true) isHostMapped.value = sharedPref.getBoolean("isHostMapped", true)
useNce.value = sharedPref.getBoolean("useNce", 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) enablePtc.value = sharedPref.getBoolean("enablePtc", true)
ignoreMissingServices.value = sharedPref.getBoolean("ignoreMissingServices", false) ignoreMissingServices.value = sharedPref.getBoolean("ignoreMissingServices", false)
enableShaderCache.value = sharedPref.getBoolean("enableShaderCache", true) enableShaderCache.value = sharedPref.getBoolean("enableShaderCache", true)
enableTextureRecompression.value = sharedPref.getBoolean("enableTextureRecompression", false) enableTextureRecompression.value =
sharedPref.getBoolean("enableTextureRecompression", false)
resScale.value = sharedPref.getFloat("resScale", 1f) resScale.value = sharedPref.getFloat("resScale", 1f)
useVirtualController.value = sharedPref.getBoolean("useVirtualController", true) useVirtualController.value = sharedPref.getBoolean("useVirtualController", true)
isGrid.value = sharedPref.getBoolean("isGrid", true) isGrid.value = sharedPref.getBoolean("isGrid", true)
@ -97,7 +107,7 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
enableGuestLogs: MutableState<Boolean>, enableGuestLogs: MutableState<Boolean>,
enableAccessLogs: MutableState<Boolean>, enableAccessLogs: MutableState<Boolean>,
enableTraceLogs: MutableState<Boolean> enableTraceLogs: MutableState<Boolean>
){ ) {
val editor = sharedPref.edit() val editor = sharedPref.edit()
editor.putBoolean("isHostMapped", isHostMapped.value) editor.putBoolean("isHostMapped", isHostMapped.value)
@ -123,7 +133,7 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
editor.putBoolean("enableTraceLogs", enableTraceLogs.value) editor.putBoolean("enableTraceLogs", enableTraceLogs.value)
editor.apply() editor.apply()
activity.storageHelper!!.onFolderSelected = previousCallback activity.storageHelper!!.onFolderSelected = previousFolderCallback
RyujinxNative.instance.loggingSetEnabled(LogLevel.Debug.ordinal, enableDebugLogs.value) RyujinxNative.instance.loggingSetEnabled(LogLevel.Debug.ordinal, enableDebugLogs.value)
RyujinxNative.instance.loggingSetEnabled(LogLevel.Info.ordinal, enableInfoLogs.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) RyujinxNative.instance.loggingSetEnabled(LogLevel.Trace.ordinal, enableTraceLogs.value)
} }
fun openGameFolder() { fun openGameFolder() {
val path = sharedPref?.getString("gameFolder", "") ?: "" val path = sharedPref?.getString("gameFolder", "") ?: ""
if (path.isEmpty()) if (path.isEmpty())
activity?.storageHelper?.storage?.openFolderPicker() activity.storageHelper?.storage?.openFolderPicker()
else else
activity?.storageHelper?.storage?.openFolderPicker( activity.storageHelper?.storage?.openFolderPicker(
activity.storageHelper!!.storage.requestCodeFolderPicker, activity.storageHelper!!.storage.requestCodeFolderPicker,
FileFullPath(activity, path) 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.Helpers
import org.ryujinx.android.MainActivity import org.ryujinx.android.MainActivity
import org.ryujinx.android.providers.DocumentProvider import org.ryujinx.android.providers.DocumentProvider
import org.ryujinx.android.viewmodels.FirmwareInstallState
import org.ryujinx.android.viewmodels.MainViewModel import org.ryujinx.android.viewmodels.MainViewModel
import org.ryujinx.android.viewmodels.SettingsViewModel import org.ryujinx.android.viewmodels.SettingsViewModel
import org.ryujinx.android.viewmodels.VulkanDriverViewModel import org.ryujinx.android.viewmodels.VulkanDriverViewModel
@ -107,6 +108,15 @@ class SettingViews {
val useVirtualController = remember { val useVirtualController = remember {
mutableStateOf(true) 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 isGrid = remember { mutableStateOf(true) }
val enableDebugLogs = remember { mutableStateOf(true) } val enableDebugLogs = remember { mutableStateOf(true) }
@ -211,36 +221,186 @@ class SettingViews {
Text(text = "Choose Folder") Text(text = "Choose Folder")
} }
} }
Button(onClick = {
fun createIntent(action: String) : Intent{ Row(
val intent = Intent(action) modifier = Modifier
intent.addCategory(Intent.CATEGORY_DEFAULT) .fillMaxWidth()
intent.data = DocumentsContract.buildRootUri(DocumentProvider.AUTHORITY, DocumentProvider.ROOT_ID) .padding(8.dp),
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) horizontalArrangement = Arrangement.SpaceBetween,
return intent 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)) Button(onClick = {
return@Button settingsViewModel.importProdKeys()
}) {
Text(text = "Import prod Keys")
} }
catch (_: ActivityNotFoundException){}
try { Button(onClick = {
mainViewModel.activity.startActivity(createIntent("android.provider.action.BROWSE")) showFirwmareDialog.value = true
return@Button }) {
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")
} }
} }
} }