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.AccessibilityEvent
|
||||||
import android.view.accessibility.AccessibilityNodeInfo
|
import android.view.accessibility.AccessibilityNodeInfo
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import expo.modules.rebreakprotection.vpn.RebreakVpnService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tamper-Lock — sichert den Schutz gegen versehentliches/impulsives Abschalten.
|
* Tamper-Lock — sichert den Schutz gegen versehentliches/impulsives Abschalten.
|
||||||
@ -46,9 +47,15 @@ class RebreakAccessibilityService : AccessibilityService() {
|
|||||||
|
|
||||||
override fun onServiceConnected() {
|
override fun onServiceConnected() {
|
||||||
super.onServiceConnected()
|
super.onServiceConnected()
|
||||||
|
instance = this
|
||||||
Log.i(TAG, "tamper-lock service connected")
|
Log.i(TAG, "tamper-lock service connected")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
if (instance === this) instance = null
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
|
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
|
||||||
if (event == null) return
|
if (event == null) return
|
||||||
|
|
||||||
@ -66,6 +73,14 @@ class RebreakAccessibilityService : AccessibilityService() {
|
|||||||
if (event.eventType != AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED &&
|
if (event.eventType != AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED &&
|
||||||
event.eventType != AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) return
|
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)
|
handleProtectedSettingsBlock(pkg, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,12 +139,27 @@ class RebreakAccessibilityService : AccessibilityService() {
|
|||||||
// Class+Rebreak-Check gebraucht. Kann null sein wenn root window flackert.
|
// Class+Rebreak-Check gebraucht. Kann null sein wenn root window flackert.
|
||||||
val pageText = collectWindowText()?.lowercase().orEmpty()
|
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)
|
// 1) High-confidence Keyword im Text → sofortiger Block (Rebreak-spezifisch)
|
||||||
val highConfHit = HIGH_CONFIDENCE_KEYWORDS.firstOrNull { pageText.contains(it) }
|
val highConfHit = HIGH_CONFIDENCE_KEYWORDS.firstOrNull { pageText.contains(it) }
|
||||||
if (highConfHit != null) {
|
if (highConfHit != null) {
|
||||||
return doBlock(pkg, className, "high-confidence:$highConfHit", now)
|
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
|
// 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
|
// ist (Wort "rebreak" im Text). Sonst würde z.B. die App-Info-Page einer
|
||||||
// beliebigen anderen App geblockt werden.
|
// beliebigen anderen App geblockt werden.
|
||||||
@ -153,6 +183,84 @@ class RebreakAccessibilityService : AccessibilityService() {
|
|||||||
return false
|
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). */
|
/** Hilft Toast+Back-Action wiederzuverwenden (DRY für die beiden Block-Pfade). */
|
||||||
private fun doBlock(pkg: String, className: String, reason: String, now: Long): Boolean {
|
private fun doBlock(pkg: String, className: String, reason: String, now: Long): Boolean {
|
||||||
Log.w(TAG, "TAMPER-BLOCK: $pkg / $className (reason=$reason)")
|
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()`
|
* Tamper nur wenn Schutz WIRKLICH live ist:
|
||||||
* setzt ihn auf false — danach ist der Schutz aus und dieser Service passiv. */
|
* - `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 {
|
private fun isProtectionEnabled(): Boolean {
|
||||||
return try {
|
return try {
|
||||||
val prefs = applicationContext.getSharedPreferences(
|
val prefs = applicationContext.getSharedPreferences(
|
||||||
"rebreak_filter_prefs",
|
"rebreak_filter_prefs",
|
||||||
android.content.Context.MODE_PRIVATE,
|
android.content.Context.MODE_PRIVATE,
|
||||||
)
|
)
|
||||||
prefs.getBoolean("filter_enabled", false)
|
prefs.getBoolean("filter_enabled", false) && RebreakVpnService.isRunning
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@ -236,6 +349,18 @@ class RebreakAccessibilityService : AccessibilityService() {
|
|||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "RebreakA11y"
|
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.
|
// Settings-Apps die wir auf Tamper-Versuche überwachen.
|
||||||
// Stock + Samsung One UI haben unterschiedliche Package-Namen,
|
// Stock + Samsung One UI haben unterschiedliche Package-Namen,
|
||||||
// wir whitelist'n alle bekannten.
|
// wir whitelist'n alle bekannten.
|
||||||
@ -244,6 +369,9 @@ class RebreakAccessibilityService : AccessibilityService() {
|
|||||||
"com.android.vpndialogs",
|
"com.android.vpndialogs",
|
||||||
"com.android.packageinstaller",
|
"com.android.packageinstaller",
|
||||||
"com.google.android.packageinstaller",
|
"com.google.android.packageinstaller",
|
||||||
|
"com.samsung.android.packageinstaller",
|
||||||
|
"com.android.permissioncontroller",
|
||||||
|
"com.google.android.permissioncontroller",
|
||||||
"com.samsung.android.app.settings",
|
"com.samsung.android.app.settings",
|
||||||
"com.samsung.accessibility",
|
"com.samsung.accessibility",
|
||||||
// Play Store: User könnte hier auf "Deinstallieren" tippen für Rebreak
|
// 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).
|
* Müssen lowercase sein (Text wird vor Match lowercased).
|
||||||
*/
|
*/
|
||||||
val HIGH_CONFIDENCE_KEYWORDS = listOf(
|
val HIGH_CONFIDENCE_KEYWORDS = listOf(
|
||||||
"rebreak filter", // VPN-Profil-Name aus Builder.setSession
|
"rebreak \u2014 schutz", // DE-Summary "ReBreak — Schutz"
|
||||||
"sichert den schutz", // aktuelle a11y-Service-Summary
|
"rebreak \u2014 protection", // EN/FR-Summary "ReBreak — Protection"
|
||||||
"filtert glücksspielseiten", // alte a11y-Service-Summary (legacy installs)
|
"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 deinstallieren",
|
||||||
"rebreak entfernen",
|
"rebreak entfernen",
|
||||||
"rebreak löschen",
|
"rebreak löschen",
|
||||||
@ -277,7 +408,8 @@ class RebreakAccessibilityService : AccessibilityService() {
|
|||||||
// App-Deinstallieren-Dialoge + App-Info-Pages
|
// App-Deinstallieren-Dialoge + App-Info-Pages
|
||||||
"Uninstaller", // com.android.packageinstaller.UninstallerActivity
|
"Uninstaller", // com.android.packageinstaller.UninstallerActivity
|
||||||
"InstalledAppDetails", // App-Info-Page (kann zu uninstall führen)
|
"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)
|
// Accessibility-Settings (paradox: A11y würde sich selbst aushebeln)
|
||||||
"AccessibilitySettings",
|
"AccessibilitySettings",
|
||||||
@ -285,5 +417,98 @@ class RebreakAccessibilityService : AccessibilityService() {
|
|||||||
"InstalledServiceActivity", // Samsung
|
"InstalledServiceActivity", // Samsung
|
||||||
"AccessibilityShortcut",
|
"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