Skip to content

Commit fc196dc

Browse files
committed
feat(android): add file-manager MTU export destination selection via SAF and export results on disconnect
1 parent d7e4e04 commit fc196dc

3 files changed

Lines changed: 69 additions & 16 deletions

File tree

android/app/src/main/java/com/masterdns/vpn/service/MasterDnsVpnService.kt

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.app.Notification
44
import android.app.PendingIntent
55
import android.content.Intent
66
import android.net.VpnService
7+
import android.net.Uri
78
import android.os.Build
89
import android.os.ParcelFileDescriptor
910
import android.os.PowerManager
@@ -20,6 +21,7 @@ import com.masterdns.vpn.util.GlobalSettingsStore
2021
import com.masterdns.vpn.util.VpnManager
2122
import kotlinx.coroutines.*
2223
import java.io.File
24+
import java.io.FileInputStream
2325
import java.io.RandomAccessFile
2426
import java.net.InetAddress
2527
import java.net.InetSocketAddress
@@ -46,6 +48,8 @@ class MasterDnsVpnService : VpnService() {
4648
private var goClientJob: Job? = null
4749
private var logTailJob: Job? = null
4850
private var wakeLock: PowerManager.WakeLock? = null
51+
private var mtuExportTargetUri: String? = null
52+
private var mtuTempOutputDir: File? = null
4953
@Volatile
5054
private var isStopping = false
5155
@Volatile
@@ -102,24 +106,31 @@ class MasterDnsVpnService : VpnService() {
102106

103107
val configFile = File(configDir, "client_config.toml")
104108
val resolversFile = File(configDir, "client_resolvers.txt")
105-
val advanced = parseAdvanced(profile.advancedJson).toMutableMap()
109+
mtuExportTargetUri = null
110+
mtuTempOutputDir = null
111+
val advanced = parseAdvanced(profile.advancedJson)
106112
val saveMtuToFile = advanced["SAVE_MTU_SERVERS_TO_FILE"].equals("true", ignoreCase = true)
107113
var runtimeProfile = profile
108114
if (saveMtuToFile) {
109-
val mtuDir = File(getExternalFilesDir(null), "masterdnsvpn_mtu")
110-
if (!mtuDir.exists()) {
111-
mtuDir.mkdirs()
112-
}
113-
val fileName = advanced["MTU_SERVERS_FILE_NAME"]
115+
val configuredPath = advanced["MTU_SERVERS_FILE_NAME"]
114116
?.trim()
115117
?.ifBlank { "masterdnsvpn_success_test_{time}.log" }
116118
?: "masterdnsvpn_success_test_{time}.log"
117-
val cleanName = File(fileName).name
118-
val outputPath = File(mtuDir, cleanName).absolutePath
119-
advanced["MTU_SERVERS_FILE_NAME"] = outputPath
120-
runtimeProfile = profile.copy(advancedJson = Gson().toJson(advanced))
121-
VpnManager.appendLog("MTU results folder: ${mtuDir.absolutePath}")
122-
VpnManager.appendLog("MTU results file pattern: $outputPath")
119+
val exportUri = advanced["MTU_EXPORT_URI"]?.trim().orEmpty()
120+
if (exportUri.isNotBlank()) {
121+
val advancedMutable = advanced.toMutableMap()
122+
val safeName = File(configuredPath).name.ifBlank { "masterdnsvpn_success_test_{time}.log" }
123+
val tmpDir = File(filesDir, "mtu_exports").apply { mkdirs() }
124+
val tmpPath = File(tmpDir, safeName).absolutePath
125+
advancedMutable["MTU_SERVERS_FILE_NAME"] = tmpPath
126+
runtimeProfile = profile.copy(advancedJson = Gson().toJson(advancedMutable))
127+
mtuExportTargetUri = exportUri
128+
mtuTempOutputDir = tmpDir
129+
VpnManager.appendLog("MTU results temp path: $tmpPath")
130+
VpnManager.appendLog("MTU export destination selected via file manager")
131+
} else {
132+
VpnManager.appendLog("MTU results target: $configuredPath")
133+
}
123134
}
124135

125136
configFile.writeText(
@@ -131,10 +142,10 @@ class MasterDnsVpnService : VpnService() {
131142
localDnsEnabledOverride = if (proxyMode) false else null
132143
)
133144
)
134-
if (profile.resolvers.isNotBlank()) {
135-
resolversFile.writeText(ConfigGenerator.generateResolvers(profile))
145+
if (runtimeProfile.resolvers.isNotBlank()) {
146+
resolversFile.writeText(ConfigGenerator.generateResolvers(runtimeProfile))
136147
} else if (!resolversFile.exists() || resolversFile.readText().isBlank()) {
137-
resolversFile.writeText(ConfigGenerator.generateResolvers(profile))
148+
resolversFile.writeText(ConfigGenerator.generateResolvers(runtimeProfile))
138149
} else {
139150
VpnManager.appendLog("Using existing client_resolvers.txt from app storage")
140151
}
@@ -280,6 +291,7 @@ class MasterDnsVpnService : VpnService() {
280291
VpnManager.updateState(VpnManager.VpnState.DISCONNECTED)
281292
VpnManager.stopTrafficMonitor()
282293
VpnManager.appendLog("VPN disconnected")
294+
exportMtuResultsIfNeeded()
283295
releaseWakeLock()
284296

285297
runCatching {
@@ -377,6 +389,25 @@ class MasterDnsVpnService : VpnService() {
377389
}
378390
}
379391

392+
private fun exportMtuResultsIfNeeded() {
393+
val target = mtuExportTargetUri?.takeIf { it.isNotBlank() } ?: return
394+
val dir = mtuTempOutputDir ?: return
395+
val latest = dir.listFiles()
396+
?.filter { it.isFile }
397+
?.maxByOrNull { it.lastModified() }
398+
?: return
399+
runCatching {
400+
val uri = Uri.parse(target)
401+
contentResolver.openOutputStream(uri, "wt")?.use { out ->
402+
FileInputStream(latest).use { input -> input.copyTo(out) }
403+
} ?: error("Cannot open selected destination")
404+
}.onSuccess {
405+
VpnManager.appendLog("MTU results exported to selected destination")
406+
}.onFailure {
407+
VpnManager.appendLog("MTU export failed: ${it.message}")
408+
}
409+
}
410+
380411
private suspend fun ensureSocksPortAvailable(port: Int) {
381412
if (!isLocalPortInUse(port)) return
382413
VpnManager.appendLog("SOCKS5 port $port is busy, attempting to free it...")

android/app/src/main/java/com/masterdns/vpn/ui/settings/SettingsScreen.kt

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ private val configFields = listOf(
140140
SettingField("MTU", "MTU_TEST_TIMEOUT", "MTU_TEST_TIMEOUT", "Probe timeout seconds", keyboardType = KeyboardType.Decimal),
141141
SettingField("MTU", "MTU_TEST_PARALLELISM", "MTU_TEST_PARALLELISM", "Parallel probe workers", keyboardType = KeyboardType.Number),
142142
SettingField("MTU", "SAVE_MTU_SERVERS_TO_FILE", "SAVE_MTU_SERVERS_TO_FILE", "Persist successful MTU resolvers to file", type = FieldType.BOOL),
143-
SettingField("MTU", "MTU_SERVERS_FILE_NAME", "MTU_SERVERS_FILE_NAME", "Output file name/path for MTU results"),
143+
SettingField("MTU", "MTU_SERVERS_FILE_NAME", "MTU_SERVERS_FILE_NAME", "Output file name/path (absolute path supported)"),
144144
SettingField("MTU", "MTU_SERVERS_FILE_FORMAT", "MTU_SERVERS_FILE_FORMAT", "Format: {IP} {UP_MTU} {DOWN-MTU}"),
145145
SettingField("MTU", "MTU_USING_SECTION_SEPARATOR_TEXT", "MTU_USING_SECTION_SEPARATOR_TEXT", "Optional separator text between sections"),
146146
SettingField("MTU", "MTU_REMOVED_SERVER_LOG_FORMAT", "MTU_REMOVED_SERVER_LOG_FORMAT", "Log format when resolver is removed"),
@@ -245,6 +245,14 @@ fun SettingsScreen(
245245
scope.launch { snackbarHostState.showSnackbar("Resolvers imported into profile") }
246246
}
247247
}
248+
val pickMtuExportLauncher = rememberLauncherForActivityResult(
249+
ActivityResultContracts.CreateDocument("text/plain")
250+
) { uri ->
251+
if (uri != null) {
252+
fieldsState["MTU_EXPORT_URI"] = uri.toString()
253+
scope.launch { snackbarHostState.showSnackbar("MTU export destination selected") }
254+
}
255+
}
248256

249257
LaunchedEffect(profile?.id) {
250258
fieldsState.clear()
@@ -352,6 +360,17 @@ fun SettingsScreen(
352360
Spacer(modifier = Modifier.width(6.dp))
353361
Text("Import client_resolvers.txt")
354362
}
363+
Spacer(modifier = Modifier.height(6.dp))
364+
Button(
365+
onClick = {
366+
val selectedName = selected.name.trim().ifBlank { "profile" }
367+
pickMtuExportLauncher.launch("${selectedName}_mtu_results.log")
368+
}
369+
) {
370+
Icon(Icons.Filled.Download, contentDescription = null)
371+
Spacer(modifier = Modifier.width(6.dp))
372+
Text("Pick MTU export destination")
373+
}
355374
}
356375

357376
val socksAuthEnabled = fieldsState["SOCKS5_AUTH"].equals("true", ignoreCase = true)
@@ -568,6 +587,7 @@ private fun defaultValuesFor(profile: ProfileEntity): Map<String, String> {
568587
put("MTU_USING_SECTION_SEPARATOR_TEXT", adv("MTU_USING_SECTION_SEPARATOR_TEXT", ""))
569588
put("MTU_REMOVED_SERVER_LOG_FORMAT", adv("MTU_REMOVED_SERVER_LOG_FORMAT", "Resolver {IP} removed at {TIME} due to {CAUSE}"))
570589
put("MTU_ADDED_SERVER_LOG_FORMAT", adv("MTU_ADDED_SERVER_LOG_FORMAT", "Resolver {IP} added back at {TIME} (UP {UP_MTU}, DOWN {DOWN_MTU})"))
590+
put("MTU_EXPORT_URI", adv("MTU_EXPORT_URI", ""))
571591
put("RX_TX_WORKERS", adv("RX_TX_WORKERS", "4"))
572592
put("TUNNEL_PROCESS_WORKERS", adv("TUNNEL_PROCESS_WORKERS", "6"))
573593
put("TUNNEL_PACKET_TIMEOUT_SECONDS", adv("TUNNEL_PACKET_TIMEOUT_SECONDS", "10.0"))

android/app/src/main/java/com/masterdns/vpn/ui/settings/SettingsViewModel.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ class SettingsViewModel @Inject constructor(
193193
"MTU_USING_SECTION_SEPARATOR_TEXT",
194194
"MTU_REMOVED_SERVER_LOG_FORMAT",
195195
"MTU_ADDED_SERVER_LOG_FORMAT",
196+
"MTU_EXPORT_URI",
196197
"RX_TX_WORKERS",
197198
"TUNNEL_PROCESS_WORKERS",
198199
"TUNNEL_PACKET_TIMEOUT_SECONDS",
@@ -267,6 +268,7 @@ class SettingsViewModel @Inject constructor(
267268
"MTU_USING_SECTION_SEPARATOR_TEXT",
268269
"MTU_REMOVED_SERVER_LOG_FORMAT",
269270
"MTU_ADDED_SERVER_LOG_FORMAT",
271+
"MTU_EXPORT_URI",
270272
"RX_TX_WORKERS",
271273
"TUNNEL_PROCESS_WORKERS",
272274
"TUNNEL_PACKET_TIMEOUT_SECONDS",

0 commit comments

Comments
 (0)