/** * Auto-Update Service — prueft und installiert App-Updates via RVS * * Flow: * 1. App sendet "update_check" mit aktueller Version an RVS * 2. RVS vergleicht → sendet "update_available" mit Download-URL * 3. App zeigt Benachrichtigung → User bestaetigt → Download + Install */ import { Alert, Linking, Platform, NativeModules } from 'react-native'; import RNFS from 'react-native-fs'; import rvs, { RVSMessage } from './rvs'; // Version aus package.json (wird beim Build eingebettet) const packageJson = require('../../package.json'); const APP_VERSION = packageJson.version || '0.0.0.0'; type UpdateCallback = (info: UpdateInfo) => void; export interface UpdateInfo { version: string; downloadUrl: string; size: number; } class UpdateService { private listeners: UpdateCallback[] = []; private checking = false; private downloading = false; constructor() { // Beim Start alte APK-Reste aus dem Cache wegraeumen — wenn diese App // laeuft, sind frueher heruntergeladene APKs entweder schon installiert // oder unvollstaendig gewesen. Spart sonst pro Update 20-30MB auf dem Handy. this.cleanupOldApks().catch(() => {}); // Auf update_available Nachrichten lauschen rvs.onMessage((msg: RVSMessage) => { if (msg.type === 'update_available' as any) { const info: UpdateInfo = { version: (msg.payload.version as string) || '', downloadUrl: (msg.payload.downloadUrl as string) || '', size: (msg.payload.size as number) || 0, }; if (info.version && this.isNewer(info.version)) { console.log(`[Update] Neue Version verfuegbar: ${info.version} (aktuell: ${APP_VERSION})`); this.listeners.forEach(cb => cb(info)); } } }); } /** Raeumt alte heruntergeladene APK-Dateien aus dem Cache auf. */ private async cleanupOldApks(): Promise { try { const files = await RNFS.readDir(RNFS.CachesDirectoryPath); const apks = files.filter(f => /\.apk$/i.test(f.name)); let freed = 0; for (const f of apks) { try { const size = parseInt(f.size as any, 10) || 0; await RNFS.unlink(f.path); freed += size; console.log(`[Update] Alte APK geloescht: ${f.name} (${(size / 1024 / 1024).toFixed(1)}MB)`); } catch (err: any) { console.warn(`[Update] APK-Loeschen fehlgeschlagen: ${f.name} (${err?.message || err})`); } } if (apks.length > 0) { console.log(`[Update] Cleanup fertig: ${apks.length} APKs entfernt, ${(freed / 1024 / 1024).toFixed(1)}MB freigegeben`); } } catch (err: any) { console.warn(`[Update] Cleanup-Fehler: ${err?.message || err}`); } } /** Bei App-Start Update pruefen */ checkForUpdate(): void { if (this.checking) return; this.checking = true; console.log(`[Update] Pruefe auf Updates (aktuell: ${APP_VERSION})`); rvs.send('update_check' as any, { version: APP_VERSION }); setTimeout(() => { this.checking = false; }, 10000); } /** Callback registrieren */ onUpdateAvailable(callback: UpdateCallback): () => void { this.listeners.push(callback); return () => { this.listeners = this.listeners.filter(cb => cb !== callback); }; } /** Update-Dialog anzeigen */ promptUpdate(info: UpdateInfo): void { const sizeMB = (info.size / 1024 / 1024).toFixed(1); Alert.alert( 'ARIA Update verfuegbar', `Version ${info.version} (${sizeMB} MB)\n\nAktuell: ${APP_VERSION}\n\nJetzt herunterladen und installieren?`, [ { text: 'Spaeter', style: 'cancel' }, { text: 'Installieren', onPress: () => this.downloadAndInstall(info), }, ], ); } /** APK ueber WebSocket herunterladen und installieren */ async downloadAndInstall(info: UpdateInfo): Promise { if (this.downloading) return; this.downloading = true; try { console.log(`[Update] Fordere APK v${info.version} an...`); Alert.alert('Download gestartet', `Version ${info.version} wird ueber RVS heruntergeladen...`); // APK ueber WebSocket anfordern rvs.send('update_download' as any, {}); // Auf update_data warten (einmalig) const apkData = await new Promise<{base64: string, fileName: string}>((resolve, reject) => { const timeout = setTimeout(() => reject(new Error('Download-Timeout (60s)')), 60000); const unsub = rvs.onMessage((msg: RVSMessage) => { if ((msg.type as string) === 'update_data') { clearTimeout(timeout); unsub(); if (msg.payload.error) { reject(new Error(msg.payload.error as string)); } else { resolve({ base64: msg.payload.base64 as string, fileName: msg.payload.fileName as string || `ARIA-${info.version}.apk`, }); } } }); }); // Vor dem Schreiben alte APKs im Cache wegraeumen — falls mehrere // Updates in einer Session gezogen werden await this.cleanupOldApks(); // Base64 als APK-Datei speichern const destPath = `${RNFS.CachesDirectoryPath}/${apkData.fileName}`; await RNFS.writeFile(destPath, apkData.base64, 'base64'); const fileSize = await RNFS.stat(destPath); console.log(`[Update] APK gespeichert: ${destPath} (${(parseInt(fileSize.size) / 1024 / 1024).toFixed(1)}MB)`); // APK installieren via natives ApkInstaller Module (FileProvider + Intent) if (Platform.OS === 'android') { try { const { ApkInstaller } = NativeModules; await ApkInstaller.install(destPath); } catch (installErr: any) { Alert.alert( 'APK heruntergeladen', `Version ${info.version} gespeichert.\n\nBitte manuell installieren:\nDateimanager → ${apkData.fileName} antippen.\n\n(${installErr.message})`, ); } } } catch (err: any) { console.error(`[Update] Fehler: ${err.message}`); Alert.alert('Update fehlgeschlagen', err.message); } finally { this.downloading = false; } } /** Versionsvergleich */ private isNewer(remote: string): boolean { const r = remote.split('.').map(Number); const l = APP_VERSION.split('.').map(Number); for (let i = 0; i < Math.max(r.length, l.length); i++) { const diff = (r[i] || 0) - (l[i] || 0); if (diff > 0) return true; if (diff < 0) return false; } return false; } getCurrentVersion(): string { return APP_VERSION; } } const updateService = new UpdateService(); export default updateService;