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)
|
||||
{
|
||||
TrafficHistory.start(this);
|
||||
AlwaysOnVpn.update(this);
|
||||
}
|
||||
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) -> {
|
||||
DiagLog.i(TAG, "UNCAUGHT EXCEPTION on " + thread.getName() + ": " +
|
||||
Log.getStackTraceString(throwable));
|
||||
CrashWatch.markJavaCrash(StrongSwanApplication.this);
|
||||
if (previousHandler != null)
|
||||
{
|
||||
previousHandler.uncaughtException(thread, throwable);
|
||||
@@ -107,6 +108,9 @@ public class StrongSwanApplication extends Application
|
||||
|
||||
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);
|
||||
|
||||
mTrustedCertificateManager = new ManagedTrustedCertificateManager(mContext, mExecutorService, mMainHandler,
|
||||
|
||||
+4
@@ -70,6 +70,10 @@ enum class EventAction
|
||||
PROFILE_UPDATE,
|
||||
/** open the server page of the profile [ShxEvent.target] */
|
||||
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(
|
||||
|
||||
@@ -70,6 +70,14 @@ class InboxActivity : AppCompatActivity()
|
||||
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_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)
|
||||
{
|
||||
|
||||
@@ -96,6 +96,16 @@ class LogActivity : AppCompatActivity()
|
||||
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()
|
||||
|
||||
+2
@@ -309,5 +309,7 @@ private fun EventAction.labelRes(): Int = when (this)
|
||||
EventAction.SET_APP_DIRECT -> R.string.events_action_direct
|
||||
EventAction.PROFILE_UPDATE -> R.string.events_action_profile_update
|
||||
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
|
||||
}
|
||||
+3
-17
@@ -658,24 +658,10 @@ private fun updateStatusText(state: UpdateState): String
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the system-level lockdown ("Block connections without VPN") is
|
||||
* active for our package. Returns false if not determinable.
|
||||
* Whether the system-level lockdown ("Block connections without VPN") is on for
|
||||
* us, as the VPN service last saw it (false before the first connection).
|
||||
*/
|
||||
internal fun Context.isVpnLockdownEnabled(): Boolean
|
||||
{
|
||||
return try
|
||||
{
|
||||
Settings.Global.getInt(
|
||||
contentResolver,
|
||||
"vpn_lockdown_$packageName",
|
||||
0
|
||||
) != 0
|
||||
}
|
||||
catch (_: Exception)
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
internal fun Context.isVpnLockdownEnabled(): Boolean = org.strongswan.android.logic.AlwaysOnVpn.isLockdown(this)
|
||||
|
||||
/**
|
||||
* 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="other">%d vertrauenswürdige Zertifikate</item>
|
||||
</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>
|
||||
@@ -1236,4 +1236,8 @@
|
||||
<item quantity="many">%d zaufanych certyfikatów</item>
|
||||
<item quantity="other">%d zaufanego certyfikatu</item>
|
||||
</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>
|
||||
@@ -1347,4 +1347,8 @@
|
||||
<string name="server_needs_password">Нет сохранённого пароля: переключиться на этот сервер не получится. Сохраните пароль в его профиле.</string>
|
||||
<string name="event_auto_pw_title">«%1$s» не подключён автоматически</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>
|
||||
@@ -1246,4 +1246,8 @@
|
||||
<item quantity="many">%d довірених сертифікатів</item>
|
||||
<item quantity="other">%d довіреного сертифіката</item>
|
||||
</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>
|
||||
@@ -1215,4 +1215,8 @@
|
||||
<plurals name="settings_certs_n">
|
||||
<item quantity="other">%d 个受信任的证书</item>
|
||||
</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>
|
||||
@@ -1200,4 +1200,8 @@
|
||||
<plurals name="settings_certs_n">
|
||||
<item quantity="other">%d 個信任的憑證</item>
|
||||
</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>
|
||||
@@ -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="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_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>
|
||||
Reference in new issue
Block a user