forked from MeloNX/MeloNX
android - implement firmware installation
This commit is contained in:
parent
60f320bc07
commit
aa87b4abd8
@ -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)
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -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()
|
||||
|
@ -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'
|
||||
|
@ -102,6 +102,8 @@ class MainActivity : BaseActivity() {
|
||||
mainViewModel!!.physicalControllerManager = physicalControllerManager
|
||||
mainViewModel!!.motionSensorManager = motionSensorManager
|
||||
|
||||
mainViewModel!!.refreshFirmwareVersion()
|
||||
|
||||
mainViewModel?.apply {
|
||||
setContent {
|
||||
RyujinxAndroidTheme {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user