android - implement firmware installation

This commit is contained in:
Emmanuel Hansen 2023-12-17 19:09:52 +00:00
parent 27059ded86
commit 2ef28525be
9 changed files with 669 additions and 283 deletions

View File

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

View File

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

View File

@ -525,6 +525,27 @@ namespace Ryujinx.HLE.FileSystem
FinishInstallation(temporaryDirectory, registeredDirectory);
}
public void InstallFirmware(Stream stream, bool isXci)
{
string contentPathString = ContentPath.GetContentPath(StorageId.BuiltInSystem);
string contentDirectory = ContentPath.GetRealPath(contentPathString);
string registeredDirectory = Path.Combine(contentDirectory, "registered");
string temporaryDirectory = Path.Combine(contentDirectory, "temp");
if (!isXci)
{
using ZipArchive archive = new ZipArchive(stream);
InstallFromZip(archive, temporaryDirectory);
}
else
{
Xci xci = new(_virtualFileSystem.KeySet, stream.AsStorage());
InstallFromCart(xci, temporaryDirectory);
}
FinishInstallation(temporaryDirectory, registeredDirectory);
}
private void FinishInstallation(string temporaryDirectory, string registeredDirectory)
{
if (Directory.Exists(registeredDirectory))
@ -643,13 +664,16 @@ namespace Ryujinx.HLE.FileSystem
throw new MissingKeyException("HeaderKey is empty. Cannot decrypt NCA headers.");
}
Dictionary<ulong, List<(NcaContentType type, string path)>> updateNcas = new();
if (Directory.Exists(firmwarePackage))
{
return VerifyAndGetVersionDirectory(firmwarePackage);
}
SystemVersion VerifyAndGetVersionDirectory(string firmwareDirectory)
{
return VerifyAndGetVersion(new LocalFileSystem(firmwareDirectory));
}
if (!File.Exists(firmwarePackage))
{
throw new FileNotFoundException("Firmware file does not exist.");
@ -657,16 +681,28 @@ namespace Ryujinx.HLE.FileSystem
FileInfo info = new(firmwarePackage);
if (info.Extension == ".zip" || info.Extension == ".xci")
{
using FileStream file = File.OpenRead(firmwarePackage);
switch (info.Extension)
var isXci = info.Extension == ".xci";
return VerifyFirmwarePackage(file, isXci);
}
return null;
}
public SystemVersion VerifyFirmwarePackage(Stream file, bool isXci)
{
case ".zip":
using (ZipArchive archive = ZipFile.OpenRead(firmwarePackage))
if (!isXci)
{
using ZipArchive archive = new ZipArchive(file, ZipArchiveMode.Read);
return VerifyAndGetVersionZip(archive);
}
case ".xci":
else
{
Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage());
if (xci.HasPartition(XciPartitionType.Update))
@ -679,17 +715,13 @@ namespace Ryujinx.HLE.FileSystem
{
throw new InvalidFirmwarePackageException("Update not found in xci file.");
}
default:
break;
}
}
SystemVersion VerifyAndGetVersionDirectory(string firmwareDirectory)
private SystemVersion VerifyAndGetVersionZip(ZipArchive archive)
{
return VerifyAndGetVersion(new LocalFileSystem(firmwareDirectory));
}
Dictionary<ulong, List<(NcaContentType type, string path)>> updateNcas = new();
SystemVersion VerifyAndGetVersionZip(ZipArchive archive)
{
SystemVersion systemVersion = null;
foreach (var entry in archive.Entries)
@ -845,8 +877,10 @@ namespace Ryujinx.HLE.FileSystem
return systemVersion;
}
SystemVersion VerifyAndGetVersion(IFileSystem filesystem)
private SystemVersion VerifyAndGetVersion(IFileSystem filesystem)
{
Dictionary<ulong, List<(NcaContentType type, string path)>> updateNcas = new();
SystemVersion systemVersion = null;
CnmtContentMetaEntry[] metaEntries = null;
@ -977,9 +1011,6 @@ namespace Ryujinx.HLE.FileSystem
return systemVersion;
}
return null;
}
public SystemVersion GetCurrentFirmwareVersion()
{
LoadEntries();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -60,6 +60,7 @@ import com.anggrayudi.storage.file.extension
import org.ryujinx.android.Helpers
import org.ryujinx.android.MainActivity
import org.ryujinx.android.providers.DocumentProvider
import org.ryujinx.android.viewmodels.FirmwareInstallState
import org.ryujinx.android.viewmodels.MainViewModel
import org.ryujinx.android.viewmodels.SettingsViewModel
import org.ryujinx.android.viewmodels.VulkanDriverViewModel
@ -107,6 +108,15 @@ class SettingViews {
val useVirtualController = remember {
mutableStateOf(true)
}
val showFirwmareDialog = remember {
mutableStateOf(false)
}
val firmwareInstallState = remember {
mutableStateOf(FirmwareInstallState.None)
}
val firmwareVersion = remember {
mutableStateOf(mainViewModel.firmwareVersion)
}
val isGrid = remember { mutableStateOf(true) }
val enableDebugLogs = remember { mutableStateOf(true) }
@ -211,37 +221,187 @@ class SettingViews {
Text(text = "Choose Folder")
}
}
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{
fun createIntent(action: String): Intent {
val intent = Intent(action)
intent.addCategory(Intent.CATEGORY_DEFAULT)
intent.data = DocumentsContract.buildRootUri(DocumentProvider.AUTHORITY, DocumentProvider.ROOT_ID)
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) {
}
catch (_: ActivityNotFoundException){}
try {
mainViewModel.activity.startActivity(createIntent("android.provider.action.BROWSE"))
return@Button
} catch (_: ActivityNotFoundException) {
}
catch (_: ActivityNotFoundException){}
try {
mainViewModel.activity.startActivity(createIntent("com.google.android.documentsui"))
return@Button
} catch (_: ActivityNotFoundException) {
}
catch (_: ActivityNotFoundException){}
try {
mainViewModel.activity.startActivity(createIntent("com.android.documentsui"))
return@Button
} catch (_: ActivityNotFoundException) {
}
catch (_: ActivityNotFoundException){}
}) {
Text(text = "Open App Folder")
}
Button(onClick = {
settingsViewModel.importProdKeys()
}) {
Text(text = "Import prod Keys")
}
Button(onClick = {
showFirwmareDialog.value = true
}) {
Text(text = "Install Firmware")
}
}
}
}
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")
}
}
}
}
}
}
ExpandableView(onCardArrowClick = { }, title = "System") {