fix(native/protection-android): a11y plugin self-heals XML, arm tamper-lock on return, truthful status check

- with-rebreak-protection-android plugin now copies the source
  accessibility_service_config.xml via withDangerousMod instead of generating
  it from a string. Eliminates the silent regression where prebuild wrote
  flagReportViewIds + missing packageNames, leaving Samsung's content scan
  unable to read OEM dialogs.
- ProtectionOnboardingSheet refresh() now calls activateFamilyControls()
  once a11y is detected as enabled, so armTamperLock() actually runs.
  Previously the sheet auto-completed on getDeviceState() alone, leaving
  tamper_armed=false and the service permanently passive.
- RebreakProtectionModule.isAccessibilityServiceEnabled() now trusts the
  AccessibilityManager list as authoritative when AM is available (even when
  empty). Settings.Secure fallback only kicks in if AM is null/exception.
  Fixes the banner falsely showing "Schutz aktiv" when the system has
  unbound the service but ENABLED_ACCESSIBILITY_SERVICES still holds the id.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-05-16 11:24:45 +02:00
parent 83b0d7a062
commit c1dd7e7320
3 changed files with 46 additions and 25 deletions

View File

@ -41,7 +41,13 @@ export function ProtectionOnboardingSheet({
const vpnActive = layers.vpn === true;
const a11yActive = layers.accessibility === true;
if (vpnActive) setVpnState('done');
if (a11yActive && vpnActive) setA11yState('done');
if (a11yActive && vpnActive) {
// Arm tamper-lock once a11y is enabled — activateFamilyControls() second
// call goes through the armTamperLock() path. Without this, the service
// is bound but stays passive because tamper_armed stays false.
const r = await protection.activateFamilyControls();
if (r.enabled) setA11yState('done');
}
} finally {
refreshInFlightRef.current = false;
}

View File

@ -427,26 +427,34 @@ class RebreakProtectionModule : Module() {
val pkg = ctx.packageName
val expectedClass = RebreakAccessibilityService::class.java.name
// Primary: AccessibilityManager API — funktioniert auf allen OEMs
// (Samsung One UI returnt manchmal null bei Settings.Secure).
// Primary + authoritative: AccessibilityManager. Eine erfolgreich
// gelieferte Liste (auch leer) ist die Wahrheit — der Service läuft nur
// dann tatsächlich, wenn er hier auftaucht. `Settings.Secure` darf nur
// einspringen wenn die AM-Abfrage technisch fehlschlägt (null/Exception);
// andernfalls würde ein Stale-Eintrag in Settings.Secure (System hat den
// Service deregistriert, ENABLED_ACCESSIBILITY_SERVICES nicht aufgeräumt)
// einen aktiven Schutz vortäuschen, obwohl der Service nicht gebunden ist.
try {
val am = ctx.getSystemService(Context.ACCESSIBILITY_SERVICE) as? AccessibilityManager
if (am != null) {
val list = am.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK)
Log.d(TAG, "a11y check via AccessibilityManager: ${list?.size ?: 0} services")
for (info in list ?: emptyList()) {
if (list != null) {
Log.d(TAG, "a11y check via AccessibilityManager: ${list.size} services")
for (info in list) {
val id = info.id ?: continue
if (id.contains(pkg) && id.contains("RebreakAccessibilityService")) {
Log.d(TAG, "a11y MATCH via AM: $id")
return true
}
}
return false
}
}
} catch (e: Exception) {
Log.w(TAG, "AccessibilityManager check failed: ${e.message}")
}
// Fallback: Settings.Secure (Standard-Android)
// Fallback NUR wenn AM technisch unverfügbar war (null/Exception oben).
val expected = ComponentName(ctx, RebreakAccessibilityService::class.java)
val expectedFlat = expected.flattenToString()
val enabled = try {
@ -457,7 +465,7 @@ class RebreakProtectionModule : Module() {
} catch (e: Exception) {
null
}
Log.d(TAG, "a11y check via Settings.Secure: expected=$expectedFlat, settings=$enabled")
Log.d(TAG, "a11y check via Settings.Secure (fallback): expected=$expectedFlat, settings=$enabled")
if (!enabled.isNullOrBlank()) {
if (enabled.contains(expectedFlat)) return true
if (enabled.contains(expectedClass)) return true

View File

@ -117,13 +117,24 @@ function ensureAccessibilityService(manifest) {
});
}
// ─── 4) String resource für a11y-service-summary ────────────────────────────
// ─── 4) String resources für a11y-service ───────────────────────────────────
const A11Y_DESCRIPTION_TEXT =
'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.';
const A11Y_SUMMARY_TEXT =
'ReBreak schützt vor Glücksspiel-Seiten in Browsern. Liest URLs in der Adressleiste, um Casino-Domains zu erkennen und zu blocken.';
'Sichert den Schutz gegen Abschalten ab';
function withA11yStringResource(config) {
return withStringsXml(config, (cfg) => {
cfg.modResults = AndroidConfig.Strings.setStringItem(
[
{
$: { name: 'accessibility_service_description', translatable: 'false' },
_: A11Y_DESCRIPTION_TEXT,
},
],
cfg.modResults,
);
cfg.modResults = AndroidConfig.Strings.setStringItem(
[
{
@ -138,16 +149,13 @@ function withA11yStringResource(config) {
}
// ─── 5) XML-config für AccessibilityService ─────────────────────────────────
// Kopiert die Source-of-Truth aus dem Modul-Verzeichnis statt einen
// hardcoded String zu pflegen — so bleibt Plugin + Service-Config immer sync.
const A11Y_CONFIG_XML = `<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeWindowContentChanged|typeWindowStateChanged"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagDefault|flagReportViewIds"
android:canRetrieveWindowContent="true"
android:notificationTimeout="100"
android:description="@string/accessibility_service_summary" />
`;
const MODULE_A11Y_XML = path.resolve(
__dirname,
'../modules/rebreak-protection/android/src/main/res/xml/accessibility_service_config.xml',
);
function withA11yConfigXml(config) {
return withDangerousMod(config, [
@ -158,10 +166,9 @@ function withA11yConfigXml(config) {
'app/src/main/res/xml',
);
fs.mkdirSync(xmlDir, { recursive: true });
fs.writeFileSync(
fs.copyFileSync(
MODULE_A11Y_XML,
path.join(xmlDir, 'accessibility_service_config.xml'),
A11Y_CONFIG_XML,
'utf8',
);
return cfg;
},