refactor(android): a11y service is now tamper-lock only — no browser URL filtering

The AccessibilityService used to also do a browser-address-bar filter (read the
URL bar of Chrome/Firefox/etc., hash-match against blocklist.bin, GLOBAL_ACTION_BACK
on a hit) as a "layer 2" alongside the VpnService DNS filter. That's redundant
(the VPN catches everything network-level, in browsers AND apps), fragile (per-browser
view-IDs), and produced ghost-blocks (VPN off, a11y still blocking sites). The DNS
filter is the protection; the a11y service's only real value-add is tamper-resistance.

So the a11y service now does ONLY the tamper-lock, and only when the user has armed
"App-Lock": block opening protection-critical settings (disable the ReBreak VPN,
uninstall the app, disable the a11y service itself). Top-level guard is now simply
`if (!isTamperLockArmed()) return` — when App-Lock isn't armed the service is fully
passive. Getting out is still via the regular deactivation cooldown (which disarms
the tamper-lock and stops the VPN).

- RebreakAccessibilityService.kt: removed browser-URL extraction, BROWSER_PACKAGES,
  URL_BAR_IDS, hashList loading, throttle bookkeeping, the block-toast. Kept the
  settings-watchdog (it already covered VPN settings via VpnSettings/vpndialogs +
  the vpn-page keyword cluster) and adjusted its keyword lists to the new a11y
  service summary (old summary kept as a legacy fallback for stale installs).
- accessibility_service_config.xml: dropped browser packages + flagRequestEnhancedWebAccessibility.
- strings.xml (de+en): a11y permission copy reframed — it safeguards the VPN/uninstall,
  it doesn't filter your browser; ends with "you can always exit via the cooldown".
- lib/protection.ts: comment-only (activateFamilyControls logic unchanged).
- locales de/en: App-Lock card copy ("Familienzugriff aktiv" → "Verriegelt — ...",
  "...ReBreak oder den Filter im Impuls abschaltest"), genericised the iOS Screen-Time
  error string.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-05-11 17:42:05 +02:00
parent a80cc8b08d
commit fc7a243c9b
7 changed files with 80 additions and 247 deletions

View File

