update) throws SQLException {
+ final var contactRecord = update.newRecord();
+ final var address = getRecipientAddress(contactRecord);
+ final var recipientId = account.getRecipientStore().resolveRecipientTrusted(connection, address);
+ final var recipient = account.getRecipientStore().getRecipient(connection, recipientId);
+
+ final var contact = recipient.getContact();
+ final var blocked = contact != null && contact.isBlocked();
+ final var profileShared = contact != null && contact.isProfileSharingEnabled();
+ final var archived = contact != null && contact.isArchived();
+ final var hidden = contact != null && contact.isHidden();
+ final var hideStory = contact != null && contact.hideStory();
+ final var muteUntil = contact == null ? 0 : contact.muteUntil();
+ final var unregisteredTimestamp = contact == null || contact.unregisteredTimestamp() == null
+ ? 0
+ : contact.unregisteredTimestamp();
+ final var contactGivenName = contact == null ? null : contact.givenName();
+ final var contactFamilyName = contact == null ? null : contact.familyName();
+ final var contactNickName = contact == null ? null : contact.nickName();
+ if (blocked != contactRecord.isBlocked()
+ || profileShared != contactRecord.isProfileSharingEnabled()
+ || archived != contactRecord.isArchived()
+ || hidden != contactRecord.isHidden()
+ || hideStory != contactRecord.shouldHideStory()
+ || muteUntil != contactRecord.getMuteUntil()
+ || unregisteredTimestamp != contactRecord.getUnregisteredTimestamp()
+ || !Objects.equals(contactRecord.getSystemGivenName().orElse(null), contactGivenName)
+ || !Objects.equals(contactRecord.getSystemFamilyName().orElse(null), contactFamilyName)
+ || !Objects.equals(contactRecord.getSystemNickname().orElse(null), contactNickName)) {
+ logger.debug("Storing new or updated contact {}", recipientId);
+ final var contactBuilder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
+ final var newContact = contactBuilder.withIsBlocked(contactRecord.isBlocked())
+ .withIsProfileSharingEnabled(contactRecord.isProfileSharingEnabled())
+ .withIsArchived(contactRecord.isArchived())
+ .withIsHidden(contactRecord.isHidden())
+ .withMuteUntil(contactRecord.getMuteUntil())
+ .withHideStory(contactRecord.shouldHideStory())
+ .withGivenName(contactRecord.getSystemGivenName().orElse(null))
+ .withFamilyName(contactRecord.getSystemFamilyName().orElse(null))
+ .withNickName(contactRecord.getSystemNickname().orElse(null))
+ .withUnregisteredTimestamp(contactRecord.getUnregisteredTimestamp() == 0
+ ? null
+ : contactRecord.getUnregisteredTimestamp());
+ account.getRecipientStore().storeContact(connection, recipientId, newContact.build());
+ }
+
+ final var profile = recipient.getProfile();
+ final var profileGivenName = profile == null ? null : profile.getGivenName();
+ final var profileFamilyName = profile == null ? null : profile.getFamilyName();
+ if (!Objects.equals(contactRecord.getProfileGivenName().orElse(null), profileGivenName) || !Objects.equals(
+ contactRecord.getProfileFamilyName().orElse(null),
+ profileFamilyName)) {
+ final var profileBuilder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile);
+ final var newProfile = profileBuilder.withGivenName(contactRecord.getProfileGivenName().orElse(null))
+ .withFamilyName(contactRecord.getProfileFamilyName().orElse(null))
+ .build();
+ account.getRecipientStore().storeProfile(connection, recipientId, newProfile);
+ }
+ if (contactRecord.getProfileKey().isPresent()) {
+ try {
+ logger.trace("Storing profile key {}", recipientId);
+ final var profileKey = new ProfileKey(contactRecord.getProfileKey().get());
+ account.getRecipientStore().storeProfileKey(connection, recipientId, profileKey);
+ } catch (InvalidInputException e) {
+ logger.warn("Received invalid contact profile key from storage");
+ }
+ }
+ if (contactRecord.getIdentityKey().isPresent() && contactRecord.getAci().isPresent()) {
+ try {
+ logger.trace("Storing identity key {}", recipientId);
+ final var identityKey = new IdentityKey(contactRecord.getIdentityKey().get());
+ account.getIdentityKeyStore()
+ .saveIdentity(connection, contactRecord.getAci().orElse(null), identityKey);
+
+ final var trustLevel = StorageSyncModels.remoteToLocal(contactRecord.getIdentityState());
+ if (trustLevel != null) {
+ account.getIdentityKeyStore()
+ .setIdentityTrustLevel(connection,
+ contactRecord.getAci().orElse(null),
+ identityKey,
+ trustLevel);
+ }
+ } catch (InvalidKeyException e) {
+ logger.warn("Received invalid contact identity key from storage");
+ }
+ }
+ account.getRecipientStore()
+ .storeStorageRecord(connection, recipientId, contactRecord.getId(), contactRecord.toProto().encode());
+ }
+
+ private static RecipientAddress getRecipientAddress(final SignalContactRecord contactRecord) {
+ return new RecipientAddress(contactRecord.getAci().orElse(null),
+ contactRecord.getPni().orElse(null),
+ contactRecord.getNumber().orElse(null),
+ contactRecord.getUsername().orElse(null));
+ }
+
+ @Override
+ public int compare(SignalContactRecord lhs, SignalContactRecord rhs) {
+ if ((lhs.getAci().isPresent() && Objects.equals(lhs.getAci(), rhs.getAci())) || (
+ lhs.getNumber().isPresent() && Objects.equals(lhs.getNumber(), rhs.getNumber())
+ ) || (lhs.getPni().isPresent() && Objects.equals(lhs.getPni(), rhs.getPni()))) {
+ return 0;
+ } else {
+ return 1;
+ }
+ }
+
+ private static boolean isValidE164(String value) {
+ return E164_PATTERN.matcher(value).matches();
+ }
+
+ private static boolean doProtosMatch(SignalContactRecord merged, SignalContactRecord other) {
+ return Arrays.equals(merged.toProto().encode(), other.toProto().encode());
+ }
+}
diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/syncStorage/DefaultStorageRecordProcessor.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/syncStorage/DefaultStorageRecordProcessor.java
new file mode 100644
index 0000000000000..68b5f9cc59943
--- /dev/null
+++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/syncStorage/DefaultStorageRecordProcessor.java
@@ -0,0 +1,98 @@
+package org.asamk.signal.manager.syncStorage;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.whispersystems.signalservice.api.storage.SignalRecord;
+import org.whispersystems.signalservice.api.storage.StorageId;
+
+import java.sql.SQLException;
+import java.util.Comparator;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * An implementation of {@link StorageRecordProcessor} that solidifies a pattern and reduces
+ * duplicate code in individual implementations.
+ *
+ * Concerning the implementation of {@link #compare(Object, Object)}, it's purpose is to detect if
+ * two items would map to the same logical entity (i.e. they would correspond to the same record in
+ * our local store). We use it for a {@link TreeSet}, so mainly it's just important that the '0'
+ * case is correct. Other cases are whatever, just make it something stable.
+ */
+abstract class DefaultStorageRecordProcessor