android: «the app closed unexpectedly» event with «Send a report» after a Java, native (daemon) or ANR death of the previous process; always-on/lockdown read from the running VpnService (the Settings.Global key never existed) with a warning when it gets turned off
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0e35a794c9
commit
2b4d0ef12b
16 files changed
+243
-17
No files matched your search
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* 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. See <http://www.fsf.org/copyleft/gpl.txt>.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||||
|
* for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.strongswan.android.logic
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.VpnService
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import org.strongswan.android.R
|
||||||
|
import org.strongswan.android.logic.events.EventAction
|
||||||
|
import org.strongswan.android.logic.events.EventCategory
|
||||||
|
import org.strongswan.android.logic.events.EventKind
|
||||||
|
import org.strongswan.android.logic.events.EventLog
|
||||||
|
|
||||||
|
/**
|
||||||
|
* «Block connections without VPN» as the system reports it. Only the running
|
||||||
|
* VpnService can ask (Android 10+), so the last answer is kept for the UI.
|
||||||
|
* When it was on and is off now, the user gets a warning.
|
||||||
|
*/
|
||||||
|
object AlwaysOnVpn
|
||||||
|
{
|
||||||
|
private const val PREF_LOCKDOWN = "pref_vpn_lockdown"
|
||||||
|
private const val EVENT_KEY = "lockdown-off"
|
||||||
|
|
||||||
|
/** Last value seen by the service; false before the first connection or below Android 10. */
|
||||||
|
@JvmStatic
|
||||||
|
fun isLockdown(context: Context): Boolean =
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context).getBoolean(PREF_LOCKDOWN, false)
|
||||||
|
|
||||||
|
/** Called by the service when the tunnel is up. */
|
||||||
|
@JvmStatic
|
||||||
|
fun update(service: VpnService)
|
||||||
|
{
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
|
||||||
|
{
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val lockdown = try
|
||||||
|
{
|
||||||
|
service.isLockdownEnabled
|
||||||
|
}
|
||||||
|
catch (_: Exception)
|
||||||
|
{
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(service)
|
||||||
|
val was = prefs.getBoolean(PREF_LOCKDOWN, false)
|
||||||
|
if (lockdown != was)
|
||||||
|
{
|
||||||
|
prefs.edit().putBoolean(PREF_LOCKDOWN, lockdown).apply()
|
||||||
|
DiagLog.i("AlwaysOnVpn", "lockdown ${if (lockdown) "on" else "off"} (always-on ${service.isAlwaysOn})")
|
||||||
|
}
|
||||||
|
val events = EventLog.getInstance(service)
|
||||||
|
if (lockdown)
|
||||||
|
{
|
||||||
|
events.resolve(EVENT_KEY)
|
||||||
|
}
|
||||||
|
else if (was)
|
||||||
|
{
|
||||||
|
events.record(
|
||||||
|
EventKind.WARN, EventCategory.CONNECTION, service.getString(R.string.event_lockdown_off_title),
|
||||||
|
service.getString(R.string.event_lockdown_off_text), EventAction.OPEN_VPN_SETTINGS, EVENT_KEY,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
@@ -946,6 +946,7 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
|
|||||||
if (state == State.CONNECTED)
|
if (state == State.CONNECTED)
|
||||||
{
|
{
|
||||||
TrafficHistory.start(this);
|
TrafficHistory.start(this);
|
||||||
|
AlwaysOnVpn.update(this);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
/*
|
||||||
|
* 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. See <http://www.fsf.org/copyleft/gpl.txt>.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||||
|
* for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.strongswan.android.logic
|
||||||
|
|
||||||
|
import android.app.ActivityManager
|
||||||
|
import android.app.ApplicationExitInfo
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import org.strongswan.android.R
|
||||||
|
import org.strongswan.android.logic.events.EventAction
|
||||||
|
import org.strongswan.android.logic.events.EventCategory
|
||||||
|
import org.strongswan.android.logic.events.EventKind
|
||||||
|
import org.strongswan.android.logic.events.EventLog
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notices that the previous process died by a crash and leaves an event with
|
||||||
|
* «Send a report». Android 11+ keeps the exit reasons itself, which also
|
||||||
|
* covers native crashes of the in-process daemon and ANRs; older versions only
|
||||||
|
* see Java exceptions, marked by the uncaught exception handler.
|
||||||
|
*/
|
||||||
|
object CrashWatch
|
||||||
|
{
|
||||||
|
private const val TAG = "CrashWatch"
|
||||||
|
private const val PREF_SEEN = "pref_crash_seen_at"
|
||||||
|
private const val PREF_JAVA_CRASH = "pref_java_crash_at"
|
||||||
|
const val EVENT_KEY = "crash"
|
||||||
|
|
||||||
|
/** From the uncaught exception handler: synchronous, the process dies right after. */
|
||||||
|
@JvmStatic
|
||||||
|
fun markJavaCrash(context: Context)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context).edit()
|
||||||
|
.putLong(PREF_JAVA_CRASH, System.currentTimeMillis()).commit()
|
||||||
|
}
|
||||||
|
catch (_: Exception)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** At process start, off the main thread. */
|
||||||
|
@JvmStatic
|
||||||
|
fun check(context: Context)
|
||||||
|
{
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
var at = prefs.getLong(PREF_JAVA_CRASH, 0L)
|
||||||
|
var what = "java exception"
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
val am = context.getSystemService(ActivityManager::class.java)
|
||||||
|
am?.getHistoricalProcessExitReasons(context.packageName, 0, 10)
|
||||||
|
?.filter { it.reason in setOf(ApplicationExitInfo.REASON_CRASH, ApplicationExitInfo.REASON_CRASH_NATIVE, ApplicationExitInfo.REASON_ANR) }
|
||||||
|
?.maxByOrNull { it.timestamp }
|
||||||
|
?.takeIf { it.timestamp > at }
|
||||||
|
?.let {
|
||||||
|
at = it.timestamp
|
||||||
|
what = "${reasonName(it.reason)}: ${it.description ?: ""}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e: Exception)
|
||||||
|
{
|
||||||
|
DiagLog.i(TAG, "exit reasons unavailable: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val seen = prefs.getLong(PREF_SEEN, -1L)
|
||||||
|
if (seen < 0L)
|
||||||
|
{ /* first start with this feature: older history is not news */
|
||||||
|
prefs.edit().putLong(PREF_SEEN, maxOf(at, System.currentTimeMillis())).apply()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (at <= seen)
|
||||||
|
{
|
||||||
|
return
|
||||||
|
}
|
||||||
|
prefs.edit().putLong(PREF_SEEN, at).apply()
|
||||||
|
DiagLog.i(TAG, "previous process crashed at $at ($what)")
|
||||||
|
EventLog.getInstance(context).record(
|
||||||
|
EventKind.WARN, EventCategory.APP, context.getString(R.string.event_crash_title),
|
||||||
|
context.getString(R.string.event_crash_text), EventAction.SEND_REPORT, EVENT_KEY,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun reasonName(reason: Int): String = when (reason)
|
||||||
|
{
|
||||||
|
ApplicationExitInfo.REASON_CRASH_NATIVE -> "native crash"
|
||||||
|
ApplicationExitInfo.REASON_ANR -> "ANR"
|
||||||
|
else -> "java crash"
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
@@ -99,6 +99,7 @@ public class StrongSwanApplication extends Application
|
|||||||
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
|
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
|
||||||
DiagLog.i(TAG, "UNCAUGHT EXCEPTION on " + thread.getName() + ": " +
|
DiagLog.i(TAG, "UNCAUGHT EXCEPTION on " + thread.getName() + ": " +
|
||||||
Log.getStackTraceString(throwable));
|
Log.getStackTraceString(throwable));
|
||||||
|
CrashWatch.markJavaCrash(StrongSwanApplication.this);
|
||||||
if (previousHandler != null)
|
if (previousHandler != null)
|
||||||
{
|
{
|
||||||
previousHandler.uncaughtException(thread, throwable);
|
previousHandler.uncaughtException(thread, throwable);
|
||||||
@@ -107,6 +108,9 @@ public class StrongSwanApplication extends Application
|
|||||||
|
|
||||||
mDatabaseHelper = new DatabaseHelper(mContext);
|
mDatabaseHelper = new DatabaseHelper(mContext);
|
||||||
|
|
||||||
|
/* «the app crashed» event with a report button for the previous process */
|
||||||
|
mExecutorService.execute(() -> CrashWatch.check(mContext));
|
||||||
|
|
||||||
mManagedConfigurationService = new ManagedConfigurationService(mContext);
|
mManagedConfigurationService = new ManagedConfigurationService(mContext);
|
||||||
|
|
||||||
mTrustedCertificateManager = new ManagedTrustedCertificateManager(mContext, mExecutorService, mMainHandler,
|
mTrustedCertificateManager = new ManagedTrustedCertificateManager(mContext, mExecutorService, mMainHandler,
|
||||||
|
|||||||
+4
@@ -70,6 +70,10 @@ enum class EventAction
|
|||||||
PROFILE_UPDATE,
|
PROFILE_UPDATE,
|
||||||
/** open the server page of the profile [ShxEvent.target] */
|
/** open the server page of the profile [ShxEvent.target] */
|
||||||
OPEN_SERVER,
|
OPEN_SERVER,
|
||||||
|
/** share the logs with support (F_Log «Send») */
|
||||||
|
SEND_REPORT,
|
||||||
|
/** the Android VPN settings (always-on, block without VPN) */
|
||||||
|
OPEN_VPN_SETTINGS,
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ShxEvent(
|
data class ShxEvent(
|
||||||
|
|||||||
@@ -70,6 +70,14 @@ class InboxActivity : AppCompatActivity()
|
|||||||
EventAction.PROFILE_UPDATE -> event.target?.let { startActivity(ServerActivity.intent(this, it, checkUpdate = true)) }
|
EventAction.PROFILE_UPDATE -> event.target?.let { startActivity(ServerActivity.intent(this, it, checkUpdate = true)) }
|
||||||
EventAction.OPEN_SERVER -> event.target?.let { startActivity(ServerActivity.intent(this, it)) }
|
EventAction.OPEN_SERVER -> event.target?.let { startActivity(ServerActivity.intent(this, it)) }
|
||||||
EventAction.OPEN_LOG -> startActivity(Intent(this, LogActivity::class.java))
|
EventAction.OPEN_LOG -> startActivity(Intent(this, LogActivity::class.java))
|
||||||
|
EventAction.SEND_REPORT -> startActivity(Intent(this, LogActivity::class.java).putExtra(LogActivity.EXTRA_SEND, true))
|
||||||
|
EventAction.OPEN_VPN_SETTINGS -> try
|
||||||
|
{
|
||||||
|
startActivity(Intent(Settings.ACTION_VPN_SETTINGS))
|
||||||
|
}
|
||||||
|
catch (_: android.content.ActivityNotFoundException)
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (action == EventAction.OPEN_HOME || action == EventAction.OPEN_ROUTING || action == EventAction.INSTALL_UPDATE)
|
if (action == EventAction.OPEN_HOME || action == EventAction.OPEN_ROUTING || action == EventAction.INSTALL_UPDATE)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -96,6 +96,16 @@ class LogActivity : AppCompatActivity()
|
|||||||
LogScreen(onBack = { finish() }, onSend = { sendLog() }, messages = messages)
|
LogScreen(onBack = { finish() }, onSend = { sendLog() }, messages = messages)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* «Send a report» from the crash event goes straight to the chooser */
|
||||||
|
if (savedInstanceState == null && intent.getBooleanExtra(EXTRA_SEND, false))
|
||||||
|
{
|
||||||
|
sendLog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object
|
||||||
|
{
|
||||||
|
const val EXTRA_SEND = "org.strongswan.android.extra.SEND_LOG"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendLog()
|
private fun sendLog()
|
||||||
|
|||||||
+2
@@ -309,5 +309,7 @@ private fun EventAction.labelRes(): Int = when (this)
|
|||||||
EventAction.SET_APP_DIRECT -> R.string.events_action_direct
|
EventAction.SET_APP_DIRECT -> R.string.events_action_direct
|
||||||
EventAction.PROFILE_UPDATE -> R.string.events_action_profile_update
|
EventAction.PROFILE_UPDATE -> R.string.events_action_profile_update
|
||||||
EventAction.OPEN_SERVER -> R.string.events_action_open_server
|
EventAction.OPEN_SERVER -> R.string.events_action_open_server
|
||||||
|
EventAction.SEND_REPORT -> R.string.diag_send_report
|
||||||
|
EventAction.OPEN_VPN_SETTINGS -> R.string.home_lockdown_enable
|
||||||
EventAction.NONE -> R.string.events_action_open
|
EventAction.NONE -> R.string.events_action_open
|
||||||
}
|
}
|
||||||
+3
-17
@@ -658,24 +658,10 @@ private fun updateStatusText(state: UpdateState): String
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether the system-level lockdown ("Block connections without VPN") is
|
* Whether the system-level lockdown ("Block connections without VPN") is on for
|
||||||
* active for our package. Returns false if not determinable.
|
* us, as the VPN service last saw it (false before the first connection).
|
||||||
*/
|
*/
|
||||||
internal fun Context.isVpnLockdownEnabled(): Boolean
|
internal fun Context.isVpnLockdownEnabled(): Boolean = org.strongswan.android.logic.AlwaysOnVpn.isLockdown(this)
|
||||||
{
|
|
||||||
return try
|
|
||||||
{
|
|
||||||
Settings.Global.getInt(
|
|
||||||
contentResolver,
|
|
||||||
"vpn_lockdown_$packageName",
|
|
||||||
0
|
|
||||||
) != 0
|
|
||||||
}
|
|
||||||
catch (_: Exception)
|
|
||||||
{
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the Samsung battery settings where the user can exclude the app from
|
* Opens the Samsung battery settings where the user can exclude the app from
|
||||||
|
|||||||
@@ -1318,4 +1318,8 @@
|
|||||||
<item quantity="one">%d vertrauenswürdiges Zertifikat</item>
|
<item quantity="one">%d vertrauenswürdiges Zertifikat</item>
|
||||||
<item quantity="other">%d vertrauenswürdige Zertifikate</item>
|
<item quantity="other">%d vertrauenswürdige Zertifikate</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<string name="event_crash_title">Die App wurde unerwartet beendet</string>
|
||||||
|
<string name="event_crash_text">SHX VPN ist zuletzt abgestürzt. Senden Sie einen Bericht, damit wir die Ursache finden.</string>
|
||||||
|
<string name="event_lockdown_off_title">Durchgehend aktives VPN wurde ausgeschaltet</string>
|
||||||
|
<string name="event_lockdown_off_text">Es war vorher an. Jetzt gehen Apps bei jedem VPN-Abbruch für einige Sekunden direkt ins Netz. Schalten Sie es in den VPN-Einstellungen von Android wieder ein.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1236,4 +1236,8 @@
|
|||||||
<item quantity="many">%d zaufanych certyfikatów</item>
|
<item quantity="many">%d zaufanych certyfikatów</item>
|
||||||
<item quantity="other">%d zaufanego certyfikatu</item>
|
<item quantity="other">%d zaufanego certyfikatu</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<string name="event_crash_title">Aplikacja nieoczekiwanie się zamknęła</string>
|
||||||
|
<string name="event_crash_text">Ostatnio SHX VPN uległ awarii. Wyślij raport, abyśmy znaleźli przyczynę.</string>
|
||||||
|
<string name="event_lockdown_off_title">Stały VPN został wyłączony</string>
|
||||||
|
<string name="event_lockdown_off_text">Wcześniej był włączony. Teraz przy każdym zerwaniu VPN aplikacje na kilka sekund łączą się bezpośrednio. Włącz go ponownie w ustawieniach VPN Androida.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1347,4 +1347,8 @@
|
|||||||
<string name="server_needs_password">Нет сохранённого пароля: переключиться на этот сервер не получится. Сохраните пароль в его профиле.</string>
|
<string name="server_needs_password">Нет сохранённого пароля: переключиться на этот сервер не получится. Сохраните пароль в его профиле.</string>
|
||||||
<string name="event_auto_pw_title">«%1$s» не подключён автоматически</string>
|
<string name="event_auto_pw_title">«%1$s» не подключён автоматически</string>
|
||||||
<string name="event_auto_pw_text">Сетевое правило или сценарий хотели включить VPN, но в профиле нет сохранённого пароля. Сохраните пароль в профиле.</string>
|
<string name="event_auto_pw_text">Сетевое правило или сценарий хотели включить VPN, но в профиле нет сохранённого пароля. Сохраните пароль в профиле.</string>
|
||||||
|
<string name="event_crash_title">Приложение аварийно завершилось</string>
|
||||||
|
<string name="event_crash_text">В прошлый раз SHX VPN закрылся из-за сбоя. Отправьте отчёт, чтобы мы нашли причину.</string>
|
||||||
|
<string name="event_lockdown_off_title">Постоянный VPN выключили</string>
|
||||||
|
<string name="event_lockdown_off_text">Раньше он был включён. Теперь при обрыве VPN приложения на секунды выходят в сеть напрямую. Включите его снова в настройках VPN Android.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1246,4 +1246,8 @@
|
|||||||
<item quantity="many">%d довірених сертифікатів</item>
|
<item quantity="many">%d довірених сертифікатів</item>
|
||||||
<item quantity="other">%d довіреного сертифіката</item>
|
<item quantity="other">%d довіреного сертифіката</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<string name="event_crash_title">Застосунок аварійно завершився</string>
|
||||||
|
<string name="event_crash_text">Минулого разу SHX VPN закрився через збій. Надішліть звіт, щоб ми знайшли причину.</string>
|
||||||
|
<string name="event_lockdown_off_title">Постійну VPN вимкнули</string>
|
||||||
|
<string name="event_lockdown_off_text">Раніше її було ввімкнено. Тепер у разі обриву VPN застосунки на секунди виходять у мережу напряму. Увімкніть її знову в налаштуваннях VPN Android.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1215,4 +1215,8 @@
|
|||||||
<plurals name="settings_certs_n">
|
<plurals name="settings_certs_n">
|
||||||
<item quantity="other">%d 个受信任的证书</item>
|
<item quantity="other">%d 个受信任的证书</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<string name="event_crash_title">应用意外关闭</string>
|
||||||
|
<string name="event_crash_text">SHX VPN 上次发生崩溃。请发送报告,帮助我们找到原因。</string>
|
||||||
|
<string name="event_lockdown_off_title">始终开启的 VPN 已被关闭</string>
|
||||||
|
<string name="event_lockdown_off_text">此前它处于开启状态。现在每次 VPN 断开时,应用都会直接联网几秒钟。请在 Android 的 VPN 设置中重新开启。</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1200,4 +1200,8 @@
|
|||||||
<plurals name="settings_certs_n">
|
<plurals name="settings_certs_n">
|
||||||
<item quantity="other">%d 個信任的憑證</item>
|
<item quantity="other">%d 個信任的憑證</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<string name="event_crash_title">應用程式意外關閉</string>
|
||||||
|
<string name="event_crash_text">SHX VPN 上次發生當機。請傳送報告,協助我們找出原因。</string>
|
||||||
|
<string name="event_lockdown_off_title">永久連線的 VPN 已被關閉</string>
|
||||||
|
<string name="event_lockdown_off_text">先前它處於開啟狀態。現在每次 VPN 中斷時,應用程式都會直接連網幾秒鐘。請在 Android 的 VPN 設定中重新開啟。</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1333,4 +1333,8 @@
|
|||||||
<string name="server_needs_password">No saved password: switching to it will not work. Save it in its profile.</string>
|
<string name="server_needs_password">No saved password: switching to it will not work. Save it in its profile.</string>
|
||||||
<string name="event_auto_pw_title">«%1$s» was not connected automatically</string>
|
<string name="event_auto_pw_title">«%1$s» was not connected automatically</string>
|
||||||
<string name="event_auto_pw_text">A network rule or a scenario wanted the VPN on, but the profile has no saved password. Save it in the profile.</string>
|
<string name="event_auto_pw_text">A network rule or a scenario wanted the VPN on, but the profile has no saved password. Save it in the profile.</string>
|
||||||
|
<string name="event_crash_title">The app closed unexpectedly</string>
|
||||||
|
<string name="event_crash_text">SHX VPN crashed last time. Send a report so we can find the cause.</string>
|
||||||
|
<string name="event_lockdown_off_title">Always-on VPN was turned off</string>
|
||||||
|
<string name="event_lockdown_off_text">It was on before. Now apps go online directly for a few seconds whenever the VPN drops. Turn it back on in the Android VPN settings.</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in new issue
Block a user