@ -105,7 +105,10 @@ export const protection = {
async activateFamilyControls(): Promise<{ enabled: boolean; error?: string }> { async activateFamilyControls(): Promise<{ enabled: boolean; error?: string }> {
if (Platform.OS === "android") { if (Platform.OS === "android") {
// Android Layer-2 = AccessibilityService (Browser-URL-Filter) + Tamper-Lock. // Android "App-Lock" = AccessibilityService als reiner Tamper-Lock (KEIN
// Browser-Filter mehr — Glücksspielseiten blockt der VpnService DNS-Filter).
// Der a11y-Service verhindert nur, dass schutz-relevante Settings geöffnet
// werden (VPN abschalten / App deinstallieren / a11y-Service abschalten).
// Two-step UX: // Two-step UX:
// (1) A11y nicht aktiv → Settings öffnen, return {enabled:false} mit // (1) A11y nicht aktiv → Settings öffnen, return {enabled:false} mit
// Marker-Error. UI fragt nach Return den State neu ab und tappt // Marker-Error. UI fragt nach Return den State neu ab und tappt

View File

@ -242,7 +242,7 @@
"activate_url_failed_msg": "Unbekannter Fehler.\nDu kannst es nochmal versuchen oder System-Einstellungen prüfen.", "activate_url_failed_msg": "Unbekannter Fehler.\nDu kannst es nochmal versuchen oder System-Einstellungen prüfen.",
"activate_settings_btn": "Einstellungen", "activate_settings_btn": "Einstellungen",
"activate_app_lock_failed_title": "App-Lock konnte nicht aktiviert werden", "activate_app_lock_failed_title": "App-Lock konnte nicht aktiviert werden",
"activate_app_lock_failed_msg": "Bildschirmzeit-Berechtigung wurde verweigert. Du kannst es nochmal versuchen.", "activate_app_lock_failed_msg": "Die nötige Berechtigung wurde verweigert. Du kannst es nochmal versuchen.",
"sync_list_failed_title": "Filter-Liste konnte nicht geladen werden", "sync_list_failed_title": "Filter-Liste konnte nicht geladen werden",
"sync_list_failed_msg": "Bitte später nochmal versuchen.", "sync_list_failed_msg": "Bitte später nochmal versuchen.",
"activation_failed_title": "Aktivierung fehlgeschlagen", "activation_failed_title": "Aktivierung fehlgeschlagen",
@ -268,8 +268,8 @@
"layers_url_filter_subtitle_active": "System-weiter Filter aktiv", "layers_url_filter_subtitle_active": "System-weiter Filter aktiv",
"layers_url_filter_subtitle_inactive": "Blockt Gambling-Seiten in Safari + Apps", "layers_url_filter_subtitle_inactive": "Blockt Gambling-Seiten in Safari + Apps",
"layers_app_lock_title": "App-Lock", "layers_app_lock_title": "App-Lock",
"layers_app_lock_subtitle_active": "Familienzugriff aktiv", "layers_app_lock_subtitle_active": "Verriegelt — Abschalten nur über die Abkühlphase",
"layers_app_lock_subtitle_inactive": "Verhindert dass du ReBreak im Impuls löschst", "layers_app_lock_subtitle_inactive": "Verhindert, dass du ReBreak oder den Filter im Impuls abschaltest",
"layers_app_lock_warning": "Sobald aktiv kannst du den Schutz nur über einen 24-Stunden-Cooldown abschalten. Das ist gewollt.", "layers_app_lock_warning": "Sobald aktiv kannst du den Schutz nur über einen 24-Stunden-Cooldown abschalten. Das ist gewollt.",
"kpi_global_label": "Geblockte Domains weltweit", "kpi_global_label": "Geblockte Domains weltweit",
"kpi_global_subtitle": "Aktive Einträge in der globalen Blockliste", "kpi_global_subtitle": "Aktive Einträge in der globalen Blockliste",

View File

@ -242,7 +242,7 @@
"activate_url_failed_msg": "Unknown error.\nYou can try again or check System Settings.", "activate_url_failed_msg": "Unknown error.\nYou can try again or check System Settings.",
"activate_settings_btn": "Settings", "activate_settings_btn": "Settings",
"activate_app_lock_failed_title": "Could not activate App Lock", "activate_app_lock_failed_title": "Could not activate App Lock",
"activate_app_lock_failed_msg": "Screen Time permission was denied. You can try again.", "activate_app_lock_failed_msg": "The required permission was denied. You can try again.",
"sync_list_failed_title": "Filter list could not be loaded", "sync_list_failed_title": "Filter list could not be loaded",
"sync_list_failed_msg": "Please try again later.", "sync_list_failed_msg": "Please try again later.",
"activation_failed_title": "Activation failed", "activation_failed_title": "Activation failed",
@ -268,8 +268,8 @@
"layers_url_filter_subtitle_active": "System-wide filter active", "layers_url_filter_subtitle_active": "System-wide filter active",
"layers_url_filter_subtitle_inactive": "Blocks gambling sites in Safari + apps", "layers_url_filter_subtitle_inactive": "Blocks gambling sites in Safari + apps",
"layers_app_lock_title": "App lock", "layers_app_lock_title": "App lock",
"layers_app_lock_subtitle_active": "Family access active", "layers_app_lock_subtitle_active": "Locked — disable only via the cooldown",
"layers_app_lock_subtitle_inactive": "Prevents you from deleting ReBreak on impulse", "layers_app_lock_subtitle_inactive": "Stops you from switching off ReBreak or the filter on impulse",
"layers_app_lock_warning": "Once active, you can only disable protection through a 24-hour cooldown. That's by design.", "layers_app_lock_warning": "Once active, you can only disable protection through a 24-hour cooldown. That's by design.",
"kpi_global_label": "Domains blocked worldwide", "kpi_global_label": "Domains blocked worldwide",
"kpi_global_subtitle": "Active entries in the global blocklist", "kpi_global_subtitle": "Active entries in the global blocklist",

View File

@ -1,119 +1,71 @@
package expo.modules.rebreakprotection.accessibility package expo.modules.rebreakprotection.accessibility
import android.accessibilityservice.AccessibilityService import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.AccessibilityServiceInfo
import android.content.Intent
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo
import android.widget.Toast import android.widget.Toast
import expo.modules.rebreakprotection.filter.HashList
import java.io.File
/** /**
* URL-Filter-Layer 2 parallel zum VpnService DNS-Filter. * Tamper-Lock sichert den Schutz gegen versehentliches/impulsives Abschalten.
* *
* Hintergrund: Der VpnService kann vom User in System-Settings ausgeschaltet * **Was dieser Service NICHT (mehr) tut:** Glücksspielseiten blocken. Das macht
* werden (Always-on VPN ist bei Free-Konsumenten-Devices nicht erzwingbar). * ausschließlich der `RebreakVpnService` (DNS-Filter, ~208k Domains) der greift
* Die Accessibility-Permission hingegen ist ihm "vertrauter" man gibt sie * network-level, also in Browsern *und* Apps, und übersteht Browser-Updates. Den
* einmal und vergisst sie. Solange der User sie nicht explizit entzieht, * früheren Browser-Adressleisten-Filter haben wir entfernt: redundant zum VPN,
* läuft dieser Filter weiter. * fragil (View-IDs pro Browser), und führte zu Geister-Blockaden (VPN aus, a11y
* blockt trotzdem noch).
* *
* Funktionsweise (parallel zu iOS NEFilterBrowserFlow): * **Was dieser Service tut:** Wenn der User App-Lock" aktiviert hat
* - Service hört auf TYPE_WINDOW_CONTENT_CHANGED + TYPE_WINDOW_STATE_CHANGED * (`tamper_armed == true`, opt-in über den App-Button) und der Schutz aktiv ist
* - Filtert nach Browser-Packages (Chrome/Firefox/Edge/Samsung/Brave/Opera) * (`filter_enabled == true`), verhindert er, dass schutz-relevante System-Settings
* - Liest die Adressleiste via AccessibilityNodeInfo * geöffnet werden können:
* - Hash-Match gegen blocklist.bin (gleiche Datei wie VpnService) * - Settings VPN ReBreak abschalten / Always-on" / Profil löschen
* - Bei Treffer: GLOBAL_ACTION_BACK + Toast * - App-Info / Play-Store ReBreak deinstallieren / Daten löschen / Force-Stop
* - Settings Bedienungshilfen den ReBreak-a11y-Service selbst abschalten
* *
* Throttling: pro Browser nur alle 600ms eine URL-Prüfung sonst feuert * Strategie: lauscht auf `TYPE_WINDOW_STATE_CHANGED` (Activity-Wechsel) +
* Chrome bei jeder Frame-Änderung Hunderte Events. * `TYPE_WINDOW_CONTENT_CHANGED` (Dialog-Inhalt lädt bei manchen OEMs nach) aus
* den Settings-/Installer-Packages matcht Activity-Klasse bzw. Window-Text gegen
* gefährliche Patterns `GLOBAL_ACTION_BACK` + Toast. Rauskommen geht nur über
* den regulären Deaktivierungs-Cooldown (der disarmed `tamper_armed` und stoppt
* das VPN danach ist dieser Service vollständig passiv).
*
* Bypass-Vektoren die das NICHT abdeckt: Safe-Mode-Reboot (Apps off), ADB/Root.
*/ */
class RebreakAccessibilityService : AccessibilityService() { class RebreakAccessibilityService : AccessibilityService() {
private lateinit var hashList: HashList
private val mainHandler = Handler(Looper.getMainLooper()) private val mainHandler = Handler(Looper.getMainLooper())
private val lastCheck = HashMap<String, Long>()
private val lastBlockedUrl = HashMap<String, String>()
private var lastSettingsCheck: Long = 0L private var lastSettingsCheck: Long = 0L
/** Nach einem TAMPER-BLOCK: 3s lang keinen weiteren Block triggern. /** Nach einem TAMPER-BLOCK: kurz keinen weiteren Block triggern. Verhindert
* Verhindert Toast-Spam wenn User legitim in Settings navigieren will und * Toast-Spam wenn alte Keyword-Matches in Page-Transitions nachziehen. */
* Page-Transitions noch alte Keyword-Matches durchziehen. */
private var lastBlockAt: Long = 0L private var lastBlockAt: Long = 0L
private val POST_BLOCK_COOLDOWN_MS = 3000L private val POST_BLOCK_COOLDOWN_MS = 3000L
override fun onCreate() {
super.onCreate()
hashList = HashList(File(applicationContext.filesDir, "blocklist.bin"))
hashList.load()
Log.i(TAG, "service created — ${hashList.count()} hashes")
}
override fun onServiceConnected() { override fun onServiceConnected() {
super.onServiceConnected() super.onServiceConnected()
// Reload bei jedem Connect — User könnte zwischenzeitlich syncBlocklist Log.i(TAG, "tamper-lock service connected")
// gemacht haben.
hashList.load()
Log.i(TAG, "service connected — ${hashList.count()} hashes loaded")
} }
override fun onAccessibilityEvent(event: AccessibilityEvent?) { override fun onAccessibilityEvent(event: AccessibilityEvent?) {
if (event == null) return if (event == null) return
// Globaler Kill-Switch: Wenn der User den Schutz NICHT will, tut dieser // Globaler Kill-Switch: Dieser Service tut NUR etwas, wenn der User den
// Service GAR NICHTS — kein Gambling-Block, kein Settings-Block. Der // App-Lock explizit armed hat. Ist er nicht armed (Default, inkl. frischem
// a11y-Service selbst kann sich nicht programmatisch deaktivieren, also // Onboarding und nach einem abgelaufenen Cooldown der `disarmTamperLock`
// ist das hier die einzige Stelle wo wir ihn vollständig stilllegen. // aufgerufen hat) → vollständig passiv. Der a11y-Service kann sich nicht
// "Schutz aktiv?" = Tamper-Lock armed (App-Lock opt-in) ODER der // selbst deaktivieren, also ist das hier die einzige Stelle wo wir ihn
// `filter_enabled`-Flag aus den SharedPrefs (den `disable()` auf false // stilllegen.
// setzt). Wer den App-Lock nicht opt-in't, hat trotzdem den normalen if (!isTamperLockArmed()) return
// Free-Blocker → dann greift `filter_enabled`.
if (!isTamperLockArmed() && !isProtectionEnabled()) return
val pkg = event.packageName?.toString() ?: return val pkg = event.packageName?.toString() ?: return
if (pkg !in WATCHED_SETTINGS_PACKAGES) return
if (event.eventType != AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED &&
event.eventType != AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) return
// Settings-Watchdog: User versucht eine Schutz-relevante Settings-Page handleProtectedSettingsBlock(pkg, event)
// zu öffnen → Sofort BACK + Toast. Wir reagieren auf STATE_CHANGED
// (Activity-Wechsel) UND CONTENT_CHANGED (Dialog-Inhalt lädt nach) —
// weil bei manchen OEMs (Samsung) der Inhalt erst NACH der Activity
// gerendert wird und der erste Scan leer wäre.
if (pkg in WATCHED_SETTINGS_PACKAGES &&
(event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED ||
event.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED)) {
if (handleProtectedSettingsBlock(pkg, event)) return
}
if (!BROWSER_PACKAGES.contains(pkg)) return
// Throttle pro Browser-Package
val now = System.currentTimeMillis()
val last = lastCheck[pkg] ?: 0L
if (now - last < THROTTLE_MS) return
lastCheck[pkg] = now
val root = rootInActiveWindow ?: return
val url = extractUrl(root, pkg) ?: return
val host = extractHost(url) ?: return
if (hashList.matchesAnySuffix(host)) {
// Vermeiden, beim selben URL ständig Back zu feuern
if (lastBlockedUrl[pkg] == url) return
lastBlockedUrl[pkg] = url
Log.i(TAG, "BLOCKED via accessibility: $host (in $pkg)")
performGlobalAction(GLOBAL_ACTION_BACK)
mainHandler.post {
Toast.makeText(
applicationContext,
"Rebreak hat diese Seite blockiert",
Toast.LENGTH_SHORT,
).show()
}
} else {
// URL nicht (mehr) blockiert → letzte-blockiert-Cache löschen
lastBlockedUrl.remove(pkg)
}
} }
override fun onInterrupt() { override fun onInterrupt() {
@ -124,26 +76,13 @@ class RebreakAccessibilityService : AccessibilityService() {
* Tamper-Protection: User versucht in System-Settings eine Schutz-relevante * Tamper-Protection: User versucht in System-Settings eine Schutz-relevante
* Seite zu öffnen (VPN deaktivieren, App löschen, A11y für Rebreak abschalten). * Seite zu öffnen (VPN deaktivieren, App löschen, A11y für Rebreak abschalten).
* *
* Strategie: TYPE_WINDOW_STATE_CHANGED wenn package + className auf eine * Aktiviert nur wenn der App-Lock armed UND der Schutz aktiv ist (`filter_enabled`).
* gefährliche Activity matchen GLOBAL_ACTION_BACK + Toast. User wird sofort * Letzteres lässt den User nach einem legitim abgelaufenen Cooldown wieder raus.
* rausgeworfen, kann nicht togglen.
*
* Bypass-Vektoren die wir damit NICHT abdecken:
* - Safe Mode (Reboot mit Power-Long-Press) Apps off A11y off freie Bahn
* - ADB / Root kein normaler User
*
* Aktiviert nur wenn User aktuell als "committed" gilt (rebreak_blocker == true
* in SharedPreferences). Sonst wäre der Lock auch beim Neuinstall-Onboarding
* aktiv und würde User aussperren.
* *
* @return true wenn die Activity geblockt wurde * @return true wenn die Activity geblockt wurde
*/ */
private fun handleProtectedSettingsBlock(pkg: String, event: AccessibilityEvent): Boolean { private fun handleProtectedSettingsBlock(pkg: String, event: AccessibilityEvent): Boolean {
// Tamper-Lock ist nur aktiv wenn User EXPLIZIT verriegelt hat (über if (!isProtectionEnabled()) return false
// App-Button "Schutz fest verriegeln"). Sonst würde der Watchdog
// sofort die Setup-Seiten blockieren und User aussperren.
if (!isTamperLockArmed()) return false
if (!isUserCommittedToProtection()) return false
if (pkg !in WATCHED_SETTINGS_PACKAGES) return false if (pkg !in WATCHED_SETTINGS_PACKAGES) return false
// Throttle: max alle 400ms eine Settings-Inspection (CONTENT_CHANGED // Throttle: max alle 400ms eine Settings-Inspection (CONTENT_CHANGED
@ -170,13 +109,11 @@ class RebreakAccessibilityService : AccessibilityService() {
className.contains(pattern, ignoreCase = true) className.contains(pattern, ignoreCase = true)
} }
// Phase 2 — Window-Content-Match: IMMER scannen (außer wir haben schon // Phase 2 — Window-Content-Match: scannen wenn kein className-Match. OEMs
// einen className-Match). OEMs benutzen für Dialoge oft className die // benutzen für Dialoge oft className die weder in unseren Patterns noch als
// weder in unseren Patterns noch als "generic container" erkannt werden // "generic container" erkannt werden (z.B. Samsung's "AppDialog"). Der
// (z.B. Samsung's "AppDialog", Stock-Android's "ManageDialog$2"). Der // Keyword-Cluster-Scan ist das Safety-Net: 2 Keywords aus dem gleichen
// Keyword-Cluster-Scan ist unsere Safety-Net: 2 Keywords aus dem // Cluster = Block. False-positive-Risk gedämpft durch Throttling.
// gleichen Cluster = Block. Default false-positive Risk durch Throttling
// (alle 400ms eine Inspection).
var contentReason: String? = null var contentReason: String? = null
if (!classMatchDangerous) { if (!classMatchDangerous) {
contentReason = scanWindowForDangerousContent() contentReason = scanWindowForDangerousContent()
@ -261,14 +198,9 @@ class RebreakAccessibilityService : AccessibilityService() {
} }
} }
/** Liest den Commitment-Flag aus SharedPreferences (gleicher Storage wie der /** "Verriegelt"-Flag User hat über App-Button App-Lock" bestätigt dass
* Plugin nutzt). User ist "committed" wenn er Schutz mal aktiv aktiviert hat * Settings-Tampering blockiert werden soll. Default: false damit Onboarding
* und ihn nicht legitim (= durch Cooldown-Ende) deaktiviert hat. */ * nicht aussperrt; nach Cooldown-Ende disarmed `disarmTamperLock` ihn wieder. */
private fun isUserCommittedToProtection(): Boolean = isProtectionEnabled()
/** "Verriegelt"-Flag User hat über App-Button "Schutz fest verriegeln"
* bestätigt dass Settings-Tampering blockiert werden soll. Default: false
* damit Onboarding nicht aussperrt. */
private fun isTamperLockArmed(): Boolean { private fun isTamperLockArmed(): Boolean {
return try { return try {
val prefs = applicationContext.getSharedPreferences( val prefs = applicationContext.getSharedPreferences(
@ -281,112 +213,8 @@ class RebreakAccessibilityService : AccessibilityService() {
} }
} }
/**
* Liest die Adressleiste aus dem Browser-View-Tree.
*
* Browser benutzen unterschiedliche View-IDs für ihre URL-Bar wir
* probieren die gängigen durch. Fallback: alle EditText-Nodes nach
* URL-artigem Inhalt scannen.
*/
private fun extractUrl(root: AccessibilityNodeInfo, pkg: String): String? {
val candidateIds = URL_BAR_IDS[pkg] ?: emptyList()
for (id in candidateIds) {
val nodes = try {
root.findAccessibilityNodeInfosByViewId(id)
} catch (e: Exception) {
null
} ?: continue
for (node in nodes) {
val text = node.text?.toString() ?: continue
if (looksLikeUrl(text)) return text
}
}
// Fallback — scanne den ganzen Tree breadth-first nach URL-artigem Text
return scanTreeForUrl(root, depth = 0)
}
private fun scanTreeForUrl(node: AccessibilityNodeInfo, depth: Int): String? {
if (depth > MAX_TREE_DEPTH) return null
val text = node.text?.toString()
if (text != null && looksLikeUrl(text)) return text
for (i in 0 until node.childCount) {
val child = node.getChild(i) ?: continue
val found = scanTreeForUrl(child, depth + 1)
if (found != null) return found
}
return null
}
private fun looksLikeUrl(text: String): Boolean {
val t = text.trim()
if (t.length < 3 || t.length > 2048) return false
// Schnell-Filter: enthält Punkt, kein Whitespace
if (' ' in t || '\n' in t) return false
// Normalisiere — Browser zeigen oft "https://" weggekürzt
val normalized = if (t.startsWith("http://") || t.startsWith("https://")) t else "https://$t"
return try {
val u = java.net.URI(normalized)
val host = u.host
host != null && host.contains('.')
} catch (_: Exception) {
false
}
}
private fun extractHost(url: String): String? {
val normalized = if (url.startsWith("http://") || url.startsWith("https://")) url else "https://$url"
return try {
java.net.URI(normalized).host?.lowercase()
} catch (_: Exception) {
null
}
}
companion object { companion object {
private const val TAG = "RebreakA11y" private const val TAG = "RebreakA11y"
private const val THROTTLE_MS = 600L
private const val MAX_TREE_DEPTH = 12
// Erweitern wir, wenn User-Reports zeigen dass ihr Browser nicht erkannt wird.
val BROWSER_PACKAGES = setOf(
"com.android.chrome", // Chrome
"com.chrome.beta",
"com.chrome.dev",
"com.chrome.canary",
"org.mozilla.firefox", // Firefox
"org.mozilla.firefox_beta",
"org.mozilla.fenix", // Firefox Fenix
"com.microsoft.emmx", // Edge
"com.sec.android.app.sbrowser", // Samsung Internet
"com.brave.browser", // Brave
"com.opera.browser", // Opera
"com.opera.mini.native",
"com.duckduckgo.mobile.android", // DuckDuckGo
"com.vivaldi.browser", // Vivaldi
"org.torproject.torbrowser", // Tor
)
// View-IDs der Adressleiste pro Browser. Bekannte Stand 2026.
// findAccessibilityNodeInfosByViewId erwartet das volle ID-Tag,
// d.h. "<package>:id/<resourceName>".
private val URL_BAR_IDS = mapOf(
"com.android.chrome" to listOf("com.android.chrome:id/url_bar"),
"com.chrome.beta" to listOf("com.chrome.beta:id/url_bar"),
"com.chrome.dev" to listOf("com.chrome.dev:id/url_bar"),
"com.chrome.canary" to listOf("com.chrome.canary:id/url_bar"),
"org.mozilla.firefox" to listOf("org.mozilla.firefox:id/mozac_browser_toolbar_url_view"),
"org.mozilla.firefox_beta" to listOf("org.mozilla.firefox_beta:id/mozac_browser_toolbar_url_view"),
"org.mozilla.fenix" to listOf("org.mozilla.fenix:id/mozac_browser_toolbar_url_view"),
"com.microsoft.emmx" to listOf("com.microsoft.emmx:id/url_bar"),
"com.sec.android.app.sbrowser" to listOf("com.sec.android.app.sbrowser:id/location_bar_edit_text"),
"com.brave.browser" to listOf("com.brave.browser:id/url_bar"),
"com.opera.browser" to listOf("com.opera.browser:id/url_field"),
"com.opera.mini.native" to listOf("com.opera.mini.native:id/url_field"),
"com.duckduckgo.mobile.android" to listOf("com.duckduckgo.mobile.android:id/omnibarTextInput"),
"com.vivaldi.browser" to listOf("com.vivaldi.browser:id/url_bar"),
)
const val ACTION_RELOAD_BLOCKLIST = "expo.modules.rebreakprotection.action.A11Y_RELOAD"
// Settings-Apps die wir auf Tamper-Versuche überwachen. // Settings-Apps die wir auf Tamper-Versuche überwachen.
// Stock + Samsung One UI haben unterschiedliche Package-Namen, // Stock + Samsung One UI haben unterschiedliche Package-Namen,
@ -402,17 +230,15 @@ class RebreakAccessibilityService : AccessibilityService() {
"com.android.vending", "com.android.vending",
) )
// Activity-Class-Patterns die geblockt werden (Substring-Match, case-insensitive).
// Decken Stock-Android + Samsung One UI ab. Patterns sind bewusst breit
// damit OEM-Variationen mitgenommen werden.
/** /**
* High-confidence Keywords wenn EINER davon im Window-Content * High-confidence Keywords wenn EINER davon im Window-Content auftaucht,
* auftaucht, blocken wir sofort. Sind alle hochspezifisch und * blocken wir sofort. Hochspezifisch zu uns. Enthält sowohl die aktuelle
* tauchen praktisch nur auf VPN/A11y/Uninstall-Detail-Pages auf. * a11y-Service-Summary als auch die alte (für stale Installs / OEM-Cache).
*/ */
val HIGH_CONFIDENCE_KEYWORDS = listOf( val HIGH_CONFIDENCE_KEYWORDS = listOf(
"rebreak filter", // VPN-Profil-Name aus Builder.setSession "rebreak filter", // VPN-Profil-Name aus Builder.setSession
"filtert glücksspielseiten", // A11y-Service-Summary "sichert den schutz", // aktuelle a11y-Service-Summary
"filtert glücksspielseiten", // alte a11y-Service-Summary (legacy installs)
"rebreak deinstallieren", "rebreak deinstallieren",
"rebreak entfernen", "rebreak entfernen",
"rebreak löschen", "rebreak löschen",
@ -441,7 +267,8 @@ class RebreakAccessibilityService : AccessibilityService() {
"bedienungshilfe", "bedienungshilfe",
"eingabehilfe", "eingabehilfe",
"accessibility", "accessibility",
"filtert glücksspiel", // unser A11y-Service-Summary "sichert den schutz", // unsere aktuelle a11y-Service-Summary
"filtert glücksspiel", // alte a11y-Service-Summary (legacy installs)
"rebreak filter", "rebreak filter",
"installierte apps", "installierte apps",
"installed services", "installed services",

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="accessibility_service_description">Rebreak filters URLs in your browser to block gambling sites — even when the VPN is off. Without this permission the app cannot fully maintain protection.</string> <string name="accessibility_service_description">Keeps your protection from being switched off on impulse: while App-Lock is on, the ReBreak VPN can\'t be disabled in Settings and the app can\'t be uninstalled. Blocking gambling sites itself is handled by the VPN — this permission only safeguards it. You can always end protection via the cooldown in the app.</string>
<string name="accessibility_service_summary">Filters gambling sites in the browser</string> <string name="accessibility_service_summary">Keeps protection from being switched off</string>
</resources> </resources>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="accessibility_service_description">Rebreak filtert URLs in deinem Browser, um Glücksspielseiten zu blockieren — auch wenn das VPN nicht aktiv ist. Ohne diese Berechtigung kann die App ihren Schutz nicht vollständig aufrechterhalten.</string> <string name="accessibility_service_description">Sichert deinen Schutz gegen impulsives Abschalten ab: Solange App-Lock aktiv ist, kann das ReBreak-VPN nicht in den Einstellungen deaktiviert und die App nicht deinstalliert werden. Das Blockieren von Glücksspielseiten selbst übernimmt das VPN — diese Berechtigung sichert es nur. Du kannst den Schutz jederzeit über die Abkühlphase in der App beenden.</string>
<string name="accessibility_service_summary">Filtert Glücksspielseiten im Browser</string> <string name="accessibility_service_summary">Sichert den Schutz gegen Abschalten ab</string>
</resources> </resources>

View File

@ -2,21 +2,24 @@
<!-- <!--
Accessibility-Service-Config für RebreakAccessibilityService. Accessibility-Service-Config für RebreakAccessibilityService.
packageNames listet die Browser-Apps, denen wir lauschen wollen — Android Der Service blockt KEINE Webseiten mehr (das macht der VpnService DNS-Filter).
ruft uns dann nur bei Events aus diesen Packages auf (System-seitiger Er dient nur noch als Tamper-Lock: verhindert, dass schutz-relevante
Filter, sehr CPU-effizient). System-Settings geöffnet werden (VPN abschalten, App deinstallieren, den
a11y-Service selbst abschalten) — und nur dann, wenn der User „App-Lock"
explizit aktiviert hat.
packageNames listet daher nur die Settings-/Installer-Apps. Android ruft uns
dann nur bei Events aus diesen Packages auf (System-seitiger Filter).
flags: flags:
flagRetrieveInteractiveWindows → wir können auch Fenster lesen die nicht flagRetrieveInteractiveWindows → auch Nicht-Vollbild-Fenster (OEM-Dialoge) lesen
Vollbild sind (Pop-up Tabs)
flagRequestEnhancedWebAccessibility → bessere DOM-Inspektion in Chrome
--> -->
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeWindowContentChanged|typeWindowStateChanged" android:accessibilityEventTypes="typeWindowContentChanged|typeWindowStateChanged"
android:accessibilityFeedbackType="feedbackGeneric" android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagDefault|flagRetrieveInteractiveWindows|flagRequestEnhancedWebAccessibility" android:accessibilityFlags="flagDefault|flagRetrieveInteractiveWindows"
android:canRetrieveWindowContent="true" android:canRetrieveWindowContent="true"
android:notificationTimeout="100" android:notificationTimeout="100"
android:packageNames="com.android.chrome,com.chrome.beta,com.chrome.dev,com.chrome.canary,org.mozilla.firefox,org.mozilla.firefox_beta,org.mozilla.fenix,com.microsoft.emmx,com.sec.android.app.sbrowser,com.brave.browser,com.opera.browser,com.opera.mini.native,com.duckduckgo.mobile.android,com.vivaldi.browser,org.torproject.torbrowser,com.android.settings,com.android.vpndialogs,com.android.packageinstaller,com.google.android.packageinstaller,com.samsung.android.app.settings,com.samsung.accessibility,com.android.vending" android:packageNames="com.android.settings,com.android.vpndialogs,com.android.packageinstaller,com.google.android.packageinstaller,com.samsung.android.app.settings,com.samsung.accessibility,com.android.vending"
android:description="@string/accessibility_service_description" android:description="@string/accessibility_service_description"
android:summary="@string/accessibility_service_summary" /> android:summary="@string/accessibility_service_summary" />