feat(android): Protection Module v2 — VPN self-heal, boot-receiver, multilang
- RebreakProtectionModule: reconcileVpn, Samsung overlay guide, openPowerDialog, armTamperLock preconditions (VPN+a11y required), openAccessibilitySettings fallback chain - RebreakVpnService: onRevoke auto-recover, blocklist self-heal on empty start, ACTION_RESTART für hard-reload nach blocklist-Änderung - VpnBootReceiver (neu): startet VPN nach Geräte-Neustart wenn filter_enabled=true - Strings: DE/EN/FR/AR a11y-Guide, Overlay-Permission, Hint-Steps - RebreakProtectionModule.ts: reconcileVpn, armTamperLock, disarmTamperLock, openPowerDialog, openAccessibilitySettings, dismissAccessibilityHint exports Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ab4b9c48e5
commit
adf0d33f1b
@ -6,10 +6,19 @@ import android.content.ComponentName
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.VpnService
|
import android.net.VpnService
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.PixelFormat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.View
|
||||||
|
import android.view.WindowManager
|
||||||
import android.view.accessibility.AccessibilityManager
|
import android.view.accessibility.AccessibilityManager
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
import expo.modules.kotlin.Promise
|
import expo.modules.kotlin.Promise
|
||||||
import expo.modules.kotlin.exception.CodedException
|
import expo.modules.kotlin.exception.CodedException
|
||||||
import expo.modules.kotlin.modules.Module
|
import expo.modules.kotlin.modules.Module
|
||||||
@ -46,6 +55,20 @@ class RebreakProtectionModule : Module() {
|
|||||||
private var pendingActivatePromise: Promise? = null
|
private var pendingActivatePromise: Promise? = null
|
||||||
private val ioExecutor = Executors.newCachedThreadPool()
|
private val ioExecutor = Executors.newCachedThreadPool()
|
||||||
|
|
||||||
|
// Sticky-Hint State: läuft als wiederholter Toast solange User in
|
||||||
|
// Accessibility-Settings navigiert. Stoppt automatisch sobald
|
||||||
|
// a11y-Service aktiviert ODER nach STICKY_HINT_MAX_SEC.
|
||||||
|
private val mainHandler = android.os.Handler(android.os.Looper.getMainLooper())
|
||||||
|
private var stickyHintRunnable: Runnable? = null
|
||||||
|
private var stickyHintStepIndex: Int = 0
|
||||||
|
private var samsungGuideOverlay: LinearLayout? = null
|
||||||
|
private var samsungGuideTextView: TextView? = null
|
||||||
|
private var samsungGuidePrevButton: Button? = null
|
||||||
|
private var samsungGuideNextButton: Button? = null
|
||||||
|
private var samsungGuideWindowManager: WindowManager? = null
|
||||||
|
private var samsungGuideLayoutParams: WindowManager.LayoutParams? = null
|
||||||
|
private var samsungGuideWatchRunnable: Runnable? = null
|
||||||
|
|
||||||
override fun definition() = ModuleDefinition {
|
override fun definition() = ModuleDefinition {
|
||||||
Name("RebreakProtection")
|
Name("RebreakProtection")
|
||||||
|
|
||||||
@ -215,30 +238,132 @@ class RebreakProtectionModule : Module() {
|
|||||||
mapOf("enabled" to isAccessibilityServiceEnabled(requireContext()))
|
mapOf("enabled" to isAccessibilityServiceEnabled(requireContext()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stoppt den Repeating-Toast-Hint manuell. Sollte vom JS aufgerufen werden
|
||||||
|
* sobald die App wieder im Foreground ist (AppState-Listener) ODER wenn
|
||||||
|
* der User explizit „Abbrechen" tippt. Der Hint stoppt sonst von alleine
|
||||||
|
* sobald a11y-Service enabled oder nach STICKY_HINT_MAX_SEC.
|
||||||
|
*/
|
||||||
|
AsyncFunction("dismissAccessibilityHint") {
|
||||||
|
stopStickyHint()
|
||||||
|
stopSamsungGuideOverlay()
|
||||||
|
Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncFunction("openPowerDialog") {
|
||||||
|
val opened = RebreakAccessibilityService.requestPowerDialog()
|
||||||
|
if (!opened) {
|
||||||
|
Log.w(TAG, "openPowerDialog: accessibility service unavailable or action rejected")
|
||||||
|
}
|
||||||
|
mapOf("opened" to opened)
|
||||||
|
}
|
||||||
|
|
||||||
AsyncFunction("openAccessibilitySettings") { promise: Promise ->
|
AsyncFunction("openAccessibilitySettings") { promise: Promise ->
|
||||||
val ctx = requireContext()
|
val ctx = requireContext()
|
||||||
val component = ComponentName(ctx, RebreakAccessibilityService::class.java)
|
|
||||||
val tries = listOf(
|
// Für den Samsung-Schrittguide brauchen wir Overlay-Recht.
|
||||||
// Direkt zur Rebreak-Detail-Seite (Android 11+, viele OEMs)
|
// Fehlt es, zuerst die Systemseite dafür öffnen (einmaliger Gatekeeper).
|
||||||
Intent("android.settings.ACCESSIBILITY_DETAILS_SETTINGS").apply {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(ctx)) {
|
||||||
putExtra(":settings:fragment_args_key", component.flattenToString())
|
if (openOverlayPermissionSettings(ctx)) {
|
||||||
val args = android.os.Bundle()
|
promise.resolve(mapOf("opened" to true, "via" to "overlay-permission"))
|
||||||
args.putString(":settings:fragment_args_key", component.flattenToString())
|
return@AsyncFunction
|
||||||
putExtra(":settings:show_fragment_args", args)
|
}
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
}
|
||||||
},
|
|
||||||
// Fallback: Allgemeine Bedienungshilfen-Liste
|
val rebreakA11yComponent = ComponentName(ctx, RebreakAccessibilityService::class.java)
|
||||||
Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS).apply {
|
val componentFlat = rebreakA11yComponent.flattenToString()
|
||||||
|
|
||||||
|
// Fragment-Args, die viele Settings-Apps (Stock + Samsung One UI) als
|
||||||
|
// Auto-Scroll + Highlight-Hint verstehen — sowohl auf der Detail-Page
|
||||||
|
// als auch (als Fallback) auf der Listen-Page.
|
||||||
|
val highlightArgs = android.os.Bundle().apply {
|
||||||
|
putString(":settings:fragment_args_key", componentFlat)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reihenfolge ist UX-kritisch — User soll IMMER so dicht wie möglich am
|
||||||
|
// Rebreak-Toggle landen (emotional belastete User, jede Extra-Tap kostet).
|
||||||
|
// 1) Modern (Android 13+): offizielles Extra → Pixel/AOSP zielsicher
|
||||||
|
// 2) Detail-Action + legacy Bundle-Args → meiste OEMs inkl. älterer Samsung
|
||||||
|
// 3) Direkter ComponentName auf AOSP-Detail-Activity → Stock-Android-Backup
|
||||||
|
// 4) Listen-Page + Fragment-Args → Samsung One UI scrollt/hebt hervor, der
|
||||||
|
// User sieht Rebreak markiert (nicht perfekt, aber 1 Tap statt 3-4)
|
||||||
|
// 5) Plain Listen-Page → letzte Notlösung, dann Toast-Anleitung
|
||||||
|
val tries = mutableListOf<Pair<String, Intent>>()
|
||||||
|
|
||||||
|
// (1) Android 13+ offizielle API
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
tries.add(
|
||||||
|
"details-modern-extra" to Intent("android.settings.ACCESSIBILITY_DETAILS_SETTINGS").apply {
|
||||||
|
// Settings.EXTRA_ACCESSIBILITY_SERVICE_COMPONENT_NAME (API 33+)
|
||||||
|
putExtra("android.provider.extra.ACCESSIBILITY_SERVICE_COMPONENT_NAME", componentFlat)
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
for (intent in tries) {
|
}
|
||||||
|
|
||||||
|
// (2) Detail-Action mit legacy Fragment-Args
|
||||||
|
tries.add(
|
||||||
|
"details-legacy-args" to Intent("android.settings.ACCESSIBILITY_DETAILS_SETTINGS").apply {
|
||||||
|
putExtra(":settings:fragment_args_key", componentFlat)
|
||||||
|
putExtra(":settings:show_fragment_args", highlightArgs)
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// (3) AOSP Detail-Activity direkt (ohne Action-Intent-Resolution)
|
||||||
|
tries.add(
|
||||||
|
"details-aosp-component" to Intent().apply {
|
||||||
|
setClassName(
|
||||||
|
"com.android.settings",
|
||||||
|
"com.android.settings.Settings\$AccessibilityDetailsSettingsActivity",
|
||||||
|
)
|
||||||
|
putExtra(":settings:fragment_args_key", componentFlat)
|
||||||
|
putExtra(":settings:show_fragment_args", highlightArgs)
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// (4) Listen-Page MIT Highlight-Args → Samsung scrollt + markiert Rebreak
|
||||||
|
tries.add(
|
||||||
|
"list-highlighted" to Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS).apply {
|
||||||
|
putExtra(":settings:fragment_args_key", componentFlat)
|
||||||
|
putExtra(":settings:show_fragment_args", highlightArgs)
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// (5) Plain Listen-Page
|
||||||
|
tries.add(
|
||||||
|
"list-plain" to Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS).apply {
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
for ((tag, intent) in tries) {
|
||||||
|
// resolveActivity vorm Start checken — sonst zeigt Android bei ungültiger
|
||||||
|
// Action einen Toast „App nicht gefunden" und wir laufen blind durch alle
|
||||||
|
// Tries (verschwendete UI-Transitions).
|
||||||
|
val resolved = intent.resolveActivity(ctx.packageManager)
|
||||||
|
if (resolved == null) {
|
||||||
|
Log.d(TAG, "openA11y: $tag → no resolver, skip")
|
||||||
|
continue
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
ctx.startActivity(intent)
|
ctx.startActivity(intent)
|
||||||
promise.resolve(mapOf("opened" to true))
|
Log.i(TAG, "openA11y: $tag → started ($resolved)")
|
||||||
|
// Bei den Fallback-Pfaden (4 + 5) Toast-Anleitung zeigen damit User
|
||||||
|
// emotional nicht in der generischen Eingabehilfe-Page verloren geht.
|
||||||
|
// Auf der Detail-Page (1-3) ist die UI klar genug, kein Toast nötig.
|
||||||
|
if (tag == "list-highlighted" || tag == "list-plain") {
|
||||||
|
if (!startSamsungGuideOverlay(ctx)) {
|
||||||
|
val hintText = currentStickyHintText(ctx)
|
||||||
|
startStickyHint(ctx, hintText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
promise.resolve(mapOf("opened" to true, "via" to tag))
|
||||||
return@AsyncFunction
|
return@AsyncFunction
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "openAccessibilitySettings try failed: ${e.message}")
|
Log.w(TAG, "openA11y: $tag → failed: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
promise.reject(CodedException("open_failed", "no accessibility settings activity available", null))
|
promise.reject(CodedException("open_failed", "no accessibility settings activity available", null))
|
||||||
@ -325,6 +450,291 @@ class RebreakProtectionModule : Module() {
|
|||||||
appContext.reactContext
|
appContext.reactContext
|
||||||
?: throw CodedException("no_context", "ReactContext nicht verfügbar", null)
|
?: throw CodedException("no_context", "ReactContext nicht verfügbar", null)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sticky-Hint: wiederholter Toast als pragmatischer Ersatz für ein „echtes"
|
||||||
|
* Overlay (SYSTEM_ALERT_WINDOW würde noch ein Permission-Grant kosten, was
|
||||||
|
* für emotional belastete User onboarding-Friction wäre).
|
||||||
|
*
|
||||||
|
* PRE-enable ist kein A11y-Service aktiv; daher können wir den aktuellen
|
||||||
|
* Settings-Screen nicht zuverlässig erkennen (Vendor-abhängig). Statt
|
||||||
|
* Timer-Switching nutzen wir deshalb retry-basierten Progress:
|
||||||
|
* 1. Öffnen → "Installierte Dienste"
|
||||||
|
* 2. Nächstes Öffnen → "ReBreak — Schutz"
|
||||||
|
* 3. Nächstes Öffnen → "oberen Schalter + Zulassen"
|
||||||
|
*
|
||||||
|
* Damit bleibt jeder Toast kontextarm, klar und kurz — speziell für User in
|
||||||
|
* emotional belasteten Situationen.
|
||||||
|
*
|
||||||
|
* Toast.LENGTH_LONG ist hart auf ~3.5s gecapt; daher posten wir alle 3s neu.
|
||||||
|
* Auto-Stop:
|
||||||
|
* - sobald a11y-Service enabled (Polling via `Settings.Secure`)
|
||||||
|
* - nach STICKY_HINT_MAX_SEC (Safety-Net)
|
||||||
|
* - manuell via `stopStickyHint()` / `dismissAccessibilityHint` (App-Return)
|
||||||
|
*
|
||||||
|
* Wir machen explizit KEIN Foreground-Detection — wenn User aus Settings in
|
||||||
|
* unsere App zurückkommt während ein Toast feuert, ist das visuell etwas
|
||||||
|
* doppelt, aber harmlos. Eskalation auf Overlay wenn das in Praxis zu viel
|
||||||
|
* Lärm macht.
|
||||||
|
*/
|
||||||
|
private fun startStickyHint(ctx: Context, text: String) {
|
||||||
|
stopStickyHint()
|
||||||
|
val intervalMs = 3000L
|
||||||
|
val maxMs = STICKY_HINT_MAX_SEC * 1000L
|
||||||
|
val startedAt = android.os.SystemClock.uptimeMillis()
|
||||||
|
val runnable = object : Runnable {
|
||||||
|
override fun run() {
|
||||||
|
if (isAccessibilityServiceEnabled(ctx)) {
|
||||||
|
resetStickyHintProgress()
|
||||||
|
stopSamsungGuideOverlay()
|
||||||
|
bringRebreakToFront(ctx)
|
||||||
|
Log.i(TAG, "stickyHint: a11y service enabled → stop")
|
||||||
|
stickyHintRunnable = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val elapsed = android.os.SystemClock.uptimeMillis() - startedAt
|
||||||
|
if (elapsed >= maxMs) {
|
||||||
|
Log.i(TAG, "stickyHint: timeout ${STICKY_HINT_MAX_SEC}s → stop")
|
||||||
|
stickyHintRunnable = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
android.widget.Toast
|
||||||
|
.makeText(ctx, text, android.widget.Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
mainHandler.postDelayed(this, intervalMs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stickyHintRunnable = runnable
|
||||||
|
mainHandler.post(runnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startSamsungGuideOverlay(ctx: Context): Boolean {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(ctx)) {
|
||||||
|
Log.w(TAG, "samsungGuide: SYSTEM_ALERT_WINDOW not granted, fallback to toast")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
stopStickyHint()
|
||||||
|
mainHandler.post {
|
||||||
|
try {
|
||||||
|
if (samsungGuideOverlay == null) {
|
||||||
|
samsungGuideWindowManager = ctx.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||||
|
|
||||||
|
val container = LinearLayout(ctx).apply {
|
||||||
|
orientation = LinearLayout.VERTICAL
|
||||||
|
setPadding(26, 20, 26, 18)
|
||||||
|
setBackgroundColor(Color.parseColor("#E6191F24"))
|
||||||
|
}
|
||||||
|
|
||||||
|
val title = TextView(ctx).apply {
|
||||||
|
text = ctx.getString(R.string.a11y_guide_title)
|
||||||
|
setTextColor(Color.WHITE)
|
||||||
|
textSize = 14f
|
||||||
|
}
|
||||||
|
|
||||||
|
val message = TextView(ctx).apply {
|
||||||
|
setTextColor(Color.WHITE)
|
||||||
|
textSize = 16f
|
||||||
|
setPadding(0, 10, 0, 14)
|
||||||
|
}
|
||||||
|
|
||||||
|
val actions = LinearLayout(ctx).apply {
|
||||||
|
orientation = LinearLayout.HORIZONTAL
|
||||||
|
layoutParams = LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val prev = Button(ctx).apply {
|
||||||
|
text = ctx.getString(R.string.a11y_guide_btn_prev)
|
||||||
|
setOnClickListener {
|
||||||
|
if (stickyHintStepIndex > 0) {
|
||||||
|
stickyHintStepIndex -= 1
|
||||||
|
updateSamsungGuideOverlayUi(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val next = Button(ctx).apply {
|
||||||
|
layoutParams = LinearLayout.LayoutParams(
|
||||||
|
0,
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||||
|
1f,
|
||||||
|
).apply {
|
||||||
|
marginStart = 12
|
||||||
|
}
|
||||||
|
setOnClickListener {
|
||||||
|
if (stickyHintStepIndex < 3) {
|
||||||
|
stickyHintStepIndex += 1
|
||||||
|
updateSamsungGuideOverlayUi(ctx)
|
||||||
|
} else {
|
||||||
|
stopSamsungGuideOverlay()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.addView(prev)
|
||||||
|
actions.addView(next)
|
||||||
|
|
||||||
|
container.addView(title)
|
||||||
|
container.addView(message)
|
||||||
|
container.addView(actions)
|
||||||
|
|
||||||
|
val lp = WindowManager.LayoutParams(
|
||||||
|
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||||
|
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
|
||||||
|
else WindowManager.LayoutParams.TYPE_PHONE,
|
||||||
|
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||||
|
PixelFormat.TRANSLUCENT,
|
||||||
|
)
|
||||||
|
|
||||||
|
samsungGuideOverlay = container
|
||||||
|
samsungGuideTextView = message
|
||||||
|
samsungGuidePrevButton = prev
|
||||||
|
samsungGuideNextButton = next
|
||||||
|
samsungGuideLayoutParams = lp
|
||||||
|
applySamsungGuideOverlayPosition()
|
||||||
|
samsungGuideWindowManager?.addView(container, lp)
|
||||||
|
}
|
||||||
|
updateSamsungGuideOverlayUi(ctx)
|
||||||
|
startSamsungGuideWatch(ctx)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "samsungGuide: failed to show overlay: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startSamsungGuideWatch(ctx: Context) {
|
||||||
|
samsungGuideWatchRunnable?.let { mainHandler.removeCallbacks(it) }
|
||||||
|
val runnable = object : Runnable {
|
||||||
|
override fun run() {
|
||||||
|
if (samsungGuideOverlay == null) {
|
||||||
|
samsungGuideWatchRunnable = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (isAccessibilityServiceEnabled(ctx)) {
|
||||||
|
resetStickyHintProgress()
|
||||||
|
stopSamsungGuideOverlay()
|
||||||
|
bringRebreakToFront(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mainHandler.postDelayed(this, 700L)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
samsungGuideWatchRunnable = runnable
|
||||||
|
mainHandler.postDelayed(runnable, 700L)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openOverlayPermissionSettings(ctx: Context): Boolean {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return false
|
||||||
|
val intent = Intent(
|
||||||
|
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
|
||||||
|
Uri.parse("package:${ctx.packageName}"),
|
||||||
|
).apply {
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}
|
||||||
|
return try {
|
||||||
|
if (intent.resolveActivity(ctx.packageManager) != null) {
|
||||||
|
ctx.startActivity(intent)
|
||||||
|
Log.i(TAG, "overlayPermission: opened settings page")
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "overlayPermission: no resolver")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "overlayPermission: failed: ${e.message}")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bringRebreakToFront(ctx: Context) {
|
||||||
|
try {
|
||||||
|
val launchIntent = ctx.packageManager.getLaunchIntentForPackage(ctx.packageName)
|
||||||
|
if (launchIntent != null) {
|
||||||
|
launchIntent.addFlags(
|
||||||
|
Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||||
|
Intent.FLAG_ACTIVITY_CLEAR_TOP or
|
||||||
|
Intent.FLAG_ACTIVITY_SINGLE_TOP,
|
||||||
|
)
|
||||||
|
ctx.startActivity(launchIntent)
|
||||||
|
Log.i(TAG, "bringToFront: launched app after a11y enabled")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "bringToFront failed: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateSamsungGuideOverlayUi(ctx: Context) {
|
||||||
|
val messageRes = when (stickyHintStepIndex) {
|
||||||
|
0 -> R.string.a11y_hint_step_open_installed
|
||||||
|
1 -> R.string.a11y_hint_step_select_rebreak
|
||||||
|
2 -> R.string.a11y_hint_step_enable_toggle
|
||||||
|
else -> R.string.a11y_hint_step_allow_confirm
|
||||||
|
}
|
||||||
|
samsungGuideTextView?.text = ctx.getString(messageRes)
|
||||||
|
samsungGuidePrevButton?.isEnabled = stickyHintStepIndex > 0
|
||||||
|
samsungGuideNextButton?.text =
|
||||||
|
if (stickyHintStepIndex < 3) ctx.getString(R.string.a11y_guide_btn_next)
|
||||||
|
else ctx.getString(R.string.a11y_guide_btn_done)
|
||||||
|
applySamsungGuideOverlayPosition()
|
||||||
|
try {
|
||||||
|
val view = samsungGuideOverlay
|
||||||
|
val lp = samsungGuideLayoutParams
|
||||||
|
if (view != null && lp != null) samsungGuideWindowManager?.updateViewLayout(view, lp)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applySamsungGuideOverlayPosition() {
|
||||||
|
samsungGuideLayoutParams?.apply {
|
||||||
|
gravity = Gravity.CENTER
|
||||||
|
y = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun currentStickyHintText(ctx: Context): String =
|
||||||
|
when (stickyHintStepIndex) {
|
||||||
|
0 -> ctx.getString(R.string.a11y_hint_step_open_installed)
|
||||||
|
1 -> ctx.getString(R.string.a11y_hint_step_select_rebreak)
|
||||||
|
2 -> ctx.getString(R.string.a11y_hint_step_enable_toggle)
|
||||||
|
else -> ctx.getString(R.string.a11y_hint_step_allow_confirm)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun advanceStickyHintStep() {
|
||||||
|
if (stickyHintStepIndex < 3) stickyHintStepIndex += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resetStickyHintProgress() {
|
||||||
|
stickyHintStepIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopSamsungGuideOverlay() {
|
||||||
|
mainHandler.post {
|
||||||
|
try {
|
||||||
|
samsungGuideOverlay?.let { view ->
|
||||||
|
samsungGuideWindowManager?.removeView(view)
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
} finally {
|
||||||
|
samsungGuideWatchRunnable?.let { mainHandler.removeCallbacks(it) }
|
||||||
|
samsungGuideWatchRunnable = null
|
||||||
|
samsungGuideOverlay = null
|
||||||
|
samsungGuideTextView = null
|
||||||
|
samsungGuidePrevButton = null
|
||||||
|
samsungGuideNextButton = null
|
||||||
|
samsungGuideWindowManager = null
|
||||||
|
samsungGuideLayoutParams = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopStickyHint() {
|
||||||
|
stickyHintRunnable?.let { mainHandler.removeCallbacks(it) }
|
||||||
|
stickyHintRunnable = null
|
||||||
|
}
|
||||||
|
|
||||||
private fun startVpnService() {
|
private fun startVpnService() {
|
||||||
val ctx = requireContext()
|
val ctx = requireContext()
|
||||||
val intent = Intent(ctx, RebreakVpnService::class.java)
|
val intent = Intent(ctx, RebreakVpnService::class.java)
|
||||||
@ -390,7 +800,7 @@ class RebreakProtectionModule : Module() {
|
|||||||
/**
|
/**
|
||||||
* Live-Check: ist unser VPN tatsächlich aktiv?
|
* Live-Check: ist unser VPN tatsächlich aktiv?
|
||||||
* 1. VpnService.prepare(ctx) == null → wir haben Permission
|
* 1. VpnService.prepare(ctx) == null → wir haben Permission
|
||||||
* 2. UND (companion-Live-Flag ODER Prefs-Flag)
|
* 2. UND der VpnService läuft wirklich (`isRunning==true`)
|
||||||
*
|
*
|
||||||
* Wenn der User in System-Settings das VPN ausgeschaltet hat, returnt
|
* Wenn der User in System-Settings das VPN ausgeschaltet hat, returnt
|
||||||
* prepare() ein non-null Intent (= "musst Permission neu holen") UND
|
* prepare() ein non-null Intent (= "musst Permission neu holen") UND
|
||||||
@ -416,7 +826,10 @@ class RebreakProtectionModule : Module() {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return live || flag
|
// Kein Fallback mehr auf `filter_enabled` allein: nach Device-Reboot kann
|
||||||
|
// das Flag noch true sein, obwohl der VpnService noch nicht wieder läuft.
|
||||||
|
// Das führte zu falschem "voll geschützt"-Banner.
|
||||||
|
return live
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -542,5 +955,7 @@ class RebreakProtectionModule : Module() {
|
|||||||
private const val KEY_TAMPER_ARMED = "tamper_armed"
|
private const val KEY_TAMPER_ARMED = "tamper_armed"
|
||||||
private const val PREF_ETAG = "rebreak_blocklist_etag"
|
private const val PREF_ETAG = "rebreak_blocklist_etag"
|
||||||
private const val PREF_LAST_SYNC = "rebreak_blocklist_last_sync"
|
private const val PREF_LAST_SYNC = "rebreak_blocklist_last_sync"
|
||||||
|
/** Max-Duration der Repeating-Toast-Hint nachdem User in Settings landet. */
|
||||||
|
private const val STICKY_HINT_MAX_SEC = 30
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,61 @@
|
|||||||
|
package expo.modules.rebreakprotection.vpn
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.VpnService
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-establishes ReBreak VPN after device reboot/package replace when the user
|
||||||
|
* had protection enabled (`filter_enabled=true`).
|
||||||
|
*/
|
||||||
|
class RebreakVpnBootReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
|
val action = intent?.action ?: return
|
||||||
|
if (action !in BOOT_ACTIONS) return
|
||||||
|
|
||||||
|
val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
val shouldBeOn = prefs.getBoolean(KEY_ENABLED, false)
|
||||||
|
if (!shouldBeOn) {
|
||||||
|
Log.i(TAG, "boot-restart skipped: filter_enabled=false")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permission can be revoked by OS/user; then we cannot silently restart.
|
||||||
|
val consentIntent = try { VpnService.prepare(context) } catch (_: Exception) { null }
|
||||||
|
if (consentIntent != null) {
|
||||||
|
Log.w(TAG, "boot-restart skipped: VPN consent required")
|
||||||
|
prefs.edit().putBoolean(KEY_ENABLED, false).putBoolean(KEY_TAMPER_ARMED, false).apply()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val startIntent = Intent(context, RebreakVpnService::class.java)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
context.startForegroundService(startIntent)
|
||||||
|
} else {
|
||||||
|
context.startService(startIntent)
|
||||||
|
}
|
||||||
|
Log.i(TAG, "boot-restart: VPN service started")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "boot-restart failed: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "RebreakVpnBoot"
|
||||||
|
private const val PREFS = "rebreak_filter_prefs"
|
||||||
|
private const val KEY_ENABLED = "filter_enabled"
|
||||||
|
private const val KEY_TAMPER_ARMED = "tamper_armed"
|
||||||
|
private val BOOT_ACTIONS = setOf(
|
||||||
|
Intent.ACTION_BOOT_COMPLETED,
|
||||||
|
Intent.ACTION_REBOOT,
|
||||||
|
Intent.ACTION_MY_PACKAGE_REPLACED,
|
||||||
|
"android.intent.action.QUICKBOOT_POWERON",
|
||||||
|
"com.htc.intent.action.QUICKBOOT_POWERON",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,6 +8,8 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.VpnService
|
import android.net.VpnService
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
@ -200,12 +202,52 @@ class RebreakVpnService : VpnService() {
|
|||||||
|
|
||||||
override fun onRevoke() {
|
override fun onRevoke() {
|
||||||
Log.i(TAG, "onRevoke: User hat VPN in System-Settings deaktiviert")
|
Log.i(TAG, "onRevoke: User hat VPN in System-Settings deaktiviert")
|
||||||
|
val prefs = applicationContext.getSharedPreferences("rebreak_filter_prefs", Context.MODE_PRIVATE)
|
||||||
|
val shouldBeOn = prefs.getBoolean("filter_enabled", false)
|
||||||
|
val tamperArmed = prefs.getBoolean("tamper_armed", false)
|
||||||
|
|
||||||
|
// Tamper-Lock aktiv: Revoke als Bypass behandeln und VPN sofort
|
||||||
|
// wiederherstellen statt den Schutz-Status zu löschen.
|
||||||
|
if (shouldBeOn && tamperArmed) {
|
||||||
|
Log.w(TAG, "onRevoke: tamper bypass detected — attempting auto-recover")
|
||||||
|
stopVpn()
|
||||||
|
tryAutoRecoverAfterRevoke()
|
||||||
|
super.onRevoke()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
clearEnabledFlag()
|
clearEnabledFlag()
|
||||||
stopVpn()
|
stopVpn()
|
||||||
stopSelf()
|
stopSelf()
|
||||||
super.onRevoke()
|
super.onRevoke()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun tryAutoRecoverAfterRevoke() {
|
||||||
|
val consentNeeded = try { VpnService.prepare(this) } catch (_: Exception) { null }
|
||||||
|
if (consentNeeded != null) {
|
||||||
|
Log.w(TAG, "auto-recover skipped: VPN consent required")
|
||||||
|
clearEnabledFlag()
|
||||||
|
stopSelf()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Handler(Looper.getMainLooper()).postDelayed({
|
||||||
|
try {
|
||||||
|
val intent = Intent(applicationContext, RebreakVpnService::class.java).apply {
|
||||||
|
action = ACTION_START
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
applicationContext.startForegroundService(intent)
|
||||||
|
} else {
|
||||||
|
applicationContext.startService(intent)
|
||||||
|
}
|
||||||
|
Log.i(TAG, "auto-recover: VPN service restart requested")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "auto-recover failed: ${e.message}")
|
||||||
|
}
|
||||||
|
}, 350)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
// KEIN clearEnabledFlag hier — onDestroy feuert auch wenn System uns
|
// KEIN clearEnabledFlag hier — onDestroy feuert auch wenn System uns
|
||||||
// wegen Low-Memory killt. Wir wollen dann beim nächsten Start wieder
|
// wegen Low-Memory killt. Wir wollen dann beim nächsten Start wieder
|
||||||
|
|||||||
@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="accessibility_service_description">يحمي حماية ReBreak من الإيقاف المتهور: طالما أن الحماية مفعّلة، لا يمكن تعطيل شبكة ReBreak VPN من الإعدادات ولا يمكن إلغاء تثبيت التطبيق. حظر مواقع المقامرة يتم عبر VPN — هذا الإذن يحميه فقط. يمكنك إنهاء الحماية في أي وقت عبر فترة التهدئة داخل التطبيق.</string>
|
||||||
|
<string name="accessibility_service_summary">ReBreak \u2014 الحماية</string>
|
||||||
|
<string name="a11y_guide_title">ReBreak خطوة بخطوة</string>
|
||||||
|
<string name="a11y_guide_btn_prev">رجوع</string>
|
||||||
|
<string name="a11y_guide_btn_next">التالي</string>
|
||||||
|
<string name="a11y_guide_btn_done">تم</string>
|
||||||
|
<string name="a11y_guide_btn_close">إغلاق</string>
|
||||||
|
<string name="a11y_overlay_permission_required">فعّل "الظهور فوق التطبيقات الأخرى" لتطبيق ReBreak.</string>
|
||||||
|
<string name="a11y_hint_step_open_installed">اضغط على «الخدمات المثبّتة».</string>
|
||||||
|
<string name="a11y_hint_step_select_rebreak">اضغط على «ReBreak — الحماية».</string>
|
||||||
|
<string name="a11y_hint_step_enable_toggle">فعّل المفتاح العلوي.</string>
|
||||||
|
<string name="a11y_hint_step_allow_confirm">اضغط على «سماح».</string>
|
||||||
|
</resources>
|
||||||
@ -1,5 +1,15 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<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_description">Safeguards your ReBreak protection from being switched off on impulse: while protection 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>
|
<string name="accessibility_service_summary">ReBreak \u2014 Protection</string>
|
||||||
|
<string name="a11y_guide_title">ReBreak step-by-step</string>
|
||||||
|
<string name="a11y_guide_btn_prev">Back</string>
|
||||||
|
<string name="a11y_guide_btn_next">Next</string>
|
||||||
|
<string name="a11y_guide_btn_done">Done</string>
|
||||||
|
<string name="a11y_guide_btn_close">Close</string>
|
||||||
|
<string name="a11y_overlay_permission_required">Enable "Display over other apps" for ReBreak.</string>
|
||||||
|
<string name="a11y_hint_step_open_installed">Tap \u201CInstalled services\u201D.</string>
|
||||||
|
<string name="a11y_hint_step_select_rebreak">Tap \u201CReBreak \u2014 Protection\u201D.</string>
|
||||||
|
<string name="a11y_hint_step_enable_toggle">Turn on the top switch.</string>
|
||||||
|
<string name="a11y_hint_step_allow_confirm">Tap \u201CAllow\u201D.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="accessibility_service_description">Protège ta protection ReBreak contre une désactivation impulsive : tant que la protection est active, le VPN ReBreak ne peut pas être désactivé dans les Paramètres et l\'application ne peut pas être désinstallée. Le blocage des sites de jeux d\'argent est assuré par le VPN — cette autorisation ne fait que le sécuriser. Tu peux mettre fin à la protection à tout moment via la phase de refroidissement dans l\'app.</string>
|
||||||
|
<string name="accessibility_service_summary">ReBreak \u2014 Protection</string>
|
||||||
|
<string name="a11y_guide_title">ReBreak pas à pas</string>
|
||||||
|
<string name="a11y_guide_btn_prev">Retour</string>
|
||||||
|
<string name="a11y_guide_btn_next">Suivant</string>
|
||||||
|
<string name="a11y_guide_btn_done">Terminé</string>
|
||||||
|
<string name="a11y_guide_btn_close">Fermer</string>
|
||||||
|
<string name="a11y_overlay_permission_required">Active «\u00A0Afficher par-dessus d\u2019autres applications\u00A0» pour ReBreak.</string>
|
||||||
|
<string name="a11y_hint_step_open_installed">Touche \u00AB\u00A0Services install\u00E9s\u00A0\u00BB.</string>
|
||||||
|
<string name="a11y_hint_step_select_rebreak">Touche \u00AB\u00A0ReBreak \u2014 Protection\u00A0\u00BB.</string>
|
||||||
|
<string name="a11y_hint_step_enable_toggle">Active l\'interrupteur du haut.</string>
|
||||||
|
<string name="a11y_hint_step_allow_confirm">Touche \u00AB\u00A0Autoriser\u00A0\u00BB.</string>
|
||||||
|
</resources>
|
||||||
@ -1,5 +1,15 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<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_description">Sichert deinen ReBreak-Schutz gegen impulsives Abschalten ab: Solange der Schutz 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>
|
<string name="accessibility_service_summary">ReBreak \u2014 Schutz</string>
|
||||||
|
<string name="a11y_guide_title">ReBreak Schritt-für-Schritt</string>
|
||||||
|
<string name="a11y_guide_btn_prev">Zurück</string>
|
||||||
|
<string name="a11y_guide_btn_next">Weiter</string>
|
||||||
|
<string name="a11y_guide_btn_done">Fertig</string>
|
||||||
|
<string name="a11y_guide_btn_close">Schließen</string>
|
||||||
|
<string name="a11y_overlay_permission_required">Aktiviere "Über anderen Apps einblenden" für ReBreak.</string>
|
||||||
|
<string name="a11y_hint_step_open_installed">Tippe auf \u201EInstallierte Dienste\u201C.</string>
|
||||||
|
<string name="a11y_hint_step_select_rebreak">Tippe auf \u201EReBreak \u2014 Schutz\u201C.</string>
|
||||||
|
<string name="a11y_hint_step_enable_toggle">Schalte den oberen Schalter ein.</string>
|
||||||
|
<string name="a11y_hint_step_allow_confirm">Tippe auf \u201EZulassen\u201C.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@ -199,8 +199,19 @@ declare class RebreakProtectionModule extends NativeModule<RebreakProtectionEven
|
|||||||
isAccessibilityEnabled(): Promise<{ enabled: boolean }>;
|
isAccessibilityEnabled(): Promise<{ enabled: boolean }>;
|
||||||
|
|
||||||
/** Android: Öffnet Settings → Bedienungshilfen, möglichst tief auf die
|
/** Android: Öffnet Settings → Bedienungshilfen, möglichst tief auf die
|
||||||
* Rebreak-Detail-Page (deep-link). Fallback: generelle A11y-Liste. */
|
* Rebreak-Detail-Page (deep-link). Fallback: generelle A11y-Liste. Bei
|
||||||
openAccessibilitySettings(): Promise<{ opened: boolean }>;
|
* Fallback startet automatisch ein Repeating-Toast-Hint („Tippe …") der
|
||||||
|
* alle 3s wiederholt wird bis a11y aktiviert oder ~30s vergangen. */
|
||||||
|
openAccessibilitySettings(): Promise<{ opened: boolean; via?: string }>;
|
||||||
|
|
||||||
|
/** Android: Stoppt den Repeating-Toast-Hint manuell. JS sollte das bei
|
||||||
|
* AppState → 'active' aufrufen, damit nicht weiter Toasts über die App
|
||||||
|
* hereinflattern wenn User zurückgekommt ist. */
|
||||||
|
dismissAccessibilityHint(): Promise<void>;
|
||||||
|
|
||||||
|
/** Android: Öffnet den System-Power-Dialog über den aktiven A11y-Service.
|
||||||
|
* Liefert `opened=false`, wenn der Service nicht aktiv ist oder OEM blockt. */
|
||||||
|
openPowerDialog(): Promise<{ opened: boolean }>;
|
||||||
|
|
||||||
/** Android: Aktiviert Tamper-Lock-Watchdog (Settings-Page-Blockade durch
|
/** Android: Aktiviert Tamper-Lock-Watchdog (Settings-Page-Blockade durch
|
||||||
* AccessibilityService). Wirft `preconditions_not_met` wenn VPN oder A11y
|
* AccessibilityService). Wirft `preconditions_not_met` wenn VPN oder A11y
|
||||||
|
|||||||
@ -63,6 +63,12 @@ class RebreakProtectionModuleWeb extends NativeModule<RebreakProtectionEvents> {
|
|||||||
async openAccessibilitySettings() {
|
async openAccessibilitySettings() {
|
||||||
return { opened: false };
|
return { opened: false };
|
||||||
}
|
}
|
||||||
|
async dismissAccessibilityHint() {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
async openPowerDialog() {
|
||||||
|
return { opened: false };
|
||||||
|
}
|
||||||
async armTamperLock() {
|
async armTamperLock() {
|
||||||
return { armed: false };
|
return { armed: false };
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user