/* eslint-disable @typescript-eslint/no-var-requires */ /** * Expo Config-Plugin — wires the Android VpnService (DNS-Filter) + * AccessibilityService (URL filter Layer 2) into AndroidManifest.xml at * prebuild time. * * Was es macht: * 1) Sorgt für `xmlns:tools` auf . * 2) Registriert mit * foregroundServiceType="systemExempted" + intent-filter * android.net.VpnService + permission BIND_VPN_SERVICE. * (`systemExempted` ist seit Android 14 der korrekte Type für * VPN-/Filter-Foreground-Services — vorher war `specialUse`+content_filter * angedacht aber bringt mehr Probleme als Nutzen.) * 3) Registriert mit * android:permission=BIND_ACCESSIBILITY_SERVICE + intent-filter * android.accessibilityservice.AccessibilityService + meta-data * android.accessibilityservice → @xml/accessibility_service_config. * * Wird aus app.config.ts via `plugins: ['./plugins/with-rebreak-protection-android']` * registriert. Idempotent — kann beliebig oft via `expo prebuild` laufen. * * Native Source: `modules/rebreak-protection/android/src/main/java/expo/modules/rebreakprotection/` * - VpnService: expo.modules.rebreakprotection.vpn.RebreakVpnService * - AccessibilityService: expo.modules.rebreakprotection.accessibility.RebreakAccessibilityService */ const { withAndroidManifest, withStringsXml, withDangerousMod, AndroidConfig, } = require('@expo/config-plugins'); const fs = require('fs'); const path = require('path'); const VPN_SERVICE_CLASS = 'expo.modules.rebreakprotection.vpn.RebreakVpnService'; const A11Y_SERVICE_CLASS = 'expo.modules.rebreakprotection.accessibility.RebreakAccessibilityService'; // ─── 1) tools-Namespace auf ────────────────────────────────────── function ensureToolsNamespace(manifest) { if (!manifest.manifest.$) manifest.manifest.$ = {}; if (!manifest.manifest.$['xmlns:tools']) { manifest.manifest.$['xmlns:tools'] = 'http://schemas.android.com/tools'; } } // ─── 2) -Tag für RebreakVpnService ───────────────────────────────── function ensureVpnService(manifest) { const application = AndroidConfig.Manifest.getMainApplicationOrThrow(manifest); if (!application.service) application.service = []; const alreadyDeclared = application.service.some( (svc) => svc.$ && svc.$['android:name'] === VPN_SERVICE_CLASS, ); if (alreadyDeclared) return; application.service.push({ $: { 'android:name': VPN_SERVICE_CLASS, 'android:permission': 'android.permission.BIND_VPN_SERVICE', 'android:foregroundServiceType': 'systemExempted', 'android:exported': 'false', }, 'intent-filter': [ { action: [{ $: { 'android:name': 'android.net.VpnService' } }], }, ], }); } // ─── 3) -Tag für RebreakAccessibilityService ─────────────────────── function ensureAccessibilityService(manifest) { const application = AndroidConfig.Manifest.getMainApplicationOrThrow(manifest); if (!application.service) application.service = []; const alreadyDeclared = application.service.some( (svc) => svc.$ && svc.$['android:name'] === A11Y_SERVICE_CLASS, ); if (alreadyDeclared) return; application.service.push({ $: { 'android:name': A11Y_SERVICE_CLASS, 'android:permission': 'android.permission.BIND_ACCESSIBILITY_SERVICE', 'android:label': '@string/accessibility_service_summary', 'android:exported': 'true', }, 'intent-filter': [ { action: [ { $: { 'android:name': 'android.accessibilityservice.AccessibilityService', }, }, ], }, ], 'meta-data': [ { $: { 'android:name': 'android.accessibilityservice', 'android:resource': '@xml/accessibility_service_config', }, }, ], }); } // ─── 4) String resource für a11y-service-summary ──────────────────────────── 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.'; function withA11yStringResource(config) { return withStringsXml(config, (cfg) => { cfg.modResults = AndroidConfig.Strings.setStringItem( [ { $: { name: 'accessibility_service_summary', translatable: 'false' }, _: A11Y_SUMMARY_TEXT, }, ], cfg.modResults, ); return cfg; }); } // ─── 5) XML-config für AccessibilityService ───────────────────────────────── const A11Y_CONFIG_XML = ` `; function withA11yConfigXml(config) { return withDangerousMod(config, [ 'android', async (cfg) => { const xmlDir = path.join( cfg.modRequest.platformProjectRoot, 'app/src/main/res/xml', ); fs.mkdirSync(xmlDir, { recursive: true }); fs.writeFileSync( path.join(xmlDir, 'accessibility_service_config.xml'), A11Y_CONFIG_XML, 'utf8', ); return cfg; }, ]); } // ─── Composition ──────────────────────────────────────────────────────────── function withRebreakProtectionAndroid(config) { config = withAndroidManifest(config, (cfg) => { ensureToolsNamespace(cfg.modResults); ensureVpnService(cfg.modResults); ensureAccessibilityService(cfg.modResults); return cfg; }); config = withA11yStringResource(config); config = withA11yConfigXml(config); return config; } module.exports = withRebreakProtectionAndroid;