diff options
author | bunnei <[email protected]> | 2023-06-05 17:41:19 -0700 |
---|---|---|
committer | GitHub <[email protected]> | 2023-06-05 17:41:19 -0700 |
commit | dc2a0b2e503703e55a32e110e02a3cd77e7ba737 (patch) | |
tree | f5bc678763248662c46aadf1084845610a6fd972 /src | |
parent | 71766f3269174daf67fe92853e6c247832717241 (diff) | |
parent | e1078ec0f48186ea6947271fff0da881d91b7d1c (diff) | |
download | yuzu-mainline-dc2a0b2e503703e55a32e110e02a3cd77e7ba737.tar.gz yuzu-mainline-dc2a0b2e503703e55a32e110e02a3cd77e7ba737.zip |
Merge pull request #10578 from PabloG02/lime-firmware&logs
Add UI to import firmware and share logs
Diffstat (limited to 'src')
9 files changed, 270 insertions, 33 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt index 67bcf8491..bdc337501 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt @@ -19,10 +19,10 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat -import androidx.core.content.ContextCompat import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding +import androidx.documentfile.provider.DocumentFile import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController @@ -40,6 +40,7 @@ import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile import org.yuzu.yuzu_emu.model.HomeSetting import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.ui.main.MainActivity +import org.yuzu.yuzu_emu.utils.FileUtil import org.yuzu.yuzu_emu.utils.GpuDriverHelper class HomeSettingsFragment : Fragment() { @@ -109,6 +110,16 @@ class HomeSettingsFragment : Fragment() { R.drawable.ic_unlock ) { mainActivity.getProdKey.launch(arrayOf("*/*")) }, HomeSetting( + R.string.install_firmware, + R.string.install_firmware_description, + R.drawable.ic_firmware + ) { mainActivity.getFirmware.launch(arrayOf("application/zip")) }, + HomeSetting( + R.string.share_log, + R.string.share_log_description, + R.drawable.ic_log + ) { shareLog() }, + HomeSetting( R.string.about, R.string.about_description, R.drawable.ic_info_outline @@ -262,6 +273,29 @@ class HomeSettingsFragment : Fragment() { .show() } + private fun shareLog() { + val file = DocumentFile.fromSingleUri( + mainActivity, + DocumentsContract.buildDocumentUri( + DocumentProvider.AUTHORITY, + "${DocumentProvider.ROOT_ID}/log/yuzu_log.txt" + ) + )!! + if (file.exists()) { + val intent = Intent(Intent.ACTION_SEND) + .setDataAndType(file.uri, FileUtil.TEXT_PLAIN) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + .putExtra(Intent.EXTRA_STREAM, file.uri) + startActivity(Intent.createChooser(intent, getText(R.string.share_log))) + } else { + Toast.makeText( + requireContext(), + getText(R.string.share_log_missing), + Toast.LENGTH_SHORT + ).show() + } + } + private fun setInsets() = ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view: View, windowInsets: WindowInsetsCompat -> val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt index 5f107b37d..36e63bb9e 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt @@ -23,17 +23,14 @@ import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.features.DocumentProvider import org.yuzu.yuzu_emu.getPublicFilesDir -import java.io.BufferedInputStream +import org.yuzu.yuzu_emu.utils.FileUtil import java.io.BufferedOutputStream import java.io.File import java.io.FileOutputStream import java.io.FilenameFilter -import java.io.IOException -import java.io.InputStream import java.time.LocalDateTime import java.time.format.DateTimeFormatter import java.util.zip.ZipEntry -import java.util.zip.ZipInputStream import java.util.zip.ZipOutputStream class ImportExportSavesFragment : DialogFragment() { @@ -125,33 +122,6 @@ class ImportExportSavesFragment : DialogFragment() { } /** - * Extracts the save files located in the given zip file and copies them to the saves folder. - * @exception IOException if the file was being created outside of the target directory - */ - private fun unzip(zipStream: InputStream, destDir: File): Boolean { - val zis = ZipInputStream(BufferedInputStream(zipStream)) - var entry: ZipEntry? = zis.nextEntry - while (entry != null) { - val entryName = entry.name - val entryFile = File(destDir, entryName) - if (!entryFile.canonicalPath.startsWith(destDir.canonicalPath + File.separator)) { - zis.close() - throw IOException("Entry is outside of the target dir: " + entryFile.name) - } - if (entry.isDirectory) { - entryFile.mkdirs() - } else { - entryFile.parentFile?.mkdirs() - entryFile.createNewFile() - entryFile.outputStream().use { fos -> zis.copyTo(fos) } - } - entry = zis.nextEntry - } - zis.close() - return true - } - - /** * Exports the save file located in the given folder path by creating a zip file and sharing it via intent. */ private fun exportSave() { @@ -204,7 +174,7 @@ class ImportExportSavesFragment : DialogFragment() { try { CoroutineScope(Dispatchers.IO).launch { - unzip(inputZip, cacheSaveDir) + FileUtil.unzip(inputZip, cacheSaveDir) cacheSaveDir.list(filterTitleId)?.forEach { savePath -> File(savesFolder, savePath).deleteRecursively() File(cacheSaveDir, savePath).copyRecursively(File(savesFolder, savePath), true) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt new file mode 100644 index 000000000..c7880d8cc --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.fragments + +import android.app.Dialog +import android.os.Bundle +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.ViewModelProvider +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding +import org.yuzu.yuzu_emu.model.TaskViewModel + + +class IndeterminateProgressDialogFragment : DialogFragment() { + private val taskViewModel: TaskViewModel by activityViewModels() + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val titleId = requireArguments().getInt(TITLE) + + val progressBinding = DialogProgressBarBinding.inflate(layoutInflater) + progressBinding.progressBar.isIndeterminate = true + val dialog = MaterialAlertDialogBuilder(requireContext()) + .setTitle(titleId) + .setView(progressBinding.root) + .create() + dialog.setCanceledOnTouchOutside(false) + + taskViewModel.isComplete.observe(this) { complete -> + if (complete) { + dialog.dismiss() + when (val result = taskViewModel.result.value) { + is String -> Toast.makeText(requireContext(), result, Toast.LENGTH_LONG).show() + is MessageDialogFragment -> result.show( + parentFragmentManager, + MessageDialogFragment.TAG + ) + } + taskViewModel.clear() + } + } + + if (taskViewModel.isRunning.value == false) { + taskViewModel.runTask() + } + return dialog + } + + companion object { + const val TAG = "IndeterminateProgressDialogFragment" + + private const val TITLE = "Title" + + fun newInstance( + activity: AppCompatActivity, + titleId: Int, + task: () -> Any + ): IndeterminateProgressDialogFragment { + val dialog = IndeterminateProgressDialogFragment() + val args = Bundle() + ViewModelProvider(activity)[TaskViewModel::class.java].task = task + args.putInt(TITLE, titleId) + dialog.arguments = args + return dialog + } + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt new file mode 100644 index 000000000..27ea725a5 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.model + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class TaskViewModel : ViewModel() { + private val _result = MutableLiveData<Any>() + val result: LiveData<Any> = _result + + private val _isComplete = MutableLiveData<Boolean>() + val isComplete: LiveData<Boolean> = _isComplete + + private val _isRunning = MutableLiveData<Boolean>() + val isRunning: LiveData<Boolean> = _isRunning + + lateinit var task: () -> Any + + init { + clear() + } + + fun clear() { + _result.value = Any() + _isComplete.value = false + _isRunning.value = false + } + + fun runTask() { + if (_isRunning.value == true) { + return + } + _isRunning.value = true + + viewModelScope.launch(Dispatchers.IO) { + val res = task() + _result.postValue(res) + _isComplete.postValue(true) + } + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt index 134085210..124f62f08 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt @@ -38,10 +38,13 @@ import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.model.SettingsViewModel import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile +import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment import org.yuzu.yuzu_emu.fragments.MessageDialogFragment import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.utils.* +import java.io.File +import java.io.FilenameFilter import java.io.IOException class MainActivity : AppCompatActivity(), ThemeProvider { @@ -319,6 +322,58 @@ class MainActivity : AppCompatActivity(), ThemeProvider { } } + val getFirmware = + registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> + if (result == null) + return@registerForActivityResult + + val inputZip = contentResolver.openInputStream(result) + if (inputZip == null) { + Toast.makeText( + applicationContext, + getString(R.string.fatal_error), + Toast.LENGTH_LONG + ).show() + return@registerForActivityResult + } + + val filterNCA = FilenameFilter { _, dirName -> dirName.endsWith(".nca") } + + val firmwarePath = + File(DirectoryInitialization.userDirectory + "/nand/system/Contents/registered/") + val cacheFirmwareDir = File("${cacheDir.path}/registered/") + + val task: () -> Any = { + var messageToShow: Any + try { + FileUtil.unzip(inputZip, cacheFirmwareDir) + val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1 + val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2 + messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) { + MessageDialogFragment.newInstance( + R.string.firmware_installed_failure, + R.string.firmware_installed_failure_description + ) + } else { + firmwarePath.deleteRecursively() + cacheFirmwareDir.copyRecursively(firmwarePath, true) + getString(R.string.save_file_imported_success) + } + } catch (e: Exception) { + messageToShow = getString(R.string.fatal_error) + } finally { + cacheFirmwareDir.deleteRecursively() + } + messageToShow + } + + IndeterminateProgressDialogFragment.newInstance( + this, + R.string.firmware_installing, + task + ).show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) + } + val getAmiiboKey = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> if (result == null) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt index 0a7b323b1..593dad8d3 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt @@ -9,10 +9,14 @@ import android.net.Uri import android.provider.DocumentsContract import androidx.documentfile.provider.DocumentFile import org.yuzu.yuzu_emu.model.MinimalDocumentFile +import java.io.BufferedInputStream +import java.io.File import java.io.FileOutputStream import java.io.IOException import java.io.InputStream import java.net.URLDecoder +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream object FileUtil { const val PATH_TREE = "tree" @@ -276,6 +280,34 @@ object FileUtil { return false } + /** + * Extracts the given zip file into the given directory. + * @exception IOException if the file was being created outside of the target directory + */ + @Throws(SecurityException::class) + fun unzip(zipStream: InputStream, destDir: File): Boolean { + ZipInputStream(BufferedInputStream(zipStream)).use { zis -> + var entry: ZipEntry? = zis.nextEntry + while (entry != null) { + val entryName = entry.name + val entryFile = File(destDir, entryName) + if (!entryFile.canonicalPath.startsWith(destDir.canonicalPath + File.separator)) { + throw SecurityException("Entry is outside of the target dir: " + entryFile.name) + } + if (entry.isDirectory) { + entryFile.mkdirs() + } else { + entryFile.parentFile?.mkdirs() + entryFile.createNewFile() + entryFile.outputStream().use { fos -> zis.copyTo(fos) } + } + entry = zis.nextEntry + } + } + + return true + } + fun isRootTreeUri(uri: Uri): Boolean { val paths = uri.pathSegments return paths.size == 2 && PATH_TREE == paths[0] diff --git a/src/android/app/src/main/res/drawable/ic_firmware.xml b/src/android/app/src/main/res/drawable/ic_firmware.xml new file mode 100644 index 000000000..61f3485e4 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_firmware.xml @@ -0,0 +1,10 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="960" + android:viewportHeight="960" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M160,840Q127,840 103.5,816.5Q80,793 80,760L80,200Q80,167 103.5,143.5Q127,120 160,120L720,120Q753,120 776.5,143.5Q800,167 800,200L800,280L840,280Q857,280 868.5,291.5Q880,303 880,320Q880,337 868.5,348.5Q857,360 840,360L800,360L800,440L840,440Q857,440 868.5,451.5Q880,463 880,480Q880,497 868.5,508.5Q857,520 840,520L800,520L800,600L840,600Q857,600 868.5,611.5Q880,623 880,640Q880,657 868.5,668.5Q857,680 840,680L800,680L800,760Q800,793 776.5,816.5Q753,840 720,840L160,840ZM160,760L720,760Q720,760 720,760Q720,760 720,760L720,200Q720,200 720,200Q720,200 720,200L160,200Q160,200 160,200Q160,200 160,200L160,760Q160,760 160,760Q160,760 160,760ZM280,680L400,680Q417,680 428.5,668.5Q440,657 440,640L440,560Q440,543 428.5,531.5Q417,520 400,520L280,520Q263,520 251.5,531.5Q240,543 240,560L240,640Q240,657 251.5,668.5Q263,680 280,680ZM520,400L600,400Q617,400 628.5,388.5Q640,377 640,360L640,320Q640,303 628.5,291.5Q617,280 600,280L520,280Q503,280 491.5,291.5Q480,303 480,320L480,360Q480,377 491.5,388.5Q503,400 520,400ZM280,480L400,480Q417,480 428.5,468.5Q440,457 440,440L440,320Q440,303 428.5,291.5Q417,280 400,280L280,280Q263,280 251.5,291.5Q240,303 240,320L240,440Q240,457 251.5,468.5Q263,480 280,480ZM520,680L600,680Q617,680 628.5,668.5Q640,657 640,640L640,480Q640,463 628.5,451.5Q617,440 600,440L520,440Q503,440 491.5,451.5Q480,463 480,480L480,640Q480,657 491.5,668.5Q503,680 520,680ZM160,200L160,200Q160,200 160,200Q160,200 160,200L160,760Q160,760 160,760Q160,760 160,760L160,760Q160,760 160,760Q160,760 160,760L160,200Q160,200 160,200Q160,200 160,200Z"/> +</vector> diff --git a/src/android/app/src/main/res/drawable/ic_log.xml b/src/android/app/src/main/res/drawable/ic_log.xml new file mode 100644 index 000000000..f55b9ad85 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_log.xml @@ -0,0 +1,10 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="960" + android:viewportHeight="960" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M360,720L600,720Q617,720 628.5,708.5Q640,697 640,680Q640,663 628.5,651.5Q617,640 600,640L360,640Q343,640 331.5,651.5Q320,663 320,680Q320,697 331.5,708.5Q343,720 360,720ZM360,560L600,560Q617,560 628.5,548.5Q640,537 640,520Q640,503 628.5,491.5Q617,480 600,480L360,480Q343,480 331.5,491.5Q320,503 320,520Q320,537 331.5,548.5Q343,560 360,560ZM240,880Q207,880 183.5,856.5Q160,833 160,800L160,160Q160,127 183.5,103.5Q207,80 240,80L527,80Q543,80 557.5,86Q572,92 583,103L777,297Q788,308 794,322.5Q800,337 800,353L800,800Q800,833 776.5,856.5Q753,880 720,880L240,880ZM520,320L520,160L240,160Q240,160 240,160Q240,160 240,160L240,800Q240,800 240,800Q240,800 240,800L720,800Q720,800 720,800Q720,800 720,800L720,360L560,360Q543,360 531.5,348.5Q520,337 520,320ZM240,160L240,160L240,320Q240,337 240,348.5Q240,360 240,360L240,360L240,160L240,320Q240,337 240,348.5Q240,360 240,360L240,360L240,800Q240,800 240,800Q240,800 240,800L240,800Q240,800 240,800Q240,800 240,800L240,160Q240,160 240,160Q240,160 240,160Z"/> +</vector> diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index b86f45385..0ae69afb4 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -96,6 +96,15 @@ <string name="save_file_invalid_zip_structure_description">The first subfolder name must be the title ID of the game.</string> <string name="import_saves">Import</string> <string name="export_saves">Export</string> + <string name="install_firmware">Install firmware</string> + <string name="install_firmware_description">Firmware must be in a ZIP archive and is needed to boot some games</string> + <string name="firmware_installing">Installing firmware</string> + <string name="firmware_installed_success">Firmware installed successfully</string> + <string name="firmware_installed_failure">Firmware installation failed</string> + <string name="firmware_installed_failure_description">Verify that the ZIP contains valid firmware and try again.</string> + <string name="share_log">Share debug logs</string> + <string name="share_log_description">Share yuzu\'s log file to debug issues</string> + <string name="share_log_missing">No log file found</string> <!-- About screen strings --> <string name="gaia_is_not_real">Gaia isn\'t real</string> |