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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user