android: Home scenario hints as in the Final 2026 mockup (trusted network pill, rule suggestion), profile update from the error and expiry cards, a stop button on errors
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1a6fb85259
commit
dcf8ace6e1
6 files changed
+282
-3
No files matched your search
+24
-1
@@ -263,6 +263,14 @@ fun HomeScreen(
|
||||
val today by produceState<org.strongswan.android.logic.stats.DayTraffic?>(null, historyChanges) {
|
||||
value = withContext(Dispatchers.IO) { org.strongswan.android.logic.stats.TrafficHistory.today(context) }
|
||||
}
|
||||
val ssid by viewModel.ssid.collectAsStateWithLifecycle()
|
||||
var hintTick by remember { mutableStateOf(0) }
|
||||
val hint by produceState<org.strongswan.android.ui.compose.home.ScenarioHint?>(null, ssid, state.vpnState, hintTick) {
|
||||
value = withContext(Dispatchers.IO) { org.strongswan.android.ui.compose.home.ScenarioHints.hint(context, ssid) }
|
||||
}
|
||||
val extras by remember { org.strongswan.android.logic.profiles.ProfileExtras.flow(context) }.collectAsStateWithLifecycle()
|
||||
val shownUuid = (state.activeProfile ?: selected)?.getUUID()?.toString()
|
||||
val hasLink = shownUuid != null && extras[shownUuid]?.source == org.strongswan.android.logic.profiles.ProfileSourceType.LINK
|
||||
val model = HomeModel(
|
||||
state = state,
|
||||
traffic = traffic,
|
||||
@@ -280,6 +288,8 @@ fun HomeScreen(
|
||||
certExpiry = certExpiry,
|
||||
connectStep = connectStep,
|
||||
today = today,
|
||||
hint = hint,
|
||||
hasLink = hasLink,
|
||||
)
|
||||
val actions = HomeActions(
|
||||
connect = { selected?.let { viewModel.connect(context, it) } },
|
||||
@@ -320,8 +330,21 @@ fun HomeScreen(
|
||||
}
|
||||
},
|
||||
details = { viewModel.openDetails(true) },
|
||||
certUpdate = { ShxNavigation.requestAddServer() },
|
||||
certUpdate = {
|
||||
if (hasLink && shownUuid != null) context.startActivity(org.strongswan.android.ui.ServerActivity.intent(context, shownUuid, checkUpdate = true))
|
||||
else ShxNavigation.requestAddServer()
|
||||
},
|
||||
updateProfile = { shownUuid?.let { context.startActivity(org.strongswan.android.ui.ServerActivity.intent(context, it, checkUpdate = true)) } },
|
||||
stats = { context.startActivity(Intent(context, org.strongswan.android.ui.StatsActivity::class.java)) },
|
||||
scenarios = { context.startActivity(Intent(context, org.strongswan.android.ui.ScenariosActivity::class.java)) },
|
||||
suggestYes = {
|
||||
ssid?.let { org.strongswan.android.ui.compose.home.ScenarioHints.accept(context, it, selected?.getUUID()?.toString()) }
|
||||
hintTick++
|
||||
},
|
||||
suggestNo = {
|
||||
ssid?.let { org.strongswan.android.ui.compose.home.ScenarioHints.decline(context, it) }
|
||||
hintTick++
|
||||
},
|
||||
certLater = {
|
||||
certAlias?.let { snoozeCertExpiry(context, it) }
|
||||
certSnoozes++
|
||||
|
||||
+9
@@ -179,6 +179,10 @@ class HomeViewModel(app: Application) : AndroidViewModel(app), VpnStateService.V
|
||||
/* (Wi-Fi up, mobile data up) for the "no internet" checklist */
|
||||
private val _networkKinds = MutableStateFlow(false to false)
|
||||
val networkKinds: StateFlow<Pair<Boolean, Boolean>> = _networkKinds
|
||||
|
||||
/* the current Wi-Fi network (null off Wi-Fi or without location access) */
|
||||
private val _ssid = MutableStateFlow<String?>(null)
|
||||
val ssid: StateFlow<String?> = _ssid
|
||||
private val underlyingNetworks = HashMap<Network, NetworkCapabilities?>()
|
||||
private val networkCallback = object : ConnectivityManager.NetworkCallback()
|
||||
{
|
||||
@@ -216,6 +220,8 @@ class HomeViewModel(app: Application) : AndroidViewModel(app), VpnStateService.V
|
||||
val caps = underlyingNetworks.values.filterNotNull()
|
||||
_networkKinds.value = caps.any { it.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) } to
|
||||
caps.any { it.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) }
|
||||
val wifi = caps.firstOrNull { it.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) }
|
||||
_ssid.value = wifi?.let { org.strongswan.android.logic.autoconnect.SsidReader.from(getApplication(), it) }
|
||||
}
|
||||
|
||||
/* true while «Fastest» pings the servers */
|
||||
@@ -436,6 +442,7 @@ class HomeViewModel(app: Application) : AndroidViewModel(app), VpnStateService.V
|
||||
|
||||
fun connect(context: Context, profile: VpnProfile)
|
||||
{
|
||||
org.strongswan.android.ui.compose.home.ScenarioHints.manualOn(context)
|
||||
val intent = Intent(context, VpnProfileControlActivity::class.java)
|
||||
intent.action = VpnProfileControlActivity.START_PROFILE
|
||||
intent.putExtra(VpnProfileControlActivity.EXTRA_VPN_PROFILE_UUID, profile.getUUID().toString())
|
||||
@@ -444,6 +451,8 @@ class HomeViewModel(app: Application) : AndroidViewModel(app), VpnStateService.V
|
||||
|
||||
fun disconnect()
|
||||
{
|
||||
/* counted per Wi-Fi for the «turn it off here automatically?» suggestion */
|
||||
org.strongswan.android.ui.compose.home.ScenarioHints.manualOff(getApplication(), if (_networkKinds.value.first) _ssid.value else null)
|
||||
service?.disconnect()
|
||||
}
|
||||
|
||||
|
||||
+44
-2
@@ -15,6 +15,10 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -66,6 +70,10 @@ internal data class HomeModel(
|
||||
val connectStep: Int = 1,
|
||||
/* today's traffic from the history */
|
||||
val today: org.strongswan.android.logic.stats.DayTraffic? = null,
|
||||
/* network rules while off (F_HomeScenario / F_ScenarioSuggest) */
|
||||
val hint: ScenarioHint? = null,
|
||||
/* the shown profile came from a link (it can be re-read) */
|
||||
val hasLink: Boolean = false,
|
||||
)
|
||||
|
||||
/** What Home can do. */
|
||||
@@ -90,6 +98,10 @@ internal class HomeActions(
|
||||
val certUpdate: () -> Unit = {},
|
||||
val certLater: () -> Unit = {},
|
||||
val stats: () -> Unit = {},
|
||||
val scenarios: () -> Unit = {},
|
||||
val suggestYes: () -> Unit = {},
|
||||
val suggestNo: () -> Unit = {},
|
||||
val updateProfile: () -> Unit = {},
|
||||
)
|
||||
|
||||
/** Two-letter badge of a server: the exit country when known, else the profile initials. */
|
||||
@@ -199,7 +211,18 @@ internal fun HomeContent(m: HomeModel, a: HomeActions)
|
||||
else ->
|
||||
{
|
||||
HeroBlob(heroLookIdle(), ShxIcons.Power, stringResource(R.string.connect), a.connect, enabled = profile != null)
|
||||
HeroCaption(stringResource(R.string.home_state_off), stringResource(R.string.home_state_off_status))
|
||||
val hint = m.hint
|
||||
when
|
||||
{
|
||||
hint?.trustedHere == true -> Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(10.dp))
|
||||
{
|
||||
Text(stringResource(R.string.home_state_off), style = ShxType.heroState, color = c.text)
|
||||
ScenarioPill(stringResource(R.string.hint_scenario_pill, hint.ssid), a.scenarios)
|
||||
Text(stringResource(if (hint.autoOnLeave) R.string.hint_on_leave else R.string.hint_trusted_off), style = ShxType.sub, color = c.textSub)
|
||||
}
|
||||
hint?.manualOffHere == true -> HeroCaption(stringResource(R.string.home_state_off), stringResource(R.string.hint_manual_off, hint.ssid), dot = c.iconMuted)
|
||||
else -> HeroCaption(stringResource(R.string.home_state_off), stringResource(R.string.home_state_off_status))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -216,6 +239,8 @@ internal fun HomeContent(m: HomeModel, a: HomeActions)
|
||||
connected && m.exit?.ip != null ->
|
||||
if (m.serverRttMs != null) stringResource(R.string.home_server_ip_rtt, maskIp(m.exit.ip), m.serverRttMs)
|
||||
else stringResource(R.string.home_server_ip, maskIp(m.exit.ip))
|
||||
!connected && !connecting && m.hint?.autoOnLeave == true -> stringResource(R.string.hint_server_on_leave, m.hint.ssid)
|
||||
!connected && !connecting && m.hint?.manualOffHere == true -> stringResource(R.string.hint_last_server)
|
||||
else -> profile?.gateway ?: ""
|
||||
}
|
||||
ServerCard(serverCode(profile, m.exit), name, details, stringResource(R.string.home_server_change), a.pickServer)
|
||||
@@ -296,13 +321,27 @@ internal fun HomeContent(m: HomeModel, a: HomeActions)
|
||||
HomeProblem.SERVER_CERT -> R.string.home_problem_cert_text
|
||||
else -> R.string.home_problem_generic_text
|
||||
}),
|
||||
primary = if (auth) stringResource(R.string.home_problem_edit_profile) to a.editProfile else stringResource(R.string.retry) to a.retry,
|
||||
primary = when
|
||||
{
|
||||
/* F_ErrAuth: a profile from a link gets its new version */
|
||||
(auth || m.problem == HomeProblem.SERVER_CERT) && m.hasLink -> stringResource(R.string.home_problem_update_profile) to a.updateProfile
|
||||
auth -> stringResource(R.string.home_problem_edit_profile) to a.editProfile
|
||||
else -> stringResource(R.string.retry) to a.retry
|
||||
},
|
||||
secondary = stringResource(R.string.show_log) to a.showLog,
|
||||
)
|
||||
if (s.profiles.size > 1)
|
||||
{
|
||||
ShxButton(stringResource(R.string.home_other_server), a.pickServer, style = ShxButtonStyle.TONAL, height = 52.dp, modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
/* the retries go on until stopped: a way out without the notification */
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.fillMaxWidth().height(44.dp).clip(RoundedCornerShape(14.dp)).clickable(role = Role.Button, onClick = a.disconnect),
|
||||
)
|
||||
{
|
||||
Text(stringResource(R.string.home_stop_vpn), style = ShxType.body.copy(fontWeight = FontWeight.Bold), color = c.accentText)
|
||||
}
|
||||
}
|
||||
connected ->
|
||||
{
|
||||
@@ -357,6 +396,9 @@ internal fun HomeContent(m: HomeModel, a: HomeActions)
|
||||
QuickTile(ShxIcons.Globe, stringResource(R.string.home_action_switch), a.pickServer, Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
m.hint?.takeIf { it.suggest > 0 }?.let { h ->
|
||||
ScenarioSuggestCard(h.ssid, h.suggest, a.suggestYes, a.suggestNo)
|
||||
}
|
||||
/* F_HomeScenario: today's traffic stays visible while off */
|
||||
m.today?.takeIf { it.total > 0 }?.let { today ->
|
||||
val share = if (today.direct > 0) (today.vpn * 100 / today.total).toInt() else null
|
||||
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Copyright (C) 2026 SHX VPN
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation; either version 2 of the License, or (at your
|
||||
* option) any later version.
|
||||
*/
|
||||
|
||||
package org.strongswan.android.ui.compose.home
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.preference.PreferenceManager
|
||||
import org.strongswan.android.R
|
||||
import org.strongswan.android.logic.autoconnect.AutoConnectManager
|
||||
import org.strongswan.android.ui.compose.design.ShxIcons
|
||||
import org.strongswan.android.ui.compose.theme.LocalShxColors
|
||||
import org.strongswan.android.ui.compose.theme.ShxType
|
||||
|
||||
/** What Home says about the network rules while the VPN is off (F_HomeScenario / F_ScenarioSuggest). */
|
||||
internal data class ScenarioHint(
|
||||
val ssid: String,
|
||||
/** off because this Wi-Fi is trusted */
|
||||
val trustedHere: Boolean,
|
||||
/** and it comes back on its own when leaving */
|
||||
val autoOnLeave: Boolean,
|
||||
/** the user switched it off by hand here last time */
|
||||
val manualOffHere: Boolean,
|
||||
/** how often by hand here, when worth suggesting a rule (≥ 3), else 0 */
|
||||
val suggest: Int,
|
||||
)
|
||||
|
||||
/** Remembers the manual switch-offs per Wi-Fi network for the suggestion. */
|
||||
object ScenarioHints
|
||||
{
|
||||
private const val PREF_COUNT = "pref_manual_off_count_"
|
||||
private const val PREF_DECLINED = "pref_manual_off_declined_"
|
||||
private const val PREF_LAST = "pref_manual_off_last"
|
||||
internal const val SUGGEST_AFTER = 3
|
||||
|
||||
/** The user turned the VPN off on [ssid]. */
|
||||
fun manualOff(context: Context, ssid: String?)
|
||||
{
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
if (ssid == null)
|
||||
{
|
||||
prefs.edit().remove(PREF_LAST).apply()
|
||||
return
|
||||
}
|
||||
prefs.edit().putInt(PREF_COUNT + ssid, prefs.getInt(PREF_COUNT + ssid, 0) + 1).putString(PREF_LAST, ssid).apply()
|
||||
}
|
||||
|
||||
/** The user turned it on again: the «off by hand» line goes away. */
|
||||
fun manualOn(context: Context)
|
||||
{
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit().remove(PREF_LAST).apply()
|
||||
}
|
||||
|
||||
fun decline(context: Context, ssid: String)
|
||||
{
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(PREF_DECLINED + ssid, true).apply()
|
||||
}
|
||||
|
||||
/** «Yes, create the scenario»: the network becomes trusted and both rules are on. */
|
||||
fun accept(context: Context, ssid: String, profileUuid: String?)
|
||||
{
|
||||
AutoConnectManager.getInstance(context).updateConfig {
|
||||
it.copy(
|
||||
enabled = true, parkOnTrusted = true, connectOnUntrusted = true,
|
||||
trustedSsids = it.trustedSsids + ssid, profileUuid = it.profileUuid ?: profileUuid,
|
||||
)
|
||||
}
|
||||
decline(context, ssid)
|
||||
}
|
||||
|
||||
internal fun hint(context: Context, ssid: String?): ScenarioHint?
|
||||
{
|
||||
ssid ?: return null
|
||||
val config = AutoConnectManager.getInstance(context).currentConfig
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val trusted = config.enabled && config.parkOnTrusted && config.trustedSsids.any { it.equals(ssid, ignoreCase = true) }
|
||||
val count = prefs.getInt(PREF_COUNT + ssid, 0)
|
||||
val suggest = if (!trusted && count >= SUGGEST_AFTER && !prefs.getBoolean(PREF_DECLINED + ssid, false)) count else 0
|
||||
return ScenarioHint(
|
||||
ssid = ssid,
|
||||
trustedHere = trusted,
|
||||
autoOnLeave = trusted && config.connectOnUntrusted,
|
||||
manualOffHere = prefs.getString(PREF_LAST, null) == ssid,
|
||||
suggest = suggest,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** «Scenario · Home_5G» pill under the hero (F_HomeScenario). */
|
||||
@Composable
|
||||
internal fun ScenarioPill(text: String, onClick: () -> Unit)
|
||||
{
|
||||
val c = LocalShxColors.current
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.height(40.dp).clip(RoundedCornerShape(20.dp)).background(c.accentContainer)
|
||||
.clickable(role = Role.Button, onClick = onClick).padding(start = 14.dp, end = 8.dp),
|
||||
)
|
||||
{
|
||||
Icon(ShxIcons.Wifi, contentDescription = null, tint = c.onAccentContainer, modifier = Modifier.size(18.dp))
|
||||
Text(text, style = ShxType.body.copy(fontWeight = FontWeight.Bold), color = c.onAccentContainer, maxLines = 1)
|
||||
Box(contentAlignment = Alignment.Center, modifier = Modifier.size(24.dp).clip(CircleShape).background(c.card))
|
||||
{
|
||||
Icon(ShxIcons.Chevron, contentDescription = null, tint = c.onAccentContainer, modifier = Modifier.size(14.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** «Turn the VPN off at home automatically?» (F_ScenarioSuggest). */
|
||||
@Composable
|
||||
internal fun ScenarioSuggestCard(ssid: String, count: Int, onYes: () -> Unit, onNo: () -> Unit)
|
||||
{
|
||||
val c = LocalShxColors.current
|
||||
val shape = RoundedCornerShape(24.dp)
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.fillMaxWidth().clip(shape).background(c.accentSoft).border(1.dp, c.accentBorder, shape).padding(16.dp),
|
||||
)
|
||||
{
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp))
|
||||
{
|
||||
Box(contentAlignment = Alignment.Center, modifier = Modifier.size(40.dp).clip(RoundedCornerShape(14.dp)).background(c.accent))
|
||||
{
|
||||
Icon(ShxIcons.Wifi, contentDescription = null, tint = Color.White, modifier = Modifier.size(20.dp))
|
||||
}
|
||||
Column(verticalArrangement = Arrangement.spacedBy(3.dp))
|
||||
{
|
||||
Text(stringResource(R.string.suggest_title), style = ShxType.cardTitle, color = c.text)
|
||||
Text(stringResource(R.string.suggest_text, count, ssid), style = ShxType.note.copy(fontSize = 13.sp, lineHeight = 18.sp, fontWeight = FontWeight.Normal), color = c.onAccentContainer)
|
||||
}
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp))
|
||||
{
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.weight(2f).height(44.dp).clip(RoundedCornerShape(14.dp)).background(c.accent).clickable(role = Role.Button, onClick = onYes),
|
||||
)
|
||||
{
|
||||
Text(stringResource(R.string.suggest_yes), style = ShxType.body.copy(fontWeight = FontWeight.ExtraBold), color = Color.White, maxLines = 1)
|
||||
}
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.weight(1f).height(44.dp).clip(RoundedCornerShape(14.dp)).border(1.dp, c.outline, RoundedCornerShape(14.dp)).clickable(role = Role.Button, onClick = onNo),
|
||||
)
|
||||
{
|
||||
Text(stringResource(R.string.suggest_no), style = ShxType.body.copy(fontWeight = FontWeight.Bold), color = c.text, maxLines = 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1280,4 +1280,16 @@
|
||||
<string name="scenario_often">Часто открываете</string>
|
||||
<string name="scenario_all_apps">Все приложения</string>
|
||||
<string name="scenario_done">Готово</string>
|
||||
<string name="hint_scenario_pill">Сценарий · %1$s</string>
|
||||
<string name="hint_on_leave">Включится сам, когда уйдёте из этой сети</string>
|
||||
<string name="hint_trusted_off">В доверенной сети VPN не нужен</string>
|
||||
<string name="hint_server_on_leave">подключится при выходе из %1$s</string>
|
||||
<string name="hint_manual_off">Выключен вручную · %1$s</string>
|
||||
<string name="hint_last_server">последний сервер</string>
|
||||
<string name="suggest_title">Выключать VPN здесь автоматически?</string>
|
||||
<string name="suggest_text">Вы уже %1$d-й раз выключаете его в сети %2$s. Вне этой сети VPN будет включаться сам.</string>
|
||||
<string name="suggest_yes">Да, создать сценарий</string>
|
||||
<string name="suggest_no">Не надо</string>
|
||||
<string name="home_problem_update_profile">Обновить профиль</string>
|
||||
<string name="home_stop_vpn">Отключить VPN</string>
|
||||
</resources>
|
||||
@@ -1266,4 +1266,16 @@
|
||||
<string name="scenario_often">Often opened</string>
|
||||
<string name="scenario_all_apps">All apps</string>
|
||||
<string name="scenario_done">Done</string>
|
||||
<string name="hint_scenario_pill">Scenario · %1$s</string>
|
||||
<string name="hint_on_leave">Turns on by itself when you leave this network</string>
|
||||
<string name="hint_trusted_off">Not needed in a trusted network</string>
|
||||
<string name="hint_server_on_leave">connects when you leave %1$s</string>
|
||||
<string name="hint_manual_off">Off by hand · %1$s</string>
|
||||
<string name="hint_last_server">last server</string>
|
||||
<string name="suggest_title">Turn the VPN off here automatically?</string>
|
||||
<string name="suggest_text">This is the %1$d. time you turn it off in %2$s. Away from here the VPN turns on by itself.</string>
|
||||
<string name="suggest_yes">Yes, create the scenario</string>
|
||||
<string name="suggest_no">No thanks</string>
|
||||
<string name="home_problem_update_profile">Update profile</string>
|
||||
<string name="home_stop_vpn">Turn VPN off</string>
|
||||
</resources>
|
||||
Reference in new issue
Block a user