diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt index e80a6d3c06..16314e4076 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt @@ -154,6 +154,10 @@ class NetplaySession( fun startGame() = nativeStartGame() + fun getPort(): Int = nativeGetPort() + + fun getExternalIpAddress(): String? = nativeGetExternalIpAddress() + fun consumeBootSessionData(): Long { return bootSessionDataPointer.also { bootSessionDataPointer = 0 @@ -235,6 +239,10 @@ class NetplaySession( private external fun nativeStartGame() + private external fun nativeGetPort(): Int + + private external fun nativeGetExternalIpAddress(): String? + // NetPlayUI callbacks @Keep diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/JoinInfo.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/JoinInfo.kt new file mode 100644 index 0000000000..ba350a3caf --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/JoinInfo.kt @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.netplay.model + +import androidx.annotation.StringRes +import org.dolphinemu.dolphinemu.R + +enum class JoinInfoType(@StringRes val labelId: Int) { + EXTERNAL(R.string.netplay_address_type_external), + LOCAL(R.string.netplay_address_type_local), +} + +sealed class JoinAddress { + data object Loading : JoinAddress() + data class Loaded(val address: String) : JoinAddress() + data class Unknown(val retry: () -> Unit) : JoinAddress() +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt index 50edaba597..923cfc314d 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.asFlow import androidx.lifecycle.viewModelScope import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -20,15 +21,25 @@ import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig import org.dolphinemu.dolphinemu.features.settings.model.StringSetting import org.dolphinemu.dolphinemu.model.GameFile import org.dolphinemu.dolphinemu.services.GameFileCacheManager +import org.dolphinemu.dolphinemu.utils.NetworkHelper class NetplayViewModel( private val netplaySession: NetplaySession, + private val networkHelper: NetworkHelper, ) : ViewModel() { val launchGame = netplaySession.launchGame val isHosting = netplaySession.isHosting + private val _joinAddresses = MutableStateFlow( + mapOf( + JoinInfoType.EXTERNAL to JoinAddress.Loading, + JoinInfoType.LOCAL to getLocalIp(), + ) + ) + val joinAddresses = _joinAddresses.asStateFlow() + val connectionLost = netplaySession.connectionLost val players = netplaySession.players @@ -61,6 +72,7 @@ class NetplayViewModel( init { if (netplaySession.isHosting) { setInitialGame() + fetchExternalIp() } } @@ -88,6 +100,24 @@ class NetplayViewModel( netplaySession.changeGame(gameFile) } + private fun getLocalIp(): JoinAddress { + val localIp = networkHelper.getLocalIpString() + ?: return JoinAddress.Unknown { _joinAddresses.value += JoinInfoType.LOCAL to getLocalIp() } + val port = netplaySession.getPort() + return JoinAddress.Loaded("$localIp:$port") + } + + private fun fetchExternalIp() { + _joinAddresses.value += JoinInfoType.EXTERNAL to JoinAddress.Loading + viewModelScope.launch(Dispatchers.IO) { + val ip = netplaySession.getExternalIpAddress() + val port = netplaySession.getPort() + val address = if (ip != null) JoinAddress.Loaded("$ip:$port") + else JoinAddress.Unknown { fetchExternalIp() } + _joinAddresses.value += JoinInfoType.EXTERNAL to address + } + } + private fun setInitialGame() { val game = gameFiles.value .find { it.getGameId() == StringSetting.NETPLAY_GAME.string } @@ -108,10 +138,13 @@ class NetplayViewModel( } } - class Factory(private val session: NetplaySession) : ViewModelProvider.Factory { + class Factory( + private val session: NetplaySession, + private val networkHelper: NetworkHelper, + ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T { - return NetplayViewModel(session) as T + return NetplayViewModel(session, networkHelper) as T } } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt index 55888dde6d..a9bf0309a5 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt @@ -21,6 +21,7 @@ import org.dolphinemu.dolphinemu.features.netplay.NetplayManager import org.dolphinemu.dolphinemu.features.netplay.model.NetplayViewModel import org.dolphinemu.dolphinemu.ui.main.ThemeProvider import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme +import org.dolphinemu.dolphinemu.utils.NetworkHelper import org.dolphinemu.dolphinemu.utils.ThemeHelper class NetplayActivity : AppCompatActivity(), ThemeProvider { @@ -37,7 +38,7 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider { return } - val viewModel = ViewModelProvider(this, NetplayViewModel.Factory(session))[NetplayViewModel::class.java] + val viewModel = ViewModelProvider(this, NetplayViewModel.Factory(session, NetworkHelper))[NetplayViewModel::class.java] viewModel.launchGame .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) @@ -62,6 +63,7 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider { onMaxBufferChanged = viewModel::setMaxBuffer, saveTransferProgress = viewModel.saveTransferProgress.collectAsState().value, gameDigestProgress = viewModel.gameDigestProgress.collectAsState().value, + joinAddresses = viewModel.joinAddresses.collectAsState().value, ) } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt index a5c20bdd22..db70f8a3e9 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt @@ -2,10 +2,11 @@ package org.dolphinemu.dolphinemu.features.netplay.ui +import android.content.Intent import android.content.res.Configuration -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -17,6 +18,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn @@ -32,11 +34,17 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Remove +import androidx.compose.material.icons.filled.Share import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -45,6 +53,7 @@ import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MediumTopAppBar +import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold @@ -62,7 +71,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalConfiguration @@ -84,6 +92,8 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import org.dolphinemu.dolphinemu.R import org.dolphinemu.dolphinemu.features.netplay.model.GameDigestProgress +import org.dolphinemu.dolphinemu.features.netplay.model.JoinAddress +import org.dolphinemu.dolphinemu.features.netplay.model.JoinInfoType import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage import org.dolphinemu.dolphinemu.features.netplay.model.Player import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress @@ -92,6 +102,7 @@ import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer import org.dolphinemu.dolphinemu.ui.theme.OutlinedBox import org.dolphinemu.dolphinemu.ui.theme.PreviewTheme +import org.dolphinemu.dolphinemu.ui.theme.ReadOnlyTextField import org.dolphinemu.dolphinemu.utils.CoilUtils import java.util.Locale @@ -113,6 +124,7 @@ fun NetplayScreen( players: List, saveTransferProgress: SaveTransferProgress?, gameDigestProgress: GameDigestProgress?, + joinAddresses: Map, ) { Scaffold( topBar = { @@ -141,8 +153,10 @@ fun NetplayScreen( .consumeWindowInsets(innerPadding) .padding(innerPadding) + // State which must live above the landscape/portrait split. var showChat by rememberSaveable { mutableStateOf(false) } var showGamePicker by rememberSaveable { mutableStateOf(false) } + var selectedJoinInfoType by rememberSaveable { mutableStateOf(JoinInfoType.EXTERNAL) } if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) { LandscapeContent( @@ -160,6 +174,9 @@ fun NetplayScreen( hostInputAuthorityEnabled = hostInputAuthorityEnabled, maxBuffer = maxBuffer, onMaxBufferChanged = onMaxBufferChanged, + joinAddresses = joinAddresses, + selectedJoinInfoType = selectedJoinInfoType, + onSelectedJoinInfoTypeChanged = { selectedJoinInfoType = it }, modifier = modifier ) } else { @@ -178,6 +195,9 @@ fun NetplayScreen( hostInputAuthorityEnabled = hostInputAuthorityEnabled, maxBuffer = maxBuffer, onMaxBufferChanged = onMaxBufferChanged, + joinAddresses = joinAddresses, + selectedJoinInfoType = selectedJoinInfoType, + onSelectedJoinInfoTypeChanged = { selectedJoinInfoType = it }, modifier = modifier ) } @@ -243,6 +263,9 @@ private fun PortraitContent( hostInputAuthorityEnabled: Boolean, maxBuffer: Int, onMaxBufferChanged: (Int) -> Unit, + joinAddresses: Map, + selectedJoinInfoType: JoinInfoType, + onSelectedJoinInfoTypeChanged: (JoinInfoType) -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -273,6 +296,9 @@ private fun PortraitContent( maxBuffer = maxBuffer, onMaxBufferChanged = onMaxBufferChanged, isHosting = isHosting, + joinAddresses = joinAddresses, + selectedJoinInfoType = selectedJoinInfoType, + onSelectedJoinInfoTypeChanged = onSelectedJoinInfoTypeChanged, modifier = Modifier .padding(horizontal = DolphinTheme.scaffoldPadding), ) @@ -299,10 +325,14 @@ private fun LandscapeContent( hostInputAuthorityEnabled: Boolean, maxBuffer: Int, onMaxBufferChanged: (Int) -> Unit, + joinAddresses: Map, + selectedJoinInfoType: JoinInfoType, + onSelectedJoinInfoTypeChanged: (JoinInfoType) -> Unit, modifier: Modifier = Modifier, ) { Row( modifier = modifier + .padding(horizontal = DolphinTheme.scaffoldPadding) ) { Chat( messages = messages, @@ -312,9 +342,10 @@ private fun LandscapeContent( modifier = Modifier .weight(1f) .fillMaxHeight() - .padding(horizontal = DolphinTheme.scaffoldPadding) ) + Spacer(modifier = Modifier.width(16.dp)) + Column( modifier = Modifier .weight(1f) @@ -331,8 +362,10 @@ private fun LandscapeContent( maxBuffer = maxBuffer, onMaxBufferChanged = onMaxBufferChanged, isHosting = isHosting, + joinAddresses = joinAddresses, + selectedJoinInfoType = selectedJoinInfoType, + onSelectedJoinInfoTypeChanged = onSelectedJoinInfoTypeChanged, modifier = Modifier - .padding(horizontal = DolphinTheme.scaffoldPadding) ) if (isHosting) { @@ -354,6 +387,9 @@ private fun PLayersAndSettings( maxBuffer: Int, onMaxBufferChanged: (Int) -> Unit, isHosting: Boolean, + joinAddresses: Map, + selectedJoinInfoType: JoinInfoType, + onSelectedJoinInfoTypeChanged: (JoinInfoType) -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -368,6 +404,16 @@ private fun PLayersAndSettings( isHosting = isHosting, ) + if (isHosting) { + MenuSpacer() + + JoinAddressSection( + joinAddresses = joinAddresses, + selectedType = selectedJoinInfoType, + onSelectedTypeChanged = onSelectedJoinInfoTypeChanged, + ) + } + MenuSpacer() OutlinedBox( @@ -479,6 +525,7 @@ private fun Chat( OutlinedBox( onClick = { onShowBottomSheetChanged(true) }, label = { Text(stringResource(R.string.netplay_chat_label)) }, + fadeContentTop = true, modifier = modifier ) { LazyColumn( @@ -532,26 +579,16 @@ private fun GamePicker( } } - Box( + ReadOnlyTextField( + value = game, + label = stringResource(R.string.netplay_game_label), + onClick = if (isHosting) { + { onShowGamePickerChanged(true) } + } else { + null + }, modifier = Modifier.fillMaxWidth() - ) { - OutlinedTextField( - value = game, - onValueChange = {}, - label = { Text(stringResource(R.string.netplay_game_label)) }, - readOnly = true, - modifier = Modifier.fillMaxWidth() - ) - if (isHosting) { - Box( - modifier = Modifier - .matchParentSize() - .padding(top = 8.dp) - .clip(MaterialTheme.shapes.extraSmall) - .clickable { onShowGamePickerChanged(true) } - ) - } - } + ) } @Composable @@ -609,6 +646,150 @@ private fun GameGridItem( } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun JoinAddressSection( + joinAddresses: Map, + selectedType: JoinInfoType, + onSelectedTypeChanged: (JoinInfoType) -> Unit, +) { + val address = joinAddresses[selectedType] ?: joinAddresses.values.first() + + @Suppress("UnusedBoxWithConstraintsScope") + BoxWithConstraints(modifier = Modifier.fillMaxWidth()) { + if (maxWidth > 392.dp) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth() + ) { + JoinInfoDropdown( + joinAddresses = joinAddresses, + selectedType = selectedType, + onSelectedTypeChanged = onSelectedTypeChanged, + modifier = Modifier.weight(0.39f), + ) + AddressRow( + address = address, + modifier = Modifier.weight(0.61f), + ) + } + } else { + Column(modifier = Modifier.fillMaxWidth()) { + JoinInfoDropdown( + joinAddresses = joinAddresses, + selectedType = selectedType, + onSelectedTypeChanged = onSelectedTypeChanged, + modifier = Modifier.fillMaxWidth(), + ) + MenuSpacer() + AddressRow( + address = address, + modifier = Modifier.fillMaxWidth(), + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun JoinInfoDropdown( + joinAddresses: Map, + selectedType: JoinInfoType, + onSelectedTypeChanged: (JoinInfoType) -> Unit, + modifier: Modifier = Modifier, +) { + var expanded by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = it }, + modifier = modifier, + ) { + OutlinedTextField( + value = stringResource(selectedType.labelId), + onValueChange = {}, + readOnly = true, + label = { Text(stringResource(R.string.netplay_host_address_label)) }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) }, + modifier = Modifier + .menuAnchor(MenuAnchorType.PrimaryNotEditable) + .fillMaxWidth() + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + ) { + joinAddresses.keys.forEach { type -> + DropdownMenuItem( + text = { Text(stringResource(type.labelId)) }, + onClick = { + onSelectedTypeChanged(type) + expanded = false + }, + contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding, + ) + } + } + } +} + +@Composable +private fun AddressRow( + address: JoinAddress, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + + ReadOnlyTextField( + value = when (address) { + is JoinAddress.Loading -> stringResource(R.string.netplay_address_loading) + is JoinAddress.Loaded -> address.address + is JoinAddress.Unknown -> stringResource(R.string.netplay_address_unknown) + }, + label = stringResource(R.string.netplay_address_label), + onClick = when (address) { + is JoinAddress.Loaded -> { + { + val intent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, address.address) + } + context.startActivity(Intent.createChooser(intent, null)) + } + } + + is JoinAddress.Unknown -> address.retry + is JoinAddress.Loading -> null + }, + textStyle = if (address is JoinAddress.Loading) { + LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.onSurfaceVariant) + } else { + null + }, + trailingIcon = { + when (address) { + is JoinAddress.Loaded -> Icon( + imageVector = Icons.Filled.Share, + contentDescription = stringResource(R.string.netplay_address_share), + ) + + is JoinAddress.Unknown -> Icon( + imageVector = Icons.Filled.Refresh, + contentDescription = stringResource(R.string.netplay_address_retry), + ) + + is JoinAddress.Loading -> CircularProgressIndicator( + modifier = Modifier.size(24.dp), + strokeWidth = 2.dp, + ) + } + }, + modifier = modifier, + ) +} + /** * A table arranged into columns sized to wrap the largest item. Except the * first column which takes up the remaining space left by the other columns. @@ -981,6 +1162,10 @@ private fun PreviewNetplayScreen() { onMaxBufferChanged = {}, saveTransferProgress = null, gameDigestProgress = null, + joinAddresses = mapOf( + JoinInfoType.EXTERNAL to JoinAddress.Loaded("203.0.113.1:2626"), + JoinInfoType.LOCAL to JoinAddress.Loaded("192.168.1.5:2626"), + ), // saveTransferProgress = SaveTransferProgress( // title = "Title", // totalSize = 1024L, diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt index d25a345862..c5fa746346 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt @@ -17,8 +17,11 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.ColorScheme import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable @@ -28,6 +31,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import com.google.android.material.color.MaterialColors @@ -205,3 +209,35 @@ fun OutlinedBox( } } } + +@Composable +fun ReadOnlyTextField( + value: String, + label: String, + modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null, + trailingIcon: (@Composable () -> Unit)? = null, + textStyle: TextStyle? = null, +) { + Box(modifier = modifier) { + OutlinedTextField( + value = value, + onValueChange = {}, + readOnly = true, + singleLine = true, + label = { Text(label) }, + trailingIcon = trailingIcon, + textStyle = textStyle ?: LocalTextStyle.current, + modifier = Modifier.fillMaxWidth() + ) + if (onClick != null) { + Box( + modifier = Modifier + .matchParentSize() + .padding(top = 8.dp) + .clip(MaterialTheme.shapes.extraSmall) + .clickable(onClick = onClick) + ) + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/NetworkHelper.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/NetworkHelper.kt index 7bd2068ee2..231f15fda1 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/NetworkHelper.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/NetworkHelper.kt @@ -75,4 +75,9 @@ object NetworkHelper { 0 } } + + @JvmStatic + fun getLocalIpString(): String? { + return getIPv4Link()?.address?.hostAddress + } } diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 9c89d3aed7..20c78eddc5 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -1016,4 +1016,12 @@ It can efficiently compress both junk data and encrypted Wii data. Close Port Forward port (UPnP) + Join info + Address + External IP + Local IP + Loading… + Unknown + Share address + Retry diff --git a/Source/Android/jni/NetPlay/Netplay.cpp b/Source/Android/jni/NetPlay/Netplay.cpp index 527ca40f40..9192404042 100644 --- a/Source/Android/jni/NetPlay/Netplay.cpp +++ b/Source/Android/jni/NetPlay/Netplay.cpp @@ -8,6 +8,7 @@ #include #include "Common/CommonTypes.h" +#include "Core/NetPlayCommon.h" #include "Core/Boot/Boot.h" #include "Core/Config/NetplaySettings.h" #include "Core/NetPlayClient.h" @@ -174,6 +175,25 @@ Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeStartGame(J server->RequestStartGame(); } +JNIEXPORT jint JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeGetPort(JNIEnv* env, + jobject obj) +{ + if (auto* server = GetServerPointer(env, obj)) + return static_cast(server->GetPort()); + return 0; +} + +JNIEXPORT jstring JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeGetExternalIpAddress( + JNIEnv* env, jobject) +{ + std::string ip = NetPlay::GetExternalIPAddress(); + if (ip.empty()) + return nullptr; + return ToJString(env, ip); +} + JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeReleaseUICallbacks(JNIEnv*, jobject,