1 package org
.asamk
.signal
.manager
.helper
;
3 import org
.asamk
.signal
.manager
.api
.GroupIdV1
;
4 import org
.asamk
.signal
.manager
.api
.GroupIdV2
;
5 import org
.asamk
.signal
.manager
.api
.Profile
;
6 import org
.asamk
.signal
.manager
.internal
.SignalDependencies
;
7 import org
.asamk
.signal
.manager
.storage
.SignalAccount
;
8 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientId
;
9 import org
.asamk
.signal
.manager
.syncStorage
.AccountRecordProcessor
;
10 import org
.asamk
.signal
.manager
.syncStorage
.ContactRecordProcessor
;
11 import org
.asamk
.signal
.manager
.syncStorage
.GroupV1RecordProcessor
;
12 import org
.asamk
.signal
.manager
.syncStorage
.GroupV2RecordProcessor
;
13 import org
.asamk
.signal
.manager
.syncStorage
.StorageSyncModels
;
14 import org
.asamk
.signal
.manager
.syncStorage
.StorageSyncValidations
;
15 import org
.asamk
.signal
.manager
.syncStorage
.WriteOperationResult
;
16 import org
.asamk
.signal
.manager
.util
.KeyUtils
;
17 import org
.signal
.core
.util
.SetUtil
;
18 import org
.signal
.libsignal
.protocol
.InvalidKeyException
;
19 import org
.slf4j
.Logger
;
20 import org
.slf4j
.LoggerFactory
;
21 import org
.whispersystems
.signalservice
.api
.storage
.RecordIkm
;
22 import org
.whispersystems
.signalservice
.api
.storage
.SignalStorageManifest
;
23 import org
.whispersystems
.signalservice
.api
.storage
.SignalStorageRecord
;
24 import org
.whispersystems
.signalservice
.api
.storage
.StorageId
;
25 import org
.whispersystems
.signalservice
.api
.storage
.StorageKey
;
26 import org
.whispersystems
.signalservice
.api
.storage
.StorageRecordConvertersKt
;
27 import org
.whispersystems
.signalservice
.api
.storage
.StorageServiceRepository
;
28 import org
.whispersystems
.signalservice
.api
.storage
.StorageServiceRepository
.ManifestIfDifferentVersionResult
;
29 import org
.whispersystems
.signalservice
.api
.storage
.StorageServiceRepository
.WriteStorageRecordsResult
;
30 import org
.whispersystems
.signalservice
.internal
.storage
.protos
.ManifestRecord
;
31 import org
.whispersystems
.signalservice
.internal
.storage
.protos
.StorageRecord
;
33 import java
.io
.IOException
;
34 import java
.sql
.Connection
;
35 import java
.sql
.SQLException
;
36 import java
.util
.ArrayList
;
37 import java
.util
.Base64
;
38 import java
.util
.Collection
;
39 import java
.util
.Collections
;
40 import java
.util
.List
;
42 import java
.util
.stream
.Collectors
;
44 import static org
.asamk
.signal
.manager
.util
.Utils
.handleResponseException
;
46 public class StorageHelper
{
48 private static final Logger logger
= LoggerFactory
.getLogger(StorageHelper
.class);
49 private static final List
<Integer
> KNOWN_TYPES
= List
.of(ManifestRecord
.Identifier
.Type
.CONTACT
.getValue(),
50 ManifestRecord
.Identifier
.Type
.GROUPV1
.getValue(),
51 ManifestRecord
.Identifier
.Type
.GROUPV2
.getValue(),
52 ManifestRecord
.Identifier
.Type
.ACCOUNT
.getValue());
54 private final SignalAccount account
;
55 private final SignalDependencies dependencies
;
56 private final Context context
;
58 public StorageHelper(final Context context
) {
59 this.account
= context
.getAccount();
60 this.dependencies
= context
.getDependencies();
61 this.context
= context
;
64 public void syncDataWithStorage() throws IOException
{
65 var storageKey
= account
.getOrCreateStorageKey();
66 if (storageKey
== null) {
67 if (!account
.isPrimaryDevice()) {
68 logger
.debug("Storage key unknown, requesting from primary device.");
69 context
.getSyncHelper().requestSyncKeys();
74 logger
.trace("Reading manifest from remote storage");
75 final var localManifestVersion
= account
.getStorageManifestVersion();
76 final var localManifest
= account
.getStorageManifest().orElse(SignalStorageManifest
.Companion
.getEMPTY());
77 final var storageServiceRepository
= dependencies
.getStorageServiceRepository();
78 final var result
= storageServiceRepository
.getStorageManifestIfDifferentVersion(storageKey
,
79 localManifestVersion
);
81 var needsForcePush
= false;
82 final var remoteManifest
= switch (result
) {
83 case ManifestIfDifferentVersionResult
.DifferentVersion diff
-> {
84 final var manifest
= diff
.getManifest();
85 storeManifestLocally(manifest
);
88 case ManifestIfDifferentVersionResult
.DecryptionError ignore
-> {
89 logger
.warn("Manifest couldn't be decrypted.");
90 if (account
.isPrimaryDevice()) {
91 needsForcePush
= true;
93 context
.getSyncHelper().requestSyncKeys();
97 case ManifestIfDifferentVersionResult
.SameVersion ignored
-> localManifest
;
98 case ManifestIfDifferentVersionResult
.NetworkError e
-> throw e
.getException();
99 case ManifestIfDifferentVersionResult
.StatusCodeError e
-> throw e
.getException();
100 default -> throw new RuntimeException("Unhandled ManifestIfDifferentVersionResult type");
103 if (remoteManifest
!= null) {
104 logger
.trace("Manifest versions: local {}, remote {}", localManifestVersion
, remoteManifest
.version
);
106 if (remoteManifest
.version
> localManifestVersion
) {
107 logger
.trace("Remote version was newer, reading records.");
108 needsForcePush
= readDataFromStorage(storageKey
, localManifest
, remoteManifest
);
109 } else if (remoteManifest
.version
< localManifest
.version
) {
110 logger
.debug("Remote storage manifest version was older. User might have switched accounts.");
112 logger
.trace("Done reading data from remote storage");
114 readRecordsWithPreviouslyUnknownTypes(storageKey
, remoteManifest
);
117 logger
.trace("Adding missing storageIds to local data");
118 account
.getRecipientStore().setMissingStorageIds();
119 account
.getGroupStore().setMissingStorageIds();
121 var needsMultiDeviceSync
= false;
123 if (account
.needsStorageKeyMigration()) {
124 logger
.debug("Storage needs force push due to new account entropy pool");
125 // Set new aep and reset previous master key and storage key
126 account
.setAccountEntropyPool(account
.getOrCreateAccountEntropyPool());
127 storageKey
= account
.getOrCreateStorageKey();
128 context
.getSyncHelper().sendKeysMessage();
129 needsForcePush
= true;
130 } else if (remoteManifest
== null) {
131 if (account
.isPrimaryDevice()) {
132 needsForcePush
= true;
134 } else if (remoteManifest
.recordIkm
== null && account
.getSelfRecipientProfile()
136 .contains(Profile
.Capability
.storageServiceEncryptionV2Capability
)) {
137 logger
.debug("The SSRE2 capability is supported, but no recordIkm is set! Force pushing.");
138 needsForcePush
= true;
141 needsMultiDeviceSync
= writeToStorage(storageKey
, remoteManifest
, needsForcePush
);
142 } catch (RetryLaterException e
) {
148 if (needsForcePush
) {
149 logger
.debug("Doing a force push.");
151 forcePushToStorage(storageKey
);
152 needsMultiDeviceSync
= true;
153 } catch (RetryLaterException e
) {
159 if (needsMultiDeviceSync
) {
160 context
.getSyncHelper().sendSyncFetchStorageMessage();
163 logger
.debug("Done syncing data with remote storage");
166 public void forcePushToStorage() throws IOException
{
167 if (!account
.isPrimaryDevice()) {
171 final var storageKey
= account
.getOrCreateStorageKey();
172 if (storageKey
== null) {
177 forcePushToStorage(storageKey
);
178 } catch (RetryLaterException e
) {
183 private boolean readDataFromStorage(
184 final StorageKey storageKey
,
185 final SignalStorageManifest localManifest
,
186 final SignalStorageManifest remoteManifest
187 ) throws IOException
{
188 var needsForcePush
= false;
189 try (final var connection
= account
.getAccountDatabase().getConnection()) {
190 connection
.setAutoCommit(false);
192 var idDifference
= findIdDifference(remoteManifest
.storageIds
, localManifest
.storageIds
);
194 if (idDifference
.hasTypeMismatches() && account
.isPrimaryDevice()) {
195 logger
.debug("Found type mismatches in the ID sets! Scheduling a force push after this sync completes.");
196 needsForcePush
= true;
199 logger
.debug("Pre-Merge ID Difference :: {}", idDifference
);
201 if (!idDifference
.isEmpty()) {
202 final var remoteOnlyRecords
= getSignalStorageRecords(storageKey
,
204 idDifference
.remoteOnlyIds());
206 if (remoteOnlyRecords
.size() != idDifference
.remoteOnlyIds().size()) {
208 "Could not find all remote-only records! Requested: {}, Found: {}. These stragglers should naturally get deleted during the sync.",
209 idDifference
.remoteOnlyIds().size(),
210 remoteOnlyRecords
.size());
213 final var unknownInserts
= processKnownRecords(connection
, remoteOnlyRecords
);
214 final var unknownDeletes
= idDifference
.localOnlyIds()
216 .filter(id
-> !KNOWN_TYPES
.contains(id
.getType()))
219 if (!idDifference
.localOnlyIds().isEmpty()) {
220 final var updated
= account
.getRecipientStore()
221 .removeStorageIdsFromLocalOnlyUnregisteredRecipients(connection
,
222 idDifference
.localOnlyIds());
226 "Found {} records that were deleted remotely but only marked unregistered locally. Removed those from local store.",
231 logger
.debug("Storage ids with unknown type: {} inserts, {} deletes",
232 unknownInserts
.size(),
233 unknownDeletes
.size());
235 account
.getUnknownStorageIdStore().addUnknownStorageIds(connection
, unknownInserts
);
236 account
.getUnknownStorageIdStore().deleteUnknownStorageIds(connection
, unknownDeletes
);
238 logger
.debug("Remote version was newer, but there were no remote-only IDs.");
241 } catch (SQLException e
) {
242 throw new RuntimeException("Failed to sync remote storage", e
);
244 return needsForcePush
;
247 private void readRecordsWithPreviouslyUnknownTypes(
248 final StorageKey storageKey
,
249 final SignalStorageManifest remoteManifest
250 ) throws IOException
{
251 try (final var connection
= account
.getAccountDatabase().getConnection()) {
252 connection
.setAutoCommit(false);
253 final var knownUnknownIds
= account
.getUnknownStorageIdStore()
254 .getUnknownStorageIds(connection
, KNOWN_TYPES
);
256 if (!knownUnknownIds
.isEmpty()) {
257 logger
.debug("We have {} unknown records that we can now process.", knownUnknownIds
.size());
259 final var remote
= getSignalStorageRecords(storageKey
, remoteManifest
, knownUnknownIds
);
261 logger
.debug("Found {} of the known-unknowns remotely.", remote
.size());
263 processKnownRecords(connection
, remote
);
264 account
.getUnknownStorageIdStore()
265 .deleteUnknownStorageIds(connection
, remote
.stream().map(SignalStorageRecord
::getId
).toList());
268 } catch (SQLException e
) {
269 throw new RuntimeException("Failed to sync remote storage", e
);
273 private boolean writeToStorage(
274 final StorageKey storageKey
,
275 final SignalStorageManifest remoteManifest
,
276 final boolean needsForcePush
277 ) throws IOException
, RetryLaterException
{
278 final WriteOperationResult remoteWriteOperation
;
279 try (final var connection
= account
.getAccountDatabase().getConnection()) {
280 connection
.setAutoCommit(false);
282 final var localStorageIds
= getAllLocalStorageIds(connection
);
283 final var idDifference
= findIdDifference(remoteManifest
.storageIds
, localStorageIds
);
284 logger
.debug("ID Difference :: {}", idDifference
);
286 final var remoteDeletes
= idDifference
.remoteOnlyIds().stream().map(StorageId
::getRaw
).toList();
287 final var remoteInserts
= buildLocalStorageRecords(connection
, idDifference
.localOnlyIds());
288 // TODO check if local storage record proto matches remote, then reset to remote storage_id
290 remoteWriteOperation
= new WriteOperationResult(new SignalStorageManifest(remoteManifest
.version
+ 1,
291 account
.getDeviceId(),
292 remoteManifest
.recordIkm
,
293 localStorageIds
), remoteInserts
, remoteDeletes
);
296 } catch (SQLException e
) {
297 throw new RuntimeException("Failed to sync remote storage", e
);
300 if (remoteWriteOperation
.isEmpty()) {
301 logger
.debug("No remote writes needed. Still at version: {}", remoteManifest
.version
);
305 logger
.debug("We have something to write remotely.");
306 logger
.debug("WriteOperationResult :: {}", remoteWriteOperation
);
308 StorageSyncValidations
.validate(remoteWriteOperation
,
311 account
.getSelfRecipientAddress());
313 final var result
= dependencies
.getStorageServiceRepository()
314 .writeStorageRecords(storageKey
,
315 remoteWriteOperation
.manifest(),
316 remoteWriteOperation
.inserts(),
317 remoteWriteOperation
.deletes());
319 case WriteStorageRecordsResult
.ConflictError ignored
-> {
320 logger
.debug("Hit a conflict when trying to resolve the conflict! Retrying.");
321 throw new RetryLaterException();
323 case WriteStorageRecordsResult
.NetworkError networkError
-> throw networkError
.getException();
324 case WriteStorageRecordsResult
.StatusCodeError statusCodeError
-> throw statusCodeError
.getException();
325 case WriteStorageRecordsResult
.Success ignored
-> {
326 logger
.debug("Saved new manifest. Now at version: {}", remoteWriteOperation
.manifest().version
);
327 storeManifestLocally(remoteWriteOperation
.manifest());
330 default -> throw new IllegalStateException("Unexpected value: " + result
);
334 private void forcePushToStorage(
335 final StorageKey storageServiceKey
336 ) throws IOException
, RetryLaterException
{
337 logger
.debug("Force pushing local state to remote storage");
339 final var currentVersion
= handleResponseException(dependencies
.getStorageServiceRepository()
340 .getManifestVersion());
341 final var newVersion
= currentVersion
+ 1;
342 final var newStorageRecords
= new ArrayList
<SignalStorageRecord
>();
343 final Map
<RecipientId
, StorageId
> newContactStorageIds
;
344 final Map
<GroupIdV1
, StorageId
> newGroupV1StorageIds
;
345 final Map
<GroupIdV2
, StorageId
> newGroupV2StorageIds
;
347 try (final var connection
= account
.getAccountDatabase().getConnection()) {
348 connection
.setAutoCommit(false);
350 final var recipientIds
= account
.getRecipientStore().getRecipientIds(connection
);
351 newContactStorageIds
= generateContactStorageIds(recipientIds
);
352 for (final var recipientId
: recipientIds
) {
353 final var storageId
= newContactStorageIds
.get(recipientId
);
354 if (storageId
.getType() == ManifestRecord
.Identifier
.Type
.ACCOUNT
.getValue()) {
355 final var recipient
= account
.getRecipientStore().getRecipient(connection
, recipientId
);
356 final var accountRecord
= StorageSyncModels
.localToRemoteRecord(connection
,
357 account
.getConfigurationStore(),
359 account
.getUsernameLink());
360 newStorageRecords
.add(new SignalStorageRecord(storageId
,
361 new StorageRecord
.Builder().account(accountRecord
).build()));
363 final var recipient
= account
.getRecipientStore().getRecipient(connection
, recipientId
);
364 final var address
= recipient
.getAddress().getIdentifier();
365 final var identity
= account
.getIdentityKeyStore().getIdentityInfo(connection
, address
);
366 final var record = StorageSyncModels
.localToRemoteRecord(recipient
, identity
);
367 newStorageRecords
.add(new SignalStorageRecord(storageId
,
368 new StorageRecord
.Builder().contact(record).build()));
372 final var groupV1Ids
= account
.getGroupStore().getGroupV1Ids(connection
);
373 newGroupV1StorageIds
= generateGroupV1StorageIds(groupV1Ids
);
374 for (final var groupId
: groupV1Ids
) {
375 final var storageId
= newGroupV1StorageIds
.get(groupId
);
376 final var group
= account
.getGroupStore().getGroup(connection
, groupId
);
377 final var record = StorageSyncModels
.localToRemoteRecord(group
);
378 newStorageRecords
.add(new SignalStorageRecord(storageId
,
379 new StorageRecord
.Builder().groupV1(record).build()));
382 final var groupV2Ids
= account
.getGroupStore().getGroupV2Ids(connection
);
383 newGroupV2StorageIds
= generateGroupV2StorageIds(groupV2Ids
);
384 for (final var groupId
: groupV2Ids
) {
385 final var storageId
= newGroupV2StorageIds
.get(groupId
);
386 final var group
= account
.getGroupStore().getGroup(connection
, groupId
);
387 final var record = StorageSyncModels
.localToRemoteRecord(group
);
388 newStorageRecords
.add(new SignalStorageRecord(storageId
,
389 new StorageRecord
.Builder().groupV2(record).build()));
393 } catch (SQLException e
) {
394 throw new RuntimeException("Failed to sync remote storage", e
);
396 final var newStorageIds
= newStorageRecords
.stream().map(SignalStorageRecord
::getId
).toList();
398 final RecordIkm recordIkm
;
399 if (account
.getSelfRecipientProfile()
401 .contains(Profile
.Capability
.storageServiceEncryptionV2Capability
)) {
402 logger
.debug("Generating and including a new recordIkm.");
403 recordIkm
= RecordIkm
.Companion
.generate();
405 logger
.debug("SSRE2 not yet supported. Not including recordIkm.");
409 final var manifest
= new SignalStorageManifest(newVersion
, account
.getDeviceId(), recordIkm
, newStorageIds
);
411 StorageSyncValidations
.validateForcePush(manifest
, newStorageRecords
, account
.getSelfRecipientAddress());
413 final WriteStorageRecordsResult result
;
414 if (newVersion
> 1) {
415 logger
.trace("Force-pushing data. Inserting {} IDs.", newStorageRecords
.size());
416 result
= dependencies
.getStorageServiceRepository()
417 .resetAndWriteStorageRecords(storageServiceKey
, manifest
, newStorageRecords
);
419 logger
.trace("First version, normal push. Inserting {} IDs.", newStorageRecords
.size());
420 result
= dependencies
.getStorageServiceRepository()
421 .writeStorageRecords(storageServiceKey
, manifest
, newStorageRecords
, Collections
.emptyList());
425 case WriteStorageRecordsResult
.ConflictError ignored
-> {
426 logger
.debug("Hit a conflict. Trying again.");
427 throw new RetryLaterException();
429 case WriteStorageRecordsResult
.NetworkError networkError
-> throw networkError
.getException();
430 case WriteStorageRecordsResult
.StatusCodeError statusCodeError
-> throw statusCodeError
.getException();
431 case WriteStorageRecordsResult
.Success ignored
-> {
432 logger
.debug("Force push succeeded. Updating local manifest version to: {}", manifest
.version
);
433 storeManifestLocally(manifest
);
435 default -> throw new IllegalStateException("Unexpected value: " + result
);
438 try (final var connection
= account
.getAccountDatabase().getConnection()) {
439 connection
.setAutoCommit(false);
440 account
.getRecipientStore().updateStorageIds(connection
, newContactStorageIds
);
441 account
.getGroupStore().updateStorageIds(connection
, newGroupV1StorageIds
, newGroupV2StorageIds
);
443 // delete all unknown storage ids
444 account
.getUnknownStorageIdStore().deleteAllUnknownStorageIds(connection
);
446 } catch (SQLException e
) {
447 throw new RuntimeException("Failed to sync remote storage", e
);
451 private Map
<RecipientId
, StorageId
> generateContactStorageIds(List
<RecipientId
> recipientIds
) {
452 final var selfRecipientId
= account
.getSelfRecipientId();
453 return recipientIds
.stream().collect(Collectors
.toMap(recipientId
-> recipientId
, recipientId
-> {
454 if (recipientId
.equals(selfRecipientId
)) {
455 return StorageId
.forAccount(KeyUtils
.createRawStorageId());
457 return StorageId
.forContact(KeyUtils
.createRawStorageId());
462 private Map
<GroupIdV1
, StorageId
> generateGroupV1StorageIds(List
<GroupIdV1
> groupIds
) {
463 return groupIds
.stream()
464 .collect(Collectors
.toMap(recipientId
-> recipientId
,
465 recipientId
-> StorageId
.forGroupV1(KeyUtils
.createRawStorageId())));
468 private Map
<GroupIdV2
, StorageId
> generateGroupV2StorageIds(List
<GroupIdV2
> groupIds
) {
469 return groupIds
.stream()
470 .collect(Collectors
.toMap(recipientId
-> recipientId
,
471 recipientId
-> StorageId
.forGroupV2(KeyUtils
.createRawStorageId())));
474 private void storeManifestLocally(
475 final SignalStorageManifest remoteManifest
477 account
.setStorageManifestVersion(remoteManifest
.version
);
478 account
.setStorageManifest(remoteManifest
);
481 private List
<SignalStorageRecord
> getSignalStorageRecords(
482 final StorageKey storageKey
,
483 final SignalStorageManifest manifest
,
484 final List
<StorageId
> storageIds
485 ) throws IOException
{
486 final var result
= dependencies
.getStorageServiceRepository()
487 .readStorageRecords(storageKey
, manifest
.recordIkm
, storageIds
);
488 return switch (result
) {
489 case StorageServiceRepository
.StorageRecordResult
.DecryptionError decryptionError
-> {
490 if (decryptionError
.getException() instanceof InvalidKeyException
) {
491 logger
.warn("Failed to read storage records, ignoring.");
493 } else if (decryptionError
.getException() instanceof IOException ioe
) {
496 throw new IOException(decryptionError
.getException());
499 case StorageServiceRepository
.StorageRecordResult
.NetworkError networkError
->
500 throw networkError
.getException();
501 case StorageServiceRepository
.StorageRecordResult
.StatusCodeError statusCodeError
->
502 throw statusCodeError
.getException();
503 case StorageServiceRepository
.StorageRecordResult
.Success success
-> success
.getRecords();
504 default -> throw new IllegalStateException("Unexpected value: " + result
);
508 private List
<StorageId
> getAllLocalStorageIds(final Connection connection
) throws SQLException
{
509 final var storageIds
= new ArrayList
<StorageId
>();
510 storageIds
.addAll(account
.getUnknownStorageIdStore().getUnknownStorageIds(connection
));
511 storageIds
.addAll(account
.getGroupStore().getStorageIds(connection
));
512 storageIds
.addAll(account
.getRecipientStore().getStorageIds(connection
));
513 storageIds
.add(account
.getRecipientStore().getSelfStorageId(connection
));
517 private List
<SignalStorageRecord
> buildLocalStorageRecords(
518 final Connection connection
,
519 final List
<StorageId
> storageIds
520 ) throws SQLException
{
521 final var records
= new ArrayList
<SignalStorageRecord
>(storageIds
.size());
522 for (final var storageId
: storageIds
) {
523 final var record = buildLocalStorageRecord(connection
, storageId
);
529 private SignalStorageRecord
buildLocalStorageRecord(
530 Connection connection
,
532 ) throws SQLException
{
533 return switch (ManifestRecord
.Identifier
.Type
.fromValue(storageId
.getType())) {
534 case ManifestRecord
.Identifier
.Type
.CONTACT
-> {
535 final var recipient
= account
.getRecipientStore().getRecipient(connection
, storageId
);
536 final var address
= recipient
.getAddress().getIdentifier();
537 final var identity
= account
.getIdentityKeyStore().getIdentityInfo(connection
, address
);
538 final var record = StorageSyncModels
.localToRemoteRecord(recipient
, identity
);
539 yield new SignalStorageRecord(storageId
, new StorageRecord
.Builder().contact(record).build());
541 case ManifestRecord
.Identifier
.Type
.GROUPV1
-> {
542 final var groupV1
= account
.getGroupStore().getGroupV1(connection
, storageId
);
543 final var record = StorageSyncModels
.localToRemoteRecord(groupV1
);
544 yield new SignalStorageRecord(storageId
, new StorageRecord
.Builder().groupV1(record).build());
546 case ManifestRecord
.Identifier
.Type
.GROUPV2
-> {
547 final var groupV2
= account
.getGroupStore().getGroupV2(connection
, storageId
);
548 final var record = StorageSyncModels
.localToRemoteRecord(groupV2
);
549 yield new SignalStorageRecord(storageId
, new StorageRecord
.Builder().groupV2(record).build());
551 case ManifestRecord
.Identifier
.Type
.ACCOUNT
-> {
552 final var selfRecipient
= account
.getRecipientStore()
553 .getRecipient(connection
, account
.getSelfRecipientId());
555 final var record = StorageSyncModels
.localToRemoteRecord(connection
,
556 account
.getConfigurationStore(),
558 account
.getUsernameLink());
559 yield new SignalStorageRecord(storageId
, new StorageRecord
.Builder().account(record).build());
561 case null, default -> {
562 throw new AssertionError("Got unknown local storage record type: " + storageId
);
568 * Given a list of all the local and remote keys you know about, this will
569 * return a result telling
570 * you which keys are exclusively remote and which are exclusively local.
572 * @param remoteIds All remote keys available.
573 * @param localIds All local keys available.
574 * @return An object describing which keys are exclusive to the remote data set
576 * exclusive to the local data set.
578 private static IdDifferenceResult
findIdDifference(
579 Collection
<StorageId
> remoteIds
,
580 Collection
<StorageId
> localIds
582 final var base64Encoder
= Base64
.getEncoder();
583 final var remoteByRawId
= remoteIds
.stream()
584 .collect(Collectors
.toMap(id
-> base64Encoder
.encodeToString(id
.getRaw()), id
-> id
));
585 final var localByRawId
= localIds
.stream()
586 .collect(Collectors
.toMap(id
-> base64Encoder
.encodeToString(id
.getRaw()), id
-> id
));
588 boolean hasTypeMismatch
= remoteByRawId
.size() != remoteIds
.size() || localByRawId
.size() != localIds
.size();
590 final var remoteOnlyRawIds
= SetUtil
.difference(remoteByRawId
.keySet(), localByRawId
.keySet());
591 final var localOnlyRawIds
= SetUtil
.difference(localByRawId
.keySet(), remoteByRawId
.keySet());
592 final var sharedRawIds
= SetUtil
.intersection(localByRawId
.keySet(), remoteByRawId
.keySet());
594 for (String rawId
: sharedRawIds
) {
595 final var remote
= remoteByRawId
.get(rawId
);
596 final var local
= localByRawId
.get(rawId
);
598 if (remote
.getType() != local
.getType() && local
.getType() != 0) {
599 remoteOnlyRawIds
.remove(rawId
);
600 localOnlyRawIds
.remove(rawId
);
601 hasTypeMismatch
= true;
602 logger
.debug("Remote type {} did not match local type {} for {}!",
609 final var remoteOnlyKeys
= remoteOnlyRawIds
.stream().map(remoteByRawId
::get
).toList();
610 final var localOnlyKeys
= localOnlyRawIds
.stream().map(localByRawId
::get
).toList();
612 return new IdDifferenceResult(remoteOnlyKeys
, localOnlyKeys
, hasTypeMismatch
);
615 private List
<StorageId
> processKnownRecords(
616 final Connection connection
,
617 List
<SignalStorageRecord
> records
618 ) throws SQLException
{
619 final var unknownRecords
= new ArrayList
<StorageId
>();
621 final var accountRecordProcessor
= new AccountRecordProcessor(account
, connection
, context
.getJobExecutor());
622 final var contactRecordProcessor
= new ContactRecordProcessor(account
, connection
, context
.getJobExecutor());
623 final var groupV1RecordProcessor
= new GroupV1RecordProcessor(account
, connection
);
624 final var groupV2RecordProcessor
= new GroupV2RecordProcessor(account
, connection
);
626 for (final var record : records
) {
627 if (record.getProto().account
!= null) {
628 logger
.debug("Reading record {} of type account", record.getId());
629 accountRecordProcessor
.process(StorageRecordConvertersKt
.toSignalAccountRecord(record.getProto().account
,
631 } else if (record.getProto().groupV1
!= null) {
632 logger
.debug("Reading record {} of type groupV1", record.getId());
633 groupV1RecordProcessor
.process(StorageRecordConvertersKt
.toSignalGroupV1Record(record.getProto().groupV1
,
635 } else if (record.getProto().groupV2
!= null) {
636 logger
.debug("Reading record {} of type groupV2", record.getId());
637 groupV2RecordProcessor
.process(StorageRecordConvertersKt
.toSignalGroupV2Record(record.getProto().groupV2
,
639 } else if (record.getProto().contact
!= null) {
640 logger
.debug("Reading record {} of type contact", record.getId());
641 contactRecordProcessor
.process(StorageRecordConvertersKt
.toSignalContactRecord(record.getProto().contact
,
644 unknownRecords
.add(record.getId());
648 return unknownRecords
;
652 * hasTypeMismatches is True if there exist some keys that have matching raw ID's but different types, otherwise false.
654 private record IdDifferenceResult(
655 List
<StorageId
> remoteOnlyIds
, List
<StorageId
> localOnlyIds
, boolean hasTypeMismatches
658 public boolean isEmpty() {
659 return remoteOnlyIds
.isEmpty() && localOnlyIds
.isEmpty();
663 private static class RetryLaterException
extends Throwable
{}