fix(android): tamper-lock can't linger armed while protection is off (stuck "locked" UI)
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) <noreply@anthropic.com>
This commit is contained in:
parent
fc7a243c9b
commit
3c2aee7bda
@ -307,7 +307,10 @@ class RebreakProtectionModule : Module() {
|
|||||||
return mapOf(
|
return mapOf(
|
||||||
"vpn" to isVpnEffectivelyOn(ctx),
|
"vpn" to isVpnEffectivelyOn(ctx),
|
||||||
"accessibility" to isAccessibilityServiceEnabled(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,
|
"blocklistCount" to count,
|
||||||
"blocklistLastSyncAt" to lastSyncAt,
|
"blocklistLastSyncAt" to lastSyncAt,
|
||||||
)
|
)
|
||||||
@ -368,8 +371,14 @@ class RebreakProtectionModule : Module() {
|
|||||||
val flag = isEnabledFlag(ctx)
|
val flag = isEnabledFlag(ctx)
|
||||||
Log.d(TAG, "isVpnEffectivelyOn: live=$live, prepareIntent=${intent != null}, flag=$flag")
|
Log.d(TAG, "isVpnEffectivelyOn: live=$live, prepareIntent=${intent != null}, flag=$flag")
|
||||||
if (intent != null) {
|
if (intent != null) {
|
||||||
// Permission entzogen → definitely off
|
// Permission entzogen → definitely off. Auch den Tamper-Lock mit-disarmen,
|
||||||
if (flag) prefs(ctx).edit().putBoolean(KEY_ENABLED, false).apply()
|
// 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 false
|
||||||
}
|
}
|
||||||
return live || flag
|
return live || flag
|
||||||
|
|||||||
@ -52,13 +52,14 @@ class RebreakAccessibilityService : AccessibilityService() {
|
|||||||
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
|
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
|
||||||
if (event == null) return
|
if (event == null) return
|
||||||
|
|
||||||
// Globaler Kill-Switch: Dieser Service tut NUR etwas, wenn der User den
|
// Globaler Kill-Switch: Dieser Service tut NUR etwas, wenn der Schutz
|
||||||
// App-Lock explizit armed hat. Ist er nicht armed (Default, inkl. frischem
|
// aktiv ist (`filter_enabled`) UND der User „App-Lock" explizit armed hat
|
||||||
// Onboarding und nach einem abgelaufenen Cooldown der `disarmTamperLock`
|
// (`tamper_armed`). Beides muss stimmen — sonst vollständig passiv (auch
|
||||||
// aufgerufen hat) → vollständig passiv. Der a11y-Service kann sich nicht
|
// kein Logging). Deckt ab: frisches Onboarding, abgelaufener Cooldown
|
||||||
// selbst deaktivieren, also ist das hier die einzige Stelle wo wir ihn
|
// (disarmTamperLock), und den Desync-Fall „tamper noch armed aber Schutz
|
||||||
// stilllegen.
|
// aus" (z.B. VPN extern revoked). Der a11y-Service kann sich nicht selbst
|
||||||
if (!isTamperLockArmed()) return
|
// deaktivieren, also ist das hier die einzige Stelle wo wir ihn stilllegen.
|
||||||
|
if (!isTamperLockArmed() || !isProtectionEnabled()) return
|
||||||
|
|
||||||
val pkg = event.packageName?.toString() ?: return
|
val pkg = event.packageName?.toString() ?: return
|
||||||
if (pkg !in WATCHED_SETTINGS_PACKAGES) return
|
if (pkg !in WATCHED_SETTINGS_PACKAGES) return
|
||||||
@ -131,10 +132,14 @@ class RebreakAccessibilityService : AccessibilityService() {
|
|||||||
performGlobalAction(GLOBAL_ACTION_BACK)
|
performGlobalAction(GLOBAL_ACTION_BACK)
|
||||||
}, 200)
|
}, 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 {
|
mainHandler.post {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
applicationContext,
|
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,
|
Toast.LENGTH_LONG,
|
||||||
).show()
|
).show()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -162,9 +162,21 @@ class RebreakVpnService : VpnService() {
|
|||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Persistierte Flag (gleicher Storage wie der Plugin) clearen — nur
|
/** Persistierte Flags (gleicher Storage wie der Plugin) clearen — nur bei
|
||||||
* bei explizitem User-Revoke (System-Settings-Toggle aus). */
|
* explizitem User-Revoke (System-Settings-Toggle aus). Mit `filter_enabled`
|
||||||
private fun clearEnabledFlag() = setEnabledFlag(false)
|
* 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. */
|
/** Setzt die Plugin-Prefs-Flag synchron mit dem tatsächlichen Service-State. */
|
||||||
private fun setEnabledFlag(value: Boolean) {
|
private fun setEnabledFlag(value: Boolean) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user