android: hint behind the system VPN consent window as in the Final 2026 mockup

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
DenozordecandClaude Opus 5.5 committed 2026-10-12 14:36:09 +07:00
1 parent 8fbebed140
commit a08c193a90
4 files changed
+71

No files matched your search

@@ -102,6 +102,7 @@ public class VpnProfileControlActivity extends AppCompatActivity implements VpnD
new ActivityResultContracts.StartActivityForResult(),
result -> {
mWaitingForResult = false;
VpnDialogBus.INSTANCE.setConsentHint(false);
if (result.getResultCode() == RESULT_OK && mProfileInfo != null)
{
onVpnServicePrepared();
@@ -224,6 +225,7 @@ public class VpnProfileControlActivity extends AppCompatActivity implements VpnD
try
{
mWaitingForResult = true;
VpnDialogBus.INSTANCE.setConsentHint(true);
mPrepareVpnService.launch(intent);
}
catch (ActivityNotFoundException ex)
@@ -232,6 +234,7 @@ public class VpnProfileControlActivity extends AppCompatActivity implements VpnD
* don't have the VPN components built into the system image.
* com.android.vpndialogs/com.android.vpndialogs.ConfirmDialog
* will not be found then */
VpnDialogBus.INSTANCE.setConsentHint(false);
showNotSupported(R.string.vpn_not_supported);
mWaitingForResult = false;
}
@@ -45,6 +45,22 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.Alignment
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.strongswan.android.ui.compose.design.ShxIcons
import org.strongswan.android.ui.compose.theme.LocalShxColors
import org.strongswan.android.ui.compose.theme.ShxType
import org.strongswan.android.R
import org.strongswan.android.ui.compose.theme.ShxSpacing
import org.strongswan.android.ui.compose.theme.ShxTheme
@@ -106,6 +122,16 @@ object VpnDialogBus
{
_dialog.value = null
}
/* F_VpnConsent: the hint behind the system VPN consent window */
private val _consentHint = MutableStateFlow(false)
val consentHint: StateFlow<Boolean> = _consentHint
@JvmStatic
fun setConsentHint(shown: Boolean)
{
_consentHint.value = shown
}
}
/** Renders the connect-flow dialogs in the app theme inside the transparent activity. */
@@ -122,6 +148,11 @@ fun ComponentActivity.installVpnDialogHost()
fun VpnDialogHost()
{
val dialog by VpnDialogBus.dialog.collectAsStateWithLifecycle()
val consentHint by VpnDialogBus.consentHint.collectAsStateWithLifecycle()
if (consentHint)
{
ConsentHint()
}
when (val d = dialog)
{
is VpnDialog.Confirmation -> ConfirmationUi(d)
@@ -289,3 +320,36 @@ private fun NotSupportedUi(dialog: VpnDialog.NotSupported)
},
)
}
/**
* Shown by the transparent host while Android's own consent dialog is on
* top (F_VpnConsent): a dimmed screen with a card at the top explaining it.
*/
@Composable
private fun ConsentHint()
{
val c = LocalShxColors.current
Box(
Modifier.fillMaxSize().background(Color(0x8C1C1B20)).statusBarsPadding().padding(start = 20.dp, end = 20.dp, top = 24.dp),
)
{
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(22.dp)).background(c.background).padding(16.dp),
)
{
Box(contentAlignment = Alignment.Center, modifier = Modifier.size(36.dp).clip(RoundedCornerShape(12.dp)).background(c.accent))
{
Icon(ShxIcons.Shield, contentDescription = null, tint = Color.White, modifier = Modifier.size(20.dp))
}
Column(verticalArrangement = Arrangement.spacedBy(3.dp))
{
Text(stringResource(R.string.consent_hint_title), style = ShxType.cardTitle, color = c.text)
Text(
stringResource(R.string.consent_hint_text),
style = ShxType.note.copy(fontSize = 13.sp, lineHeight = 19.sp, fontWeight = FontWeight.Normal), color = c.textSoft,
)
}
}
}
}
@@ -1121,4 +1121,6 @@
<string name="battery_not_done">Шаг 1 ещё не сделан — VPN может отключаться в фоне</string>
<string name="battery_ok">Готово: Android не будет усыплять VPN</string>
<string name="battery_check_again">Проверить снова</string>
<string name="consent_hint_title">Сейчас Android спросит разрешение</string>
<string name="consent_hint_text">Это стандартное окно для любого VPN. Нажмите «ОК» — без этого подключиться нельзя.</string>
</resources>
@@ -1107,4 +1107,6 @@
<string name="battery_not_done">Step 1 is not done yet — the VPN may stop in the background</string>
<string name="battery_ok">Done: Android will not put the VPN to sleep</string>
<string name="battery_check_again">Check again</string>
<string name="consent_hint_title">Android will now ask for permission</string>
<string name="consent_hint_text">This is the standard window for any VPN. Tap «OK» — without it the VPN cannot connect.</string>
</resources>