chahinebrini af87893eb9 fix(android): self-heal — restart VpnService if it should be running but isn't
After an APK reinstall (or an OS low-memory kill that START_STICKY didn't recover
promptly), the VpnService dies but `filter_enabled` stays true. isVpnEffectivelyOn
then reports vpn:true (from the flag) → tamperLock:true → lockedIn:true → the green
"protection active" card with no toggles, while in reality nothing is filtering.

New native reconcileVpn(): if `filter_enabled` && !RebreakVpnService.isRunning &&
VpnService.prepare()==null → startVpnService(). Wired into _layout.tsx enforceProtection()
(runs on launch / foreground / 15s poll), called before reading combined state. No-op
on iOS/web. If the VPN consent was revoked, isVpnEffectivelyOn already clears the flag,
so that case self-resolves too.

Net behavior: while `filter_enabled` is true (user hasn't exited via the cooldown),
the app keeps the VPN alive. Exiting still goes through the cooldown → forceDisable →
filter_enabled=false → reconcile leaves it off. DiGA-compliant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 20:10:43 +02:00

98 lines
3.8 KiB
TypeScript

import { NativeModule, requireNativeModule } from 'expo';
import type {
ActivateResult,
DeviceLayers,
DisableResult,
HealthProbeOpts,
HealthProbeResult,
RebreakProtectionEvents,
SyncBlocklistOpts,
SyncBlocklistResult,
SystemSettingsTarget,
} from './RebreakProtection.types';
declare class RebreakProtectionModule extends NativeModule<RebreakProtectionEvents> {
/**
* iOS: aktiviert NUR den NEFilter (URL-Filter Layer).
* Triggert iOS-Dialog "Filter-Konfiguration zulassen".
*/
activateUrlFilter(): Promise<{ enabled: boolean; error?: string }>;
/**
* iOS: aktiviert NUR Family Controls (Auth + denyAppRemoval = der Lock).
* Triggert iOS-Dialog "Bildschirmzeit verwalten".
* Sobald aktiv, kann der User den Schutz nur über Cooldown deaktivieren.
*/
activateFamilyControls(): Promise<{ enabled: boolean; error?: string }>;
/**
* Aktiviert ALLE Schutz-Layer in einem Call (legacy, beide Dialoge nacheinander).
* Bevorzugt activateUrlFilter() + activateFamilyControls() einzeln aufrufen.
*/
activate(): Promise<ActivateResult>;
/**
* Schaltet ALLE Schutz-Layer ab. NUR aufrufen wenn JS-Layer verifiziert
* hat dass der 24h-Cooldown abgelaufen ist. Native-Modul prüft das nicht
* — der Backend-Cooldown ist Single Source of Truth, das ist Aufgabe der
* JS-Schicht.
*/
disable(): Promise<DisableResult>;
/** Aktueller Device-State. Polling- und Health-Check-Pfad. */
getDeviceState(): Promise<DeviceLayers>;
/**
* Lädt blocklist.bin vom Server, schreibt atomisch in App-Group/internal
* storage, postet Reload-Notification an die Filter-Extension. Server
* respondet 304 wenn ETag matched → updated=false. Plan-aware:
* Free → nur personal-domains (≤5), Pro/Legend → 208k+ + personal.
*/
syncBlocklist(opts: SyncBlocklistOpts): Promise<SyncBlocklistResult>;
/**
* E2E-Verifikation: Hidden WebView lädt eine bekannte Gambling-Domain,
* prüft ob WebKit/Browser den Load aborted (Filter funktioniert) oder die
* Page lädt (Filter ist tot — Alarm).
*/
runHealthProbe(opts?: HealthProbeOpts): Promise<HealthProbeResult>;
/** Öffnet System-Settings auf dem entsprechenden Tab. */
openSystemSettings(target?: SystemSettingsTarget): Promise<void>;
// ─── Android-spezifische Methoden (auf iOS undefined zur Laufzeit) ───────
/** Android: Live-Check ob unser AccessibilityService aktuell als enabled
* registriert ist (Settings.Secure + AccessibilityManager). */
isAccessibilityEnabled(): Promise<{ enabled: boolean }>;
/** Android: Öffnet Settings → Bedienungshilfen, möglichst tief auf die
* Rebreak-Detail-Page (deep-link). Fallback: generelle A11y-Liste. */
openAccessibilitySettings(): Promise<{ opened: boolean }>;
/** Android: Aktiviert Tamper-Lock-Watchdog (Settings-Page-Blockade durch
* AccessibilityService). Wirft `preconditions_not_met` wenn VPN oder A11y
* nicht beide live. */
armTamperLock(): Promise<{ armed: boolean }>;
/** Android: Disarm Tamper-Lock. Schutz-Layers laufen weiter, aber Settings-
* Watchdog blockt nicht mehr. Im normalen Flow nur nach Cooldown-Ablauf. */
disarmTamperLock(): Promise<{ armed: boolean }>;
/** Android: kombinierter Status aller 3 Layers + Blocklist-Count. */
getProtectionStatus(): Promise<{
vpnEnabled: boolean;
accessibilityEnabled: boolean;
blocklistCount: number;
tamperArmed: boolean;
}>;
/** Android: Wenn der Schutz an sein soll (`filter_enabled`) der VpnService
* aber nicht läuft (Reinstall / Low-Mem-Kill) → neu starten. Bei App-Start /
* Foreground aufrufen, damit der State nicht „an aber tot" bleibt. */
reconcileVpn(): Promise<{ restarted: boolean; needsConsent?: boolean }>;
}
export default requireNativeModule<RebreakProtectionModule>('RebreakProtection');