summaryrefslogtreecommitdiff
path: root/Source/Android
diff options
context:
space:
mode:
authorTom Pratt <tom.pratt@outlook.com>2026-04-25 17:39:41 +0200
committerTom Pratt <tompratt@squareup.com>2026-05-19 12:02:57 +0200
commit8141a746713b87be595c94fe4598eb198395b8da (patch)
tree8cb64d65b671ffc3fedeed0e03491066f62442c4 /Source/Android
parent480968f05e03df0c860e84ac294593db91d74296 (diff)
Show save transfer progress
When transferring saves from the host. Equivalent of ChunkedProgressDialog in QT.
Diffstat (limited to 'Source/Android')
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt45
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt4
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/SaveTransferProgress.kt13
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt1
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt118
-rw-r--r--Source/Android/app/src/main/res/values/strings.xml1
-rw-r--r--Source/Android/jni/AndroidCommon/IDCache.cpp24
-rw-r--r--Source/Android/jni/AndroidCommon/IDCache.h3
-rw-r--r--Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp33
9 files changed, 227 insertions, 15 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 f4eb178cb4..882016dfa9 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
@@ -13,7 +13,9 @@ import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
@@ -26,6 +28,7 @@ import kotlinx.coroutines.withContext
import org.dolphinemu.dolphinemu.features.netplay.model.ConnectionType
import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage
import org.dolphinemu.dolphinemu.features.netplay.model.Player
+import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress
object Netplay {
@Keep
@@ -87,6 +90,9 @@ object Netplay {
)
val padBuffer = _padBuffer.asSharedFlow()
+ private val _saveTransferProgress = MutableStateFlow<SaveTransferProgress?>(null)
+ val saveTransferProgress = _saveTransferProgress.asStateFlow()
+
suspend fun join(): Boolean = withContext(Dispatchers.IO) {
val scope = createSessionScope()
@@ -140,6 +146,7 @@ object Netplay {
_game.resetReplayCache()
_hostInputAuthorityEnabled.resetReplayCache()
_padBuffer.resetReplayCache()
+ _saveTransferProgress.value = null
}
private fun createSessionScope(): CoroutineScope {
@@ -234,6 +241,44 @@ object Netplay {
_padBuffer.tryEmit(buffer)
}
+ @Keep
+ @JvmStatic
+ fun onShowChunkedProgressDialog(title: String, dataSize: Long, playerIds: IntArray) {
+ val players = _players.replayCache.firstOrNull()
+ _saveTransferProgress.value = SaveTransferProgress(
+ title = title,
+ totalSize = dataSize,
+ playerProgresses = playerIds.map { playerId ->
+ SaveTransferProgress.PlayerProgress(
+ playerId = playerId,
+ name = players?.find { it.pid == playerId }?.name ?: "Invalid Player ID",
+ progress = 0,
+ )
+ },
+ )
+ }
+
+ @Keep
+ @JvmStatic
+ fun onSetChunkedProgress(playerId: Int, progress: Long) {
+ val current = _saveTransferProgress.value
+ _saveTransferProgress.value = current?.copy(
+ playerProgresses = current.playerProgresses.map {
+ if (it.playerId == playerId) {
+ it.copy(progress = progress)
+ } else {
+ it
+ }
+ }
+ )
+ }
+
+ @Keep
+ @JvmStatic
+ fun onHideChunkedProgressDialog() {
+ _saveTransferProgress.value = null
+ }
+
// Settings
object Settings {
@JvmStatic
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 955fb876dd..620a66ff5a 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
@@ -2,8 +2,6 @@
package org.dolphinemu.dolphinemu.features.netplay.model
-import android.app.Application
-import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.DelicateCoroutinesApi
@@ -42,6 +40,8 @@ class NetplayViewModel : ViewModel() {
private val _maxBuffer = MutableStateFlow(Netplay.Settings.getClientBufferSize())
val maxBuffer = _maxBuffer.asStateFlow()
+ val saveTransferProgress = Netplay.saveTransferProgress
+
init {
if (!Netplay.isClientConnected()) {
_goBack.trySend(Unit)
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/SaveTransferProgress.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/SaveTransferProgress.kt
new file mode 100644
index 0000000000..e16cbab8ca
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/SaveTransferProgress.kt
@@ -0,0 +1,13 @@
+package org.dolphinemu.dolphinemu.features.netplay.model
+
+data class SaveTransferProgress(
+ val title: String,
+ val totalSize: Long,
+ val playerProgresses: List<PlayerProgress>
+) {
+ data class PlayerProgress(
+ val playerId: Int,
+ val name: String,
+ val progress: Long,
+ )
+}
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 463ad31506..8a8f860825 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
@@ -52,6 +52,7 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider {
hostInputAuthorityEnabled = viewModel.hostInputAuthority.collectAsState().value,
maxBuffer = viewModel.maxBuffer.collectAsState().value,
onMaxBufferChanged = viewModel::setMaxBuffer,
+ saveTransferProgress = viewModel.saveTransferProgress.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 80914e952c..40fe1fe5ec 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
@@ -34,6 +34,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
+import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MediumTopAppBar
@@ -70,10 +71,12 @@ import kotlinx.coroutines.flow.emptyFlow
import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage
import org.dolphinemu.dolphinemu.features.netplay.model.Player
+import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress
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 java.util.Locale
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -87,6 +90,7 @@ fun NetplayScreen(
maxBuffer: Int,
onMaxBufferChanged: (Int) -> Unit,
players: List<Player>,
+ saveTransferProgress: SaveTransferProgress?,
) {
Scaffold(
topBar = {
@@ -136,16 +140,31 @@ fun NetplayScreen(
LaunchedEffect(Unit) {
connectionLost.collect { showConnectionLostDialog = true }
}
- if (showConnectionLostDialog) {
- AlertDialog(
- text = { Text(stringResource(R.string.netplay_connection_lost)) },
- confirmButton = {
- TextButton(onClick = onBackClicked) {
- Text(stringResource(R.string.ok))
- }
- },
- onDismissRequest = onBackClicked,
- )
+
+ var dismissSaveTransferProgressDialog by remember { mutableStateOf(false) }
+ if (saveTransferProgress == null) {
+ dismissSaveTransferProgressDialog = false
+ }
+
+ when {
+ showConnectionLostDialog -> {
+ AlertDialog(
+ text = { Text(stringResource(R.string.netplay_connection_lost)) },
+ confirmButton = {
+ TextButton(onClick = onBackClicked) {
+ Text(stringResource(R.string.ok))
+ }
+ },
+ onDismissRequest = onBackClicked,
+ )
+ }
+
+ saveTransferProgress != null && !dismissSaveTransferProgressDialog -> {
+ SaveTransferProgressDialog(
+ saveTransferProgress = saveTransferProgress,
+ onDismiss = { dismissSaveTransferProgressDialog = true },
+ )
+ }
}
}
}
@@ -519,6 +538,68 @@ private fun BufferInput(
}
}
+@Composable
+private fun SaveTransferProgressDialog(
+ saveTransferProgress: SaveTransferProgress,
+ onDismiss: () -> Unit,
+) {
+ AlertDialog(
+ title = { Text(saveTransferProgress.title) },
+ text = {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
+ saveTransferProgress.playerProgresses.forEachIndexed { index, playerProgress ->
+ SaveTransferProgressRow(
+ playerProgress = playerProgress,
+ totalSize = saveTransferProgress.totalSize,
+ )
+
+ if (index < saveTransferProgress.playerProgresses.lastIndex) {
+ HorizontalDivider()
+ }
+ }
+ }
+ },
+ confirmButton = {
+ TextButton(onClick = onDismiss) {
+ Text(stringResource(R.string.netplay_save_transfer_progress_close))
+ }
+ },
+ onDismissRequest = onDismiss,
+ )
+}
+
+@Composable
+private fun SaveTransferProgressRow(
+ playerProgress: SaveTransferProgress.PlayerProgress,
+ totalSize: Long,
+) {
+ fun formatMib(bytes: Long) = String.format(Locale.US, "%.2f", bytes / 1024f / 1024f)
+ val progressFraction = (playerProgress.progress.toFloat() / totalSize).coerceIn(0f, 1f)
+
+ Column(
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ LinearProgressIndicator(
+ progress = { progressFraction },
+ modifier = Modifier.fillMaxWidth(),
+ )
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ Text(
+ text = playerProgress.name,
+ modifier = Modifier.weight(1f),
+ )
+ Spacer(modifier = Modifier.width(16.dp))
+ Text(
+ text = "${formatMib(playerProgress.progress)}/${formatMib(totalSize)} MiB",
+ )
+ }
+ }
+}
+
@Preview
@Composable
private fun NetplayScreenPreview() {
@@ -589,5 +670,22 @@ private fun PreviewNetplayScreen() {
hostInputAuthorityEnabled = true,
maxBuffer = 10,
onMaxBufferChanged = {},
+ saveTransferProgress = null,
+// saveTransferProgress = SaveTransferProgress(
+// title = "Title",
+// totalSize = 1024L,
+// playerProgresses = listOf(
+// SaveTransferProgress.PlayerProgress(
+// playerId = 1,
+// name = "Player 1",
+// progress = 256,
+// ),
+// SaveTransferProgress.PlayerProgress(
+// playerId = 2,
+// name = "Player 2",
+// progress = 512,
+// ),
+// ),
+// ),
)
}
diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml
index 7b4311c996..cc1866d473 100644
--- a/Source/Android/app/src/main/res/values/strings.xml
+++ b/Source/Android/app/src/main/res/values/strings.xml
@@ -1009,4 +1009,5 @@ It can efficiently compress both junk data and encrypted Wii data.
<string name="netplay_players_mapping">Mapping</string>
<string name="netplay_max_buffer">Max buffer</string>
<string name="netplay_connection_lost">Netplay connection lost</string>
+ <string name="netplay_save_transfer_progress_close">Close</string>
</resources>
diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp
index 461576f217..150cb8162e 100644
--- a/Source/Android/jni/AndroidCommon/IDCache.cpp
+++ b/Source/Android/jni/AndroidCommon/IDCache.cpp
@@ -40,6 +40,9 @@ static jmethodID s_netplay_on_host_input_authority_changed;
static jmethodID s_netplay_on_pad_buffer_changed;
static jmethodID s_netplay_on_chat_message_received;
static jmethodID s_netplay_update;
+static jmethodID s_netplay_on_show_chunked_progress_dialog;
+static jmethodID s_netplay_on_set_chunked_progress;
+static jmethodID s_netplay_on_hide_chunked_progress_dialog;
static jclass s_netplay_player_class;
static jmethodID s_netplay_player_constructor;
@@ -309,6 +312,21 @@ jmethodID GetNetplayUpdate()
return s_netplay_update;
}
+jmethodID GetNetplayOnShowChunkedProgressDialog()
+{
+ return s_netplay_on_show_chunked_progress_dialog;
+}
+
+jmethodID GetNetplayOnSetChunkedProgress()
+{
+ return s_netplay_on_set_chunked_progress;
+}
+
+jmethodID GetNetplayOnHideChunkedProgressDialog()
+{
+ return s_netplay_on_hide_chunked_progress_dialog;
+}
+
jclass GetNetplayPlayerClass()
{
return s_netplay_player_class;
@@ -741,6 +759,12 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
s_netplay_on_chat_message_received =
env->GetStaticMethodID(netplay_class, "onChatMessageReceived", "(Ljava/lang/String;)V");
s_netplay_update = env->GetStaticMethodID(netplay_class, "onUpdate", "([Lorg/dolphinemu/dolphinemu/features/netplay/model/Player;)V");
+ s_netplay_on_show_chunked_progress_dialog =
+ env->GetStaticMethodID(netplay_class, "onShowChunkedProgressDialog", "(Ljava/lang/String;J[I)V");
+ s_netplay_on_set_chunked_progress =
+ env->GetStaticMethodID(netplay_class, "onSetChunkedProgress", "(IJ)V");
+ s_netplay_on_hide_chunked_progress_dialog =
+ env->GetStaticMethodID(netplay_class, "onHideChunkedProgressDialog", "()V");
env->DeleteLocalRef(netplay_class);
const jclass netplay_player_class =
diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h
index c535fcb9ae..4041b5060e 100644
--- a/Source/Android/jni/AndroidCommon/IDCache.h
+++ b/Source/Android/jni/AndroidCommon/IDCache.h
@@ -43,6 +43,9 @@ jmethodID GetNetplayOnHostInputAuthorityChanged();
jmethodID GetNetplayOnPadBufferChanged();
jmethodID GetNetplayOnChatMessageReceived();
jmethodID GetNetplayUpdate();
+jmethodID GetNetplayOnShowChunkedProgressDialog();
+jmethodID GetNetplayOnSetChunkedProgress();
+jmethodID GetNetplayOnHideChunkedProgressDialog();
jclass GetNetplayPlayerClass();
jmethodID GetNetplayPlayerConstructor();
diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp
index 21dd77d019..fa013017dc 100644
--- a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp
+++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp
@@ -186,9 +186,36 @@ void NetPlayUICallbacks::ShowGameDigestDialog(const std::string&) {}
void NetPlayUICallbacks::SetGameDigestProgress(int, int) {}
void NetPlayUICallbacks::SetGameDigestResult(int, const std::string&) {}
void NetPlayUICallbacks::AbortGameDigest() {}
-void NetPlayUICallbacks::ShowChunkedProgressDialog(const std::string&, u64, std::span<const int>) {}
-void NetPlayUICallbacks::HideChunkedProgressDialog() {}
-void NetPlayUICallbacks::SetChunkedProgress(int, u64) {}
+
+void NetPlayUICallbacks::ShowChunkedProgressDialog(const std::string& title, u64 data_size,
+ std::span<const int> players)
+{
+ JNIEnv* env = IDCache::GetEnvForThread();
+
+ jintArray j_players = env->NewIntArray(static_cast<jsize>(players.size()));
+ env->SetIntArrayRegion(j_players, 0, static_cast<jsize>(players.size()), players.data());
+
+ env->CallStaticVoidMethod(IDCache::GetNetplayClass(),
+ IDCache::GetNetplayOnShowChunkedProgressDialog(),
+ ToJString(env, title), static_cast<jlong>(data_size), j_players);
+ env->DeleteLocalRef(j_players);
+}
+
+void NetPlayUICallbacks::HideChunkedProgressDialog()
+{
+ JNIEnv* env = IDCache::GetEnvForThread();
+ env->CallStaticVoidMethod(IDCache::GetNetplayClass(),
+ IDCache::GetNetplayOnHideChunkedProgressDialog());
+}
+
+void NetPlayUICallbacks::SetChunkedProgress(int pid, u64 progress)
+{
+ JNIEnv* env = IDCache::GetEnvForThread();
+ env->CallStaticVoidMethod(IDCache::GetNetplayClass(),
+ IDCache::GetNetplayOnSetChunkedProgress(),
+ static_cast<jint>(pid), static_cast<jlong>(progress));
+}
+
void NetPlayUICallbacks::SetHostWiiSyncData(std::vector<u64>, std::string) {}
} // namespace NetPlay