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:
parent
83b0d7a062
commit
c1dd7e7320
@ -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;
|
||||
}
|
||||
|
||||
@ -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()) {
|
||||
val id = info.id ?: continue
|
||||
if (id.contains(pkg) && id.contains("RebreakAccessibilityService")) {
|
||||
Log.d(TAG, "a11y MATCH via AM: $id")
|
||||
return true
|
||||
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
|
||||
|
||||
@ -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;
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user