service: Make [XmppState] and [PreferencesState] migratable
This commit is contained in:
parent
cf353ce5ce
commit
75811d7dee
@ -30,6 +30,7 @@ files:
|
|||||||
jid: String?
|
jid: String?
|
||||||
displayName: String?
|
displayName: String?
|
||||||
avatarUrl: String?
|
avatarUrl: String?
|
||||||
|
avatarHash: String?
|
||||||
conversations:
|
conversations:
|
||||||
type: List<Conversation>?
|
type: List<Conversation>?
|
||||||
deserialise: true
|
deserialise: true
|
||||||
|
@ -54,16 +54,14 @@ Future<void> performPreStart(PerformPreStartCommand command, { dynamic extra })
|
|||||||
final id = extra as String;
|
final id = extra as String;
|
||||||
|
|
||||||
final xmpp = GetIt.I.get<XmppService>();
|
final xmpp = GetIt.I.get<XmppService>();
|
||||||
final account = await xmpp.getAccountData();
|
|
||||||
final settings = await xmpp.getConnectionSettings();
|
final settings = await xmpp.getConnectionSettings();
|
||||||
final state = await xmpp.getXmppState();
|
final state = await xmpp.getXmppState();
|
||||||
final preferences = await GetIt.I.get<PreferencesService>().getPreferences();
|
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());
|
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();
|
await GetIt.I.get<RosterService>().loadRosterFromDatabase();
|
||||||
|
|
||||||
// Check some permissions
|
// Check some permissions
|
||||||
@ -80,9 +78,10 @@ Future<void> performPreStart(PerformPreStartCommand command, { dynamic extra })
|
|||||||
sendEvent(
|
sendEvent(
|
||||||
PreStartDoneEvent(
|
PreStartDoneEvent(
|
||||||
state: "logged_in",
|
state: "logged_in",
|
||||||
jid: account.jid,
|
jid: state.jid,
|
||||||
displayName: account.displayName,
|
displayName: state.displayName,
|
||||||
avatarUrl: state.avatarUrl,
|
avatarUrl: state.avatarUrl,
|
||||||
|
avatarHash: state.avatarHash,
|
||||||
permissionsToRequest: permissions,
|
permissionsToRequest: permissions,
|
||||||
preferences: preferences,
|
preferences: preferences,
|
||||||
conversations: await GetIt.I.get<DatabaseService>().loadConversations(),
|
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 {
|
Future<void> performSetAvatar(SetAvatarCommand command, { dynamic extra }) async {
|
||||||
await GetIt.I.get<XmppService>().modifyXmppState((state) => state.copyWith(
|
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);
|
GetIt.I.get<AvatarService>().publishAvatar(command.path, command.hash);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import "dart:convert";
|
import "dart:convert";
|
||||||
|
|
||||||
import "package:moxxyv2/shared/preferences.dart";
|
import "package:moxxyv2/shared/preferences.dart";
|
||||||
|
import "package:moxxyv2/shared/migrator.dart";
|
||||||
|
|
||||||
import "package:flutter_secure_storage/flutter_secure_storage.dart";
|
import "package:flutter_secure_storage/flutter_secure_storage.dart";
|
||||||
import "package:logging/logging.dart";
|
import "package:logging/logging.dart";
|
||||||
@ -9,16 +10,67 @@ const currentVersion = 7;
|
|||||||
const preferencesVersionKey = "prefs_version";
|
const preferencesVersionKey = "prefs_version";
|
||||||
const preferencesDataKey = "prefs_data";
|
const preferencesDataKey = "prefs_data";
|
||||||
|
|
||||||
class PreferencesService {
|
class _PreferencesMigrator extends Migrator<PreferencesState> {
|
||||||
int _version = -1;
|
|
||||||
PreferencesState? _preferences;
|
|
||||||
final FlutterSecureStorage _storage = const FlutterSecureStorage(
|
final FlutterSecureStorage _storage = const FlutterSecureStorage(
|
||||||
aOptions: AndroidOptions(encryptedSharedPreferences: true)
|
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?
|
// TODO: Deduplicate with XmppService. Maybe a StorageService?
|
||||||
Future<String?> _readKeyOrNull(String key) async {
|
Future<String?> _readKeyOrNull(String key) async {
|
||||||
if (await _storage.containsKey(key: key)) {
|
if (await _storage.containsKey(key: key)) {
|
||||||
@ -27,111 +79,45 @@ class PreferencesService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _commitPreferences() async {
|
@override
|
||||||
await _storage.write(key: preferencesVersionKey, value: _version.toString());
|
Future<Map<String, dynamic>?> loadRawData() async {
|
||||||
await _storage.write(key: preferencesDataKey, value: json.encode(_preferences!.toJson()));
|
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 {
|
Future<void> _loadPreferences() async {
|
||||||
final version = int.parse((await _readKeyOrNull(preferencesVersionKey)) ?? "-1");
|
_preferences = await _migrator.load();
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<PreferencesState> getPreferences() async {
|
Future<PreferencesState> getPreferences() async {
|
||||||
@ -144,6 +130,6 @@ class PreferencesService {
|
|||||||
if (_preferences == null) await _loadPreferences();
|
if (_preferences == null) await _loadPreferences();
|
||||||
|
|
||||||
_preferences = func(_preferences!);
|
_preferences = func(_preferences!);
|
||||||
await _commitPreferences();
|
await _migrator.commit(currentVersion, _preferences!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -194,10 +194,9 @@ void onStart() {
|
|||||||
]);
|
]);
|
||||||
GetIt.I.registerSingleton<XmppConnection>(connection);
|
GetIt.I.registerSingleton<XmppConnection>(connection);
|
||||||
|
|
||||||
final account = await xmpp.getAccountData();
|
|
||||||
final settings = await xmpp.getConnectionSettings();
|
final settings = await xmpp.getConnectionSettings();
|
||||||
|
|
||||||
if (account!= null && settings != null) {
|
if (settings != null && settings.jid != null && settings.password != null) {
|
||||||
xmpp.connect(settings, false);
|
xmpp.connect(settings, false);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
@ -22,9 +22,11 @@ class XmppState with _$XmppState {
|
|||||||
String? srid,
|
String? srid,
|
||||||
String? resource,
|
String? resource,
|
||||||
String? jid,
|
String? jid,
|
||||||
|
String? displayName,
|
||||||
String? password,
|
String? password,
|
||||||
String? lastRosterVersion,
|
String? lastRosterVersion,
|
||||||
@Default("") avatarUrl,
|
@Default("") avatarUrl,
|
||||||
|
@Default("") avatarHash,
|
||||||
@Default(false) bool askedStoragePermission
|
@Default(false) bool askedStoragePermission
|
||||||
}) = _XmppState;
|
}) = _XmppState;
|
||||||
|
|
||||||
|
@ -4,8 +4,8 @@ import "dart:convert";
|
|||||||
import "package:moxxyv2/ui/helpers.dart";
|
import "package:moxxyv2/ui/helpers.dart";
|
||||||
import "package:moxxyv2/shared/events.dart";
|
import "package:moxxyv2/shared/events.dart";
|
||||||
import "package:moxxyv2/shared/helpers.dart";
|
import "package:moxxyv2/shared/helpers.dart";
|
||||||
import "package:moxxyv2/shared/account.dart";
|
|
||||||
import "package:moxxyv2/shared/eventhandler.dart";
|
import "package:moxxyv2/shared/eventhandler.dart";
|
||||||
|
import "package:moxxyv2/shared/migrator.dart";
|
||||||
import "package:moxxyv2/shared/models/message.dart";
|
import "package:moxxyv2/shared/models/message.dart";
|
||||||
import "package:moxxyv2/xmpp/settings.dart";
|
import "package:moxxyv2/xmpp/settings.dart";
|
||||||
import "package:moxxyv2/xmpp/jid.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:logging/logging.dart";
|
||||||
import "package:permission_handler/permission_handler.dart";
|
import "package:permission_handler/permission_handler.dart";
|
||||||
|
|
||||||
|
const currentXmppStateVersion = 1;
|
||||||
const xmppStateKey = "xmppState";
|
const xmppStateKey = "xmppState";
|
||||||
const xmppAccountDataKey = "xmppAccount";
|
const xmppStateVersionKey = "xmppState_version";
|
||||||
|
|
||||||
class XmppService {
|
class _XmppStateMigrator extends Migrator<XmppState> {
|
||||||
final FlutterSecureStorage _storage = const FlutterSecureStorage(
|
final FlutterSecureStorage _storage = const FlutterSecureStorage(
|
||||||
aOptions: AndroidOptions(encryptedSharedPreferences: true)
|
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 Logger _log;
|
||||||
final EventHandler _eventHandler;
|
final EventHandler _eventHandler;
|
||||||
|
final _XmppStateMigrator _migrator;
|
||||||
bool loginTriggeredFromUI = false;
|
bool loginTriggeredFromUI = false;
|
||||||
String _currentlyOpenedChatJid;
|
String _currentlyOpenedChatJid;
|
||||||
StreamSubscription<ConnectivityResult>? _networkStateSubscription;
|
StreamSubscription<ConnectivityResult>? _networkStateSubscription;
|
||||||
XmppState? _state;
|
XmppState? _state;
|
||||||
|
|
||||||
ConnectivityResult _currentConnectionType;
|
ConnectivityResult _currentConnectionType;
|
||||||
|
|
||||||
XmppService() :
|
XmppService() :
|
||||||
@ -59,6 +102,7 @@ class XmppService {
|
|||||||
_state = null,
|
_state = null,
|
||||||
_currentConnectionType = ConnectivityResult.none,
|
_currentConnectionType = ConnectivityResult.none,
|
||||||
_eventHandler = EventHandler(),
|
_eventHandler = EventHandler(),
|
||||||
|
_migrator = _XmppStateMigrator(),
|
||||||
_log = Logger("XmppService") {
|
_log = Logger("XmppService") {
|
||||||
_eventHandler.addMatchers([
|
_eventHandler.addMatchers([
|
||||||
EventTypeMatcher<ConnectionStateChangedEvent>(_onConnectionStateChanged),
|
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 {
|
Future<XmppState> getXmppState() async {
|
||||||
if (_state != null) return _state!;
|
if (_state != null) return _state!;
|
||||||
|
|
||||||
final data = await _readKeyOrNull(xmppStateKey);
|
_state = await _migrator.load();
|
||||||
// GetIt.I.get<Logger>().finest("data != null: " + (data != null).toString());
|
|
||||||
|
|
||||||
if (data == null) {
|
|
||||||
_state = XmppState();
|
|
||||||
await _commitXmppState();
|
|
||||||
return _state!;
|
|
||||||
}
|
|
||||||
|
|
||||||
_state = XmppState.fromJson(json.decode(data));
|
|
||||||
return _state!;
|
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.
|
/// A wrapper to modify the [XmppState] and commit it.
|
||||||
Future<void> modifyXmppState(XmppState Function(XmppState) func) async {
|
Future<void> modifyXmppState(XmppState Function(XmppState) func) async {
|
||||||
_state = func(_state!);
|
_state = func(_state!);
|
||||||
await _commitXmppState();
|
await _migrator.commit(currentXmppStateVersion, _state!);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ConnectionSettings?> getConnectionSettings() async {
|
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.
|
/// Returns the JID of the chat that is currently opened. Null, if none is open.
|
||||||
String? getCurrentlyOpenedChatJid() => _currentlyOpenedChatJid;
|
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].
|
/// Sends a message to [jid] with the body of [body].
|
||||||
Future<void> sendMessage({
|
Future<void> sendMessage({
|
||||||
required String body,
|
required String body,
|
||||||
@ -365,10 +366,11 @@ class XmppService {
|
|||||||
|
|
||||||
if (loginTriggeredFromUI) {
|
if (loginTriggeredFromUI) {
|
||||||
// TODO: Trigger another event so the UI can see this aswell
|
// TODO: Trigger another event so the UI can see this aswell
|
||||||
await setAccountData(AccountState(
|
await modifyXmppState((state) => state.copyWith(
|
||||||
jid: connection.getConnectionSettings().jid.toString(),
|
jid: connection.getConnectionSettings().jid.toString(),
|
||||||
displayName: connection.getConnectionSettings().jid.local,
|
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