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 }> {
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:
// (1) A11y nicht aktiv → Settings öffnen, return {enabled:false} mit
// 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_settings_btn": "Einstellungen",
"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_msg": "Bitte später nochmal versuchen.",
"activation_failed_title": "Aktivierung fehlgeschlagen",
@ -268,8 +268,8 @@
"layers_url_filter_subtitle_active": "System-weiter Filter aktiv",
"layers_url_filter_subtitle_inactive": "Blockt Gambling-Seiten in Safari + Apps",
"layers_app_lock_title": "App-Lock",
"layers_app_lock_subtitle_active": "Familienzugriff aktiv",
"layers_app_lock_subtitle_inactive": "Verhindert dass du ReBreak im Impuls löschst",
"layers_app_lock_subtitle_active": "Verriegelt — Abschalten nur über die Abkühlphase",
"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.",
"kpi_global_label": "Geblockte Domains weltweit",
"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_settings_btn": "Settings",
"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_msg": "Please try again later.",
"activation_failed_title": "Activation failed",
@ -268,8 +268,8 @@
"layers_url_filter_subtitle_active": "System-wide filter active",
"layers_url_filter_subtitle_inactive": "Blocks gambling sites in Safari + apps",
"layers_app_lock_title": "App lock",
"layers_app_lock_subtitle_active": "Family access active",
"layers_app_lock_subtitle_inactive": "Prevents you from deleting ReBreak on impulse",
"layers_app_lock_subtitle_active": "Locked — disable only via the cooldown",
"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.",
"kpi_global_label": "Domains blocked worldwide",
"kpi_global_subtitle": "Active entries in the global blocklist",

View File

@ -1,119 +1,71 @@
package expo.modules.rebreakprotection.accessibility
import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.AccessibilityServiceInfo
import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
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
* werden (Always-on VPN ist bei Free-Konsumenten-Devices nicht erzwingbar).
* Die Accessibility-Permission hingegen ist ihm "vertrauter" man gibt sie
* einmal und vergisst sie. Solange der User sie nicht explizit entzieht,
* läuft dieser Filter weiter.
* **Was dieser Service NICHT (mehr) tut:** Glücksspielseiten blocken. Das macht
* ausschließlich der `RebreakVpnService` (DNS-Filter, ~208k Domains) der greift
* network-level, also in Browsern *und* Apps, und übersteht Browser-Updates. Den
* früheren Browser-Adressleisten-Filter haben wir entfernt: redundant zum VPN,
* fragil (View-IDs pro Browser), und führte zu Geister-Blockaden (VPN aus, a11y
* blockt trotzdem noch).
*
* Funktionsweise (parallel zu iOS NEFilterBrowserFlow):
* - Service hört auf TYPE_WINDOW_CONTENT_CHANGED + TYPE_WINDOW_STATE_CHANGED
* - Filtert nach Browser-Packages (Chrome/Firefox/Edge/Samsung/Brave/Opera)
* - Liest die Adressleiste via AccessibilityNodeInfo
* - Hash-Match gegen blocklist.bin (gleiche Datei wie VpnService)
* - Bei Treffer: GLOBAL_ACTION_BACK + Toast
* **Was dieser Service tut:** Wenn der User App-Lock" aktiviert hat
* (`tamper_armed == true`, opt-in über den App-Button) und der Schutz aktiv ist
* (`filter_enabled == true`), verhindert er, dass schutz-relevante System-Settings
* geöffnet werden können:
* - Settings VPN ReBreak abschalten / Always-on" / Profil löschen
* - 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
* Chrome bei jeder Frame-Änderung Hunderte Events.
* Strategie: lauscht auf `TYPE_WINDOW_STATE_CHANGED` (Activity-Wechsel) +
* `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() {
private lateinit var hashList: HashList
private val mainHandler = Handler(Looper.getMainLooper())
private val lastCheck = HashMap<String, Long>()
private val lastBlockedUrl = HashMap<String, String>()
private var lastSettingsCheck: Long = 0L
/** Nach einem TAMPER-BLOCK: 3s lang keinen weiteren Block triggern.
* Verhindert Toast-Spam wenn User legitim in Settings navigieren will und
* Page-Transitions noch alte Keyword-Matches durchziehen. */
/** Nach einem TAMPER-BLOCK: kurz keinen weiteren Block triggern. Verhindert
* Toast-Spam wenn alte Keyword-Matches in Page-Transitions nachziehen. */
private var lastBlockAt: Long = 0L
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() {
super.onServiceConnected()
// Reload bei jedem Connect — User könnte zwischenzeitlich syncBlocklist
// gemacht haben.
hashList.load()
Log.i(TAG, "service connected — ${hashList.count()} hashes loaded")
Log.i(TAG, "tamper-lock service connected")
}
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
if (event == null) return
// Globaler Kill-Switch: Wenn der User den Schutz NICHT will, tut dieser
// Service GAR NICHTS — kein Gambling-Block, kein Settings-Block. Der
// a11y-Service selbst kann sich nicht programmatisch deaktivieren, also
// ist das hier die einzige Stelle wo wir ihn vollständig stilllegen.
// "Schutz aktiv?" = Tamper-Lock armed (App-Lock opt-in) ODER der
// `filter_enabled`-Flag aus den SharedPrefs (den `disable()` auf false
// setzt). Wer den App-Lock nicht opt-in't, hat trotzdem den normalen
// Free-Blocker → dann greift `filter_enabled`.
if (!isTamperLockArmed() && !isProtectionEnabled()) 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
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
// 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)
}
handleProtectedSettingsBlock(pkg, event)
}
override fun onInterrupt() {
@ -124,26 +76,13 @@ class RebreakAccessibilityService : AccessibilityService() {
* Tamper-Protection: User versucht in System-Settings eine Schutz-relevante
* Seite zu öffnen (VPN deaktivieren, App löschen, A11y für Rebreak abschalten).
*
* Strategie: TYPE_WINDOW_STATE_CHANGED wenn package + className auf eine
* gefährliche Activity matchen GLOBAL_ACTION_BACK + Toast. User wird sofort
* 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.
* Aktiviert nur wenn der App-Lock armed UND der Schutz aktiv ist (`filter_enabled`).
* Letzteres lässt den User nach einem legitim abgelaufenen Cooldown wieder raus.
*
* @return true wenn die Activity geblockt wurde
*/
private fun handleProtectedSettingsBlock(pkg: String, event: AccessibilityEvent): Boolean {
// Tamper-Lock ist nur aktiv wenn User EXPLIZIT verriegelt hat (über
// 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 (!isProtectionEnabled()) return false
if (pkg !in WATCHED_SETTINGS_PACKAGES) return false
// Throttle: max alle 400ms eine Settings-Inspection (CONTENT_CHANGED
@ -170,13 +109,11 @@ class RebreakAccessibilityService : AccessibilityService() {
className.contains(pattern, ignoreCase = true)
}
// Phase 2 — Window-Content-Match: IMMER scannen (außer wir haben schon
// einen className-Match). OEMs benutzen für Dialoge oft className die
// weder in unseren Patterns noch als "generic container" erkannt werden
// (z.B. Samsung's "AppDialog", Stock-Android's "ManageDialog$2"). Der
// Keyword-Cluster-Scan ist unsere Safety-Net: 2 Keywords aus dem
// gleichen Cluster = Block. Default false-positive Risk durch Throttling
// (alle 400ms eine Inspection).
// Phase 2 — Window-Content-Match: scannen wenn kein className-Match. OEMs
// benutzen für Dialoge oft className die weder in unseren Patterns noch als
// "generic container" erkannt werden (z.B. Samsung's "AppDialog"). Der
// Keyword-Cluster-Scan ist das Safety-Net: 2 Keywords aus dem gleichen
// Cluster = Block. False-positive-Risk gedämpft durch Throttling.
var contentReason: String? = null
if (!classMatchDangerous) {
contentReason = scanWindowForDangerousContent()
@ -261,14 +198,9 @@ class RebreakAccessibilityService : AccessibilityService() {
}
}
/** Liest den Commitment-Flag aus SharedPreferences (gleicher Storage wie der
* Plugin nutzt). User ist "committed" wenn er Schutz mal aktiv aktiviert hat
* und ihn nicht legitim (= durch Cooldown-Ende) deaktiviert hat. */
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. */
/** "Verriegelt"-Flag User hat über App-Button App-Lock" bestätigt dass
* Settings-Tampering blockiert werden soll. Default: false damit Onboarding
* nicht aussperrt; nach Cooldown-Ende disarmed `disarmTamperLock` ihn wieder. */
private fun isTamperLockArmed(): Boolean {
return try {
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 {
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.
// Stock + Samsung One UI haben unterschiedliche Package-Namen,
@ -402,17 +230,15 @@ class RebreakAccessibilityService : AccessibilityService() {
"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
* auftaucht, blocken wir sofort. Sind alle hochspezifisch und
* tauchen praktisch nur auf VPN/A11y/Uninstall-Detail-Pages auf.
* High-confidence Keywords wenn EINER davon im Window-Content auftaucht,
* blocken wir sofort. Hochspezifisch zu uns. Enthält sowohl die aktuelle
* a11y-Service-Summary als auch die alte (für stale Installs / OEM-Cache).
*/
val HIGH_CONFIDENCE_KEYWORDS = listOf(
"rebreak filter", // VPN-Profil-Name aus Builder.setSession
"filtert glücksspielseiten", // A11y-Service-Summary
"rebreak filter", // VPN-Profil-Name aus Builder.setSession
"sichert den schutz", // aktuelle a11y-Service-Summary
"filtert glücksspielseiten", // alte a11y-Service-Summary (legacy installs)
"rebreak deinstallieren",
"rebreak entfernen",
"rebreak löschen",
@ -441,7 +267,8 @@ class RebreakAccessibilityService : AccessibilityService() {
"bedienungshilfe",
"eingabehilfe",
"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",
"installierte apps",
"installed services",

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<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_summary">Filters gambling sites in the browser</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">Keeps protection from being switched off</string>
</resources>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<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_summary">Filtert Glücksspielseiten im Browser</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">Sichert den Schutz gegen Abschalten ab</string>
</resources>

View File

@ -2,21 +2,24 @@
<!--
Accessibility-Service-Config für RebreakAccessibilityService.
packageNames listet die Browser-Apps, denen wir lauschen wollen — Android
ruft uns dann nur bei Events aus diesen Packages auf (System-seitiger
Filter, sehr CPU-effizient).
Der Service blockt KEINE Webseiten mehr (das macht der VpnService DNS-Filter).
Er dient nur noch als Tamper-Lock: verhindert, dass schutz-relevante
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:
flagRetrieveInteractiveWindows → wir können auch Fenster lesen die nicht
Vollbild sind (Pop-up Tabs)
flagRequestEnhancedWebAccessibility → bessere DOM-Inspektion in Chrome
flagRetrieveInteractiveWindows → auch Nicht-Vollbild-Fenster (OEM-Dialoge) lesen
-->
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeWindowContentChanged|typeWindowStateChanged"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagDefault|flagRetrieveInteractiveWindows|flagRequestEnhancedWebAccessibility"
android:accessibilityFlags="flagDefault|flagRetrieveInteractiveWindows"
android:canRetrieveWindowContent="true"
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:summary="@string/accessibility_service_summary" />