From 3c2aee7bda9467297fb4349ec3af0edf23e43544 Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Mon, 11 May 2026 18:34:45 +0200 Subject: [PATCH] fix(android): tamper-lock can't linger armed while protection is off (stuck "locked" UI) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Repro: after a reinstall / external VPN-revoke, `filter_enabled` flipped to false but `tamper_armed` stayed true. Result: buildDeviceState reported tamperLock:true purely from `tamper_armed` → UI mapped that to appDeletionLock:true → lockedIn:true → showed the green "protected & locked" card with no toggles → no way to reactivate. (The a11y service didn't block — handleProtectedSettingsBlock checks isProtectionEnabled — but it kept logging every settings-navigation, wasting CPU.) "Armed but disabled" is an invalid state. - RebreakAccessibilityService: top guard is now `if (!isTamperLockArmed() || !isProtectionEnabled()) return` — fully passive (no logging) whenever protection is off, regardless of a stale tamper flag. - RebreakProtectionModule.buildDeviceState: tamperLock = tamper_armed && filter_enabled. - RebreakProtectionModule.isVpnEffectivelyOn (revoke branch) and RebreakVpnService.onRevoke now clear `tamper_armed` together with `filter_enabled` — the two can't desync. Self-heals: opening the blocker page after the update re-fetches state → tamperLock:false → toggles back. Also: the tamper-block toast is now Lyra-voiced instead of a shield emoji (a real avatar image isn't possible — Android 11+ ignores Toast.setView() for app toasts; lyra-persona can refine the wording). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../RebreakProtectionModule.kt | 15 ++++++++++--- .../RebreakAccessibilityService.kt | 21 ++++++++++++------- .../vpn/RebreakVpnService.kt | 18 +++++++++++++--- 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/apps/rebreak-native/modules/rebreak-protection/android/src/main/java/expo/modules/rebreakprotection/RebreakProtectionModule.kt b/apps/rebreak-native/modules/rebreak-protection/android/src/main/java/expo/modules/rebreakprotection/RebreakProtectionModule.kt index 63e9b33..98133ac 100644 --- a/apps/rebreak-native/modules/rebreak-protection/android/src/main/java/expo/modules/rebreakprotection/RebreakProtectionModule.kt +++ b/apps/rebreak-native/modules/rebreak-protection/android/src/main/java/expo/modules/rebreakprotection/RebreakProtectionModule.kt @@ -307,7 +307,10 @@ class RebreakProtectionModule : Module() { return mapOf( "vpn" to isVpnEffectivelyOn(ctx), "accessibility" to isAccessibilityServiceEnabled(ctx), - "tamperLock" to isTamperLockArmed(ctx), + // Ein armed-aber-Schutz-aus Tamper-Lock ist effektiv KEIN Lock — sonst + // zeigt die UI „verriegelt" ohne dass der User je rauskommt (Desync-Fall: + // `tamper_armed` noch true, aber `filter_enabled` schon false). + "tamperLock" to (isTamperLockArmed(ctx) && isEnabledFlag(ctx)), "blocklistCount" to count, "blocklistLastSyncAt" to lastSyncAt, ) @@ -368,8 +371,14 @@ class RebreakProtectionModule : Module() { val flag = isEnabledFlag(ctx) Log.d(TAG, "isVpnEffectivelyOn: live=$live, prepareIntent=${intent != null}, flag=$flag") if (intent != null) { - // Permission entzogen → definitely off - if (flag) prefs(ctx).edit().putBoolean(KEY_ENABLED, false).apply() + // Permission entzogen → definitely off. Auch den Tamper-Lock mit-disarmen, + // sonst bleibt der State desynct (tamper armed, Schutz aus) → „verriegelt"-UI. + if (flag) { + prefs(ctx).edit() + .putBoolean(KEY_ENABLED, false) + .putBoolean(KEY_TAMPER_ARMED, false) + .apply() + } return false } return live || flag diff --git a/apps/rebreak-native/modules/rebreak-protection/android/src/main/java/expo/modules/rebreakprotection/accessibility/RebreakAccessibilityService.kt b/apps/rebreak-native/modules/rebreak-protection/android/src/main/java/expo/modules/rebreakprotection/accessibility/RebreakAccessibilityService.kt index 32217d1..a549420 100644 --- a/apps/rebreak-native/modules/rebreak-protection/android/src/main/java/expo/modules/rebreakprotection/accessibility/RebreakAccessibilityService.kt +++ b/apps/rebreak-native/modules/rebreak-protection/android/src/main/java/expo/modules/rebreakprotection/accessibility/RebreakAccessibilityService.kt @@ -52,13 +52,14 @@ class RebreakAccessibilityService : AccessibilityService() { override fun onAccessibilityEvent(event: AccessibilityEvent?) { if (event == null) return - // Globaler Kill-Switch: Dieser Service tut NUR etwas, wenn der User den - // App-Lock explizit armed hat. Ist er nicht armed (Default, inkl. frischem - // Onboarding und nach einem abgelaufenen Cooldown der `disarmTamperLock` - // aufgerufen hat) → vollständig passiv. Der a11y-Service kann sich nicht - // selbst deaktivieren, also ist das hier die einzige Stelle wo wir ihn - // stilllegen. - if (!isTamperLockArmed()) return + // Globaler Kill-Switch: Dieser Service tut NUR etwas, wenn der Schutz + // aktiv ist (`filter_enabled`) UND der User „App-Lock" explizit armed hat + // (`tamper_armed`). Beides muss stimmen — sonst vollständig passiv (auch + // kein Logging). Deckt ab: frisches Onboarding, abgelaufener Cooldown + // (disarmTamperLock), und den Desync-Fall „tamper noch armed aber Schutz + // aus" (z.B. VPN extern revoked). Der a11y-Service kann sich nicht selbst + // deaktivieren, also ist das hier die einzige Stelle wo wir ihn stilllegen. + if (!isTamperLockArmed() || !isProtectionEnabled()) return val pkg = event.packageName?.toString() ?: return if (pkg !in WATCHED_SETTINGS_PACKAGES) return @@ -131,10 +132,14 @@ class RebreakAccessibilityService : AccessibilityService() { performGlobalAction(GLOBAL_ACTION_BACK) }, 200) + // Toast in Lyra's Stimme statt eines kühlen Shield-Icons (Android 11+ + // ignoriert setView/Custom-Layouts für App-Toasts → kein echtes Avatar- + // Bild möglich; deshalb signiert der Text mit „Lyra:"). lyra-persona darf + // den Wortlaut gern noch feinschleifen. mainHandler.post { Toast.makeText( applicationContext, - "🛡 Diese Einstellung ist während des Schutzes gesperrt", + "Lyra: Das ist während deines Schutzes gesperrt 💛 Wenn du wirklich raus willst, geht das in der App.", Toast.LENGTH_LONG, ).show() } diff --git a/apps/rebreak-native/modules/rebreak-protection/android/src/main/java/expo/modules/rebreakprotection/vpn/RebreakVpnService.kt b/apps/rebreak-native/modules/rebreak-protection/android/src/main/java/expo/modules/rebreakprotection/vpn/RebreakVpnService.kt index 4d1ec96..61d237c 100644 --- a/apps/rebreak-native/modules/rebreak-protection/android/src/main/java/expo/modules/rebreakprotection/vpn/RebreakVpnService.kt +++ b/apps/rebreak-native/modules/rebreak-protection/android/src/main/java/expo/modules/rebreakprotection/vpn/RebreakVpnService.kt @@ -162,9 +162,21 @@ class RebreakVpnService : VpnService() { super.onDestroy() } - /** Persistierte Flag (gleicher Storage wie der Plugin) clearen — nur - * bei explizitem User-Revoke (System-Settings-Toggle aus). */ - private fun clearEnabledFlag() = setEnabledFlag(false) + /** Persistierte Flags (gleicher Storage wie der Plugin) clearen — nur bei + * explizitem User-Revoke (System-Settings-Toggle aus). Mit `filter_enabled` + * geht auch `tamper_armed` weg: ein Tamper-Lock ohne aktiven Schutz ist + * Unsinn und würde den State desynchen → „verriegelt"-UI ohne Ausweg. */ + private fun clearEnabledFlag() { + try { + applicationContext.getSharedPreferences("rebreak_filter_prefs", Context.MODE_PRIVATE) + .edit() + .putBoolean("filter_enabled", false) + .putBoolean("tamper_armed", false) + .apply() + } catch (e: Exception) { + Log.w(TAG, "clearEnabledFlag failed: ${e.message}") + } + } /** Setzt die Plugin-Prefs-Flag synchron mit dem tatsächlichen Service-State. */ private fun setEnabledFlag(value: Boolean) {