import 'dart:collection' show HashSet; import 'dart:convert'; import 'dart:io'; import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:restart_app/restart_app.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly_database_old.dart' show TwonlyDatabaseOld; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/storage.dart'; class DatabaseMigrationView extends StatefulWidget { const DatabaseMigrationView({super.key}); @override State createState() => _DatabaseMigrationViewState(); } class _DatabaseMigrationViewState extends State { bool _isMigrating = false; bool _isMigratingFinished = false; int _contactsMigrated = 0; int _storedMediaFiles = 0; Future startMigration() async { setState(() { _isMigrating = true; }); final oldDatabase = TwonlyDatabaseOld(); final oldContacts = await oldDatabase.contacts.select().get(); for (final oldContact in oldContacts) { try { if (oldContact.deleted) continue; Uint8List? avatarSvg; if (oldContact.avatarSvg != null) { avatarSvg = Uint8List.fromList( gzip.encode(utf8.encode(oldContact.avatarSvg!)), ); } await twonlyDB.contactsDao.insertContact( ContactsCompanion( userId: Value(oldContact.userId), username: Value(oldContact.username), displayName: Value(oldContact.displayName), nickName: Value(oldContact.nickName), avatarSvgCompressed: Value(avatarSvg), senderProfileCounter: const Value(0), accepted: Value(oldContact.accepted), requested: Value(oldContact.requested), blocked: Value(oldContact.blocked), verified: Value(oldContact.verified), createdAt: Value(oldContact.createdAt), ), ); setState(() { _contactsMigrated += 1; }); await twonlyDB.groupsDao.createNewDirectChat( oldContact.userId, GroupsCompanion( pinned: Value(oldContact.pinned), archived: Value(oldContact.archived), groupName: Value(getContactDisplayNameOld(oldContact)), totalMediaCounter: Value(oldContact.totalMediaCounter), alsoBestFriend: Value(oldContact.alsoBestFriend), createdAt: Value(oldContact.createdAt), lastFlameCounterChange: Value(oldContact.lastFlameCounterChange), lastFlameSync: Value(oldContact.lastFlameSync), lastMessageExchange: Value(oldContact.lastMessageExchange), lastMessageReceived: Value(oldContact.lastMessageReceived), lastMessageSend: Value(oldContact.lastMessageSend), flameCounter: Value(oldContact.flameCounter), maxFlameCounter: Value(oldContact.flameCounter), maxFlameCounterFrom: Value(DateTime.now()), ), ); } catch (e) { Log.error(e); } } final folders = ['memories', 'send', 'received']; final alreadyCopied = HashSet(); for (final folder in folders) { final memoriesPath = Directory( join( (await getApplicationSupportDirectory()).path, 'media', folder, ), ); if (memoriesPath.existsSync()) { final files = memoriesPath.listSync(); for (final file in files) { try { if (file.path.contains('thumbnail')) continue; late MediaType type; if (file.path.contains('mp4')) { type = MediaType.video; } else if (file.path.contains('png')) { type = MediaType.image; } else { continue; } final bytes = File(file.path).readAsBytesSync(); final digest = (await Sha256().hash(bytes)).bytes; if (alreadyCopied.contains(digest)) { continue; } alreadyCopied.add(digest); final stat = FileStat.statSync(file.path); final mediaFile = await twonlyDB.mediaFilesDao.insertMedia( MediaFilesCompanion( type: Value(type), createdAt: Value(stat.modified), stored: const Value(true), ), ); final mediaService = MediaFileService(mediaFile!); File(file.path).copySync(mediaService.storedPath.path); setState(() { _storedMediaFiles += 1; }); } catch (e) { Log.error(e); } } } } final oldContactPreKeys = await oldDatabase.signalContactPreKeys.select().get(); for (final oldContactPreKey in oldContactPreKeys) { try { await twonlyDB .into(twonlyDB.signalContactPreKeys) .insert(SignalContactPreKey.fromJson(oldContactPreKey.toJson())); } catch (e) { Log.error(e); } } final oldSignalSessionStores = await oldDatabase.signalSessionStores.select().get(); for (final oldSignalSessionStore in oldSignalSessionStores) { try { await twonlyDB.into(twonlyDB.signalSessionStores).insert( SignalSessionStore.fromJson(oldSignalSessionStore.toJson()), ); } catch (e) { Log.error(e); } } final oldSignalSenderKeyStores = await oldDatabase.signalSenderKeyStores.select().get(); for (final oldSignalSenderKeyStore in oldSignalSenderKeyStores) { try { await twonlyDB.into(twonlyDB.signalSenderKeyStores).insert( SignalSenderKeyStore.fromJson(oldSignalSenderKeyStore.toJson()), ); } catch (e) { Log.error(e); } } final oldSignalPreyKeyStores = await oldDatabase.signalPreKeyStores.select().get(); for (final oldSignalPreyKeyStore in oldSignalPreyKeyStores) { try { await twonlyDB .into(twonlyDB.signalPreKeyStores) .insert(SignalPreKeyStore.fromJson(oldSignalPreyKeyStore.toJson())); } catch (e) { Log.error(e); } } final oldSignalIdentityKeyStores = await oldDatabase.signalIdentityKeyStores.select().get(); for (final oldSignalIdentityKeyStore in oldSignalIdentityKeyStores) { try { await twonlyDB.into(twonlyDB.signalIdentityKeyStores).insert( SignalIdentityKeyStore.fromJson( oldSignalIdentityKeyStore.toJson(), ), ); } catch (e) { Log.error(e); } } final oldSignalContactSignedPreKeys = await oldDatabase.signalContactSignedPreKeys.select().get(); for (final oldSignalContactSignedPreKey in oldSignalContactSignedPreKeys) { try { await twonlyDB.into(twonlyDB.signalContactSignedPreKeys).insert( SignalContactSignedPreKey.fromJson( oldSignalContactSignedPreKey.toJson(), ), ); } catch (e) { Log.error(e); } } await updateUserdata((u) { u.appVersion = 62; return u; }); setState(() { _isMigratingFinished = true; }); } @override Widget build(BuildContext context) { return Scaffold( body: Padding( padding: const EdgeInsets.all(12), child: _isMigratingFinished ? ListView( children: [ const SizedBox(height: 40), const Text( 'Deine Daten wurden migriert.', textAlign: TextAlign.center, style: TextStyle( fontSize: 35, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 40), ...[ '$_contactsMigrated Kontakte', '$_storedMediaFiles gespeicherte Mediendateien', ].map( (e) => Text( e, textAlign: TextAlign.center, style: const TextStyle(fontSize: 17), ), ), const SizedBox(height: 40), const Text( 'Sollte du feststellen, dass es bei der Migration Fehler gab, zum Beispiel, dass Bilder fehlen, dann melde dies bitte über das Feedback-Formular. Du hast dafür eine Woche Zeit, danach werden deine alte Daten unwiederruflich gelöscht.', textAlign: TextAlign.center, style: TextStyle(fontSize: 12), ), const SizedBox(height: 30), FilledButton( onPressed: () { Restart.restartApp( notificationTitle: 'Deine Daten wurden migriert.', notificationBody: 'Click here to open the app again', ); }, child: const Text( 'App neu starten', ), ), ], ) : _isMigrating ? ListView( children: [ const SizedBox(height: 40), const Text( 'Deine Daten werden migriert.', textAlign: TextAlign.center, style: TextStyle( fontSize: 35, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 40), const Center( child: SizedBox( width: 80, height: 80, child: CircularProgressIndicator(), ), ), const SizedBox(height: 40), const Text( 'twonly während der Migration NICHT schließen!', textAlign: TextAlign.center, style: TextStyle(fontSize: 20, color: Colors.red), ), const SizedBox(height: 40), const Text( 'Aktueller Status', textAlign: TextAlign.center, style: TextStyle(fontSize: 20), ), ...[ '$_contactsMigrated Kontakte', '$_storedMediaFiles gespeicherte Mediendateien', ].map( (e) => Text( e, textAlign: TextAlign.center, style: const TextStyle(fontSize: 17), ), ), ], ) : ListView( children: [ const SizedBox(height: 40), const Text( 'twonly. Besser als je zuvor.', textAlign: TextAlign.center, style: TextStyle( fontSize: 35, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 30), const Text( 'Das sind die neuen Features.', textAlign: TextAlign.center, style: TextStyle(fontSize: 20), ), const SizedBox(height: 10), ...[ 'Gruppen', 'Nachrichten bearbeiten & löschen', ].map( (e) => Text( e, textAlign: TextAlign.center, style: const TextStyle(fontSize: 17), ), ), const Text( 'Technische Neuerungen', textAlign: TextAlign.center, style: TextStyle(fontSize: 17), ), ...[ 'Client-to-Client (C2C) Protokoll umgestellt auf ProtoBuf.', 'Verwendung von UUIDs in der Datenbank', 'Von Grund auf neues Datenbank-Schema', 'Verbesserung der Zuverlässigkeit von C2C Nachrichten', 'Verbesserung von Videos', ].map( (e) => Text( e, textAlign: TextAlign.center, style: const TextStyle(fontSize: 10), ), ), const SizedBox(height: 50), const Text( 'Was bedeutet das für dich?', textAlign: TextAlign.center, style: TextStyle(fontSize: 20), ), const Text( 'Aufgrund der technischen Umstellung müssen wir deine alte Datenbank sowie deine gespeicherten Bilder migieren. Durch die Migration gehen einige Informationen verloren.', textAlign: TextAlign.center, style: TextStyle(fontSize: 14), ), const SizedBox(height: 10), const Text( 'Was nach der Migration erhalten bleibt.', textAlign: TextAlign.center, style: TextStyle(fontSize: 15), ), ...[ 'Gespeicherte Bilder', 'Kontakte', 'Flammen', ].map( (e) => Text( e, textAlign: TextAlign.center, style: const TextStyle(fontSize: 13), ), ), const SizedBox(height: 10), const Text( 'Was durch die Migration verloren geht.', textAlign: TextAlign.center, style: TextStyle(fontSize: 15, color: Colors.red), ), ...[ 'Text-Nachrichten und Reaktionen', 'Alles, was gesendet wurde, aber noch nicht empfangen wurde, wie Nachrichten und Bilder.', ].map( (e) => Text( e, textAlign: TextAlign.center, style: const TextStyle(fontSize: 13), ), ), const SizedBox(height: 30), FilledButton( onPressed: startMigration, child: const Text( 'Jetzt starten', ), ), ], ), ), ); } }