From aa87b4abd82312044e7a4fceedbed4f4964cf63b Mon Sep 17 00:00:00 2001
From: Emmanuel Hansen <emmausssss@gmail.com>
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<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()
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<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
 }
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<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
 
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<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
 }
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")
                             }
                         }
                     }