diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt index 5535e45711..4ee68df3ff 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt @@ -56,6 +56,12 @@ import androidx.window.layout.WindowLayoutInfo import com.google.android.material.color.MaterialColors import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.textview.MaterialTextView +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.yuzu.yuzu_emu.HomeNavigationDirections import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R @@ -85,12 +91,11 @@ import org.yuzu.yuzu_emu.utils.ViewUtils import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible import org.yuzu.yuzu_emu.utils.collect import org.yuzu.yuzu_emu.utils.CustomSettingsHandler -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlinx.coroutines.Dispatchers +import java.io.ByteArrayOutputStream +import java.io.File +import kotlin.coroutines.coroutineContext import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine -import java.io.File class EmulationFragment : Fragment(), SurfaceHolder.Callback { private lateinit var emulationState: EmulationState @@ -125,6 +130,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { private var perfStatsRunnable: Runnable? = null private var socRunnable: Runnable? = null private var isAmiiboPickerOpen = false + private var amiiboLoadJob: Job? = null private val loadAmiiboLauncher = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> @@ -136,26 +142,46 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { return@registerForActivityResult } - val data = try { - requireContext().contentResolver.openInputStream(uri)?.use { it.readBytes() } - } catch (e: Exception) { - Log.error("[EmulationFragment] Failed to read amiibo: ${e.message}") - showAmiiboDialog(R.string.amiibo_unknown_error) + if (!NativeLibrary.isRunning()) { + showAmiiboDialog(R.string.amiibo_wrong_state) return@registerForActivityResult } - val amiiboData = data ?: run { - showAmiiboDialog(R.string.amiibo_not_valid) - return@registerForActivityResult - } + amiiboLoadJob?.cancel() + val owner = viewLifecycleOwner + amiiboLoadJob = owner.lifecycleScope.launch { + val bytes = readAmiiboFile(uri) - if (amiiboData.isEmpty()) { - showAmiiboDialog(R.string.amiibo_not_valid) - return@registerForActivityResult - } + if (!isAdded) { + return@launch + } + + if (bytes == null || bytes.isEmpty()) { + showAmiiboDialog( + if (bytes == null) R.string.amiibo_unknown_error else R.string.amiibo_not_valid + ) + return@launch + } - val result = NativeLibrary.loadAmiibo(amiiboData) - handleAmiiboLoadResult(result) + val result = withContext(Dispatchers.IO) { + if (NativeLibrary.isRunning()) { + NativeLibrary.loadAmiibo(bytes) + } else { + AmiiboLoadResult.WrongDeviceState.value + } + } + + if (!isAdded) { + return@launch + } + handleAmiiboLoadResult(result) + }.also { job -> + job.invokeOnCompletion { + if (amiiboLoadJob == job) { + amiiboLoadJob = null + } + } + } } override fun onAttach(context: Context) { @@ -943,6 +969,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { when (AmiiboState.fromValue(NativeLibrary.getVirtualAmiiboState())) { AmiiboState.TagNearby -> { + amiiboLoadJob?.cancel() NativeInput.onRemoveNfcTag() showAmiiboDialog(R.string.amiibo_removed_message) } @@ -995,6 +1022,31 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { .show() } + private suspend fun readAmiiboFile(uri: Uri): ByteArray? = + withContext(Dispatchers.IO) { + val resolver = context?.contentResolver ?: return@withContext null + try { + resolver.openInputStream(uri)?.use { stream -> + val buffer = ByteArrayOutputStream() + val chunk = ByteArray(DEFAULT_BUFFER_SIZE) + while (true) { + coroutineContext.ensureActive() + val read = stream.read(chunk) + if (read == -1) { + break + } + buffer.write(chunk, 0, read) + } + buffer.toByteArray() + } + } catch (ce: CancellationException) { + throw ce + } catch (e: Exception) { + Log.error("[EmulationFragment] Failed to read amiibo: ${e.message}") + null + } + } + override fun onPause() { if (this::emulationState.isInitialized) { if (emulationState.isRunning && emulationActivity?.isInPictureInPictureMode != true) { @@ -1007,6 +1059,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { override fun onDestroyView() { super.onDestroyView() + amiiboLoadJob?.cancel() + amiiboLoadJob = null _binding = null isAmiiboPickerOpen = false }