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:
parent
a80cc8b08d
commit
fc7a243c9b
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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" />
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user