fix: Android full tunnel mode requires credentials + deployment IDs UI refactor (#124)

Real bug I introduced in #94: Full mode was skipping the credential check that apps_script mode enforces, but Full mode does talk to CodeFull.gs on Apps Script and needs the same auth_key + deployment ID. Users flipping to Full mode with empty fields would silently fail.

Two sites fixed:
- MhrvVpnService.kt — changed `mode == APPS_SCRIPT` gate to `mode != GOOGLE_ONLY`
- HomeScreen.kt — removed the `cfg.mode == Mode.FULL` bypass in the Start button's enabled-state

Also includes a UX improvement for the Deployment IDs editor (per-row field with add/remove buttons instead of raw newline-separated text), which makes multi-deployment setups easier to manage on Android.

Rust-side 75 tests still green, Kotlin compiles clean. Android-only diff so no Rust CI impact.
This commit is contained in:
vahidlazio
2026-04-24 16:15:12 +02:00
committed by GitHub
parent 9e2b8e5f3e
commit f9f4845567
4 changed files with 82 additions and 28 deletions
@@ -93,11 +93,13 @@ class MhrvVpnService : VpnService() {
// the instant I tap Start". See issue #73.
startForeground(NOTIF_ID, buildNotif(cfg.listenPort))
// Deployment ID + auth key are only required in apps_script mode.
// google_only (bootstrap) and full (tunnel) modes run without them.
val needsAppsScriptCreds = cfg.mode == Mode.APPS_SCRIPT
if (needsAppsScriptCreds && (!cfg.hasDeploymentId || cfg.authKey.isBlank())) {
Log.e(TAG, "Config is incomplete — can't start proxy in apps_script mode")
// Deployment ID + auth key are required for apps_script and full
// modes — both talk to Apps Script. Only google_only (bootstrap)
// runs without them. Closes #73 regression where google_only
// users hit this branch and crashed on startForeground timeout.
val needsCreds = cfg.mode != Mode.GOOGLE_ONLY
if (needsCreds && (!cfg.hasDeploymentId || cfg.authKey.isBlank())) {
Log.e(TAG, "Config is incomplete — deployment ID + auth key required for ${cfg.mode}")
try { stopForeground(STOP_FOREGROUND_REMOVE) } catch (_: Throwable) {}
stopSelf()
return
@@ -418,7 +418,6 @@ fun HomeScreen(
},
enabled = (isVpnRunning ||
cfg.mode == Mode.GOOGLE_ONLY ||
cfg.mode == Mode.FULL ||
(cfg.hasDeploymentId && cfg.authKey.isNotBlank())) && !transitionCooldown,
colors = ButtonDefaults.buttonColors(
containerColor = if (isVpnRunning) ErrRed else OkGreen,
@@ -689,7 +688,7 @@ private fun ConnectionModeDropdown(
}
// =========================================================================
// Deployment IDs editor (multi-line, one URL/ID per line).
// Deployment IDs editor — one row per ID, with add/remove buttons.
// =========================================================================
@Composable
@@ -698,26 +697,79 @@ private fun DeploymentIdsField(
onChange: (List<String>) -> Unit,
enabled: Boolean = true,
) {
// Treat the list as newline-joined text. Keep trailing newlines so the
// cursor behaves naturally while the user is adding a new entry.
var raw by remember(urls) { mutableStateOf(urls.joinToString("\n")) }
var newEntry by remember { mutableStateOf("") }
OutlinedTextField(
value = raw,
onValueChange = {
raw = it
val parsed = it.split("\n").map(String::trim).filter(String::isNotBlank)
onChange(parsed)
},
label = { Text(stringResource(R.string.field_deployment_urls)) },
enabled = enabled,
modifier = Modifier.fillMaxWidth(),
minLines = 2,
maxLines = 6,
supportingText = {
Text(stringResource(R.string.help_deployment_urls))
},
)
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(
stringResource(R.string.field_deployment_urls),
style = MaterialTheme.typography.labelLarge,
)
// Existing entries — each with its own row and a remove button.
urls.forEachIndexed { index, url ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
OutlinedTextField(
value = url,
onValueChange = { edited ->
val updated = urls.toMutableList()
updated[index] = edited
onChange(updated)
},
enabled = enabled,
modifier = Modifier.weight(1f),
singleLine = true,
textStyle = MaterialTheme.typography.bodySmall,
label = { Text("#${index + 1}") },
)
IconButton(
onClick = {
onChange(urls.filterIndexed { i, _ -> i != index })
},
enabled = enabled,
) {
Text("", color = MaterialTheme.colorScheme.error)
}
}
}
// "Add" row: text field + button.
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
OutlinedTextField(
value = newEntry,
onValueChange = { newEntry = it },
enabled = enabled,
modifier = Modifier.weight(1f),
singleLine = true,
placeholder = { Text("Paste URL or ID") },
)
Spacer(Modifier.width(8.dp))
Button(
onClick = {
val trimmed = newEntry.trim()
if (trimmed.isNotBlank()) {
onChange(urls + trimmed)
newEntry = ""
}
},
enabled = enabled && newEntry.isNotBlank(),
contentPadding = PaddingValues(horizontal = 12.dp),
) {
Text("+ Add")
}
}
Text(
stringResource(R.string.help_deployment_urls),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
// =========================================================================
@@ -52,7 +52,7 @@
<string name="lang_toggle_cd">تغییر زبان</string>
<!-- Supporting / helper text -->
<string name="help_deployment_urls">یکی در هر خط. می‌توانید URL کامل (https://script.google.com/macros/s/.../exec) یا فقط ID خام بگذارید — ترکیبی هم قبول است. چند ID به‌صورت چرخشی (round-robin) استفاده می‌شوند.</string>
<string name="help_deployment_urls">URL کامل (https://script.google.com/macros/s/.../exec) یا فقط ID خام. چند ID به‌صورت چرخشی استفاده می‌شوند — بیشتر ID = سرعت بیشتر در حالت تونل کامل.</string>
<string name="help_auth_key">همان رمز مشترکی که داخل Apps Script گذاشتید.</string>
<string name="help_mode_vpn_tun">هنگام اتصال، مجوز VPN سیستم درخواست می‌شود. تمام ترافیک دستگاه به‌صورت خودکار رد می‌شود.</string>
<string name="help_mode_proxy_only">بدون VPN سیستم. بعد از اتصال، پروکسی Wi-Fi را روی 127.0.0.1:%1$d (HTTP) یا %2$d (SOCKS5) تنظیم کنید. فقط برنامه‌هایی که تنظیمات پروکسی را رعایت می‌کنند رد می‌شوند.</string>
+1 -1
View File
@@ -52,7 +52,7 @@
<string name="lang_toggle_cd">Switch language</string>
<!-- Supporting / helper text -->
<string name="help_deployment_urls">One per line. Full URLs (https://script.google.com/macros/s/.../exec) or bare IDs — mix as you like. Multiple IDs are rotated round-robin.</string>
<string name="help_deployment_urls">Full URLs (https://script.google.com/macros/s/.../exec) or bare IDs. Multiple IDs are rotated round-robin — more IDs = more pipeline throughput in full mode.</string>
<string name="help_auth_key">The shared secret you set in the Apps Script.</string>
<string name="help_mode_vpn_tun">Requests the OS VPN grant on Connect. All device traffic is routed automatically.</string>
<string name="help_mode_proxy_only">No OS VPN. Set your Wi-Fi proxy to 127.0.0.1:%1$d (HTTP) or %2$d (SOCKS5) after Connect. Only apps that honour the proxy settings will tunnel.</string>