android: one order for the network rules and the scenarios (top wins), hand actions beat a running scenario until it ends
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a39993075e
commit
df1575fe27
9 files changed
+197
-23
No files matched your search
@@ -333,6 +333,7 @@ class PauseReceiver : BroadcastReceiver()
|
|||||||
{
|
{
|
||||||
if (intent?.action == PauseManager.ACTION_PAUSE)
|
if (intent?.action == PauseManager.ACTION_PAUSE)
|
||||||
{
|
{
|
||||||
|
org.strongswan.android.logic.scenarios.ScenarioEngine.onManual(context)
|
||||||
val service = VpnStateService.getLiveInstance() ?: return
|
val service = VpnStateService.getLiveInstance() ?: return
|
||||||
val profile = service.profile ?: return
|
val profile = service.profile ?: return
|
||||||
PauseManager.pause(context, service, profile.getUUID().toString(), 15)
|
PauseManager.pause(context, service, profile.getUUID().toString(), 15)
|
||||||
|
|||||||
+6
-1
@@ -252,7 +252,12 @@ class AutoConnectManager private constructor(
|
|||||||
whiteListBlocksReconnect = WhiteListManager.getInstance(context).shouldBlockReconnect(),
|
whiteListBlocksReconnect = WhiteListManager.getInstance(context).shouldBlockReconnect(),
|
||||||
)
|
)
|
||||||
/* the user paused the VPN: the pause alarm brings it back, not a network change */
|
/* the user paused the VPN: the pause alarm brings it back, not a network change */
|
||||||
return if (action == Action.CONNECT && PauseManager.isPaused(context)) Action.IGNORE else action
|
if (action == Action.CONNECT && PauseManager.isPaused(context))
|
||||||
|
{
|
||||||
|
return Action.IGNORE
|
||||||
|
}
|
||||||
|
/* a user scenario above the network rules is in force */
|
||||||
|
return if (action != Action.IGNORE && org.strongswan.android.logic.scenarios.ScenarioEngine.inControl(context)) Action.IGNORE else action
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Re-runs the connect decision, e.g. when the white-list gate clears. */
|
/** Re-runs the connect decision, e.g. when the white-list gate clears. */
|
||||||
|
|||||||
+118
-3
@@ -106,6 +106,50 @@ internal fun activeScenario(list: List<Scenario>, now: ScenarioInputs): Scenario
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The two network rules of F_Automation, as entries of the common order. */
|
||||||
|
const val BUILTIN_TRUSTED = "builtin:trusted"
|
||||||
|
const val BUILTIN_UNTRUSTED = "builtin:untrusted"
|
||||||
|
|
||||||
|
/** What the built-in network rules say about the current network. */
|
||||||
|
internal data class BuiltinRules(
|
||||||
|
val enabled: Boolean,
|
||||||
|
val parkOnTrusted: Boolean,
|
||||||
|
val connectOnUntrusted: Boolean,
|
||||||
|
val trustedSsids: Set<String>,
|
||||||
|
)
|
||||||
|
|
||||||
|
/** The order of everything: [order] first (known ids), the rest after it (built-ins before new scenarios). */
|
||||||
|
internal fun orderedIds(order: List<String>, scenarios: List<Scenario>): List<String>
|
||||||
|
{
|
||||||
|
val all = listOf(BUILTIN_TRUSTED, BUILTIN_UNTRUSTED) + scenarios.map { it.id }
|
||||||
|
return order.filter { it in all } + all.filter { it !in order }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The top entry of the common order whose condition holds (built-in rule
|
||||||
|
* id or scenario id), null when nothing applies. «Upper wins».
|
||||||
|
*/
|
||||||
|
internal fun topEntry(order: List<String>, scenarios: List<Scenario>, rules: BuiltinRules, now: ScenarioInputs): String?
|
||||||
|
{
|
||||||
|
val byId = scenarios.associateBy { it.id }
|
||||||
|
val trustedHere = now.wifi && now.ssid != null && rules.trustedSsids.any { it.equals(now.ssid, ignoreCase = true) }
|
||||||
|
for (id in orderedIds(order, scenarios))
|
||||||
|
{
|
||||||
|
val holds = when (id)
|
||||||
|
{
|
||||||
|
BUILTIN_TRUSTED -> rules.enabled && rules.parkOnTrusted && trustedHere
|
||||||
|
/* like the auto-connect: a Wi-Fi with an unreadable name counts as foreign */
|
||||||
|
BUILTIN_UNTRUSTED -> rules.enabled && rules.connectOnUntrusted && ((now.wifi && !trustedHere) || (!now.wifi && now.mobile))
|
||||||
|
else -> byId[id]?.let { activeScenario(listOf(it), now) != null } ?: false
|
||||||
|
}
|
||||||
|
if (holds)
|
||||||
|
{
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs the user scenarios of F_Automation. Re-evaluated on network
|
* Runs the user scenarios of F_Automation. Re-evaluated on network
|
||||||
* changes, at the edges of time windows (alarm), while an app trigger
|
* changes, at the edges of time windows (alarm), while an app trigger
|
||||||
@@ -120,6 +164,9 @@ object ScenarioEngine
|
|||||||
private const val PREF_APPLIED = "pref_scenario_applied"
|
private const val PREF_APPLIED = "pref_scenario_applied"
|
||||||
private const val PREF_WAS_ON = "pref_scenario_was_on"
|
private const val PREF_WAS_ON = "pref_scenario_was_on"
|
||||||
private const val PREF_WAS_PROFILE = "pref_scenario_was_profile"
|
private const val PREF_WAS_PROFILE = "pref_scenario_was_profile"
|
||||||
|
/* the scenario the user overrode by hand: it stays out until it ends */
|
||||||
|
private const val PREF_SUSPENDED = "pref_scenario_suspended"
|
||||||
|
private const val PREF_ORDER = "pref_scenario_order"
|
||||||
|
|
||||||
private val _list = MutableStateFlow<List<Scenario>>(emptyList())
|
private val _list = MutableStateFlow<List<Scenario>>(emptyList())
|
||||||
val list: StateFlow<List<Scenario>> = _list
|
val list: StateFlow<List<Scenario>> = _list
|
||||||
@@ -200,15 +247,26 @@ object ScenarioEngine
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
val now = inputs(app)
|
val now = inputs(app)
|
||||||
val active = activeScenario(_list.value, now)
|
val top = topEntry(order(app), _list.value, builtinRules(app), now)
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(app)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(app)
|
||||||
|
/* a scenario overridden by hand stays out until it ends */
|
||||||
|
val suspended = prefs.getString(PREF_SUSPENDED, null)
|
||||||
|
if (suspended != null)
|
||||||
|
{
|
||||||
|
if (top == suspended)
|
||||||
|
{
|
||||||
|
return
|
||||||
|
}
|
||||||
|
prefs.edit().remove(PREF_SUSPENDED).apply()
|
||||||
|
}
|
||||||
|
val active = _list.value.firstOrNull { it.id == top }
|
||||||
val applied = prefs.getString(PREF_APPLIED, null)
|
val applied = prefs.getString(PREF_APPLIED, null)
|
||||||
if (active?.id == applied)
|
if (active?.id == applied && active != null)
|
||||||
{
|
{
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (active == null)
|
if (active == null)
|
||||||
{
|
{ /* nothing, or a built-in network rule above: the auto-connect acts */
|
||||||
restoreIfApplied(app)
|
restoreIfApplied(app)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -255,6 +313,63 @@ object ScenarioEngine
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The common order of the built-in rules and the scenarios (F_Automation, top wins). */
|
||||||
|
fun order(context: Context): List<String>
|
||||||
|
{
|
||||||
|
val raw = PreferenceManager.getDefaultSharedPreferences(context).getString(PREF_ORDER, null)
|
||||||
|
return orderedIds(raw?.split(',')?.filter { it.isNotBlank() } ?: emptyList(), flow(context).value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveOrder(context: Context, order: List<String>)
|
||||||
|
{
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context).edit().putString(PREF_ORDER, order.joinToString(",")).apply()
|
||||||
|
_orderChanges.value = _orderChanges.value + 1
|
||||||
|
evaluate(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _orderChanges = MutableStateFlow(0)
|
||||||
|
/** Bumps when the order changed (the list screen redraws). */
|
||||||
|
val orderChanges: StateFlow<Int> = _orderChanges
|
||||||
|
|
||||||
|
/** Moves [id] one place up (-1) or down (+1) in the common order. */
|
||||||
|
fun move(context: Context, id: String, delta: Int)
|
||||||
|
{
|
||||||
|
val list = order(context).toMutableList()
|
||||||
|
val i = list.indexOf(id)
|
||||||
|
val j = i + delta
|
||||||
|
if (i < 0 || j < 0 || j >= list.size)
|
||||||
|
{
|
||||||
|
return
|
||||||
|
}
|
||||||
|
list.add(j, list.removeAt(i))
|
||||||
|
saveOrder(context, list)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user turned the VPN on or off or paused it by hand: the running
|
||||||
|
* scenario steps back until it ends and puts nothing back afterwards.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun onManual(context: Context)
|
||||||
|
{
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context.applicationContext)
|
||||||
|
val applied = prefs.getString(PREF_APPLIED, null) ?: return
|
||||||
|
DiagLog.i(TAG, "manual action, scenario steps back until it ends")
|
||||||
|
prefs.edit().putString(PREF_SUSPENDED, applied)
|
||||||
|
.remove(PREF_APPLIED).remove(PREF_WAS_ON).remove(PREF_WAS_PROFILE).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A scenario is in force: the built-in network rules (auto-connect) stand aside. */
|
||||||
|
@JvmStatic
|
||||||
|
fun inControl(context: Context): Boolean =
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context.applicationContext).getString(PREF_APPLIED, null) != null
|
||||||
|
|
||||||
|
private fun builtinRules(context: Context): BuiltinRules
|
||||||
|
{
|
||||||
|
val c = org.strongswan.android.logic.autoconnect.AutoConnectManager.getInstance(context).currentConfig
|
||||||
|
return BuiltinRules(c.enabled, c.parkOnTrusted, c.connectOnUntrusted, c.trustedSsids)
|
||||||
|
}
|
||||||
|
|
||||||
private fun restoreIfApplied(context: Context)
|
private fun restoreIfApplied(context: Context)
|
||||||
{
|
{
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
|||||||
+32
-18
@@ -148,6 +148,7 @@ private fun ScenariosScreen(onBack: () -> Unit)
|
|||||||
val scenarios by remember { ScenarioEngine.flow(context) }.collectAsStateWithLifecycle()
|
val scenarios by remember { ScenarioEngine.flow(context) }.collectAsStateWithLifecycle()
|
||||||
var editing by remember { mutableStateOf<Scenario?>(null) }
|
var editing by remember { mutableStateOf<Scenario?>(null) }
|
||||||
var pickRulesServer by remember { mutableStateOf(false) }
|
var pickRulesServer by remember { mutableStateOf(false) }
|
||||||
|
val orderChanges by ScenarioEngine.orderChanges.collectAsStateWithLifecycle()
|
||||||
var creating by remember { mutableStateOf(false) }
|
var creating by remember { mutableStateOf(false) }
|
||||||
var resumes by remember { mutableIntStateOf(0) }
|
var resumes by remember { mutableIntStateOf(0) }
|
||||||
val owner = LocalLifecycleOwner.current
|
val owner = LocalLifecycleOwner.current
|
||||||
@@ -205,10 +206,24 @@ private fun ScenariosScreen(onBack: () -> Unit)
|
|||||||
stringResource(R.string.scenarios_intro), style = ShxType.note.copy(fontSize = 14.sp, lineHeight = 20.sp, fontWeight = FontWeight.Normal),
|
stringResource(R.string.scenarios_intro), style = ShxType.note.copy(fontSize = 14.sp, lineHeight = 20.sp, fontWeight = FontWeight.Normal),
|
||||||
color = c.textSoft, modifier = Modifier.padding(start = 4.dp, end = 4.dp, bottom = 4.dp),
|
color = c.textSoft, modifier = Modifier.padding(start = 4.dp, end = 4.dp, bottom = 4.dp),
|
||||||
)
|
)
|
||||||
RuleCard(ShxIcons.Wifi, c.accentContainer, c.onAccentContainer, stringResource(R.string.scenarios_rule_trusted), stringResource(R.string.scenario_do_off), config.enabled && config.parkOnTrusted, null)
|
/* one order for the network rules and the scenarios: the top one that applies wins */
|
||||||
{ on -> updateAuto { it.copy(parkOnTrusted = on, connectOnUntrusted = if (on && !it.enabled) true else it.connectOnUntrusted) } }
|
val order = remember(scenarios, orderChanges) { ScenarioEngine.order(context) }
|
||||||
RuleCard(ShxIcons.Phone, c.accentContainer, c.onAccentContainer, stringResource(R.string.scenarios_rule_untrusted), stringResource(R.string.scenario_do_on), config.enabled && config.connectOnUntrusted, null)
|
order.forEach { id ->
|
||||||
{ on -> updateAuto { it.copy(connectOnUntrusted = on, parkOnTrusted = if (on && !it.enabled) true else it.parkOnTrusted) } }
|
when (id)
|
||||||
|
{
|
||||||
|
org.strongswan.android.logic.scenarios.BUILTIN_TRUSTED ->
|
||||||
|
RuleCard(ShxIcons.Wifi, c.accentContainer, c.onAccentContainer, stringResource(R.string.scenarios_rule_trusted), stringResource(R.string.scenario_do_off), config.enabled && config.parkOnTrusted, null)
|
||||||
|
{ on -> updateAuto { it.copy(parkOnTrusted = on, connectOnUntrusted = if (on && !it.enabled) true else it.connectOnUntrusted) } }
|
||||||
|
org.strongswan.android.logic.scenarios.BUILTIN_UNTRUSTED ->
|
||||||
|
RuleCard(ShxIcons.Phone, c.accentContainer, c.onAccentContainer, stringResource(R.string.scenarios_rule_untrusted), stringResource(R.string.scenario_do_on), config.enabled && config.connectOnUntrusted, null)
|
||||||
|
{ on -> updateAuto { it.copy(connectOnUntrusted = on, parkOnTrusted = if (on && !it.enabled) true else it.parkOnTrusted) } }
|
||||||
|
else -> scenarios.firstOrNull { it.id == id }?.let { s ->
|
||||||
|
val (icon, bg, fg) = triggerLook(s.trigger)
|
||||||
|
RuleCard(icon, bg, fg, ScenarioEngine.describeWhen(context, s), stringResource(ScenarioEngine.actionLabel(s.action)), s.enabled, onClick = { editing = s })
|
||||||
|
{ on -> ScenarioEngine.save(context, scenarios.map { if (it.id == s.id) it.copy(enabled = on) else it }) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
/* the server the two network rules connect (was the old «Trusted networks» screen) */
|
/* the server the two network rules connect (was the old «Trusted networks» screen) */
|
||||||
if (config.enabled)
|
if (config.enabled)
|
||||||
{
|
{
|
||||||
@@ -227,11 +242,6 @@ private fun ScenariosScreen(onBack: () -> Unit)
|
|||||||
Icon(ShxIcons.Chevron, contentDescription = null, tint = c.textSub, modifier = Modifier.size(18.dp))
|
Icon(ShxIcons.Chevron, contentDescription = null, tint = c.textSub, modifier = Modifier.size(18.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scenarios.forEach { s ->
|
|
||||||
val (icon, bg, fg) = triggerLook(s.trigger)
|
|
||||||
RuleCard(icon, bg, fg, ScenarioEngine.describeWhen(context, s), stringResource(ScenarioEngine.actionLabel(s.action)), s.enabled, onClick = { editing = s })
|
|
||||||
{ on -> ScenarioEngine.save(context, scenarios.map { if (it.id == s.id) it.copy(enabled = on) else it }) }
|
|
||||||
}
|
|
||||||
ShxSectionHeader(stringResource(R.string.scenarios_trusted).uppercase(), Modifier.padding(start = 6.dp, end = 6.dp, top = 10.dp))
|
ShxSectionHeader(stringResource(R.string.scenarios_trusted).uppercase(), Modifier.padding(start = 6.dp, end = 6.dp, top = 10.dp))
|
||||||
ShxPanel()
|
ShxPanel()
|
||||||
{
|
{
|
||||||
@@ -323,12 +333,9 @@ private fun ScenariosScreen(onBack: () -> Unit)
|
|||||||
editing = null
|
editing = null
|
||||||
},
|
},
|
||||||
onDelete = editing?.let { e -> { ScenarioEngine.save(context, scenarios.filter { it.id != e.id }); editing = null } },
|
onDelete = editing?.let { e -> { ScenarioEngine.save(context, scenarios.filter { it.id != e.id }); editing = null } },
|
||||||
onRaise = editing?.takeIf { e -> scenarios.indexOfFirst { it.id == e.id } > 0 }?.let { e ->
|
onMove = editing?.let { e ->
|
||||||
{
|
{ delta: Int ->
|
||||||
val i = scenarios.indexOfFirst { it.id == e.id }
|
ScenarioEngine.move(context, e.id, delta)
|
||||||
val list = scenarios.toMutableList()
|
|
||||||
list.add(i - 1, list.removeAt(i))
|
|
||||||
ScenarioEngine.save(context, list)
|
|
||||||
editing = null
|
editing = null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -389,7 +396,7 @@ private fun ScenarioSheet(
|
|||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onSave: (Scenario) -> Unit,
|
onSave: (Scenario) -> Unit,
|
||||||
onDelete: (() -> Unit)?,
|
onDelete: (() -> Unit)?,
|
||||||
onRaise: (() -> Unit)?,
|
onMove: ((Int) -> Unit)?,
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
val c = LocalShxColors.current
|
val c = LocalShxColors.current
|
||||||
@@ -508,11 +515,18 @@ private fun ScenarioSheet(
|
|||||||
Text(stringResource(R.string.scenario_app_access), style = ShxType.meta.copy(fontWeight = FontWeight.Normal, lineHeight = 17.sp), color = c.onWarningContainer,
|
Text(stringResource(R.string.scenario_app_access), style = ShxType.meta.copy(fontWeight = FontWeight.Normal, lineHeight = 17.sp), color = c.onWarningContainer,
|
||||||
modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(14.dp)).background(c.warningContainer).padding(12.dp))
|
modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(14.dp)).background(c.warningContainer).padding(12.dp))
|
||||||
}
|
}
|
||||||
if (onDelete != null || onRaise != null)
|
if (onMove != null)
|
||||||
|
{
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(10.dp))
|
||||||
|
{
|
||||||
|
ShxButton(stringResource(R.string.scenario_raise), { onMove(-1) }, style = ShxButtonStyle.SOFT, height = 48.dp, modifier = Modifier.weight(1f), icon = ShxIcons.ArrowUp)
|
||||||
|
ShxButton(stringResource(R.string.scenario_lower), { onMove(1) }, style = ShxButtonStyle.SOFT, height = 48.dp, modifier = Modifier.weight(1f), icon = ShxIcons.ArrowDown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (onDelete != null)
|
||||||
{
|
{
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp))
|
Row(horizontalArrangement = Arrangement.spacedBy(10.dp))
|
||||||
{
|
{
|
||||||
onRaise?.let { ShxButton(stringResource(R.string.scenario_raise), it, style = ShxButtonStyle.SOFT, height = 48.dp, modifier = Modifier.weight(1f)) }
|
|
||||||
onDelete?.let { ShxButton(stringResource(R.string.server_delete_confirm), it, style = ShxButtonStyle.DANGER, height = 48.dp, modifier = Modifier.weight(1f)) }
|
onDelete?.let { ShxButton(stringResource(R.string.server_delete_confirm), it, style = ShxButtonStyle.DANGER, height = 48.dp, modifier = Modifier.weight(1f)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -536,11 +536,13 @@ public class VpnProfileControlActivity extends AppCompatActivity implements VpnD
|
|||||||
String action = normalizeAction(intent);
|
String action = normalizeAction(intent);
|
||||||
|
|
||||||
if (START_PROFILE.equals(action))
|
if (START_PROFILE.equals(action))
|
||||||
{
|
{ /* app, tile, widget, shortcut: a hand action wins over a scenario */
|
||||||
|
org.strongswan.android.logic.scenarios.ScenarioEngine.onManual(this);
|
||||||
startVpnProfile(intent);
|
startVpnProfile(intent);
|
||||||
}
|
}
|
||||||
else if (DISCONNECT.equals(action))
|
else if (DISCONNECT.equals(action))
|
||||||
{
|
{
|
||||||
|
org.strongswan.android.logic.scenarios.ScenarioEngine.onManual(this);
|
||||||
disconnect(intent);
|
disconnect(intent);
|
||||||
}
|
}
|
||||||
else if (TOGGLE_VPN.equals(action))
|
else if (TOGGLE_VPN.equals(action))
|
||||||
|
|||||||
+4
@@ -451,6 +451,7 @@ class HomeViewModel(app: Application) : AndroidViewModel(app), VpnStateService.V
|
|||||||
|
|
||||||
fun disconnect()
|
fun disconnect()
|
||||||
{
|
{
|
||||||
|
org.strongswan.android.logic.scenarios.ScenarioEngine.onManual(getApplication())
|
||||||
/* counted per Wi-Fi for the «turn it off here automatically?» suggestion */
|
/* 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)
|
org.strongswan.android.ui.compose.home.ScenarioHints.manualOff(getApplication(), if (_networkKinds.value.first) _ssid.value else null)
|
||||||
service?.disconnect()
|
service?.disconnect()
|
||||||
@@ -464,6 +465,7 @@ class HomeViewModel(app: Application) : AndroidViewModel(app), VpnStateService.V
|
|||||||
/** Disconnects for [minutes]; the tunnel comes back by itself when the pause ends. */
|
/** Disconnects for [minutes]; the tunnel comes back by itself when the pause ends. */
|
||||||
fun pause(minutes: Int)
|
fun pause(minutes: Int)
|
||||||
{
|
{
|
||||||
|
org.strongswan.android.logic.scenarios.ScenarioEngine.onManual(getApplication())
|
||||||
val svc = service ?: return
|
val svc = service ?: return
|
||||||
val profile = _ui.value.activeProfile ?: _ui.value.selected ?: return
|
val profile = _ui.value.activeProfile ?: _ui.value.selected ?: return
|
||||||
PauseManager.pause(getApplication(), svc, profile.getUUID().toString(), minutes)
|
PauseManager.pause(getApplication(), svc, profile.getUUID().toString(), minutes)
|
||||||
@@ -477,6 +479,7 @@ class HomeViewModel(app: Application) : AndroidViewModel(app), VpnStateService.V
|
|||||||
/** Pause until 07:00 (today or tomorrow). */
|
/** Pause until 07:00 (today or tomorrow). */
|
||||||
fun pauseUntilMorning()
|
fun pauseUntilMorning()
|
||||||
{
|
{
|
||||||
|
org.strongswan.android.logic.scenarios.ScenarioEngine.onManual(getApplication())
|
||||||
val svc = service ?: return
|
val svc = service ?: return
|
||||||
val profile = _ui.value.activeProfile ?: _ui.value.selected ?: return
|
val profile = _ui.value.activeProfile ?: _ui.value.selected ?: return
|
||||||
PauseManager.pauseUntil(getApplication(), svc, profile.getUUID().toString(), PauseManager.nextMorning(System.currentTimeMillis()))
|
PauseManager.pauseUntil(getApplication(), svc, profile.getUUID().toString(), PauseManager.nextMorning(System.currentTimeMillis()))
|
||||||
@@ -485,6 +488,7 @@ class HomeViewModel(app: Application) : AndroidViewModel(app), VpnStateService.V
|
|||||||
/** Pause until the device moves to another network. */
|
/** Pause until the device moves to another network. */
|
||||||
fun pauseUntilNetworkChange()
|
fun pauseUntilNetworkChange()
|
||||||
{
|
{
|
||||||
|
org.strongswan.android.logic.scenarios.ScenarioEngine.onManual(getApplication())
|
||||||
val svc = service ?: return
|
val svc = service ?: return
|
||||||
val profile = _ui.value.activeProfile ?: _ui.value.selected ?: return
|
val profile = _ui.value.activeProfile ?: _ui.value.selected ?: return
|
||||||
PauseManager.pauseUntilNetworkChange(getApplication(), svc, profile.getUUID().toString())
|
PauseManager.pauseUntilNetworkChange(getApplication(), svc, profile.getUUID().toString())
|
||||||
|
|||||||
@@ -1337,4 +1337,5 @@
|
|||||||
<string name="picker_title">Выберите сервер</string>
|
<string name="picker_title">Выберите сервер</string>
|
||||||
<string name="picker_all">Все серверы</string>
|
<string name="picker_all">Все серверы</string>
|
||||||
<string name="picker_add">Добавить</string>
|
<string name="picker_add">Добавить</string>
|
||||||
|
<string name="scenario_lower">Опустить ниже</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1323,4 +1323,5 @@
|
|||||||
<string name="picker_title">Choose a server</string>
|
<string name="picker_title">Choose a server</string>
|
||||||
<string name="picker_all">All servers</string>
|
<string name="picker_all">All servers</string>
|
||||||
<string name="picker_add">Add</string>
|
<string name="picker_add">Add</string>
|
||||||
|
<string name="scenario_lower">Move down</string>
|
||||||
</resources>
|
</resources>
|
||||||
+31
@@ -55,4 +55,35 @@ class ScenariosTest
|
|||||||
assertThat(activeScenario(listOf(mobile), idle.copy(wifi = true))).isNull()
|
assertThat(activeScenario(listOf(mobile), idle.copy(wifi = true))).isNull()
|
||||||
assertThat(activeScenario(listOf(mobile), idle)?.id).isEqualTo("m")
|
assertThat(activeScenario(listOf(mobile), idle)?.id).isEqualTo("m")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val rules = BuiltinRules(enabled = true, parkOnTrusted = true, connectOnUntrusted = true, trustedSsids = setOf("Home_5G"))
|
||||||
|
private val night = Scenario(id = "n", trigger = ScenarioTrigger.TIME, startMinute = 60, endMinute = 420, action = ScenarioAction.PAUSE)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun builtinsComeFirstByDefault()
|
||||||
|
{
|
||||||
|
assertThat(orderedIds(emptyList(), listOf(night))).containsExactly(BUILTIN_TRUSTED, BUILTIN_UNTRUSTED, "n")
|
||||||
|
/* unknown ids in the stored order are dropped, new ones go last */
|
||||||
|
assertThat(orderedIds(listOf("n", "gone", BUILTIN_TRUSTED), listOf(night))).containsExactly("n", BUILTIN_TRUSTED, BUILTIN_UNTRUSTED)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun upperRuleWins()
|
||||||
|
{
|
||||||
|
val nightOnMobile = idle.copy(minute = 3 * 60)
|
||||||
|
/* default order: the mobile rule is above the night pause */
|
||||||
|
assertThat(topEntry(emptyList(), listOf(night), rules, nightOnMobile)).isEqualTo(BUILTIN_UNTRUSTED)
|
||||||
|
/* the user moved the night pause up */
|
||||||
|
assertThat(topEntry(listOf("n"), listOf(night), rules, nightOnMobile)).isEqualTo("n")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun trustedWifiAndDisabledRules()
|
||||||
|
{
|
||||||
|
val home = idle.copy(wifi = true, ssid = "home_5g", mobile = false)
|
||||||
|
assertThat(topEntry(emptyList(), emptyList(), rules, home)).isEqualTo(BUILTIN_TRUSTED)
|
||||||
|
assertThat(topEntry(emptyList(), emptyList(), rules, home.copy(ssid = "Cafe"))).isEqualTo(BUILTIN_UNTRUSTED)
|
||||||
|
assertThat(topEntry(emptyList(), listOf(night), rules.copy(enabled = false), idle.copy(minute = 3 * 60))).isEqualTo("n")
|
||||||
|
assertThat(topEntry(emptyList(), emptyList(), rules.copy(enabled = false), home)).isNull()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user