diff options
Diffstat (limited to 'Source/Android/app/src/main/java/org')
4 files changed, 147 insertions, 19 deletions
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt index 6f8532a15c..fcad9ddb3f 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt @@ -4,8 +4,11 @@ package org.dolphinemu.dolphinemu.features.netplay import androidx.annotation.Keep +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.withContext import org.dolphinemu.dolphinemu.features.netplay.model.ConnectionType object Netplay { @@ -21,6 +24,12 @@ object Netplay { private val _launchGame = Channel<String>(Channel.CONFLATED) val launchGame = _launchGame.receiveAsFlow() + private val _connectionErrors = Channel<String>(Channel.BUFFERED) + val connectionErrors = _connectionErrors.receiveAsFlow() + + @JvmStatic + external fun isClientConnected(): Boolean + @JvmStatic external fun getNickname(): String @@ -63,13 +72,13 @@ object Netplay { @JvmStatic external fun getIndexPassword(): String - fun saveSetup( + suspend fun saveSetup( nickname: String, connectionType: ConnectionType, address: String, hostCode: String, connectPort: Int, - ) { + ) = withContext(Dispatchers.IO) { SaveSetup( nickname = nickname, traversalChoice = connectionType.configValue, @@ -108,16 +117,50 @@ object Netplay { indexPassword: String, ) - fun join() { + suspend fun join(): Boolean = withContext(Dispatchers.IO) { netPlayClientPointer = Join() + val isConnected = netPlayClientPointer != 0L && isClientConnected() + + if (!isActive) { + releaseNetplayClient() + return@withContext false + } + + if (isConnected) { + return@withContext true + } + + releaseNetplayClient() + false + } + + private fun releaseNetplayClient() { + if (netPlayClientPointer != 0L) { + ReleaseNetplayClient() + netPlayClientPointer = 0 + } + _launchGame.flush() + _connectionErrors.flush() } @JvmStatic private external fun Join(): Long @JvmStatic + private external fun ReleaseNetplayClient() + + @JvmStatic fun onBootGame(gameFilePath: String, bootSessionDataPointer: Long) { this.bootSessionDataPointer = bootSessionDataPointer _launchGame.trySend(gameFilePath) } + + @JvmStatic + fun onConnectionError(message: String) { + _connectionErrors.trySend(message) + } +} + +private fun Channel<String>.flush() { + while (this.tryReceive().isSuccess) Unit } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionRole.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionRole.kt index a54d082566..a9f86d5982 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionRole.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionRole.kt @@ -5,9 +5,17 @@ import org.dolphinemu.dolphinemu.R sealed class ConnectionRole( @StringRes val labelId: Int, + @StringRes val loadingLabelId: Int, ) { - object Connect : ConnectionRole(R.string.netplay_connection_role_connect) - object Host : ConnectionRole(R.string.netplay_connection_role_host) + object Connect : ConnectionRole( + labelId = R.string.netplay_connection_role_connect, + loadingLabelId = R.string.netplay_connection_role_connect_loading, + ) + + object Host : ConnectionRole( + labelId = R.string.netplay_connection_role_host, + loadingLabelId = R.string.netplay_connection_role_host_loading, + ) companion object { val all: List<ConnectionRole> diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt index 5ede907e85..19175e4df8 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt @@ -3,8 +3,15 @@ package org.dolphinemu.dolphinemu.features.netplay.model import androidx.lifecycle.ViewModel +import androidx.lifecycle.asFlow +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.Channel.Factory.CONFLATED import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch import org.dolphinemu.dolphinemu.features.netplay.Netplay import org.dolphinemu.dolphinemu.services.GameFileCacheManager @@ -27,6 +34,14 @@ class NetplaySetupViewModel : ViewModel() { private val _connectPort = MutableStateFlow(Netplay.getConnectPort().toString()) val connectPort = _connectPort.asStateFlow() + private val _showNetplayScreen = Channel<Unit>(CONFLATED) + val showNetplayScreen = _showNetplayScreen.receiveAsFlow() + + private val _connecting = MutableStateFlow(false) + val connecting = _connecting.asStateFlow() + + val errors = Netplay.connectionErrors + init { GameFileCacheManager.startLoad() } @@ -60,17 +75,24 @@ class NetplaySetupViewModel : ViewModel() { } fun connect() { - if (GameFileCacheManager.isLoading().value == true) { - return - } + _connecting.value = true - Netplay.saveSetup( - nickname = nickname.value, - connectionType = connectionType.value, - address = ipAddress.value, - hostCode = hostCode.value, - connectPort = connectPort.value.toInt(), - ) - Netplay.join() + viewModelScope.launch { + GameFileCacheManager.isLoading().asFlow().first { it == false } + + Netplay.saveSetup( + nickname = nickname.value, + connectionType = connectionType.value, + address = ipAddress.value, + hostCode = hostCode.value, + connectPort = connectPort.value.toInt(), + ) + + if (Netplay.join()) { + _showNetplayScreen.trySend(Unit) + } + + _connecting.value = false + } } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt index ca1a892901..5032546cb6 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt @@ -11,19 +11,24 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.AlertDialog import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -35,7 +40,9 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.SecondaryTabRow import androidx.compose.material3.Tab import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -45,8 +52,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.dolphinemu.dolphinemu.R @@ -60,6 +71,10 @@ import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer import org.dolphinemu.dolphinemu.utils.ThemeHelper +private data class ErrorDialogState(val message: String) { + val onDismissed = CompletableDeferred<Unit>() +} + class NetplaySetupActivity : AppCompatActivity(), ThemeProvider { override var themeId: Int = 0 private lateinit var viewModel: NetplaySetupViewModel @@ -75,10 +90,16 @@ class NetplaySetupActivity : AppCompatActivity(), ThemeProvider { viewModel = ViewModelProvider(this)[NetplaySetupViewModel::class.java] + viewModel.showNetplayScreen + .onEach { /* launch NetplayActivity */ } + .launchIn(lifecycleScope) + setContent { DolphinTheme { NetplaySetupScreen( onBackClicked = { finish() }, + connecting = viewModel.connecting.collectAsState().value, + errors = viewModel.errors, nickname = viewModel.nickname.collectAsState().value, onNicknameChanged = viewModel::setNickname, connectionType = viewModel.connectionType.collectAsState().value, @@ -118,6 +139,8 @@ class NetplaySetupActivity : AppCompatActivity(), ThemeProvider { @Composable private fun NetplaySetupScreen( onBackClicked: () -> Unit, + connecting: Boolean, + errors: Flow<String>, connectionRole: ConnectionRole, onConnectionRoleChanged: (ConnectionRole) -> Unit, nickname: String, @@ -150,10 +173,40 @@ private fun NetplaySetupScreen( ExtendedFloatingActionButton( onClick = onConnectClicked, ) { - Text(stringResource(connectionRole.labelId)) + if (connecting) { + CircularProgressIndicator( + modifier = Modifier.size(18.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.onPrimaryContainer, + ) + Spacer(Modifier.width(12.dp)) + Text(stringResource(connectionRole.loadingLabelId)) + } else { + Text(stringResource(connectionRole.labelId)) + } } } ) { innerPadding -> + var activeErrorDialog by remember { mutableStateOf<ErrorDialogState?>(null) } + LaunchedEffect(Unit) { + errors.collect { message -> + activeErrorDialog = ErrorDialogState(message) + activeErrorDialog?.onDismissed?.await() + activeErrorDialog = null + } + } + activeErrorDialog?.let { activeErrorDialog -> + AlertDialog( + text = { Text(activeErrorDialog.message) }, + confirmButton = { + TextButton(onClick = { activeErrorDialog.onDismissed.complete(Unit) }) { + Text("Dismiss") + } + }, + onDismissRequest = { activeErrorDialog.onDismissed.complete(Unit) }, + ) + } + Column( modifier = Modifier .fillMaxSize() @@ -332,12 +385,14 @@ private fun NetplaySetupScreenPreview() { MaterialTheme { NetplaySetupScreen( onBackClicked = {}, + connecting = false, + errors = emptyFlow(), + connectionRole = ConnectionRole.Connect, + onConnectionRoleChanged = {}, nickname = "Preview nickname", onNicknameChanged = {}, connectionType = ConnectionType.DirectConnection, onConnectionTypeChanged = {}, - connectionRole = ConnectionRole.Connect, - onConnectionRoleChanged = {}, ipAddress = "127.0.0.1", onIpAddressChanged = {}, connectPort = "2626", |
