fix(android): block Force Stop + App-Info auf Samsung OneUI
Samsung nutzt generische Klassen (SubSettings, FrameLayout) statt InstalledAppDetails für die App-Info-Seite — classGuard schlug fehl, Tamper-Lock griff nicht. Fixes: - classGuard für Uninstall-Pfad entfernt: Text-Kombination "rebreak" + Deinstall-Keyword reicht (App-Liste zeigt Buttons nie inline) - Force-Stop-Bestätigungsdialog explizit erkannt via "stopp erzwingen" + "abbrechen" + "ok" (Dialog nennt App-Namen nie) - Throttle-Reset bei TYPE_WINDOW_STATE_CHANGED: eliminiert 400ms-Fenster zwischen Activity-Wechsel und erstem CONTENT_CHANGED-Check - ApplicationDetail (ohne 's') zu Pattern-Listen: Samsung OEM-Variante - accessibility_service_summary korrigiert: "ReBreak — Schutz" statt "Sichert den Schutz gegen Abschalten ab" (HIGH_CONFIDENCE_KEYWORD muss matchen) Getestet auf Samsung Galaxy A50 (One UI): App-Info-Seite wird sofort per BACK-Action geblockt, Lyra-Toast erscheint. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2e056c7257
commit
59766f8530
@ -0,0 +1,7 @@
|
||||
<resources>
|
||||
<string name="app_name">ReBreak</string>
|
||||
<string name="expo_splash_screen_resize_mode" translatable="false">cover</string>
|
||||
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
|
||||
<string name="accessibility_service_description" translatable="false">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" translatable="false">ReBreak — Schutz</string>
|
||||
</resources>
|
||||
@ -7,6 +7,7 @@ import android.util.Log
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import android.widget.Toast
|
||||
import expo.modules.rebreakprotection.vpn.RebreakVpnService
|
||||
|
||||
/**
|
||||
* Tamper-Lock — sichert den Schutz gegen versehentliches/impulsives Abschalten.
|
||||
@ -46,9 +47,15 @@ class RebreakAccessibilityService : AccessibilityService() {
|
||||
|
||||
override fun onServiceConnected() {
|
||||
super.onServiceConnected()
|
||||
instance = this
|
||||
Log.i(TAG, "tamper-lock service connected")
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (instance === this) instance = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
|
||||
if (event == null) return
|
||||
|
||||
@ -66,6 +73,14 @@ class RebreakAccessibilityService : AccessibilityService() {
|
||||
if (event.eventType != AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED &&
|
||||
event.eventType != AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) return
|
||||
|
||||
// Bei neuem Activity-Wechsel (STATE_CHANGED) den Throttle-Timer resetten,
|
||||
// damit der nachfolgende CONTENT_CHANGED (Seite lädt Inhalt nach) sofort
|
||||
// geprüft wird — sonst entsteht ein 400ms-Fenster in dem "Stopp erzwingen"
|
||||
// getappt werden kann bevor wir den Block auslösen.
|
||||
if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
|
||||
lastSettingsCheck = 0L
|
||||
}
|
||||
|
||||
handleProtectedSettingsBlock(pkg, event)
|
||||
}
|
||||
|
||||
@ -124,12 +139,27 @@ class RebreakAccessibilityService : AccessibilityService() {
|
||||
// Class+Rebreak-Check gebraucht. Kann null sein wenn root window flackert.
|
||||
val pageText = collectWindowText()?.lowercase().orEmpty()
|
||||
|
||||
// 0) VPN-Surface-Sperre NUR auf klaren Activity-Klassen — keine
|
||||
// Text-Heuristik mehr, damit die Settings-Suche und generelle Settings-
|
||||
// Navigation nicht fälschlich gesperrt werden.
|
||||
if (isVpnSurface(className) && shouldBlockVpnSurface(pkg, pageText)) {
|
||||
return doBlock(pkg, className, "vpn-surface", now)
|
||||
}
|
||||
|
||||
// 1) High-confidence Keyword im Text → sofortiger Block (Rebreak-spezifisch)
|
||||
val highConfHit = HIGH_CONFIDENCE_KEYWORDS.firstOrNull { pageText.contains(it) }
|
||||
if (highConfHit != null) {
|
||||
return doBlock(pkg, className, "high-confidence:$highConfHit", now)
|
||||
}
|
||||
|
||||
// 1b) Samsung/OEM-Fallback: manche OneUI-Updates ändern Klassenamen,
|
||||
// behalten aber den sichtbaren Screen-Text (Rebreak + konkrete Aktion).
|
||||
// Strikt gehalten, damit keine globalen Settings-False-Positives entstehen.
|
||||
val actionTextMatch = hasRebreakScopedDangerAction(pkg, className, pageText)
|
||||
if (actionTextMatch != null) {
|
||||
return doBlock(pkg, className, "rebreak-action:$actionTextMatch", now)
|
||||
}
|
||||
|
||||
// 2) Activity-Class-Match — aber NUR blocken wenn Page klar über Rebreak
|
||||
// ist (Wort "rebreak" im Text). Sonst würde z.B. die App-Info-Page einer
|
||||
// beliebigen anderen App geblockt werden.
|
||||
@ -153,6 +183,84 @@ class RebreakAccessibilityService : AccessibilityService() {
|
||||
return false
|
||||
}
|
||||
|
||||
private fun shouldBlockVpnSurface(pkg: String, pageText: String): Boolean {
|
||||
// System VPN-Dialog ist immer ein echter Manipulationspfad.
|
||||
if (pkg == "com.android.vpndialogs") return true
|
||||
// Für generische VPN-Seiten nur blocken, wenn Rebreak klar sichtbar ist
|
||||
// und Always-on-Kontext vorhanden ist. So bleibt der allgemeine VPN-Menu-
|
||||
// Überblick navigierbar.
|
||||
if (pageText.isBlank()) return false
|
||||
val hasRebreak = pageText.contains("rebreak") || pageText.contains("re break")
|
||||
if (!hasRebreak) return false
|
||||
return STRICT_VPN_LOCK_CONTEXT_KEYWORDS.any { pageText.contains(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Enge Text-Heuristik für OEM-Varianten:
|
||||
* - Seite muss klar zu Rebreak gehören (außer Force-Stop-Dialog — der nennt
|
||||
* die App nie, aber ist eindeutig durch seinen spezifischen Text erkennbar)
|
||||
* - und eine konkrete gefährliche Aktion zeigen
|
||||
*/
|
||||
private fun hasRebreakScopedDangerAction(pkg: String, className: String, pageText: String): String? {
|
||||
if (pageText.isBlank()) return null
|
||||
|
||||
// Spezialfall: Force-Stop-Bestätigungsdialog — nennt niemals die App ("ReBreak"),
|
||||
// ist aber durch seinen fixen System-Text eindeutig identifizierbar. Tritt nur
|
||||
// auf wenn User bereits auf der ReBreak-App-Info-Seite war (welche wir blocken).
|
||||
// Der Dialog zeigt exakt "stopp erzwingen" + "abbrechen" + "ok".
|
||||
if (pageText.contains("stopp erzwingen") &&
|
||||
pageText.contains("abbrechen") &&
|
||||
pageText.contains("ok")) {
|
||||
return "force-stop-dialog"
|
||||
}
|
||||
|
||||
val hasRebreak = pageText.contains("rebreak") || pageText.contains("re break")
|
||||
if (!hasRebreak) return null
|
||||
|
||||
val isGenericSettingsPkg = pkg == "com.android.settings" || pkg == "com.samsung.android.app.settings"
|
||||
|
||||
// VPN-Surface: nur wenn "Always-on"-Kontext sichtbar.
|
||||
if (isGenericSettingsPkg && STRICT_VPN_LOCK_CONTEXT_KEYWORDS.any { pageText.contains(it) }) {
|
||||
return "vpn-surface:rebreak"
|
||||
}
|
||||
|
||||
// Uninstall/Force-Stop: classGuard entfällt — Samsung OneUI nutzt generische
|
||||
// Klassen (SubSettings, FrameLayout, ViewGroup). Die Text-Kombination
|
||||
// "rebreak" + Deinstall-/Erzwingen-Keyword auf einer Settings-Seite ist
|
||||
// spezifisch genug: Die App-Liste zeigt diese Aktionen nie inline.
|
||||
val uninstallAction = DANGER_ACTION_KEYWORDS_UNINSTALL.firstOrNull { pageText.contains(it) }
|
||||
if (uninstallAction != null) {
|
||||
return "uninstall:$uninstallAction"
|
||||
}
|
||||
|
||||
// VPN-/A11y-Actions: weiterhin classGuard erforderlich (diese Keywords
|
||||
// kommen auch auf harmlosen Settings-Seiten vor).
|
||||
if (isGenericSettingsPkg) {
|
||||
val classGuard = FALLBACK_CLASS_GUARD_PATTERNS.any {
|
||||
className.contains(it, ignoreCase = true)
|
||||
}
|
||||
if (!classGuard) return null
|
||||
}
|
||||
|
||||
val vpnAction = DANGER_ACTION_KEYWORDS_VPN.firstOrNull { pageText.contains(it) }
|
||||
if (vpnAction != null && DANGER_CONTEXT_KEYWORDS_VPN.any { pageText.contains(it) }) {
|
||||
return "vpn:$vpnAction"
|
||||
}
|
||||
|
||||
val a11yAction = DANGER_ACTION_KEYWORDS_A11Y.firstOrNull { pageText.contains(it) }
|
||||
if (a11yAction != null && DANGER_CONTEXT_KEYWORDS_A11Y.any { pageText.contains(it) }) {
|
||||
return "a11y:$a11yAction"
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun isVpnSurface(className: String): Boolean {
|
||||
return VPN_SURFACE_ACTIVITY_PATTERNS.any {
|
||||
className.contains(it, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
||||
/** Hilft Toast+Back-Action wiederzuverwenden (DRY für die beiden Block-Pfade). */
|
||||
private fun doBlock(pkg: String, className: String, reason: String, now: Long): Boolean {
|
||||
Log.w(TAG, "TAMPER-BLOCK: $pkg / $className (reason=$reason)")
|
||||
@ -203,16 +311,21 @@ class RebreakAccessibilityService : AccessibilityService() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Liest den `filter_enabled`-Flag aus SharedPreferences (gleicher Storage +
|
||||
* Key wie `RebreakProtectionModule.saveEnabled` / `KEY_ENABLED`). `disable()`
|
||||
* setzt ihn auf false — danach ist der Schutz aus und dieser Service passiv. */
|
||||
/**
|
||||
* Tamper nur wenn Schutz WIRKLICH live ist:
|
||||
* - `filter_enabled` muss true sein (User-Wunsch)
|
||||
* - UND VpnService muss tatsächlich laufen (`isRunning`)
|
||||
*
|
||||
* So verhindern wir, dass A11y-Blockaden aktiv sind während der VPN-Layer
|
||||
* nach Reboot/Crash noch nicht wieder hochgefahren ist.
|
||||
*/
|
||||
private fun isProtectionEnabled(): Boolean {
|
||||
return try {
|
||||
val prefs = applicationContext.getSharedPreferences(
|
||||
"rebreak_filter_prefs",
|
||||
android.content.Context.MODE_PRIVATE,
|
||||
)
|
||||
prefs.getBoolean("filter_enabled", false)
|
||||
prefs.getBoolean("filter_enabled", false) && RebreakVpnService.isRunning
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
@ -236,6 +349,18 @@ class RebreakAccessibilityService : AccessibilityService() {
|
||||
companion object {
|
||||
private const val TAG = "RebreakA11y"
|
||||
|
||||
@Volatile
|
||||
private var instance: RebreakAccessibilityService? = null
|
||||
|
||||
fun requestPowerDialog(): Boolean {
|
||||
val svc = instance ?: return false
|
||||
return try {
|
||||
svc.performGlobalAction(GLOBAL_ACTION_POWER_DIALOG)
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Settings-Apps die wir auf Tamper-Versuche überwachen.
|
||||
// Stock + Samsung One UI haben unterschiedliche Package-Namen,
|
||||
// wir whitelist'n alle bekannten.
|
||||
@ -244,6 +369,9 @@ class RebreakAccessibilityService : AccessibilityService() {
|
||||
"com.android.vpndialogs",
|
||||
"com.android.packageinstaller",
|
||||
"com.google.android.packageinstaller",
|
||||
"com.samsung.android.packageinstaller",
|
||||
"com.android.permissioncontroller",
|
||||
"com.google.android.permissioncontroller",
|
||||
"com.samsung.android.app.settings",
|
||||
"com.samsung.accessibility",
|
||||
// Play Store: User könnte hier auf "Deinstallieren" tippen für Rebreak
|
||||
@ -258,9 +386,12 @@ class RebreakAccessibilityService : AccessibilityService() {
|
||||
* Müssen lowercase sein (Text wird vor Match lowercased).
|
||||
*/
|
||||
val HIGH_CONFIDENCE_KEYWORDS = listOf(
|
||||
"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 \u2014 schutz", // DE-Summary "ReBreak — Schutz"
|
||||
"rebreak \u2014 protection", // EN/FR-Summary "ReBreak — Protection"
|
||||
"rebreak \u2014 الحماية", // AR-Summary "ReBreak — الحماية"
|
||||
"sichert den schutz", // legacy DE-Summary
|
||||
"keeps protection from", // legacy EN-Summary
|
||||
"filtert glücksspielseiten", // ganz alte a11y-Service-Summary
|
||||
"rebreak deinstallieren",
|
||||
"rebreak entfernen",
|
||||
"rebreak löschen",
|
||||
@ -277,7 +408,8 @@ class RebreakAccessibilityService : AccessibilityService() {
|
||||
// App-Deinstallieren-Dialoge + App-Info-Pages
|
||||
"Uninstaller", // com.android.packageinstaller.UninstallerActivity
|
||||
"InstalledAppDetails", // App-Info-Page (kann zu uninstall führen)
|
||||
"ApplicationDetails", // Variations
|
||||
"ApplicationDetails", // AOSP
|
||||
"ApplicationDetail", // Samsung OneUI: ApplicationDetailActivity (kein 's')
|
||||
|
||||
// Accessibility-Settings (paradox: A11y würde sich selbst aushebeln)
|
||||
"AccessibilitySettings",
|
||||
@ -285,5 +417,98 @@ class RebreakAccessibilityService : AccessibilityService() {
|
||||
"InstalledServiceActivity", // Samsung
|
||||
"AccessibilityShortcut",
|
||||
)
|
||||
|
||||
/** VPN-Surface-Activities die wir immer blocken solange Tamper aktiv ist. */
|
||||
val VPN_SURFACE_ACTIVITY_PATTERNS = listOf(
|
||||
"VpnSettings",
|
||||
"VpnConfig",
|
||||
"VpnDialog",
|
||||
"ManageDialog",
|
||||
"ConfirmAddOns",
|
||||
"AlwaysOnVpn",
|
||||
)
|
||||
|
||||
// Text-Fallback (strictly scoped): nur mit sichtbarem Rebreak-Bezug.
|
||||
// Alles lowercase halten.
|
||||
val DANGER_ACTION_KEYWORDS_VPN = listOf(
|
||||
"trennen",
|
||||
"disconnect",
|
||||
"deaktivieren",
|
||||
"disable",
|
||||
"ausschalten",
|
||||
"turn off",
|
||||
"stoppen",
|
||||
"stop",
|
||||
"entfernen",
|
||||
"remove",
|
||||
"löschen",
|
||||
"delete",
|
||||
)
|
||||
|
||||
val DANGER_CONTEXT_KEYWORDS_VPN = listOf(
|
||||
"vpn",
|
||||
"always-on",
|
||||
"immer eingeschaltet",
|
||||
"immer aktiviert",
|
||||
"immer ein",
|
||||
"verbunden",
|
||||
"connected",
|
||||
"vpn-profil",
|
||||
"vpn profile",
|
||||
)
|
||||
|
||||
val STRICT_VPN_LOCK_CONTEXT_KEYWORDS = listOf(
|
||||
"immer ein",
|
||||
"always-on",
|
||||
"always on",
|
||||
"immer eingeschaltet",
|
||||
"immer aktiviert",
|
||||
)
|
||||
|
||||
val DANGER_ACTION_KEYWORDS_UNINSTALL = listOf(
|
||||
"deinstallieren",
|
||||
"uninstall",
|
||||
"entfernen",
|
||||
"remove",
|
||||
"löschen",
|
||||
"delete",
|
||||
"erzwingen",
|
||||
"force stop",
|
||||
)
|
||||
|
||||
val DANGER_ACTION_KEYWORDS_A11Y = listOf(
|
||||
"deaktivieren",
|
||||
"disable",
|
||||
"ausschalten",
|
||||
"turn off",
|
||||
"stoppen",
|
||||
"stop",
|
||||
)
|
||||
|
||||
val DANGER_CONTEXT_KEYWORDS_A11Y = listOf(
|
||||
"eingabehilfe",
|
||||
"barrierefreiheit",
|
||||
"accessibility",
|
||||
"installierte dienste",
|
||||
"installed services",
|
||||
"dienste",
|
||||
"services",
|
||||
)
|
||||
|
||||
val FALLBACK_CLASS_GUARD_PATTERNS = listOf(
|
||||
"Vpn",
|
||||
"Accessibility",
|
||||
"Uninstall",
|
||||
"InstalledAppDetails",
|
||||
"ApplicationDetails",
|
||||
"ApplicationDetail", // Samsung OneUI (kein 's')
|
||||
"Package",
|
||||
"Permission",
|
||||
"InstalledService",
|
||||
"Manage",
|
||||
"Details",
|
||||
"Detail", // Samsung fallback (kein 's')
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user