Android: чинит R8/ProGuard, обработку VPN permission и debug-инспектор

- consumerProguardFiles + усиленные keep-правила для Jackson — без них R8 в
  релизной сборке ломал десериализацию TunnelConfig, и кнопка подключения
  переставала что-либо делать без единой ошибки в логах.
- Проверка на null у builder.establish() и корректный resolve/reject запроса
  VPN-разрешения.
- WebView.setWebContentsDebuggingEnabled включается только под BuildConfig.DEBUG,
  а не безусловно в релизе.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-08 22:51:36 +07:00
parent 79c9cb48aa
commit 00765b12cd
3 changed files with 50 additions and 4 deletions
@@ -10,6 +10,13 @@ android {
defaultConfig {
minSdk = 21
// Без этого proguard-rules.pro лежит мёртвым грузом — app-модуль (где
// реально включена минификация в release) собирает *.pro только из
// своей собственной директории, а этот модуль в другой. Без keep-правил
// R8 переименовывает TunnelConfig, Jackson теряет reflection-доступ к
// конструктору и падает "Cannot construct instance of `j0.a`..." —
// именно так и было при первом релизном (не dev) билде.
consumerProguardFiles("proguard-rules.pro")
}
compileOptions {
@@ -3,6 +3,13 @@
}
-keep class uniffi.netrunner_client.** { *; }
# Jackson находит @JsonCreator/@JsonProperty только через runtime-reflection
# без сохранённых атрибутов аннотаций и параметров R8 может вырезать саму
# видимость аннотаций, даже если сами классы/поля ниже помечены -keep.
-keepattributes *Annotation*,Signature,InnerClasses,EnclosingMethod
-keep class com.fasterxml.jackson.** { *; }
-dontwarn com.fasterxml.jackson.**
-keep class app.netrunner.vpn.TunnelConfig {
@com.fasterxml.jackson.annotation.JsonProperty <fields>;
<init>(...);
@@ -14,5 +21,10 @@
-keepnames class app.netrunner.vpn.TunVpnService { *; }
-keepnames class app.netrunner.vpn.TunnelConfig { *; }
# @InvokeArg-классы Tauri-плагина (VpnPlugin$VpnArgs, LaunchAppArgs) тот же
# reflection-путь десериализации, что и у TunnelConfig; VpnArgs как раз и
# оборачивает TunnelConfig в реальном invoke-запросе.
-keep class app.netrunner.vpn.VpnPlugin$* { *; }
-keep class com.sun.jna.** { *; }
-dontwarn com.sun.jna.**
@@ -113,7 +113,18 @@ class TunVpnService : VpnService() {
try { builder.addDisallowedApplication(packageName) } catch (e: Exception) { Log.e(TAG, "Exclude fail") }
}
vpnInterface = builder.establish() ?: return
vpnInterface = builder.establish()
if (vpnInterface == null) {
// Раньше здесь было тихое `?: return` — ни одной строки в логах, ни
// сброса статуса на JS-стороне (кнопка навсегда виснет в
// "ПОДКЛЮЧЕНИЕ..."). Типичные причины: в Настройках Android
// "Always-on VPN" залочен на другое приложение, другой VPN уже
// держит интерфейс, либо разрешение отозвано после prepare().
Log.e(TAG, "VpnService.Builder.establish() returned null — VPN interface not created (Always-on VPN on another app? Another VPN active? Permission revoked?)")
sendStatusBroadcast("idle")
stopSelf()
return
}
val fd = vpnInterface!!.detachFd()
try {
@@ -363,6 +374,12 @@ class VpnPlugin(private val activity: Activity) : Plugin(activity) {
override fun load(webView: WebView) {
super.load(webView)
// Tauri сам по себе нигде не зовёт setWebContentsDebuggingEnabled — в
// релизной сборке chrome://inspect не видит WebView вообще. Включаем
// только в debug-сборках (BuildConfig.DEBUG), а не безусловно.
if (BuildConfig.DEBUG) {
WebView.setWebContentsDebuggingEnabled(true)
}
try { System.loadLibrary("netrunner_client") } catch (e: Exception) { Log.e(TAG, "Lib fail") }
requestNotificationPermission()
@@ -412,12 +429,16 @@ class VpnPlugin(private val activity: Activity) : Plugin(activity) {
val args = invoke.parseArgs(VpnArgs::class.java)
val intent = VpnService.prepare(activity)
if (intent != null) {
// Диалог разрешения VPN ещё не показан/не подтверждён — resolve()
// здесь был бы преждевременным: JS увидел бы "успех" ещё до того,
// как юзер вообще ответил на системный диалог. Настоящий
// resolve()/reject() — в onVpnPermissionResult, по факту ответа.
pendingArgs = args
startActivityForResult(invoke, intent, "onVpnPermissionResult")
} else {
startVpnService(args)
invoke.resolve()
}
invoke.resolve()
}
private fun startVpnService(args: VpnArgs) {
@@ -434,9 +455,15 @@ class VpnPlugin(private val activity: Activity) : Plugin(activity) {
@ActivityCallback
fun onVpnPermissionResult(invoke: Invoke, result: ActivityResult) {
if (result.resultCode == Activity.RESULT_OK) pendingArgs?.let { startVpnService(it) }
if (result.resultCode == Activity.RESULT_OK) {
pendingArgs?.let { startVpnService(it) }
invoke.resolve()
} else {
// Юзер отклонил системный диалог VPN-разрешения — раньше это тихо
// резолвилось как успех, и кнопка навсегда зависала в "ПОДКЛЮЧЕНИЕ...".
invoke.reject("VPN permission denied")
}
pendingArgs = null
invoke.resolve()
}
@Command