Files
ARIA-AGENT/android/src/services/updater.ts
T
duffyduck cd89e36ec2 fix: alte APKs im Cache werden jetzt aufgeraeumt
Die heruntergeladenen Update-APKs (~20-30MB pro Release) landeten in
CachesDirectoryPath und wurden nie geloescht. Bei regelmaessigen
Updates sammelt sich das auf mehrere 100MB an.

Fix: cleanupOldApks() wird gerufen
  - einmal beim App-Start (Constructor) — alte APKs sind sowieso nicht
    mehr relevant, die aktuelle Version laeuft ja aus dem System
  - vor jedem neuen Download — falls jemand zwei Updates in einer
    Session zieht

Loescht alle *.apk Dateien im CachesDirectoryPath und loggt die
freigemachte Groesse pro Datei.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 01:22:22 +02:00

192 lines
6.6 KiB
TypeScript

/**
* 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<void> {
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<void> {
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;