Skip to content

Commit 90e8699

Browse files
authored
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.
1 parent 16d0590 commit 90e8699

4 files changed

Lines changed: 83 additions & 29 deletions

File tree

android/app/src/main/java/com/therealaleph/mhrv/MhrvVpnService.kt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,13 @@ class MhrvVpnService : VpnService() {
9393
// the instant I tap Start". See issue #73.
9494
startForeground(NOTIF_ID, buildNotif(cfg.listenPort))
9595

96-
// Deployment ID + auth key are only required in apps_script mode.
97-
// google_only (bootstrap) and full (tunnel) modes run without them.
98-
val needsAppsScriptCreds = cfg.mode == Mode.APPS_SCRIPT
99-
if (needsAppsScriptCreds && (!cfg.hasDeploymentId || cfg.authKey.isBlank())) {
100-
Log.e(TAG, "Config is incomplete — can't start proxy in apps_script mode")
96+
// Deployment ID + auth key are required for apps_script and full
97+
// modes — both talk to Apps Script. Only google_only (bootstrap)
98+
// runs without them. Closes #73 regression where google_only
99+
// users hit this branch and crashed on startForeground timeout.
100+
val needsCreds = cfg.mode != Mode.GOOGLE_ONLY
101+
if (needsCreds && (!cfg.hasDeploymentId || cfg.authKey.isBlank())) {
102+
Log.e(TAG, "Config is incomplete — deployment ID + auth key required for ${cfg.mode}")
101103
try { stopForeground(STOP_FOREGROUND_REMOVE) } catch (_: Throwable) {}
102104
stopSelf()
103105
return

android/app/src/main/java/com/therealaleph/mhrv/ui/HomeScreen.kt

Lines changed: 74 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,6 @@ fun HomeScreen(
418418
},
419419
enabled = (isVpnRunning ||
420420
cfg.mode == Mode.GOOGLE_ONLY ||
421-
cfg.mode == Mode.FULL ||
422421
(cfg.hasDeploymentId && cfg.authKey.isNotBlank())) && !transitionCooldown,
423422
colors = ButtonDefaults.buttonColors(
424423
containerColor = if (isVpnRunning) ErrRed else OkGreen,
@@ -689,7 +688,7 @@ private fun ConnectionModeDropdown(
689688
}
690689

691690
// =========================================================================
692-
// Deployment IDs editor (multi-line, one URL/ID per line).
691+
// Deployment IDs editor one row per ID, with add/remove buttons.
693692
// =========================================================================
694693

695694
@Composable
@@ -698,26 +697,79 @@ private fun DeploymentIdsField(
698697
onChange: (List<String>) -> Unit,
699698
enabled: Boolean = true,
700699
) {
701-
// Treat the list as newline-joined text. Keep trailing newlines so the
702-
// cursor behaves naturally while the user is adding a new entry.
703-
var raw by remember(urls) { mutableStateOf(urls.joinToString("\n")) }
704-
705-
OutlinedTextField(
706-
value = raw,
707-
onValueChange = {
708-
raw = it
709-
val parsed = it.split("\n").map(String::trim).filter(String::isNotBlank)
710-
onChange(parsed)
711-
},
712-
label = { Text(stringResource(R.string.field_deployment_urls)) },
713-
enabled = enabled,
714-
modifier = Modifier.fillMaxWidth(),
715-
minLines = 2,
716-
maxLines = 6,
717-
supportingText = {
718-
Text(stringResource(R.string.help_deployment_urls))
719-
},
720-
)
700+
var newEntry by remember { mutableStateOf("") }
701+
702+
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
703+
Text(
704+
stringResource(R.string.field_deployment_urls),
705+
style = MaterialTheme.typography.labelLarge,
706+
)
707+
708+
// Existing entries — each with its own row and a remove button.
709+
urls.forEachIndexed { index, url ->
710+
Row(
711+
verticalAlignment = Alignment.CenterVertically,
712+
modifier = Modifier.fillMaxWidth(),
713+
) {
714+
OutlinedTextField(
715+
value = url,
716+
onValueChange = { edited ->
717+
val updated = urls.toMutableList()
718+
updated[index] = edited
719+
onChange(updated)
720+
},
721+
enabled = enabled,
722+
modifier = Modifier.weight(1f),
723+
singleLine = true,
724+
textStyle = MaterialTheme.typography.bodySmall,
725+
label = { Text("#${index + 1}") },
726+
)
727+
IconButton(
728+
onClick = {
729+
onChange(urls.filterIndexed { i, _ -> i != index })
730+
},
731+
enabled = enabled,
732+
) {
733+
Text("", color = MaterialTheme.colorScheme.error)
734+
}
735+
}
736+
}
737+
738+
// "Add" row: text field + button.
739+
Row(
740+
verticalAlignment = Alignment.CenterVertically,
741+
modifier = Modifier.fillMaxWidth(),
742+
) {
743+
OutlinedTextField(
744+
value = newEntry,
745+
onValueChange = { newEntry = it },
746+
enabled = enabled,
747+
modifier = Modifier.weight(1f),
748+
singleLine = true,
749+
placeholder = { Text("Paste URL or ID") },
750+
)
751+
Spacer(Modifier.width(8.dp))
752+
Button(
753+
onClick = {
754+
val trimmed = newEntry.trim()
755+
if (trimmed.isNotBlank()) {
756+
onChange(urls + trimmed)
757+
newEntry = ""
758+
}
759+
},
760+
enabled = enabled && newEntry.isNotBlank(),
761+
contentPadding = PaddingValues(horizontal = 12.dp),
762+
) {
763+
Text("+ Add")
764+
}
765+
}
766+
767+
Text(
768+
stringResource(R.string.help_deployment_urls),
769+
style = MaterialTheme.typography.labelSmall,
770+
color = MaterialTheme.colorScheme.onSurfaceVariant,
771+
)
772+
}
721773
}
722774

723775
// =========================================================================

android/app/src/main/res/values-fa/strings.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
<string name="lang_toggle_cd">تغییر زبان</string>
5353

5454
<!-- Supporting / helper text -->
55-
<string name="help_deployment_urls">یکی در هر خط. می‌توانید URL کامل (https://script.google.com/macros/s/.../exec) یا فقط ID خام بگذارید — ترکیبی هم قبول است. چند ID به‌صورت چرخشی (round-robin) استفاده می‌شوند.</string>
55+
<string name="help_deployment_urls">URL کامل (https://script.google.com/macros/s/.../exec) یا فقط ID خام. چند ID به‌صورت چرخشی استفاده می‌شوند — بیشتر ID = سرعت بیشتر در حالت تونل کامل.</string>
5656
<string name="help_auth_key">همان رمز مشترکی که داخل Apps Script گذاشتید.</string>
5757
<string name="help_mode_vpn_tun">هنگام اتصال، مجوز VPN سیستم درخواست می‌شود. تمام ترافیک دستگاه به‌صورت خودکار رد می‌شود.</string>
5858
<string name="help_mode_proxy_only">بدون VPN سیستم. بعد از اتصال، پروکسی Wi-Fi را روی 127.0.0.1:%1$d (HTTP) یا %2$d (SOCKS5) تنظیم کنید. فقط برنامه‌هایی که تنظیمات پروکسی را رعایت می‌کنند رد می‌شوند.</string>

android/app/src/main/res/values/strings.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
<string name="lang_toggle_cd">Switch language</string>
5353

5454
<!-- Supporting / helper text -->
55-
<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>
55+
<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>
5656
<string name="help_auth_key">The shared secret you set in the Apps Script.</string>
5757
<string name="help_mode_vpn_tun">Requests the OS VPN grant on Connect. All device traffic is routed automatically.</string>
5858
<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>

0 commit comments

Comments
 (0)