LCOV - code coverage report
Current view: top level - lib/src/database - hive_collections_database.dart (source / functions) Hit Total Coverage
Test: merged.info Lines: 490 762 64.3 %
Date: 2024-07-12 20:20:16 Functions: 0 0 -

          Line data    Source code
       1             : /*
       2             :  *   Famedly Matrix SDK
       3             :  *   Copyright (C) 2019, 2020, 2021 Famedly GmbH
       4             :  *
       5             :  *   This program is free software: you can redistribute it and/or modify
       6             :  *   it under the terms of the GNU Affero General Public License as
       7             :  *   published by the Free Software Foundation, either version 3 of the
       8             :  *   License, or (at your option) any later version.
       9             :  *
      10             :  *   This program is distributed in the hope that it will be useful,
      11             :  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
      12             :  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
      13             :  *   GNU Affero General Public License for more details.
      14             :  *
      15             :  *   You should have received a copy of the GNU Affero General Public License
      16             :  *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
      17             :  */
      18             : 
      19             : import 'dart:async';
      20             : import 'dart:convert';
      21             : import 'dart:math';
      22             : import 'dart:typed_data';
      23             : 
      24             : import 'package:collection/collection.dart';
      25             : import 'package:hive/hive.dart';
      26             : 
      27             : import 'package:matrix/encryption/utils/olm_session.dart';
      28             : import 'package:matrix/encryption/utils/outbound_group_session.dart';
      29             : import 'package:matrix/encryption/utils/ssss_cache.dart';
      30             : import 'package:matrix/encryption/utils/stored_inbound_group_session.dart';
      31             : import 'package:matrix/matrix.dart';
      32             : import 'package:matrix/src/utils/copy_map.dart';
      33             : import 'package:matrix/src/utils/queued_to_device_event.dart';
      34             : import 'package:matrix/src/utils/run_benchmarked.dart';
      35             : 
      36             : /// This database does not support file caching!
      37             : @Deprecated(
      38             :     'Use [MatrixSdkDatabase] instead. Don\'t forget to properly migrate!')
      39             : class HiveCollectionsDatabase extends DatabaseApi {
      40             :   static const int version = 7;
      41             :   final String name;
      42             :   final String? path;
      43             :   final HiveCipher? key;
      44             :   final Future<BoxCollection> Function(
      45             :     String name,
      46             :     Set<String> boxNames, {
      47             :     String? path,
      48             :     HiveCipher? key,
      49             :   }) collectionFactory;
      50             :   late BoxCollection _collection;
      51             :   late CollectionBox<String> _clientBox;
      52             :   late CollectionBox<Map> _accountDataBox;
      53             :   late CollectionBox<Map> _roomsBox;
      54             :   late CollectionBox<Map> _toDeviceQueueBox;
      55             : 
      56             :   /// Key is a tuple as TupleKey(roomId, type) where stateKey can be
      57             :   /// an empty string.
      58             :   late CollectionBox<Map> _roomStateBox;
      59             : 
      60             :   /// Key is a tuple as TupleKey(roomId, userId)
      61             :   late CollectionBox<Map> _roomMembersBox;
      62             : 
      63             :   /// Key is a tuple as TupleKey(roomId, type)
      64             :   late CollectionBox<Map> _roomAccountDataBox;
      65             :   late CollectionBox<Map> _inboundGroupSessionsBox;
      66             :   late CollectionBox<Map> _outboundGroupSessionsBox;
      67             :   late CollectionBox<Map> _olmSessionsBox;
      68             : 
      69             :   /// Key is a tuple as TupleKey(userId, deviceId)
      70             :   late CollectionBox<Map> _userDeviceKeysBox;
      71             : 
      72             :   /// Key is the user ID as a String
      73             :   late CollectionBox<bool> _userDeviceKeysOutdatedBox;
      74             : 
      75             :   /// Key is a tuple as TupleKey(userId, publicKey)
      76             :   late CollectionBox<Map> _userCrossSigningKeysBox;
      77             :   late CollectionBox<Map> _ssssCacheBox;
      78             :   late CollectionBox<Map> _presencesBox;
      79             : 
      80             :   /// Key is a tuple as Multikey(roomId, fragmentId) while the default
      81             :   /// fragmentId is an empty String
      82             :   late CollectionBox<List> _timelineFragmentsBox;
      83             : 
      84             :   /// Key is a tuple as TupleKey(roomId, eventId)
      85             :   late CollectionBox<Map> _eventsBox;
      86             : 
      87             :   /// Key is a tuple as TupleKey(userId, deviceId)
      88             :   late CollectionBox<String> _seenDeviceIdsBox;
      89             : 
      90             :   late CollectionBox<String> _seenDeviceKeysBox;
      91             : 
      92           1 :   String get _clientBoxName => 'box_client';
      93             : 
      94           1 :   String get _accountDataBoxName => 'box_account_data';
      95             : 
      96           1 :   String get _roomsBoxName => 'box_rooms';
      97             : 
      98           1 :   String get _toDeviceQueueBoxName => 'box_to_device_queue';
      99             : 
     100           1 :   String get _roomStateBoxName => 'box_room_states';
     101             : 
     102           1 :   String get _roomMembersBoxName => 'box_room_members';
     103             : 
     104           1 :   String get _roomAccountDataBoxName => 'box_room_account_data';
     105             : 
     106           1 :   String get _inboundGroupSessionsBoxName => 'box_inbound_group_session';
     107             : 
     108           1 :   String get _outboundGroupSessionsBoxName => 'box_outbound_group_session';
     109             : 
     110           1 :   String get _olmSessionsBoxName => 'box_olm_session';
     111             : 
     112           1 :   String get _userDeviceKeysBoxName => 'box_user_device_keys';
     113             : 
     114           1 :   String get _userDeviceKeysOutdatedBoxName => 'box_user_device_keys_outdated';
     115             : 
     116           1 :   String get _userCrossSigningKeysBoxName => 'box_cross_signing_keys';
     117             : 
     118           1 :   String get _ssssCacheBoxName => 'box_ssss_cache';
     119             : 
     120           1 :   String get _presencesBoxName => 'box_presences';
     121             : 
     122           1 :   String get _timelineFragmentsBoxName => 'box_timeline_fragments';
     123             : 
     124           1 :   String get _eventsBoxName => 'box_events';
     125             : 
     126           1 :   String get _seenDeviceIdsBoxName => 'box_seen_device_ids';
     127             : 
     128           1 :   String get _seenDeviceKeysBoxName => 'box_seen_device_keys';
     129             : 
     130           1 :   HiveCollectionsDatabase(
     131             :     this.name,
     132             :     this.path, {
     133             :     this.key,
     134             :     this.collectionFactory = BoxCollection.open,
     135             :   });
     136             : 
     137           0 :   @override
     138             :   int get maxFileSize => 0;
     139             : 
     140           1 :   Future<void> open() async {
     141           3 :     _collection = await collectionFactory(
     142           1 :       name,
     143             :       {
     144           1 :         _clientBoxName,
     145           1 :         _accountDataBoxName,
     146           1 :         _roomsBoxName,
     147           1 :         _toDeviceQueueBoxName,
     148           1 :         _roomStateBoxName,
     149           1 :         _roomMembersBoxName,
     150           1 :         _roomAccountDataBoxName,
     151           1 :         _inboundGroupSessionsBoxName,
     152           1 :         _outboundGroupSessionsBoxName,
     153           1 :         _olmSessionsBoxName,
     154           1 :         _userDeviceKeysBoxName,
     155           1 :         _userDeviceKeysOutdatedBoxName,
     156           1 :         _userCrossSigningKeysBoxName,
     157           1 :         _ssssCacheBoxName,
     158           1 :         _presencesBoxName,
     159           1 :         _timelineFragmentsBoxName,
     160           1 :         _eventsBoxName,
     161           1 :         _seenDeviceIdsBoxName,
     162           1 :         _seenDeviceKeysBoxName,
     163             :       },
     164           1 :       key: key,
     165           1 :       path: path,
     166             :     );
     167           3 :     _clientBox = await _collection.openBox(
     168           1 :       _clientBoxName,
     169             :       preload: true,
     170             :     );
     171           3 :     _accountDataBox = await _collection.openBox(
     172           1 :       _accountDataBoxName,
     173             :       preload: true,
     174             :     );
     175           3 :     _roomsBox = await _collection.openBox(
     176           1 :       _roomsBoxName,
     177             :       preload: true,
     178             :     );
     179           3 :     _roomStateBox = await _collection.openBox(
     180           1 :       _roomStateBoxName,
     181             :     );
     182           3 :     _roomMembersBox = await _collection.openBox(
     183           1 :       _roomMembersBoxName,
     184             :     );
     185           3 :     _toDeviceQueueBox = await _collection.openBox(
     186           1 :       _toDeviceQueueBoxName,
     187             :       preload: true,
     188             :     );
     189           3 :     _roomAccountDataBox = await _collection.openBox(
     190           1 :       _roomAccountDataBoxName,
     191             :       preload: true,
     192             :     );
     193           3 :     _inboundGroupSessionsBox = await _collection.openBox(
     194           1 :       _inboundGroupSessionsBoxName,
     195             :     );
     196           3 :     _outboundGroupSessionsBox = await _collection.openBox(
     197           1 :       _outboundGroupSessionsBoxName,
     198             :     );
     199           3 :     _olmSessionsBox = await _collection.openBox(
     200           1 :       _olmSessionsBoxName,
     201             :     );
     202           3 :     _userDeviceKeysBox = await _collection.openBox(
     203           1 :       _userDeviceKeysBoxName,
     204             :     );
     205           3 :     _userDeviceKeysOutdatedBox = await _collection.openBox(
     206           1 :       _userDeviceKeysOutdatedBoxName,
     207             :     );
     208           3 :     _userCrossSigningKeysBox = await _collection.openBox(
     209           1 :       _userCrossSigningKeysBoxName,
     210             :     );
     211           3 :     _ssssCacheBox = await _collection.openBox(
     212           1 :       _ssssCacheBoxName,
     213             :     );
     214           3 :     _presencesBox = await _collection.openBox(
     215           1 :       _presencesBoxName,
     216             :     );
     217           3 :     _timelineFragmentsBox = await _collection.openBox(
     218           1 :       _timelineFragmentsBoxName,
     219             :     );
     220           3 :     _eventsBox = await _collection.openBox(
     221           1 :       _eventsBoxName,
     222             :     );
     223           3 :     _seenDeviceIdsBox = await _collection.openBox(
     224           1 :       _seenDeviceIdsBoxName,
     225             :     );
     226           3 :     _seenDeviceKeysBox = await _collection.openBox(
     227           1 :       _seenDeviceKeysBoxName,
     228             :     );
     229             : 
     230             :     // Check version and check if we need a migration
     231           3 :     final currentVersion = int.tryParse(await _clientBox.get('version') ?? '');
     232             :     if (currentVersion == null) {
     233           3 :       await _clientBox.put('version', version.toString());
     234           0 :     } else if (currentVersion != version) {
     235           0 :       await _migrateFromVersion(currentVersion);
     236             :     }
     237             : 
     238             :     return;
     239             :   }
     240             : 
     241           0 :   Future<void> _migrateFromVersion(int currentVersion) async {
     242           0 :     Logs().i('Migrate store database from version $currentVersion to $version');
     243           0 :     await clearCache();
     244           0 :     await _clientBox.put('version', version.toString());
     245             :   }
     246             : 
     247           1 :   @override
     248           2 :   Future<void> clear() => transaction(() async {
     249           2 :         await _clientBox.clear();
     250           2 :         await _accountDataBox.clear();
     251           2 :         await _roomsBox.clear();
     252           2 :         await _roomStateBox.clear();
     253           2 :         await _roomMembersBox.clear();
     254           2 :         await _toDeviceQueueBox.clear();
     255           2 :         await _roomAccountDataBox.clear();
     256           2 :         await _inboundGroupSessionsBox.clear();
     257           2 :         await _outboundGroupSessionsBox.clear();
     258           2 :         await _olmSessionsBox.clear();
     259           2 :         await _userDeviceKeysBox.clear();
     260           2 :         await _userDeviceKeysOutdatedBox.clear();
     261           2 :         await _userCrossSigningKeysBox.clear();
     262           2 :         await _ssssCacheBox.clear();
     263           2 :         await _presencesBox.clear();
     264           2 :         await _timelineFragmentsBox.clear();
     265           2 :         await _eventsBox.clear();
     266           2 :         await _seenDeviceIdsBox.clear();
     267           2 :         await _seenDeviceKeysBox.clear();
     268           2 :         await _collection.deleteFromDisk();
     269             :       });
     270             : 
     271           1 :   @override
     272           2 :   Future<void> clearCache() => transaction(() async {
     273           2 :         await _roomsBox.clear();
     274           2 :         await _accountDataBox.clear();
     275           2 :         await _roomAccountDataBox.clear();
     276           2 :         await _roomStateBox.clear();
     277           2 :         await _roomMembersBox.clear();
     278           2 :         await _eventsBox.clear();
     279           2 :         await _timelineFragmentsBox.clear();
     280           2 :         await _outboundGroupSessionsBox.clear();
     281           2 :         await _presencesBox.clear();
     282           2 :         await _clientBox.delete('prev_batch');
     283             :       });
     284             : 
     285           1 :   @override
     286           2 :   Future<void> clearSSSSCache() => _ssssCacheBox.clear();
     287             : 
     288           1 :   @override
     289           2 :   Future<void> close() async => _collection.close();
     290             : 
     291           1 :   @override
     292             :   Future<void> deleteFromToDeviceQueue(int id) async {
     293           3 :     await _toDeviceQueueBox.delete(id.toString());
     294             :     return;
     295             :   }
     296             : 
     297           1 :   @override
     298             :   Future<void> deleteOldFiles(int savedAt) async {
     299             :     return;
     300             :   }
     301             : 
     302           1 :   @override
     303           2 :   Future<void> forgetRoom(String roomId) => transaction(() async {
     304           4 :         await _timelineFragmentsBox.delete(TupleKey(roomId, '').toString());
     305           2 :         final eventsBoxKeys = await _eventsBox.getAllKeys();
     306           1 :         for (final key in eventsBoxKeys) {
     307           0 :           final multiKey = TupleKey.fromString(key);
     308           0 :           if (multiKey.parts.first != roomId) continue;
     309           0 :           await _eventsBox.delete(key);
     310             :         }
     311           2 :         final roomStateBoxKeys = await _roomStateBox.getAllKeys();
     312           1 :         for (final key in roomStateBoxKeys) {
     313           0 :           final multiKey = TupleKey.fromString(key);
     314           0 :           if (multiKey.parts.first != roomId) continue;
     315           0 :           await _roomStateBox.delete(key);
     316             :         }
     317           2 :         final roomMembersBoxKeys = await _roomMembersBox.getAllKeys();
     318           1 :         for (final key in roomMembersBoxKeys) {
     319           0 :           final multiKey = TupleKey.fromString(key);
     320           0 :           if (multiKey.parts.first != roomId) continue;
     321           0 :           await _roomMembersBox.delete(key);
     322             :         }
     323           2 :         final roomAccountDataBoxKeys = await _roomAccountDataBox.getAllKeys();
     324           1 :         for (final key in roomAccountDataBoxKeys) {
     325           0 :           final multiKey = TupleKey.fromString(key);
     326           0 :           if (multiKey.parts.first != roomId) continue;
     327           0 :           await _roomAccountDataBox.delete(key);
     328             :         }
     329           2 :         await _roomsBox.delete(roomId);
     330             :       });
     331             : 
     332           1 :   @override
     333             :   Future<Map<String, BasicEvent>> getAccountData() =>
     334           1 :       runBenchmarked<Map<String, BasicEvent>>('Get all account data from store',
     335           1 :           () async {
     336           1 :         final accountData = <String, BasicEvent>{};
     337           2 :         final raws = await _accountDataBox.getAllValues();
     338           2 :         for (final entry in raws.entries) {
     339           3 :           accountData[entry.key] = BasicEvent(
     340           1 :             type: entry.key,
     341           2 :             content: copyMap(entry.value),
     342             :           );
     343             :         }
     344             :         return accountData;
     345             :       });
     346             : 
     347           1 :   @override
     348             :   Future<Map<String, dynamic>?> getClient(String name) =>
     349           2 :       runBenchmarked('Get Client from store', () async {
     350           1 :         final map = <String, dynamic>{};
     351           2 :         final keys = await _clientBox.getAllKeys();
     352           2 :         for (final key in keys) {
     353           1 :           if (key == 'version') continue;
     354           2 :           final value = await _clientBox.get(key);
     355           1 :           if (value != null) map[key] = value;
     356             :         }
     357           1 :         if (map.isEmpty) return null;
     358             :         return map;
     359             :       });
     360             : 
     361           1 :   @override
     362             :   Future<Event?> getEventById(String eventId, Room room) async {
     363           5 :     final raw = await _eventsBox.get(TupleKey(room.id, eventId).toString());
     364             :     if (raw == null) return null;
     365           2 :     return Event.fromJson(copyMap(raw), room);
     366             :   }
     367             : 
     368             :   /// Loads a whole list of events at once from the store for a specific room
     369           1 :   Future<List<Event>> _getEventsByIds(List<String> eventIds, Room room) async {
     370             :     final keys = eventIds
     371           1 :         .map(
     372           4 :           (eventId) => TupleKey(room.id, eventId).toString(),
     373             :         )
     374           1 :         .toList();
     375           2 :     final rawEvents = await _eventsBox.getAll(keys);
     376             :     return rawEvents
     377           2 :         .map((rawEvent) =>
     378           2 :             rawEvent != null ? Event.fromJson(copyMap(rawEvent), room) : null)
     379           1 :         .whereNotNull()
     380           1 :         .toList();
     381             :   }
     382             : 
     383           1 :   @override
     384             :   Future<List<Event>> getEventList(
     385             :     Room room, {
     386             :     int start = 0,
     387             :     bool onlySending = false,
     388             :     int? limit,
     389             :   }) =>
     390           2 :       runBenchmarked<List<Event>>('Get event list', () async {
     391             :         // Get the synced event IDs from the store
     392           3 :         final timelineKey = TupleKey(room.id, '').toString();
     393           1 :         final timelineEventIds = List<String>.from(
     394           2 :             (await _timelineFragmentsBox.get(timelineKey)) ?? []);
     395             : 
     396             :         // Get the local stored SENDING events from the store
     397             :         late final List<String> sendingEventIds;
     398           1 :         if (start != 0) {
     399           0 :           sendingEventIds = [];
     400             :         } else {
     401           3 :           final sendingTimelineKey = TupleKey(room.id, 'SENDING').toString();
     402           1 :           sendingEventIds = List<String>.from(
     403           3 :               (await _timelineFragmentsBox.get(sendingTimelineKey)) ?? []);
     404             :         }
     405             : 
     406             :         // Combine those two lists while respecting the start and limit parameters.
     407           1 :         final end = min(timelineEventIds.length,
     408           2 :             start + (limit ?? timelineEventIds.length));
     409           2 :         final eventIds = List<String>.from([
     410             :           ...sendingEventIds,
     411           2 :           ...(start < timelineEventIds.length && !onlySending
     412           3 :               ? timelineEventIds.getRange(start, end).toList()
     413           0 :               : [])
     414             :         ]);
     415             : 
     416           1 :         return await _getEventsByIds(eventIds, room);
     417             :       });
     418             : 
     419           0 :   @override
     420             :   Future<List<String>> getEventIdList(
     421             :     Room room, {
     422             :     int start = 0,
     423             :     bool includeSending = false,
     424             :     int? limit,
     425             :   }) =>
     426           0 :       runBenchmarked<List<String>>('Get event id list', () async {
     427             :         // Get the synced event IDs from the store
     428           0 :         final timelineKey = TupleKey(room.id, '').toString();
     429           0 :         final timelineEventIds = List<String>.from(
     430           0 :             (await _timelineFragmentsBox.get(timelineKey)) ?? []);
     431             : 
     432             :         // Get the local stored SENDING events from the store
     433             :         late final List<String> sendingEventIds;
     434             :         if (!includeSending) {
     435           0 :           sendingEventIds = [];
     436             :         } else {
     437           0 :           final sendingTimelineKey = TupleKey(room.id, 'SENDING').toString();
     438           0 :           sendingEventIds = List<String>.from(
     439           0 :               (await _timelineFragmentsBox.get(sendingTimelineKey)) ?? []);
     440             :         }
     441             : 
     442             :         // Combine those two lists while respecting the start and limit parameters.
     443           0 :         final eventIds = sendingEventIds + timelineEventIds;
     444           0 :         if (limit != null && eventIds.length > limit) {
     445           0 :           eventIds.removeRange(limit, eventIds.length);
     446             :         }
     447             : 
     448             :         return eventIds;
     449             :       });
     450             : 
     451           1 :   @override
     452             :   Future<Uint8List?> getFile(Uri mxcUri) async {
     453             :     return null;
     454             :   }
     455             : 
     456           1 :   @override
     457             :   Future<StoredInboundGroupSession?> getInboundGroupSession(
     458             :     String roomId,
     459             :     String sessionId,
     460             :   ) async {
     461           2 :     final raw = await _inboundGroupSessionsBox.get(sessionId);
     462             :     if (raw == null) return null;
     463           2 :     return StoredInboundGroupSession.fromJson(copyMap(raw));
     464             :   }
     465             : 
     466           1 :   @override
     467             :   Future<List<StoredInboundGroupSession>>
     468             :       getInboundGroupSessionsToUpload() async {
     469           2 :     final sessions = (await _inboundGroupSessionsBox.getAllValues())
     470           1 :         .values
     471           1 :         .where((rawSession) => rawSession['uploaded'] == false)
     472           1 :         .take(50)
     473           1 :         .map(
     474           0 :           (json) => StoredInboundGroupSession.fromJson(
     475           0 :             copyMap(json),
     476             :           ),
     477             :         )
     478           1 :         .toList();
     479             :     return sessions;
     480             :   }
     481             : 
     482           1 :   @override
     483             :   Future<List<String>> getLastSentMessageUserDeviceKey(
     484             :       String userId, String deviceId) async {
     485             :     final raw =
     486           4 :         await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString());
     487           1 :     if (raw == null) return <String>[];
     488           0 :     return <String>[raw['last_sent_message']];
     489             :   }
     490             : 
     491           1 :   @override
     492             :   Future<void> storeOlmSession(String identityKey, String sessionId,
     493             :       String pickle, int lastReceived) async {
     494           3 :     final rawSessions = (await _olmSessionsBox.get(identityKey)) ?? {};
     495           2 :     rawSessions[sessionId] = <String, dynamic>{
     496             :       'identity_key': identityKey,
     497             :       'pickle': pickle,
     498             :       'session_id': sessionId,
     499             :       'last_received': lastReceived,
     500             :     };
     501           2 :     await _olmSessionsBox.put(identityKey, rawSessions);
     502             :     return;
     503             :   }
     504             : 
     505           1 :   @override
     506             :   Future<List<OlmSession>> getOlmSessions(
     507             :       String identityKey, String userId) async {
     508           2 :     final rawSessions = await _olmSessionsBox.get(identityKey);
     509           2 :     if (rawSessions == null || rawSessions.isEmpty) return <OlmSession>[];
     510           1 :     return rawSessions.values
     511           4 :         .map((json) => OlmSession.fromJson(copyMap(json), userId))
     512           1 :         .toList();
     513             :   }
     514             : 
     515           1 :   @override
     516             :   Future<Map<String, Map>> getAllOlmSessions() =>
     517           2 :       _olmSessionsBox.getAllValues();
     518             : 
     519           1 :   @override
     520             :   Future<List<OlmSession>> getOlmSessionsForDevices(
     521             :       List<String> identityKeys, String userId) async {
     522           1 :     final sessions = await Future.wait(
     523           3 :         identityKeys.map((identityKey) => getOlmSessions(identityKey, userId)));
     524           3 :     return <OlmSession>[for (final sublist in sessions) ...sublist];
     525             :   }
     526             : 
     527           1 :   @override
     528             :   Future<OutboundGroupSession?> getOutboundGroupSession(
     529             :       String roomId, String userId) async {
     530           2 :     final raw = await _outboundGroupSessionsBox.get(roomId);
     531             :     if (raw == null) return null;
     532           2 :     return OutboundGroupSession.fromJson(copyMap(raw), userId);
     533             :   }
     534             : 
     535           1 :   @override
     536             :   Future<Room?> getSingleRoom(Client client, String roomId,
     537             :       {bool loadImportantStates = true}) async {
     538             :     // Get raw room from database:
     539           2 :     final roomData = await _roomsBox.get(roomId);
     540             :     if (roomData == null) return null;
     541           2 :     final room = Room.fromJson(copyMap(roomData), client);
     542             : 
     543             :     // Get important states:
     544             :     if (loadImportantStates) {
     545           1 :       final dbKeys = client.importantStateEvents
     546           4 :           .map((state) => TupleKey(roomId, state).toString())
     547           1 :           .toList();
     548           2 :       final rawStates = await _roomStateBox.getAll(dbKeys);
     549           2 :       for (final rawState in rawStates) {
     550           1 :         if (rawState == null || rawState[''] == null) continue;
     551           4 :         room.setState(Event.fromJson(copyMap(rawState['']), room));
     552             :       }
     553             :     }
     554             : 
     555             :     return room;
     556             :   }
     557             : 
     558           1 :   @override
     559             :   Future<List<Room>> getRoomList(Client client) =>
     560           2 :       runBenchmarked<List<Room>>('Get room list from store', () async {
     561           1 :         final rooms = <String, Room>{};
     562           1 :         final userID = client.userID;
     563             : 
     564           2 :         final rawRooms = await _roomsBox.getAllValues();
     565             : 
     566           1 :         final getRoomStateRequests = <String, Future<List>>{};
     567           1 :         final getRoomMembersRequests = <String, Future<List>>{};
     568             : 
     569           2 :         for (final raw in rawRooms.values) {
     570             :           // Get the room
     571           2 :           final room = Room.fromJson(copyMap(raw), client);
     572             :           // Get the "important" room states. All other states will be loaded once
     573             :           // `getUnimportantRoomStates()` is called.
     574           1 :           final dbKeys = client.importantStateEvents
     575           5 :               .map((state) => TupleKey(room.id, state).toString())
     576           1 :               .toList();
     577           4 :           getRoomStateRequests[room.id] = _roomStateBox.getAll(
     578             :             dbKeys,
     579             :           );
     580             : 
     581             :           // Add to the list and continue.
     582           2 :           rooms[room.id] = room;
     583             :         }
     584             : 
     585           2 :         for (final room in rooms.values) {
     586             :           // Add states to the room
     587           2 :           final statesList = await getRoomStateRequests[room.id];
     588             :           if (statesList != null) {
     589           2 :             for (final states in statesList) {
     590             :               if (states == null) continue;
     591           0 :               final stateEvents = states.values
     592           0 :                   .map((raw) => room.membership == Membership.invite
     593           0 :                       ? StrippedStateEvent.fromJson(copyMap(raw))
     594           0 :                       : Event.fromJson(copyMap(raw), room))
     595           0 :                   .toList();
     596           0 :               for (final state in stateEvents) {
     597           0 :                 room.setState(state);
     598             :               }
     599             :             }
     600             : 
     601             :             // now that we have the state we can continue
     602           0 :             final membersToPostload = <String>{if (userID != null) userID};
     603             :             // If the room is a direct chat, those IDs should be there too
     604           1 :             if (room.isDirectChat) {
     605           0 :               membersToPostload.add(room.directChatMatrixID!);
     606             :             }
     607             : 
     608             :             // the lastEvent message preview might have an author we need to fetch, if it is a group chat
     609           1 :             if (room.lastEvent != null && !room.isDirectChat) {
     610           0 :               membersToPostload.add(room.lastEvent!.senderId);
     611             :             }
     612             : 
     613             :             // if the room has no name and no canonical alias, its name is calculated
     614             :             // based on the heroes of the room
     615           1 :             if (room.getState(EventTypes.RoomName) == null &&
     616           1 :                 room.getState(EventTypes.RoomCanonicalAlias) == null) {
     617             :               // we don't have a name and no canonical alias, so we'll need to
     618             :               // post-load the heroes
     619           2 :               final heroes = room.summary.mHeroes;
     620             :               if (heroes != null) {
     621           1 :                 membersToPostload.addAll(heroes);
     622             :               }
     623             :             }
     624             :             // Load members
     625             :             final membersDbKeys = membersToPostload
     626           1 :                 .map((member) => TupleKey(room.id, member).toString())
     627           1 :                 .toList();
     628           4 :             getRoomMembersRequests[room.id] = _roomMembersBox.getAll(
     629             :               membersDbKeys,
     630             :             );
     631             :           }
     632             :         }
     633             : 
     634           2 :         for (final room in rooms.values) {
     635             :           // Add members to the room
     636           2 :           final members = await getRoomMembersRequests[room.id];
     637             :           if (members != null) {
     638           1 :             for (final member in members) {
     639             :               if (member == null) continue;
     640           0 :               room.setState(room.membership == Membership.invite
     641           0 :                   ? StrippedStateEvent.fromJson(copyMap(member))
     642           0 :                   : Event.fromJson(copyMap(member), room));
     643             :             }
     644             :           }
     645             :         }
     646             : 
     647             :         // Get the room account data
     648           2 :         final roomAccountDataRaws = await _roomAccountDataBox.getAllValues();
     649           1 :         for (final entry in roomAccountDataRaws.entries) {
     650           0 :           final keys = TupleKey.fromString(entry.key);
     651           0 :           final basicRoomEvent = BasicRoomEvent.fromJson(
     652           0 :             copyMap(entry.value),
     653             :           );
     654           0 :           final roomId = keys.parts.first;
     655           0 :           if (rooms.containsKey(roomId)) {
     656           0 :             rooms[roomId]!.roomAccountData[basicRoomEvent.type] =
     657             :                 basicRoomEvent;
     658             :           } else {
     659           0 :             Logs().w(
     660           0 :                 'Found account data for unknown room $roomId. Delete now...');
     661           0 :             await _roomAccountDataBox
     662           0 :                 .delete(TupleKey(roomId, basicRoomEvent.type).toString());
     663             :           }
     664             :         }
     665             : 
     666           2 :         return rooms.values.toList();
     667             :       });
     668             : 
     669           1 :   @override
     670             :   Future<SSSSCache?> getSSSSCache(String type) async {
     671           2 :     final raw = await _ssssCacheBox.get(type);
     672             :     if (raw == null) return null;
     673           2 :     return SSSSCache.fromJson(copyMap(raw));
     674             :   }
     675             : 
     676           1 :   @override
     677             :   Future<List<QueuedToDeviceEvent>> getToDeviceEventQueue() async {
     678           2 :     final raws = await _toDeviceQueueBox.getAllValues();
     679           3 :     final copiedRaws = raws.entries.map((entry) {
     680           2 :       final copiedRaw = copyMap(entry.value);
     681           3 :       copiedRaw['id'] = int.parse(entry.key);
     682           3 :       copiedRaw['content'] = jsonDecode(copiedRaw['content'] as String);
     683             :       return copiedRaw;
     684           1 :     }).toList();
     685           4 :     return copiedRaws.map((raw) => QueuedToDeviceEvent.fromJson(raw)).toList();
     686             :   }
     687             : 
     688           1 :   @override
     689             :   Future<List<Event>> getUnimportantRoomEventStatesForRoom(
     690             :       List<String> events, Room room) async {
     691           4 :     final keys = (await _roomStateBox.getAllKeys()).where((key) {
     692           1 :       final tuple = TupleKey.fromString(key);
     693           4 :       return tuple.parts.first == room.id && !events.contains(tuple.parts[1]);
     694             :     });
     695             : 
     696           1 :     final unimportantEvents = <Event>[];
     697           1 :     for (final key in keys) {
     698           0 :       final states = await _roomStateBox.get(key);
     699             :       if (states == null) continue;
     700           0 :       unimportantEvents.addAll(
     701           0 :           states.values.map((raw) => Event.fromJson(copyMap(raw), room)));
     702             :     }
     703           2 :     return unimportantEvents.where((event) => event.stateKey != null).toList();
     704             :   }
     705             : 
     706           1 :   @override
     707             :   Future<User?> getUser(String userId, Room room) async {
     708             :     final state =
     709           5 :         await _roomMembersBox.get(TupleKey(room.id, userId).toString());
     710             :     if (state == null) return null;
     711           0 :     return Event.fromJson(copyMap(state), room).asUser;
     712             :   }
     713             : 
     714           1 :   @override
     715             :   Future<Map<String, DeviceKeysList>> getUserDeviceKeys(Client client) =>
     716           1 :       runBenchmarked<Map<String, DeviceKeysList>>(
     717           1 :           'Get all user device keys from store', () async {
     718             :         final deviceKeysOutdated =
     719           2 :             await _userDeviceKeysOutdatedBox.getAllKeys();
     720           1 :         if (deviceKeysOutdated.isEmpty) {
     721           1 :           return {};
     722             :         }
     723           0 :         final res = <String, DeviceKeysList>{};
     724           0 :         final userDeviceKeysBoxKeys = await _userDeviceKeysBox.getAllKeys();
     725             :         final userCrossSigningKeysBoxKeys =
     726           0 :             await _userCrossSigningKeysBox.getAllKeys();
     727           0 :         for (final userId in deviceKeysOutdated) {
     728           0 :           final deviceKeysBoxKeys = userDeviceKeysBoxKeys.where((tuple) {
     729           0 :             final tupleKey = TupleKey.fromString(tuple);
     730           0 :             return tupleKey.parts.first == userId;
     731             :           });
     732             :           final crossSigningKeysBoxKeys =
     733           0 :               userCrossSigningKeysBoxKeys.where((tuple) {
     734           0 :             final tupleKey = TupleKey.fromString(tuple);
     735           0 :             return tupleKey.parts.first == userId;
     736             :           });
     737           0 :           final childEntries = await Future.wait(
     738           0 :             deviceKeysBoxKeys.map(
     739           0 :               (key) async {
     740           0 :                 final userDeviceKey = await _userDeviceKeysBox.get(key);
     741             :                 if (userDeviceKey == null) return null;
     742           0 :                 return copyMap(userDeviceKey);
     743             :               },
     744             :             ),
     745             :           );
     746           0 :           final crossSigningEntries = await Future.wait(
     747           0 :             crossSigningKeysBoxKeys.map(
     748           0 :               (key) async {
     749           0 :                 final crossSigningKey = await _userCrossSigningKeysBox.get(key);
     750             :                 if (crossSigningKey == null) return null;
     751           0 :                 return copyMap(crossSigningKey);
     752             :               },
     753             :             ),
     754             :           );
     755           0 :           res[userId] = DeviceKeysList.fromDbJson(
     756           0 :               {
     757           0 :                 'client_id': client.id,
     758             :                 'user_id': userId,
     759           0 :                 'outdated': await _userDeviceKeysOutdatedBox.get(userId),
     760             :               },
     761             :               childEntries
     762           0 :                   .where((c) => c != null)
     763           0 :                   .toList()
     764           0 :                   .cast<Map<String, dynamic>>(),
     765             :               crossSigningEntries
     766           0 :                   .where((c) => c != null)
     767           0 :                   .toList()
     768           0 :                   .cast<Map<String, dynamic>>(),
     769             :               client);
     770             :         }
     771             :         return res;
     772             :       });
     773             : 
     774           1 :   @override
     775             :   Future<List<User>> getUsers(Room room) async {
     776           1 :     final users = <User>[];
     777           2 :     final keys = (await _roomMembersBox.getAllKeys())
     778           1 :         .where((key) => TupleKey.fromString(key).parts.first == room.id)
     779           1 :         .toList();
     780           2 :     final states = await _roomMembersBox.getAll(keys);
     781           1 :     states.removeWhere((state) => state == null);
     782           1 :     for (final state in states) {
     783           0 :       users.add(Event.fromJson(copyMap(state!), room).asUser);
     784             :     }
     785             : 
     786             :     return users;
     787             :   }
     788             : 
     789           1 :   @override
     790             :   Future<int> insertClient(
     791             :       String name,
     792             :       String homeserverUrl,
     793             :       String token,
     794             :       DateTime? tokenExpiresAt,
     795             :       String? refreshToken,
     796             :       String userId,
     797             :       String? deviceId,
     798             :       String? deviceName,
     799             :       String? prevBatch,
     800             :       String? olmAccount) async {
     801           2 :     await transaction(() async {
     802           2 :       await _clientBox.put('homeserver_url', homeserverUrl);
     803           2 :       await _clientBox.put('token', token);
     804           2 :       await _clientBox.put('user_id', userId);
     805             :       if (refreshToken == null) {
     806           0 :         await _clientBox.delete('refresh_token');
     807             :       } else {
     808           2 :         await _clientBox.put('refresh_token', refreshToken);
     809             :       }
     810             :       if (tokenExpiresAt == null) {
     811           0 :         await _clientBox.delete('token_expires_at');
     812             :       } else {
     813           2 :         await _clientBox.put(
     814             :           'token_expires_at',
     815           2 :           tokenExpiresAt.millisecondsSinceEpoch.toString(),
     816             :         );
     817             :       }
     818             :       if (deviceId == null) {
     819           0 :         await _clientBox.delete('device_id');
     820             :       } else {
     821           2 :         await _clientBox.put('device_id', deviceId);
     822             :       }
     823             :       if (deviceName == null) {
     824           0 :         await _clientBox.delete('device_name');
     825             :       } else {
     826           2 :         await _clientBox.put('device_name', deviceName);
     827             :       }
     828             :       if (prevBatch == null) {
     829           0 :         await _clientBox.delete('prev_batch');
     830             :       } else {
     831           2 :         await _clientBox.put('prev_batch', prevBatch);
     832             :       }
     833             :       if (olmAccount == null) {
     834           0 :         await _clientBox.delete('olm_account');
     835             :       } else {
     836           2 :         await _clientBox.put('olm_account', olmAccount);
     837             :       }
     838           2 :       await _clientBox.delete('sync_filter_id');
     839             :     });
     840             :     return 0;
     841             :   }
     842             : 
     843           1 :   @override
     844             :   Future<int> insertIntoToDeviceQueue(
     845             :       String type, String txnId, String content) async {
     846           2 :     final id = DateTime.now().millisecondsSinceEpoch;
     847           4 :     await _toDeviceQueueBox.put(id.toString(), {
     848             :       'type': type,
     849             :       'txn_id': txnId,
     850             :       'content': content,
     851             :     });
     852             :     return id;
     853             :   }
     854             : 
     855           1 :   @override
     856             :   Future<void> markInboundGroupSessionAsUploaded(
     857             :       String roomId, String sessionId) async {
     858           2 :     final raw = await _inboundGroupSessionsBox.get(sessionId);
     859             :     if (raw == null) {
     860           0 :       Logs().w(
     861             :           'Tried to mark inbound group session as uploaded which was not found in the database!');
     862             :       return;
     863             :     }
     864           1 :     raw['uploaded'] = true;
     865           2 :     await _inboundGroupSessionsBox.put(sessionId, raw);
     866             :     return;
     867             :   }
     868             : 
     869           1 :   @override
     870             :   Future<void> markInboundGroupSessionsAsNeedingUpload() async {
     871           2 :     final keys = await _inboundGroupSessionsBox.getAllKeys();
     872           2 :     for (final sessionId in keys) {
     873           2 :       final raw = await _inboundGroupSessionsBox.get(sessionId);
     874             :       if (raw == null) continue;
     875           1 :       raw['uploaded'] = false;
     876           2 :       await _inboundGroupSessionsBox.put(sessionId, raw);
     877             :     }
     878             :     return;
     879             :   }
     880             : 
     881           1 :   @override
     882             :   Future<void> removeEvent(String eventId, String roomId) async {
     883           4 :     await _eventsBox.delete(TupleKey(roomId, eventId).toString());
     884           2 :     final keys = await _timelineFragmentsBox.getAllKeys();
     885           2 :     for (final key in keys) {
     886           1 :       final multiKey = TupleKey.fromString(key);
     887           3 :       if (multiKey.parts.first != roomId) continue;
     888           2 :       final eventIds = await _timelineFragmentsBox.get(key) ?? [];
     889           1 :       final prevLength = eventIds.length;
     890           3 :       eventIds.removeWhere((id) => id == eventId);
     891           2 :       if (eventIds.length < prevLength) {
     892           2 :         await _timelineFragmentsBox.put(key, eventIds);
     893             :       }
     894             :     }
     895             :     return;
     896             :   }
     897             : 
     898           0 :   @override
     899             :   Future<void> removeOutboundGroupSession(String roomId) async {
     900           0 :     await _outboundGroupSessionsBox.delete(roomId);
     901             :     return;
     902             :   }
     903             : 
     904           1 :   @override
     905             :   Future<void> removeUserCrossSigningKey(
     906             :       String userId, String publicKey) async {
     907           1 :     await _userCrossSigningKeysBox
     908           3 :         .delete(TupleKey(userId, publicKey).toString());
     909             :     return;
     910             :   }
     911             : 
     912           0 :   @override
     913             :   Future<void> removeUserDeviceKey(String userId, String deviceId) async {
     914           0 :     await _userDeviceKeysBox.delete(TupleKey(userId, deviceId).toString());
     915             :     return;
     916             :   }
     917             : 
     918           1 :   @override
     919             :   Future<void> setBlockedUserCrossSigningKey(
     920             :       bool blocked, String userId, String publicKey) async {
     921           1 :     final raw = await _userCrossSigningKeysBox
     922           3 :         .get(TupleKey(userId, publicKey).toString());
     923           1 :     raw!['blocked'] = blocked;
     924           2 :     await _userCrossSigningKeysBox.put(
     925           2 :       TupleKey(userId, publicKey).toString(),
     926             :       raw,
     927             :     );
     928             :     return;
     929             :   }
     930             : 
     931           1 :   @override
     932             :   Future<void> setBlockedUserDeviceKey(
     933             :       bool blocked, String userId, String deviceId) async {
     934             :     final raw =
     935           4 :         await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString());
     936           1 :     raw!['blocked'] = blocked;
     937           2 :     await _userDeviceKeysBox.put(
     938           2 :       TupleKey(userId, deviceId).toString(),
     939             :       raw,
     940             :     );
     941             :     return;
     942             :   }
     943             : 
     944           0 :   @override
     945             :   Future<void> setLastActiveUserDeviceKey(
     946             :       int lastActive, String userId, String deviceId) async {
     947             :     final raw =
     948           0 :         await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString());
     949           0 :     raw!['last_active'] = lastActive;
     950           0 :     await _userDeviceKeysBox.put(
     951           0 :       TupleKey(userId, deviceId).toString(),
     952             :       raw,
     953             :     );
     954             :   }
     955             : 
     956           0 :   @override
     957             :   Future<void> setLastSentMessageUserDeviceKey(
     958             :       String lastSentMessage, String userId, String deviceId) async {
     959             :     final raw =
     960           0 :         await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString());
     961           0 :     raw!['last_sent_message'] = lastSentMessage;
     962           0 :     await _userDeviceKeysBox.put(
     963           0 :       TupleKey(userId, deviceId).toString(),
     964             :       raw,
     965             :     );
     966             :   }
     967             : 
     968           1 :   @override
     969             :   Future<void> setRoomPrevBatch(
     970             :       String? prevBatch, String roomId, Client client) async {
     971           2 :     final raw = await _roomsBox.get(roomId);
     972             :     if (raw == null) return;
     973           2 :     final room = Room.fromJson(copyMap(raw), client);
     974           1 :     room.prev_batch = prevBatch;
     975           3 :     await _roomsBox.put(roomId, room.toJson());
     976             :     return;
     977             :   }
     978             : 
     979           1 :   @override
     980             :   Future<void> setVerifiedUserCrossSigningKey(
     981             :       bool verified, String userId, String publicKey) async {
     982           1 :     final raw = (await _userCrossSigningKeysBox
     983           3 :             .get(TupleKey(userId, publicKey).toString())) ??
     984           0 :         {};
     985           1 :     raw['verified'] = verified;
     986           2 :     await _userCrossSigningKeysBox.put(
     987           2 :       TupleKey(userId, publicKey).toString(),
     988             :       raw,
     989             :     );
     990             :     return;
     991             :   }
     992             : 
     993           1 :   @override
     994             :   Future<void> setVerifiedUserDeviceKey(
     995             :       bool verified, String userId, String deviceId) async {
     996             :     final raw =
     997           4 :         await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString());
     998           1 :     raw!['verified'] = verified;
     999           2 :     await _userDeviceKeysBox.put(
    1000           2 :       TupleKey(userId, deviceId).toString(),
    1001             :       raw,
    1002             :     );
    1003             :     return;
    1004             :   }
    1005             : 
    1006           1 :   @override
    1007             :   Future<void> storeAccountData(String type, String content) async {
    1008           4 :     await _accountDataBox.put(type, copyMap(jsonDecode(content)));
    1009             :     return;
    1010             :   }
    1011             : 
    1012           1 :   @override
    1013             :   Future<void> storeEventUpdate(EventUpdate eventUpdate, Client client) async {
    1014             :     // Ephemerals should not be stored
    1015           2 :     if (eventUpdate.type == EventUpdateType.ephemeral) return;
    1016             : 
    1017           2 :     final tmpRoom = client.getRoomById(eventUpdate.roomID) ??
    1018           2 :         Room(id: eventUpdate.roomID, client: client);
    1019             : 
    1020             :     // In case of this is a redaction event
    1021           3 :     if (eventUpdate.content['type'] == EventTypes.Redaction) {
    1022           0 :       final eventId = eventUpdate.content.tryGet<String>('redacts');
    1023             :       final event =
    1024           0 :           eventId != null ? await getEventById(eventId, tmpRoom) : null;
    1025             :       if (event != null) {
    1026           0 :         event.setRedactionEvent(Event.fromJson(eventUpdate.content, tmpRoom));
    1027           0 :         await _eventsBox.put(
    1028           0 :             TupleKey(eventUpdate.roomID, event.eventId).toString(),
    1029           0 :             event.toJson());
    1030             : 
    1031           0 :         if (tmpRoom.lastEvent?.eventId == event.eventId) {
    1032           0 :           await _roomStateBox.put(
    1033           0 :             TupleKey(eventUpdate.roomID, event.type).toString(),
    1034           0 :             {'': event.toJson()},
    1035             :           );
    1036             :         }
    1037             :       }
    1038             :     }
    1039             : 
    1040             :     // Store a common message event
    1041             :     if ({
    1042           1 :       EventUpdateType.timeline,
    1043           1 :       EventUpdateType.history,
    1044           1 :       EventUpdateType.decryptedTimelineQueue
    1045           2 :     }.contains(eventUpdate.type)) {
    1046           2 :       final eventId = eventUpdate.content['event_id'];
    1047             :       // Is this ID already in the store?
    1048           1 :       final prevEvent = await _eventsBox
    1049           4 :           .get(TupleKey(eventUpdate.roomID, eventId).toString());
    1050             :       final prevStatus = prevEvent == null
    1051             :           ? null
    1052           0 :           : () {
    1053           0 :               final json = copyMap(prevEvent);
    1054           0 :               final statusInt = json.tryGet<int>('status') ??
    1055             :                   json
    1056           0 :                       .tryGetMap<String, dynamic>('unsigned')
    1057           0 :                       ?.tryGet<int>(messageSendingStatusKey);
    1058           0 :               return statusInt == null ? null : eventStatusFromInt(statusInt);
    1059           0 :             }();
    1060             : 
    1061             :       // calculate the status
    1062           1 :       final newStatus = eventStatusFromInt(
    1063           2 :         eventUpdate.content.tryGet<int>('status') ??
    1064           1 :             eventUpdate.content
    1065           1 :                 .tryGetMap<String, dynamic>('unsigned')
    1066           0 :                 ?.tryGet<int>(messageSendingStatusKey) ??
    1067           1 :             EventStatus.synced.intValue,
    1068             :       );
    1069             : 
    1070             :       // Is this the response to a sending event which is already synced? Then
    1071             :       // there is nothing to do here.
    1072           1 :       if (!newStatus.isSynced && prevStatus != null && prevStatus.isSynced) {
    1073             :         return;
    1074             :       }
    1075             : 
    1076           1 :       final status = newStatus.isError || prevStatus == null
    1077             :           ? newStatus
    1078           0 :           : latestEventStatus(
    1079             :               prevStatus,
    1080             :               newStatus,
    1081             :             );
    1082             : 
    1083             :       // Add the status and the sort order to the content so it get stored
    1084           3 :       eventUpdate.content['unsigned'] ??= <String, dynamic>{};
    1085           3 :       eventUpdate.content['unsigned'][messageSendingStatusKey] =
    1086           3 :           eventUpdate.content['status'] = status.intValue;
    1087             : 
    1088             :       // In case this event has sent from this account we have a transaction ID
    1089           1 :       final transactionId = eventUpdate.content
    1090           1 :           .tryGetMap<String, dynamic>('unsigned')
    1091           1 :           ?.tryGet<String>('transaction_id');
    1092           5 :       await _eventsBox.put(TupleKey(eventUpdate.roomID, eventId).toString(),
    1093           1 :           eventUpdate.content);
    1094             : 
    1095             :       // Update timeline fragments
    1096           3 :       final key = TupleKey(eventUpdate.roomID, status.isSent ? '' : 'SENDING')
    1097           1 :           .toString();
    1098             : 
    1099             :       final eventIds =
    1100           4 :           List<String>.from(await _timelineFragmentsBox.get(key) ?? []);
    1101             : 
    1102           1 :       if (!eventIds.contains(eventId)) {
    1103           2 :         if (eventUpdate.type == EventUpdateType.history) {
    1104           1 :           eventIds.add(eventId);
    1105             :         } else {
    1106           1 :           eventIds.insert(0, eventId);
    1107             :         }
    1108           2 :         await _timelineFragmentsBox.put(key, eventIds);
    1109           0 :       } else if (status.isSynced &&
    1110             :           prevStatus != null &&
    1111           0 :           prevStatus.isSent &&
    1112           0 :           eventUpdate.type != EventUpdateType.history) {
    1113             :         // Status changes from 1 -> 2? Make sure event is correctly sorted.
    1114           0 :         eventIds.remove(eventId);
    1115           0 :         eventIds.insert(0, eventId);
    1116             :       }
    1117             : 
    1118             :       // If event comes from server timeline, remove sending events with this ID
    1119           1 :       if (status.isSent) {
    1120           3 :         final key = TupleKey(eventUpdate.roomID, 'SENDING').toString();
    1121             :         final eventIds =
    1122           4 :             List<String>.from(await _timelineFragmentsBox.get(key) ?? []);
    1123           1 :         final i = eventIds.indexWhere((id) => id == eventId);
    1124           2 :         if (i != -1) {
    1125           0 :           await _timelineFragmentsBox.put(key, eventIds..removeAt(i));
    1126             :         }
    1127             :       }
    1128             : 
    1129             :       // Is there a transaction id? Then delete the event with this id.
    1130           2 :       if (!status.isError && !status.isSending && transactionId != null) {
    1131           0 :         await removeEvent(transactionId, eventUpdate.roomID);
    1132             :       }
    1133             :     }
    1134             : 
    1135           2 :     final stateKey = eventUpdate.content['state_key'];
    1136             :     // Store a common state event
    1137             :     if (stateKey != null &&
    1138             :         // Don't store events as state updates when paginating backwards.
    1139           2 :         (eventUpdate.type == EventUpdateType.timeline ||
    1140           2 :             eventUpdate.type == EventUpdateType.state ||
    1141           2 :             eventUpdate.type == EventUpdateType.inviteState)) {
    1142           3 :       if (eventUpdate.content['type'] == EventTypes.RoomMember) {
    1143           0 :         await _roomMembersBox.put(
    1144           0 :             TupleKey(
    1145           0 :               eventUpdate.roomID,
    1146           0 :               eventUpdate.content['state_key'],
    1147           0 :             ).toString(),
    1148           0 :             eventUpdate.content);
    1149             :       } else {
    1150           1 :         final key = TupleKey(
    1151           1 :           eventUpdate.roomID,
    1152           2 :           eventUpdate.content['type'],
    1153           1 :         ).toString();
    1154           4 :         final stateMap = copyMap(await _roomStateBox.get(key) ?? {});
    1155             : 
    1156           2 :         stateMap[stateKey] = eventUpdate.content;
    1157           2 :         await _roomStateBox.put(key, stateMap);
    1158             :       }
    1159             :     }
    1160             : 
    1161             :     // Store a room account data event
    1162           2 :     if (eventUpdate.type == EventUpdateType.accountData) {
    1163           0 :       await _roomAccountDataBox.put(
    1164           0 :         TupleKey(
    1165           0 :           eventUpdate.roomID,
    1166           0 :           eventUpdate.content['type'],
    1167           0 :         ).toString(),
    1168           0 :         eventUpdate.content,
    1169             :       );
    1170             :     }
    1171             :   }
    1172             : 
    1173           1 :   @override
    1174             :   Future<void> storeFile(Uri mxcUri, Uint8List bytes, int time) async {
    1175             :     return;
    1176             :   }
    1177             : 
    1178           1 :   @override
    1179             :   Future<void> storeInboundGroupSession(
    1180             :       String roomId,
    1181             :       String sessionId,
    1182             :       String pickle,
    1183             :       String content,
    1184             :       String indexes,
    1185             :       String allowedAtIndex,
    1186             :       String senderKey,
    1187             :       String senderClaimedKey) async {
    1188           2 :     await _inboundGroupSessionsBox.put(
    1189             :         sessionId,
    1190           1 :         StoredInboundGroupSession(
    1191             :           roomId: roomId,
    1192             :           sessionId: sessionId,
    1193             :           pickle: pickle,
    1194             :           content: content,
    1195             :           indexes: indexes,
    1196             :           allowedAtIndex: allowedAtIndex,
    1197             :           senderKey: senderKey,
    1198             :           senderClaimedKeys: senderClaimedKey,
    1199             :           uploaded: false,
    1200           1 :         ).toJson());
    1201             :     return;
    1202             :   }
    1203             : 
    1204           1 :   @override
    1205             :   Future<void> storeOutboundGroupSession(
    1206             :       String roomId, String pickle, String deviceIds, int creationTime) async {
    1207           3 :     await _outboundGroupSessionsBox.put(roomId, <String, dynamic>{
    1208             :       'room_id': roomId,
    1209             :       'pickle': pickle,
    1210             :       'device_ids': deviceIds,
    1211             :       'creation_time': creationTime,
    1212             :     });
    1213             :     return;
    1214             :   }
    1215             : 
    1216           0 :   @override
    1217             :   Future<void> storePrevBatch(
    1218             :     String prevBatch,
    1219             :   ) async {
    1220           0 :     if ((await _clientBox.getAllKeys()).isEmpty) return;
    1221           0 :     await _clientBox.put('prev_batch', prevBatch);
    1222             :     return;
    1223             :   }
    1224             : 
    1225           1 :   @override
    1226             :   Future<void> storeRoomUpdate(
    1227             :     String roomId,
    1228             :     SyncRoomUpdate roomUpdate,
    1229             :     Event? lastEvent,
    1230             :     Client client,
    1231             :   ) async {
    1232             :     // Leave room if membership is leave
    1233           1 :     if (roomUpdate is LeftRoomUpdate) {
    1234           0 :       await forgetRoom(roomId);
    1235             :       return;
    1236             :     }
    1237           1 :     final membership = roomUpdate is LeftRoomUpdate
    1238             :         ? Membership.leave
    1239           1 :         : roomUpdate is InvitedRoomUpdate
    1240             :             ? Membership.invite
    1241             :             : Membership.join;
    1242             :     // Make sure room exists
    1243           2 :     final currentRawRoom = await _roomsBox.get(roomId);
    1244             :     if (currentRawRoom == null) {
    1245           2 :       await _roomsBox.put(
    1246             :           roomId,
    1247           1 :           roomUpdate is JoinedRoomUpdate
    1248           1 :               ? Room(
    1249             :                   client: client,
    1250             :                   id: roomId,
    1251             :                   membership: membership,
    1252             :                   highlightCount:
    1253           1 :                       roomUpdate.unreadNotifications?.highlightCount?.toInt() ??
    1254             :                           0,
    1255             :                   notificationCount: roomUpdate
    1256           1 :                           .unreadNotifications?.notificationCount
    1257           0 :                           ?.toInt() ??
    1258             :                       0,
    1259           1 :                   prev_batch: roomUpdate.timeline?.prevBatch,
    1260           1 :                   summary: roomUpdate.summary,
    1261             :                   lastEvent: lastEvent,
    1262           1 :                 ).toJson()
    1263           0 :               : Room(
    1264             :                   client: client,
    1265             :                   id: roomId,
    1266             :                   membership: membership,
    1267             :                   lastEvent: lastEvent,
    1268           0 :                 ).toJson());
    1269           0 :     } else if (roomUpdate is JoinedRoomUpdate) {
    1270           0 :       final currentRoom = Room.fromJson(copyMap(currentRawRoom), client);
    1271           0 :       await _roomsBox.put(
    1272             :           roomId,
    1273           0 :           Room(
    1274             :             client: client,
    1275             :             id: roomId,
    1276             :             membership: membership,
    1277             :             highlightCount:
    1278           0 :                 roomUpdate.unreadNotifications?.highlightCount?.toInt() ??
    1279           0 :                     currentRoom.highlightCount,
    1280             :             notificationCount:
    1281           0 :                 roomUpdate.unreadNotifications?.notificationCount?.toInt() ??
    1282           0 :                     currentRoom.notificationCount,
    1283             :             prev_batch:
    1284           0 :                 roomUpdate.timeline?.prevBatch ?? currentRoom.prev_batch,
    1285           0 :             summary: RoomSummary.fromJson(currentRoom.summary.toJson()
    1286           0 :               ..addAll(roomUpdate.summary?.toJson() ?? {})),
    1287             :             lastEvent: lastEvent,
    1288           0 :           ).toJson());
    1289             :     }
    1290             :   }
    1291             : 
    1292           0 :   @override
    1293             :   Future<void> deleteTimelineForRoom(String roomId) =>
    1294           0 :       _timelineFragmentsBox.delete(TupleKey(roomId, '').toString());
    1295             : 
    1296           1 :   @override
    1297             :   Future<void> storeSSSSCache(
    1298             :       String type, String keyId, String ciphertext, String content) async {
    1299           2 :     await _ssssCacheBox.put(
    1300             :         type,
    1301           1 :         SSSSCache(
    1302             :           type: type,
    1303             :           keyId: keyId,
    1304             :           ciphertext: ciphertext,
    1305             :           content: content,
    1306           1 :         ).toJson());
    1307             :   }
    1308             : 
    1309           1 :   @override
    1310             :   Future<void> storeSyncFilterId(
    1311             :     String syncFilterId,
    1312             :   ) async {
    1313           2 :     await _clientBox.put('sync_filter_id', syncFilterId);
    1314             :   }
    1315             : 
    1316           1 :   @override
    1317             :   Future<void> storeUserCrossSigningKey(String userId, String publicKey,
    1318             :       String content, bool verified, bool blocked) async {
    1319           2 :     await _userCrossSigningKeysBox.put(
    1320           2 :       TupleKey(userId, publicKey).toString(),
    1321           1 :       {
    1322             :         'user_id': userId,
    1323             :         'public_key': publicKey,
    1324             :         'content': content,
    1325             :         'verified': verified,
    1326             :         'blocked': blocked,
    1327             :       },
    1328             :     );
    1329             :   }
    1330             : 
    1331           1 :   @override
    1332             :   Future<void> storeUserDeviceKey(String userId, String deviceId,
    1333             :       String content, bool verified, bool blocked, int lastActive) async {
    1334           5 :     await _userDeviceKeysBox.put(TupleKey(userId, deviceId).toString(), {
    1335             :       'user_id': userId,
    1336             :       'device_id': deviceId,
    1337             :       'content': content,
    1338             :       'verified': verified,
    1339             :       'blocked': blocked,
    1340             :       'last_active': lastActive,
    1341             :       'last_sent_message': '',
    1342             :     });
    1343             :     return;
    1344             :   }
    1345             : 
    1346           1 :   @override
    1347             :   Future<void> storeUserDeviceKeysInfo(String userId, bool outdated) async {
    1348           2 :     await _userDeviceKeysOutdatedBox.put(userId, outdated);
    1349             :     return;
    1350             :   }
    1351             : 
    1352           1 :   @override
    1353             :   Future<void> transaction(Future<void> Function() action) =>
    1354           2 :       _collection.transaction(action);
    1355             : 
    1356           1 :   @override
    1357             :   Future<void> updateClient(
    1358             :     String homeserverUrl,
    1359             :     String token,
    1360             :     DateTime? tokenExpiresAt,
    1361             :     String? refreshToken,
    1362             :     String userId,
    1363             :     String? deviceId,
    1364             :     String? deviceName,
    1365             :     String? prevBatch,
    1366             :     String? olmAccount,
    1367             :   ) async {
    1368           2 :     await transaction(() async {
    1369           2 :       await _clientBox.put('homeserver_url', homeserverUrl);
    1370           2 :       await _clientBox.put('token', token);
    1371             :       if (tokenExpiresAt == null) {
    1372           0 :         await _clientBox.delete('token_expires_at');
    1373             :       } else {
    1374           2 :         await _clientBox.put('token_expires_at',
    1375           2 :             tokenExpiresAt.millisecondsSinceEpoch.toString());
    1376             :       }
    1377             :       if (refreshToken == null) {
    1378           0 :         await _clientBox.delete('refresh_token');
    1379             :       } else {
    1380           2 :         await _clientBox.put('refresh_token', refreshToken);
    1381             :       }
    1382           2 :       await _clientBox.put('user_id', userId);
    1383             :       if (deviceId == null) {
    1384           0 :         await _clientBox.delete('device_id');
    1385             :       } else {
    1386           2 :         await _clientBox.put('device_id', deviceId);
    1387             :       }
    1388             :       if (deviceName == null) {
    1389           0 :         await _clientBox.delete('device_name');
    1390             :       } else {
    1391           2 :         await _clientBox.put('device_name', deviceName);
    1392             :       }
    1393             :       if (prevBatch == null) {
    1394           0 :         await _clientBox.delete('prev_batch');
    1395             :       } else {
    1396           2 :         await _clientBox.put('prev_batch', prevBatch);
    1397             :       }
    1398             :       if (olmAccount == null) {
    1399           0 :         await _clientBox.delete('olm_account');
    1400             :       } else {
    1401           2 :         await _clientBox.put('olm_account', olmAccount);
    1402             :       }
    1403             :     });
    1404             :     return;
    1405             :   }
    1406             : 
    1407           1 :   @override
    1408             :   Future<void> updateClientKeys(
    1409             :     String olmAccount,
    1410             :   ) async {
    1411           2 :     await _clientBox.put('olm_account', olmAccount);
    1412             :     return;
    1413             :   }
    1414             : 
    1415           1 :   @override
    1416             :   Future<void> updateInboundGroupSessionAllowedAtIndex(
    1417             :       String allowedAtIndex, String roomId, String sessionId) async {
    1418           2 :     final raw = await _inboundGroupSessionsBox.get(sessionId);
    1419             :     if (raw == null) {
    1420           0 :       Logs().w(
    1421             :           'Tried to update inbound group session as uploaded which wasnt found in the database!');
    1422             :       return;
    1423             :     }
    1424           1 :     raw['allowed_at_index'] = allowedAtIndex;
    1425           2 :     await _inboundGroupSessionsBox.put(sessionId, raw);
    1426             :     return;
    1427             :   }
    1428             : 
    1429           1 :   @override
    1430             :   Future<void> updateInboundGroupSessionIndexes(
    1431             :       String indexes, String roomId, String sessionId) async {
    1432           2 :     final raw = await _inboundGroupSessionsBox.get(sessionId);
    1433             :     if (raw == null) {
    1434           0 :       Logs().w(
    1435             :           'Tried to update inbound group session indexes of a session which was not found in the database!');
    1436             :       return;
    1437             :     }
    1438           1 :     final json = copyMap(raw);
    1439           1 :     json['indexes'] = indexes;
    1440           2 :     await _inboundGroupSessionsBox.put(sessionId, json);
    1441             :     return;
    1442             :   }
    1443             : 
    1444           1 :   @override
    1445             :   Future<List<StoredInboundGroupSession>> getAllInboundGroupSessions() async {
    1446           2 :     final rawSessions = await _inboundGroupSessionsBox.getAllValues();
    1447           1 :     return rawSessions.values
    1448           1 :         .map((raw) => StoredInboundGroupSession.fromJson(copyMap(raw)))
    1449           1 :         .toList();
    1450             :   }
    1451             : 
    1452           0 :   @override
    1453             :   Future<void> addSeenDeviceId(
    1454             :     String userId,
    1455             :     String deviceId,
    1456             :     String publicKeys,
    1457             :   ) =>
    1458           0 :       _seenDeviceIdsBox.put(TupleKey(userId, deviceId).toString(), publicKeys);
    1459             : 
    1460           0 :   @override
    1461             :   Future<void> addSeenPublicKey(
    1462             :     String publicKey,
    1463             :     String deviceId,
    1464             :   ) =>
    1465           0 :       _seenDeviceKeysBox.put(publicKey, deviceId);
    1466             : 
    1467           0 :   @override
    1468             :   Future<String?> deviceIdSeen(userId, deviceId) async {
    1469             :     final raw =
    1470           0 :         await _seenDeviceIdsBox.get(TupleKey(userId, deviceId).toString());
    1471             :     if (raw == null) return null;
    1472             :     return raw;
    1473             :   }
    1474             : 
    1475           0 :   @override
    1476             :   Future<String?> publicKeySeen(String publicKey) async {
    1477           0 :     final raw = await _seenDeviceKeysBox.get(publicKey);
    1478             :     if (raw == null) return null;
    1479             :     return raw;
    1480             :   }
    1481             : 
    1482           1 :   @override
    1483             :   Future<void> storePresence(String userId, CachedPresence presence) =>
    1484           3 :       _presencesBox.put(userId, presence.toJson());
    1485             : 
    1486           1 :   @override
    1487             :   Future<CachedPresence?> getPresence(String userId) async {
    1488           2 :     final rawPresence = await _presencesBox.get(userId);
    1489             :     if (rawPresence == null) return null;
    1490             : 
    1491           2 :     return CachedPresence.fromJson(copyMap(rawPresence));
    1492             :   }
    1493             : 
    1494           0 :   @override
    1495             :   Future<String> exportDump() async {
    1496           0 :     final dataMap = {
    1497           0 :       _clientBoxName: await _clientBox.getAllValues(),
    1498           0 :       _accountDataBoxName: await _accountDataBox.getAllValues(),
    1499           0 :       _roomsBoxName: await _roomsBox.getAllValues(),
    1500           0 :       _roomStateBoxName: await _roomStateBox.getAllValues(),
    1501           0 :       _roomMembersBoxName: await _roomMembersBox.getAllValues(),
    1502           0 :       _toDeviceQueueBoxName: await _toDeviceQueueBox.getAllValues(),
    1503           0 :       _roomAccountDataBoxName: await _roomAccountDataBox.getAllValues(),
    1504           0 :       _inboundGroupSessionsBoxName:
    1505           0 :           await _inboundGroupSessionsBox.getAllValues(),
    1506           0 :       _outboundGroupSessionsBoxName:
    1507           0 :           await _outboundGroupSessionsBox.getAllValues(),
    1508           0 :       _olmSessionsBoxName: await _olmSessionsBox.getAllValues(),
    1509           0 :       _userDeviceKeysBoxName: await _userDeviceKeysBox.getAllValues(),
    1510           0 :       _userDeviceKeysOutdatedBoxName:
    1511           0 :           await _userDeviceKeysOutdatedBox.getAllValues(),
    1512           0 :       _userCrossSigningKeysBoxName:
    1513           0 :           await _userCrossSigningKeysBox.getAllValues(),
    1514           0 :       _ssssCacheBoxName: await _ssssCacheBox.getAllValues(),
    1515           0 :       _presencesBoxName: await _presencesBox.getAllValues(),
    1516           0 :       _timelineFragmentsBoxName: await _timelineFragmentsBox.getAllValues(),
    1517           0 :       _eventsBoxName: await _eventsBox.getAllValues(),
    1518           0 :       _seenDeviceIdsBoxName: await _seenDeviceIdsBox.getAllValues(),
    1519           0 :       _seenDeviceKeysBoxName: await _seenDeviceKeysBox.getAllValues(),
    1520             :     };
    1521           0 :     final json = jsonEncode(dataMap);
    1522           0 :     await clear();
    1523             :     return json;
    1524             :   }
    1525             : 
    1526           0 :   @override
    1527             :   Future<bool> importDump(String export) async {
    1528             :     try {
    1529           0 :       await clear();
    1530           0 :       await open();
    1531           0 :       final json = Map.from(jsonDecode(export)).cast<String, Map>();
    1532           0 :       for (final key in json[_clientBoxName]!.keys) {
    1533           0 :         await _clientBox.put(key, json[_clientBoxName]![key]);
    1534             :       }
    1535           0 :       for (final key in json[_accountDataBoxName]!.keys) {
    1536           0 :         await _accountDataBox.put(key, json[_accountDataBoxName]![key]);
    1537             :       }
    1538           0 :       for (final key in json[_roomsBoxName]!.keys) {
    1539           0 :         await _roomsBox.put(key, json[_roomsBoxName]![key]);
    1540             :       }
    1541           0 :       for (final key in json[_roomStateBoxName]!.keys) {
    1542           0 :         await _roomStateBox.put(key, json[_roomStateBoxName]![key]);
    1543             :       }
    1544           0 :       for (final key in json[_roomMembersBoxName]!.keys) {
    1545           0 :         await _roomMembersBox.put(key, json[_roomMembersBoxName]![key]);
    1546             :       }
    1547           0 :       for (final key in json[_toDeviceQueueBoxName]!.keys) {
    1548           0 :         await _toDeviceQueueBox.put(key, json[_toDeviceQueueBoxName]![key]);
    1549             :       }
    1550           0 :       for (final key in json[_roomAccountDataBoxName]!.keys) {
    1551           0 :         await _roomAccountDataBox.put(key, json[_roomAccountDataBoxName]![key]);
    1552             :       }
    1553           0 :       for (final key in json[_inboundGroupSessionsBoxName]!.keys) {
    1554           0 :         await _inboundGroupSessionsBox.put(
    1555           0 :             key, json[_inboundGroupSessionsBoxName]![key]);
    1556             :       }
    1557           0 :       for (final key in json[_outboundGroupSessionsBoxName]!.keys) {
    1558           0 :         await _outboundGroupSessionsBox.put(
    1559           0 :             key, json[_outboundGroupSessionsBoxName]![key]);
    1560             :       }
    1561           0 :       for (final key in json[_olmSessionsBoxName]!.keys) {
    1562           0 :         await _olmSessionsBox.put(key, json[_olmSessionsBoxName]![key]);
    1563             :       }
    1564           0 :       for (final key in json[_userDeviceKeysBoxName]!.keys) {
    1565           0 :         await _userDeviceKeysBox.put(key, json[_userDeviceKeysBoxName]![key]);
    1566             :       }
    1567           0 :       for (final key in json[_userDeviceKeysOutdatedBoxName]!.keys) {
    1568           0 :         await _userDeviceKeysOutdatedBox.put(
    1569           0 :             key, json[_userDeviceKeysOutdatedBoxName]![key]);
    1570             :       }
    1571           0 :       for (final key in json[_userCrossSigningKeysBoxName]!.keys) {
    1572           0 :         await _userCrossSigningKeysBox.put(
    1573           0 :             key, json[_userCrossSigningKeysBoxName]![key]);
    1574             :       }
    1575           0 :       for (final key in json[_ssssCacheBoxName]!.keys) {
    1576           0 :         await _ssssCacheBox.put(key, json[_ssssCacheBoxName]![key]);
    1577             :       }
    1578           0 :       for (final key in json[_presencesBoxName]!.keys) {
    1579           0 :         await _presencesBox.put(key, json[_presencesBoxName]![key]);
    1580             :       }
    1581           0 :       for (final key in json[_timelineFragmentsBoxName]!.keys) {
    1582           0 :         await _timelineFragmentsBox.put(
    1583           0 :             key, json[_timelineFragmentsBoxName]![key]);
    1584             :       }
    1585           0 :       for (final key in json[_seenDeviceIdsBoxName]!.keys) {
    1586           0 :         await _seenDeviceIdsBox.put(key, json[_seenDeviceIdsBoxName]![key]);
    1587             :       }
    1588           0 :       for (final key in json[_seenDeviceKeysBoxName]!.keys) {
    1589           0 :         await _seenDeviceKeysBox.put(key, json[_seenDeviceKeysBoxName]![key]);
    1590             :       }
    1591             :       return true;
    1592             :     } catch (e, s) {
    1593           0 :       Logs().e('Database import error: ', e, s);
    1594             :       return false;
    1595             :     }
    1596             :   }
    1597             : 
    1598           0 :   @override
    1599           0 :   Future<void> delete() => _collection.deleteFromDisk();
    1600             : }
    1601             : 
    1602             : class TupleKey {
    1603             :   final List<String> parts;
    1604             : 
    1605          32 :   TupleKey(String key1, [String? key2, String? key3])
    1606          32 :       : parts = [
    1607             :           key1,
    1608          32 :           if (key2 != null) key2,
    1609           0 :           if (key3 != null) key3,
    1610             :         ];
    1611             : 
    1612           0 :   const TupleKey.byParts(this.parts);
    1613             : 
    1614          11 :   TupleKey.fromString(String multiKeyString)
    1615          22 :       : parts = multiKeyString.split('|').toList();
    1616             : 
    1617          32 :   @override
    1618          64 :   String toString() => parts.join('|');
    1619             : 
    1620           0 :   @override
    1621           0 :   bool operator ==(other) => parts.toString() == other.toString();
    1622             : 
    1623           0 :   @override
    1624           0 :   int get hashCode => Object.hashAll(parts);
    1625             : }

Generated by: LCOV version 1.14