service: Make [XmppState] and [PreferencesState] migratable
This commit is contained in:
		
							parent
							
								
									cf353ce5ce
								
							
						
					
					
						commit
						75811d7dee
					
				@ -30,6 +30,7 @@ files:
 | 
			
		||||
          jid: String?
 | 
			
		||||
          displayName: String?
 | 
			
		||||
          avatarUrl: String?
 | 
			
		||||
          avatarHash: String?
 | 
			
		||||
          conversations:
 | 
			
		||||
            type: List<Conversation>?
 | 
			
		||||
            deserialise: true
 | 
			
		||||
 | 
			
		||||
@ -54,16 +54,14 @@ Future<void> performPreStart(PerformPreStartCommand command, { dynamic extra })
 | 
			
		||||
  final id = extra as String;
 | 
			
		||||
  
 | 
			
		||||
  final xmpp = GetIt.I.get<XmppService>();
 | 
			
		||||
  final account = await xmpp.getAccountData();
 | 
			
		||||
  final settings = await xmpp.getConnectionSettings();
 | 
			
		||||
  final state = await xmpp.getXmppState();
 | 
			
		||||
  final preferences = await GetIt.I.get<PreferencesService>().getPreferences();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  GetIt.I.get<Logger>().finest("account != null: " + (account != null).toString());
 | 
			
		||||
  GetIt.I.get<Logger>().finest("settings != null: " + (settings != null).toString());
 | 
			
		||||
 | 
			
		||||
  if (account!= null && settings != null) {
 | 
			
		||||
  if (settings != null && settings.jid != null && settings.password != null) {
 | 
			
		||||
    await GetIt.I.get<RosterService>().loadRosterFromDatabase();
 | 
			
		||||
 | 
			
		||||
    // Check some permissions
 | 
			
		||||
@ -80,9 +78,10 @@ Future<void> performPreStart(PerformPreStartCommand command, { dynamic extra })
 | 
			
		||||
    sendEvent(
 | 
			
		||||
      PreStartDoneEvent(
 | 
			
		||||
        state: "logged_in",
 | 
			
		||||
        jid: account.jid,
 | 
			
		||||
        displayName: account.displayName,
 | 
			
		||||
        jid: state.jid,
 | 
			
		||||
        displayName: state.displayName,
 | 
			
		||||
        avatarUrl: state.avatarUrl,
 | 
			
		||||
        avatarHash: state.avatarHash,
 | 
			
		||||
        permissionsToRequest: permissions,
 | 
			
		||||
        preferences: preferences,
 | 
			
		||||
        conversations: await GetIt.I.get<DatabaseService>().loadConversations(),
 | 
			
		||||
@ -264,7 +263,8 @@ Future<void> performRequestDownload(RequestDownloadCommand command, { dynamic ex
 | 
			
		||||
 | 
			
		||||
Future<void> performSetAvatar(SetAvatarCommand command, { dynamic extra }) async {
 | 
			
		||||
  await GetIt.I.get<XmppService>().modifyXmppState((state) => state.copyWith(
 | 
			
		||||
      avatarUrl: command.path
 | 
			
		||||
      avatarUrl: command.path,
 | 
			
		||||
      avatarHash: command.hash
 | 
			
		||||
  ));
 | 
			
		||||
  GetIt.I.get<AvatarService>().publishAvatar(command.path, command.hash);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import "dart:convert";
 | 
			
		||||
 | 
			
		||||
import "package:moxxyv2/shared/preferences.dart";
 | 
			
		||||
import "package:moxxyv2/shared/migrator.dart";
 | 
			
		||||
 | 
			
		||||
import "package:flutter_secure_storage/flutter_secure_storage.dart";
 | 
			
		||||
import "package:logging/logging.dart";
 | 
			
		||||
@ -9,15 +10,66 @@ const currentVersion = 7;
 | 
			
		||||
const preferencesVersionKey = "prefs_version";
 | 
			
		||||
const preferencesDataKey = "prefs_data";
 | 
			
		||||
 | 
			
		||||
class PreferencesService {
 | 
			
		||||
  int _version = -1;
 | 
			
		||||
  PreferencesState? _preferences;
 | 
			
		||||
class _PreferencesMigrator extends Migrator<PreferencesState> {
 | 
			
		||||
  final FlutterSecureStorage _storage = const FlutterSecureStorage(
 | 
			
		||||
    aOptions: AndroidOptions(encryptedSharedPreferences: true)
 | 
			
		||||
  );
 | 
			
		||||
  final Logger _log;
 | 
			
		||||
 | 
			
		||||
  PreferencesService() : _log = Logger("PreferencesService");
 | 
			
		||||
  _PreferencesMigrator() : super(
 | 
			
		||||
    currentVersion,
 | 
			
		||||
    [
 | 
			
		||||
      Migration<PreferencesState>(1, (data) => PreferencesState(
 | 
			
		||||
          sendChatMarkers: data["sendChatMarkers"]!,
 | 
			
		||||
          sendChatStates: data["sendChatStates"]!,
 | 
			
		||||
          showSubscriptionRequests: data["showSubscriptionRequests"]!
 | 
			
		||||
      )),
 | 
			
		||||
      Migration<PreferencesState>(2, (data) => PreferencesState(
 | 
			
		||||
          sendChatMarkers: data["sendChatMarkers"]!,
 | 
			
		||||
          sendChatStates: data["sendChatStates"]!,
 | 
			
		||||
          showSubscriptionRequests: data["showSubscriptionRequests"]!,
 | 
			
		||||
          autoDownloadWifi: data["autoDownloadWifi"]!,
 | 
			
		||||
          autoDownloadMobile: data["autoDownloadMobile"]!
 | 
			
		||||
      )),
 | 
			
		||||
      Migration<PreferencesState>(3, (data) => PreferencesState(
 | 
			
		||||
          sendChatMarkers: data["sendChatMarkers"]!,
 | 
			
		||||
          sendChatStates: data["sendChatStates"]!,
 | 
			
		||||
          showSubscriptionRequests: data["showSubscriptionRequests"]!,
 | 
			
		||||
          autoDownloadWifi: data["autoDownloadWifi"]!,
 | 
			
		||||
          autoDownloadMobile: data["autoDownloadMobile"]!,
 | 
			
		||||
          maximumAutoDownloadSize: data["maximumAutoDownloadSize"]!
 | 
			
		||||
      )),
 | 
			
		||||
      Migration<PreferencesState>(4, (data) => PreferencesState(
 | 
			
		||||
          sendChatMarkers: data["sendChatMarkers"]!,
 | 
			
		||||
          sendChatStates: data["sendChatStates"]!,
 | 
			
		||||
          showSubscriptionRequests: data["showSubscriptionRequests"]!,
 | 
			
		||||
          autoDownloadWifi: data["autoDownloadWifi"]!,
 | 
			
		||||
          autoDownloadMobile: data["autoDownloadMobile"]!,
 | 
			
		||||
          maximumAutoDownloadSize: data["maximumAutoDownloadSize"]!,
 | 
			
		||||
          backgroundPath: data["backgroundPath"]!
 | 
			
		||||
      )),
 | 
			
		||||
      Migration<PreferencesState>(5, (data) => PreferencesState(
 | 
			
		||||
          sendChatMarkers: data["sendChatMarkers"]!,
 | 
			
		||||
          sendChatStates: data["sendChatStates"]!,
 | 
			
		||||
          showSubscriptionRequests: data["showSubscriptionRequests"]!,
 | 
			
		||||
          autoDownloadWifi: data["autoDownloadWifi"]!,
 | 
			
		||||
          autoDownloadMobile: data["autoDownloadMobile"]!,
 | 
			
		||||
          maximumAutoDownloadSize: data["maximumAutoDownloadSize"]!,
 | 
			
		||||
          backgroundPath: data["backgroundPath"]!,
 | 
			
		||||
          isAvatarPublic: data["isAvatarPublic"]!
 | 
			
		||||
      )),
 | 
			
		||||
      Migration<PreferencesState>(6, (data) => PreferencesState(
 | 
			
		||||
          sendChatMarkers: data["sendChatMarkers"]!,
 | 
			
		||||
          sendChatStates: data["sendChatStates"]!,
 | 
			
		||||
          showSubscriptionRequests: data["showSubscriptionRequests"]!,
 | 
			
		||||
          autoDownloadWifi: data["autoDownloadWifi"]!,
 | 
			
		||||
          autoDownloadMobile: data["autoDownloadMobile"]!,
 | 
			
		||||
          maximumAutoDownloadSize: data["maximumAutoDownloadSize"]!,
 | 
			
		||||
          backgroundPath: data["backgroundPath"]!,
 | 
			
		||||
          isAvatarPublic: data["isAvatarPublic"]!,
 | 
			
		||||
          autoAcceptSubscriptionRequests: data["autoAcceptSubscriptionRequests"]
 | 
			
		||||
      ))
 | 
			
		||||
    ]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  // TODO: Deduplicate with XmppService. Maybe a StorageService?
 | 
			
		||||
  Future<String?> _readKeyOrNull(String key) async {
 | 
			
		||||
@ -28,110 +80,44 @@ class PreferencesService {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  Future<void> _commitPreferences() async {
 | 
			
		||||
    await _storage.write(key: preferencesVersionKey, value: _version.toString());
 | 
			
		||||
    await _storage.write(key: preferencesDataKey, value: json.encode(_preferences!.toJson()));
 | 
			
		||||
  @override
 | 
			
		||||
  Future<Map<String, dynamic>?> loadRawData() async {
 | 
			
		||||
    final raw = await _readKeyOrNull(preferencesDataKey);
 | 
			
		||||
    if (raw != null) return json.decode(raw);
 | 
			
		||||
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<int?> loadVersion() async {
 | 
			
		||||
    final raw = await _readKeyOrNull(preferencesVersionKey);
 | 
			
		||||
    if (raw != null) return int.parse(raw);
 | 
			
		||||
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  PreferencesState fromData(Map<String, dynamic> data) => PreferencesState.fromJson(data);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  PreferencesState fromDefault() => PreferencesState();
 | 
			
		||||
  
 | 
			
		||||
  @override
 | 
			
		||||
  Future<void> commit(int version, PreferencesState data) async {
 | 
			
		||||
    await _storage.write(key: preferencesVersionKey, value: version.toString());
 | 
			
		||||
    await _storage.write(key: preferencesDataKey, value: json.encode(data.toJson()));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class PreferencesService {
 | 
			
		||||
  PreferencesState? _preferences;
 | 
			
		||||
  final _PreferencesMigrator _migrator;
 | 
			
		||||
  final Logger _log;
 | 
			
		||||
 | 
			
		||||
  PreferencesService() : _migrator = _PreferencesMigrator(), _log = Logger("PreferencesService");
 | 
			
		||||
  
 | 
			
		||||
  Future<void> _loadPreferences() async {
 | 
			
		||||
    final version = int.parse((await _readKeyOrNull(preferencesVersionKey)) ?? "-1");
 | 
			
		||||
    final dataRaw = await _readKeyOrNull(preferencesDataKey);
 | 
			
		||||
 | 
			
		||||
    if (version < 1 || dataRaw == null) {
 | 
			
		||||
      _log.finest("Creating preferences...");
 | 
			
		||||
      _preferences = PreferencesState();
 | 
			
		||||
      _version = currentVersion;
 | 
			
		||||
      await _commitPreferences();
 | 
			
		||||
    } else if(version < 2) {
 | 
			
		||||
      final data = json.decode(dataRaw);
 | 
			
		||||
 | 
			
		||||
      _log.finest("Upgrading from a 0 < version < 2 to current version");
 | 
			
		||||
      _preferences = PreferencesState(
 | 
			
		||||
        sendChatMarkers: data["sendChatMarkers"]!,
 | 
			
		||||
        sendChatStates: data["sendChatStates"]!,
 | 
			
		||||
        showSubscriptionRequests: data["showSubscriptionRequests"]!
 | 
			
		||||
      );
 | 
			
		||||
      _version = currentVersion;
 | 
			
		||||
      await _commitPreferences();
 | 
			
		||||
    } else if (version < 3) {
 | 
			
		||||
      final data = json.decode(dataRaw);
 | 
			
		||||
 | 
			
		||||
      _log.finest("Upgrading from a 1 < version < 3 to current version");
 | 
			
		||||
      _preferences = PreferencesState(
 | 
			
		||||
        sendChatMarkers: data["sendChatMarkers"]!,
 | 
			
		||||
        sendChatStates: data["sendChatStates"]!,
 | 
			
		||||
        showSubscriptionRequests: data["showSubscriptionRequests"]!,
 | 
			
		||||
        autoDownloadWifi: data["autoDownloadWifi"]!,
 | 
			
		||||
        autoDownloadMobile: data["autoDownloadMobile"]!
 | 
			
		||||
      );
 | 
			
		||||
      _version = currentVersion;
 | 
			
		||||
      await _commitPreferences();
 | 
			
		||||
    } else if (version < 4) {
 | 
			
		||||
      final data = json.decode(dataRaw);
 | 
			
		||||
 | 
			
		||||
      _log.finest("Upgrading from a 2 < version < 4 to current version");
 | 
			
		||||
      _preferences = PreferencesState(
 | 
			
		||||
        sendChatMarkers: data["sendChatMarkers"]!,
 | 
			
		||||
        sendChatStates: data["sendChatStates"]!,
 | 
			
		||||
        showSubscriptionRequests: data["showSubscriptionRequests"]!,
 | 
			
		||||
        autoDownloadWifi: data["autoDownloadWifi"]!,
 | 
			
		||||
        autoDownloadMobile: data["autoDownloadMobile"]!,
 | 
			
		||||
        maximumAutoDownloadSize: data["maximumAutoDownloadSize"]!
 | 
			
		||||
      );
 | 
			
		||||
      _version = currentVersion;
 | 
			
		||||
      await _commitPreferences();
 | 
			
		||||
    } else if (version < 5) {
 | 
			
		||||
      final data = json.decode(dataRaw);
 | 
			
		||||
 | 
			
		||||
      _log.finest("Upgrading from a 4 < version < 5 to current version");
 | 
			
		||||
      _preferences = PreferencesState(
 | 
			
		||||
        sendChatMarkers: data["sendChatMarkers"]!,
 | 
			
		||||
        sendChatStates: data["sendChatStates"]!,
 | 
			
		||||
        showSubscriptionRequests: data["showSubscriptionRequests"]!,
 | 
			
		||||
        autoDownloadWifi: data["autoDownloadWifi"]!,
 | 
			
		||||
        autoDownloadMobile: data["autoDownloadMobile"]!,
 | 
			
		||||
        maximumAutoDownloadSize: data["maximumAutoDownloadSize"]!,
 | 
			
		||||
        backgroundPath: data["backgroundPath"]!
 | 
			
		||||
      );
 | 
			
		||||
      _version = currentVersion;
 | 
			
		||||
      await _commitPreferences();
 | 
			
		||||
    } else if (version < 6) {
 | 
			
		||||
      final data = json.decode(dataRaw);
 | 
			
		||||
 | 
			
		||||
      _log.finest("Upgrading from a 5 < version < 6 to current version");
 | 
			
		||||
      _preferences = PreferencesState(
 | 
			
		||||
        sendChatMarkers: data["sendChatMarkers"]!,
 | 
			
		||||
        sendChatStates: data["sendChatStates"]!,
 | 
			
		||||
        showSubscriptionRequests: data["showSubscriptionRequests"]!,
 | 
			
		||||
        autoDownloadWifi: data["autoDownloadWifi"]!,
 | 
			
		||||
        autoDownloadMobile: data["autoDownloadMobile"]!,
 | 
			
		||||
        maximumAutoDownloadSize: data["maximumAutoDownloadSize"]!,
 | 
			
		||||
        backgroundPath: data["backgroundPath"]!,
 | 
			
		||||
        isAvatarPublic: data["isAvatarPublic"]!
 | 
			
		||||
      );
 | 
			
		||||
      _version = currentVersion;
 | 
			
		||||
      await _commitPreferences();
 | 
			
		||||
    } else if (version < 7) {
 | 
			
		||||
      final data = json.decode(dataRaw);
 | 
			
		||||
 | 
			
		||||
      _log.finest("Upgrading from a 6 < version < 7 to current version");
 | 
			
		||||
      _preferences = PreferencesState(
 | 
			
		||||
        sendChatMarkers: data["sendChatMarkers"]!,
 | 
			
		||||
        sendChatStates: data["sendChatStates"]!,
 | 
			
		||||
        showSubscriptionRequests: data["showSubscriptionRequests"]!,
 | 
			
		||||
        autoDownloadWifi: data["autoDownloadWifi"]!,
 | 
			
		||||
        autoDownloadMobile: data["autoDownloadMobile"]!,
 | 
			
		||||
        maximumAutoDownloadSize: data["maximumAutoDownloadSize"]!,
 | 
			
		||||
        backgroundPath: data["backgroundPath"]!,
 | 
			
		||||
        isAvatarPublic: data["isAvatarPublic"]!,
 | 
			
		||||
        autoAcceptSubscriptionRequests: data["autoAcceptSubscriptionRequests"]
 | 
			
		||||
      );
 | 
			
		||||
      _version = currentVersion;
 | 
			
		||||
      await _commitPreferences();
 | 
			
		||||
    } else {
 | 
			
		||||
      _version = currentVersion;
 | 
			
		||||
      _preferences = PreferencesState.fromJson(json.decode(dataRaw));
 | 
			
		||||
    } 
 | 
			
		||||
    _preferences = await _migrator.load();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<PreferencesState> getPreferences() async {
 | 
			
		||||
@ -144,6 +130,6 @@ class PreferencesService {
 | 
			
		||||
    if (_preferences == null) await _loadPreferences();
 | 
			
		||||
 | 
			
		||||
    _preferences = func(_preferences!);
 | 
			
		||||
    await _commitPreferences();
 | 
			
		||||
    await _migrator.commit(currentVersion, _preferences!);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -194,10 +194,9 @@ void onStart() {
 | 
			
		||||
      ]);
 | 
			
		||||
      GetIt.I.registerSingleton<XmppConnection>(connection);
 | 
			
		||||
 | 
			
		||||
      final account = await xmpp.getAccountData();
 | 
			
		||||
      final settings = await xmpp.getConnectionSettings();
 | 
			
		||||
 | 
			
		||||
      if (account!= null && settings != null) {
 | 
			
		||||
      if (settings != null && settings.jid != null && settings.password != null) {
 | 
			
		||||
        xmpp.connect(settings, false);
 | 
			
		||||
      }
 | 
			
		||||
  })();
 | 
			
		||||
 | 
			
		||||
@ -22,9 +22,11 @@ class XmppState with _$XmppState {
 | 
			
		||||
      String? srid,
 | 
			
		||||
      String? resource,
 | 
			
		||||
      String? jid,
 | 
			
		||||
      String? displayName,
 | 
			
		||||
      String? password,
 | 
			
		||||
      String? lastRosterVersion,
 | 
			
		||||
      @Default("") avatarUrl,
 | 
			
		||||
      @Default("") avatarHash,
 | 
			
		||||
      @Default(false) bool askedStoragePermission
 | 
			
		||||
  }) = _XmppState;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -4,8 +4,8 @@ import "dart:convert";
 | 
			
		||||
import "package:moxxyv2/ui/helpers.dart";
 | 
			
		||||
import "package:moxxyv2/shared/events.dart";
 | 
			
		||||
import "package:moxxyv2/shared/helpers.dart";
 | 
			
		||||
import "package:moxxyv2/shared/account.dart";
 | 
			
		||||
import "package:moxxyv2/shared/eventhandler.dart";
 | 
			
		||||
import "package:moxxyv2/shared/migrator.dart";
 | 
			
		||||
import "package:moxxyv2/shared/models/message.dart";
 | 
			
		||||
import "package:moxxyv2/xmpp/settings.dart";
 | 
			
		||||
import "package:moxxyv2/xmpp/jid.dart";
 | 
			
		||||
@ -37,20 +37,63 @@ import "package:flutter_background_service/flutter_background_service.dart";
 | 
			
		||||
import "package:logging/logging.dart";
 | 
			
		||||
import "package:permission_handler/permission_handler.dart";
 | 
			
		||||
 | 
			
		||||
const currentXmppStateVersion = 1;
 | 
			
		||||
const xmppStateKey = "xmppState";
 | 
			
		||||
const xmppAccountDataKey = "xmppAccount";
 | 
			
		||||
const xmppStateVersionKey = "xmppState_version";
 | 
			
		||||
 | 
			
		||||
class XmppService {
 | 
			
		||||
class _XmppStateMigrator extends Migrator<XmppState> {
 | 
			
		||||
  final FlutterSecureStorage _storage = const FlutterSecureStorage(
 | 
			
		||||
    aOptions: AndroidOptions(encryptedSharedPreferences: true)
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  _XmppStateMigrator() : super(currentXmppStateVersion, []);
 | 
			
		||||
 | 
			
		||||
  // TODO: Deduplicate
 | 
			
		||||
  Future<String?> _readKeyOrNull(String key) async {
 | 
			
		||||
    if (await _storage.containsKey(key: key)) {
 | 
			
		||||
      return await _storage.read(key: key);
 | 
			
		||||
    } else {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<Map<String, dynamic>?> loadRawData() async {
 | 
			
		||||
    final raw = await _readKeyOrNull(xmppStateKey);
 | 
			
		||||
    if (raw != null) return json.decode(raw);
 | 
			
		||||
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<int?> loadVersion() async {
 | 
			
		||||
    final raw = await _readKeyOrNull(xmppStateVersionKey);
 | 
			
		||||
    if (raw != null) return int.parse(raw);
 | 
			
		||||
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  XmppState fromData(Map<String, dynamic> data) => XmppState.fromJson(data);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  XmppState fromDefault() => XmppState();
 | 
			
		||||
  
 | 
			
		||||
  @override
 | 
			
		||||
  Future<void> commit(int version, XmppState data) async {
 | 
			
		||||
    await _storage.write(key: xmppStateVersionKey, value: currentXmppStateVersion.toString());
 | 
			
		||||
    await _storage.write(key: xmppStateKey, value: json.encode(data.toJson()));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class XmppService {
 | 
			
		||||
  final Logger _log;
 | 
			
		||||
  final EventHandler _eventHandler;
 | 
			
		||||
  final _XmppStateMigrator _migrator;
 | 
			
		||||
  bool loginTriggeredFromUI = false;
 | 
			
		||||
  String _currentlyOpenedChatJid;
 | 
			
		||||
  StreamSubscription<ConnectivityResult>? _networkStateSubscription;
 | 
			
		||||
  XmppState? _state;
 | 
			
		||||
  
 | 
			
		||||
  ConnectivityResult _currentConnectionType;
 | 
			
		||||
  
 | 
			
		||||
  XmppService() :
 | 
			
		||||
@ -59,6 +102,7 @@ class XmppService {
 | 
			
		||||
    _state = null,
 | 
			
		||||
    _currentConnectionType = ConnectivityResult.none,
 | 
			
		||||
    _eventHandler = EventHandler(),
 | 
			
		||||
    _migrator = _XmppStateMigrator(),
 | 
			
		||||
    _log = Logger("XmppService") {
 | 
			
		||||
      _eventHandler.addMatchers([
 | 
			
		||||
          EventTypeMatcher<ConnectionStateChangedEvent>(_onConnectionStateChanged),
 | 
			
		||||
@ -77,41 +121,17 @@ class XmppService {
 | 
			
		||||
      ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  Future<String?> _readKeyOrNull(String key) async {
 | 
			
		||||
    if (await _storage.containsKey(key: key)) {
 | 
			
		||||
      return await _storage.read(key: key);
 | 
			
		||||
    } else {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  Future<XmppState> getXmppState() async {
 | 
			
		||||
    if (_state != null) return _state!;
 | 
			
		||||
 | 
			
		||||
    final data = await _readKeyOrNull(xmppStateKey);
 | 
			
		||||
    // GetIt.I.get<Logger>().finest("data != null: " + (data != null).toString());
 | 
			
		||||
 | 
			
		||||
    if (data == null) {
 | 
			
		||||
      _state = XmppState();
 | 
			
		||||
      await _commitXmppState();
 | 
			
		||||
    _state = await _migrator.load();
 | 
			
		||||
    return _state!;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    _state = XmppState.fromJson(json.decode(data));
 | 
			
		||||
    return _state!;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  Future<void> _commitXmppState() async {
 | 
			
		||||
    // final logger = GetIt.I.get<Logger>();
 | 
			
		||||
    // logger.finest("Commiting _xmppState to EncryptedSharedPrefs");
 | 
			
		||||
    // logger.finest("=> ${json.encode(_state!.toJson())}");
 | 
			
		||||
    await _storage.write(key: xmppStateKey, value: json.encode(_state!.toJson()));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// A wrapper to modify the [XmppState] and commit it.
 | 
			
		||||
  Future<void> modifyXmppState(XmppState Function(XmppState) func) async {
 | 
			
		||||
    _state = func(_state!);
 | 
			
		||||
    await _commitXmppState();
 | 
			
		||||
    await _migrator.commit(currentXmppStateVersion, _state!);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<ConnectionSettings?> getConnectionSettings() async {
 | 
			
		||||
@ -152,25 +172,6 @@ class XmppService {
 | 
			
		||||
  /// Returns the JID of the chat that is currently opened. Null, if none is open.
 | 
			
		||||
  String? getCurrentlyOpenedChatJid() => _currentlyOpenedChatJid;
 | 
			
		||||
  
 | 
			
		||||
  /// Load the [AccountState] from storage. Returns null if not found.
 | 
			
		||||
  Future<AccountState?> getAccountData() async {
 | 
			
		||||
    final data = await _readKeyOrNull(xmppAccountDataKey);
 | 
			
		||||
    if (data == null) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return AccountState.fromJson(jsonDecode(data));
 | 
			
		||||
  }
 | 
			
		||||
  /// Save [state] to storage such that it can be loaded again by [getAccountData].
 | 
			
		||||
  Future<void> setAccountData(AccountState state) async {
 | 
			
		||||
    return await _storage.write(key: xmppAccountDataKey, value: jsonEncode(state.toJson()));
 | 
			
		||||
  }
 | 
			
		||||
  /// Removes the account data from storage.
 | 
			
		||||
  Future<void> removeAccountData() async {
 | 
			
		||||
    // TODO: This sometimes fails
 | 
			
		||||
    await _storage.delete(key: xmppAccountDataKey);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Sends a message to [jid] with the body of [body].
 | 
			
		||||
  Future<void> sendMessage({
 | 
			
		||||
      required String body,
 | 
			
		||||
@ -365,10 +366,11 @@ class XmppService {
 | 
			
		||||
      
 | 
			
		||||
      if (loginTriggeredFromUI) {
 | 
			
		||||
        // TODO: Trigger another event so the UI can see this aswell
 | 
			
		||||
        await setAccountData(AccountState(
 | 
			
		||||
        await modifyXmppState((state) => state.copyWith(
 | 
			
		||||
            jid: connection.getConnectionSettings().jid.toString(),
 | 
			
		||||
            displayName: connection.getConnectionSettings().jid.local,
 | 
			
		||||
            avatarUrl: ""
 | 
			
		||||
            avatarUrl: "",
 | 
			
		||||
            avatarHash: ""
 | 
			
		||||
        ));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1,16 +0,0 @@
 | 
			
		||||
import "package:freezed_annotation/freezed_annotation.dart";
 | 
			
		||||
 | 
			
		||||
part "account.freezed.dart";
 | 
			
		||||
part "account.g.dart";
 | 
			
		||||
 | 
			
		||||
@freezed
 | 
			
		||||
class AccountState with _$AccountState {
 | 
			
		||||
  factory AccountState({
 | 
			
		||||
      @Default("") String jid,
 | 
			
		||||
      @Default("") String displayName,
 | 
			
		||||
      @Default("") String avatarUrl
 | 
			
		||||
  }) = _AccountState;
 | 
			
		||||
  
 | 
			
		||||
  // JSON serialization
 | 
			
		||||
  factory AccountState.fromJson(Map<String, dynamic> json) => _$AccountStateFromJson(json);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										57
									
								
								lib/shared/migrator.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								lib/shared/migrator.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,57 @@
 | 
			
		||||
class Migration<T> {
 | 
			
		||||
  final int version;
 | 
			
		||||
  /// Return a version that is upgraded to the newest version.
 | 
			
		||||
  final Function(Map<String, dynamic>) migrationFunction;
 | 
			
		||||
 | 
			
		||||
  Migration(this.version, this.migrationFunction);
 | 
			
		||||
 | 
			
		||||
  bool canMigrate(int version) => version <= this.version;  
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
abstract class Migrator<T> {
 | 
			
		||||
  final int latestVersion;
 | 
			
		||||
  final List<Migration<T>> migrations;
 | 
			
		||||
 | 
			
		||||
  Migrator(this.latestVersion, this.migrations) {
 | 
			
		||||
    migrations.sort((a, b) => -1 * a.version.compareTo(b.version));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Override: Return the raw data or null if not set yet.
 | 
			
		||||
  Future<Map<String, dynamic>?> loadRawData();
 | 
			
		||||
 | 
			
		||||
  /// Override: Return the version or null if not set yet.
 | 
			
		||||
  Future<int?> loadVersion();
 | 
			
		||||
  
 | 
			
		||||
  /// Override: Return [T] from [data] if the data is already at the newest version.
 | 
			
		||||
  T fromData(Map<String, dynamic> data);
 | 
			
		||||
 | 
			
		||||
  /// Override: If no data is available
 | 
			
		||||
  T fromDefault();
 | 
			
		||||
 | 
			
		||||
  /// Override: Commit the latest version and data back to the store.
 | 
			
		||||
  Future<void> commit(int version, T data);
 | 
			
		||||
  
 | 
			
		||||
  Future<T> load() async {
 | 
			
		||||
    final version = await loadVersion();
 | 
			
		||||
    final data = await loadRawData();
 | 
			
		||||
    if (version == null || data == null) {
 | 
			
		||||
      final ret = fromDefault();
 | 
			
		||||
      await commit(latestVersion, ret);
 | 
			
		||||
      return ret;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    if (version == latestVersion) return fromData(data);
 | 
			
		||||
 | 
			
		||||
    for (final migration in migrations) {
 | 
			
		||||
      if (migration.canMigrate(version)) {
 | 
			
		||||
        final ret = migration.migrationFunction(data);
 | 
			
		||||
        await commit(latestVersion, ret);
 | 
			
		||||
        return ret;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    final ret = fromDefault();
 | 
			
		||||
    await commit(latestVersion, ret);
 | 
			
		||||
    return ret;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										187
									
								
								test/migrations.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								test/migrations.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,187 @@
 | 
			
		||||
import "package:moxxyv2/shared/migrator.dart";
 | 
			
		||||
 | 
			
		||||
import "package:test/test.dart";
 | 
			
		||||
 | 
			
		||||
class Greeting {
 | 
			
		||||
  final String action;
 | 
			
		||||
  final String entity;
 | 
			
		||||
  final bool beNice;
 | 
			
		||||
 | 
			
		||||
  Greeting(this.entity, this.action, this.beNice);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class TestMigrator extends Migrator<Greeting> {
 | 
			
		||||
  final void Function(int, Greeting) onCommited;
 | 
			
		||||
  TestMigrator(this.onCommited) : super(
 | 
			
		||||
    2, // Latest version
 | 
			
		||||
    [
 | 
			
		||||
      Migration<Greeting>(
 | 
			
		||||
        1,
 | 
			
		||||
        (data) => Greeting(
 | 
			
		||||
          data["name"]!,
 | 
			
		||||
          data["action"]!,
 | 
			
		||||
          true
 | 
			
		||||
        )
 | 
			
		||||
      )
 | 
			
		||||
    ]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<Map<String, dynamic>?> loadRawData() async {
 | 
			
		||||
    return {
 | 
			
		||||
      "name": "Welt",
 | 
			
		||||
      "action": "welcome"
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<int?> loadVersion() async => 1;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Greeting fromData(Map<String, dynamic> data) => Greeting(
 | 
			
		||||
    data["name"],
 | 
			
		||||
    data["action"],
 | 
			
		||||
    data["beNice"]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Greeting fromDefault() => Greeting(
 | 
			
		||||
    "Moxxy",
 | 
			
		||||
    "hug",
 | 
			
		||||
    true
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  Future<void> commit(int version, Greeting data) async {
 | 
			
		||||
    onCommited(version, data);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class NoDataMigrator extends Migrator<Greeting> {
 | 
			
		||||
  final void Function(int, Greeting) onCommited;
 | 
			
		||||
  NoDataMigrator(this.onCommited) : super(
 | 
			
		||||
    2, // Latest version
 | 
			
		||||
    [
 | 
			
		||||
      Migration<Greeting>(
 | 
			
		||||
        1,
 | 
			
		||||
        (data) => Greeting(
 | 
			
		||||
          data["name"]!,
 | 
			
		||||
          data["action"]!,
 | 
			
		||||
          true
 | 
			
		||||
        )
 | 
			
		||||
      )
 | 
			
		||||
    ]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<Map<String, dynamic>?> loadRawData() async => null;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<int?> loadVersion() async => null;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Greeting fromData(Map<String, dynamic> data) => Greeting(
 | 
			
		||||
    data["name"],
 | 
			
		||||
    data["action"],
 | 
			
		||||
    data["beNice"]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Greeting fromDefault() => Greeting(
 | 
			
		||||
    "Moxxyv2",
 | 
			
		||||
    "hug_more",
 | 
			
		||||
    true
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  Future<void> commit(int version, Greeting data) async {
 | 
			
		||||
    onCommited(version, data);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class MultipleStagedMigrator extends Migrator<Greeting> {
 | 
			
		||||
  final void Function(int, Greeting) onCommited;
 | 
			
		||||
  MultipleStagedMigrator(this.onCommited) : super(
 | 
			
		||||
    3, // Latest version
 | 
			
		||||
    [
 | 
			
		||||
      Migration<Greeting>(
 | 
			
		||||
        1,
 | 
			
		||||
        (data) => Greeting(
 | 
			
		||||
          data["name"]!,
 | 
			
		||||
          "hug1",
 | 
			
		||||
          true
 | 
			
		||||
        )
 | 
			
		||||
      ),
 | 
			
		||||
      Migration<Greeting>(
 | 
			
		||||
        2,
 | 
			
		||||
        (data) => Greeting(
 | 
			
		||||
          data["name"]!,
 | 
			
		||||
          "hug2",
 | 
			
		||||
          true
 | 
			
		||||
        )
 | 
			
		||||
      )
 | 
			
		||||
    ]
 | 
			
		||||
  );
 | 
			
		||||
  
 | 
			
		||||
  @override
 | 
			
		||||
  Future<Map<String, dynamic>?> loadRawData() async {
 | 
			
		||||
    return {
 | 
			
		||||
      "name": "Welt",
 | 
			
		||||
      "action": "welcome"
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<int?> loadVersion() async => 2;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Greeting fromData(Map<String, dynamic> data) => Greeting(
 | 
			
		||||
    data["name"],
 | 
			
		||||
    data["action"],
 | 
			
		||||
    data["beNice"]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Greeting fromDefault() => Greeting(
 | 
			
		||||
    "Moxxyv2",
 | 
			
		||||
    "hug_more",
 | 
			
		||||
    true
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  Future<void> commit(int version, Greeting data) async {
 | 
			
		||||
    onCommited(version, data);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void main() {
 | 
			
		||||
  test("Test a simple migration", () async {
 | 
			
		||||
      final mig = TestMigrator((v, g) {
 | 
			
		||||
          expect(v, 2);
 | 
			
		||||
      });
 | 
			
		||||
      final greeting = await mig.load();
 | 
			
		||||
 | 
			
		||||
      expect(greeting.entity, "Welt");
 | 
			
		||||
      expect(greeting.action, "welcome");
 | 
			
		||||
      expect(greeting.beNice, true);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test("Test loading data where there was none", () async {
 | 
			
		||||
      final mig = NoDataMigrator((v, g) {
 | 
			
		||||
          expect(v, 2);
 | 
			
		||||
      });
 | 
			
		||||
      final greeting = await mig.load();
 | 
			
		||||
 | 
			
		||||
      expect(greeting.entity, "Moxxyv2");
 | 
			
		||||
      expect(greeting.action, "hug_more");
 | 
			
		||||
      expect(greeting.beNice, true);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test("Test that only the correct stage is ran", () async {
 | 
			
		||||
      final mig = MultipleStagedMigrator((v, g) {
 | 
			
		||||
          expect(v, 3);
 | 
			
		||||
      });
 | 
			
		||||
      final greeting = await mig.load();
 | 
			
		||||
 | 
			
		||||
      expect(greeting.entity, "Welt");
 | 
			
		||||
      expect(greeting.action, "hug2");
 | 
			
		||||
      expect(greeting.beNice, true);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user