diff --git a/app/schemas/im.conversations.android.database.ConversationsDatabase/1.json b/app/schemas/im.conversations.android.database.ConversationsDatabase/1.json index a53965c10..d0e218100 100644 --- a/app/schemas/im.conversations.android.database.ConversationsDatabase/1.json +++ b/app/schemas/im.conversations.android.database.ConversationsDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "bc04f3d0c58f7e50f5c7973a7a06c9eb", + "identityHash": "b5e8a59bbd86e133c0bc2edd303ad2a0", "entities": [ { "tableName": "account", @@ -2432,12 +2432,103 @@ ] } ] + }, + { + "tableName": "service_record_cache", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `domain` TEXT NOT NULL, `ip` BLOB, `hostname` TEXT, `port` INTEGER NOT NULL, `directTls` INTEGER NOT NULL, `priority` INTEGER NOT NULL, `authenticated` INTEGER NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serviceRecord.ip", + "columnName": "ip", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "serviceRecord.hostname", + "columnName": "hostname", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serviceRecord.port", + "columnName": "port", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceRecord.directTls", + "columnName": "directTls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceRecord.priority", + "columnName": "priority", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceRecord.authenticated", + "columnName": "authenticated", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_service_record_cache_accountId_domain", + "unique": true, + "columnNames": [ + "accountId", + "domain" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_service_record_cache_accountId_domain` ON `${TABLE_NAME}` (`accountId`, `domain`)" + } + ], + "foreignKeys": [ + { + "table": "account", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "id" + ] + } + ] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bc04f3d0c58f7e50f5c7973a7a06c9eb')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b5e8a59bbd86e133c0bc2edd303ad2a0')" ] } } \ No newline at end of file diff --git a/app/src/main/java/im/conversations/android/database/ConversationsDatabase.java b/app/src/main/java/im/conversations/android/database/ConversationsDatabase.java index fab217091..c6abb0860 100644 --- a/app/src/main/java/im/conversations/android/database/ConversationsDatabase.java +++ b/app/src/main/java/im/conversations/android/database/ConversationsDatabase.java @@ -17,6 +17,7 @@ import im.conversations.android.database.dao.MessageDao; import im.conversations.android.database.dao.NickDao; import im.conversations.android.database.dao.PresenceDao; import im.conversations.android.database.dao.RosterDao; +import im.conversations.android.database.dao.ServiceRecordDao; import im.conversations.android.database.entity.AccountEntity; import im.conversations.android.database.entity.AvatarAdditionalEntity; import im.conversations.android.database.entity.AvatarEntity; @@ -49,6 +50,7 @@ import im.conversations.android.database.entity.NickEntity; import im.conversations.android.database.entity.PresenceEntity; import im.conversations.android.database.entity.RosterItemEntity; import im.conversations.android.database.entity.RosterItemGroupEntity; +import im.conversations.android.database.entity.ServiceRecordCacheEntity; @Database( entities = { @@ -83,7 +85,8 @@ import im.conversations.android.database.entity.RosterItemGroupEntity; PresenceEntity.class, MessageReactionEntity.class, RosterItemEntity.class, - RosterItemGroupEntity.class + RosterItemGroupEntity.class, + ServiceRecordCacheEntity.class }, version = 1) @TypeConverters(Converters.class) @@ -130,4 +133,6 @@ public abstract class ConversationsDatabase extends RoomDatabase { public abstract PresenceDao presenceDao(); public abstract RosterDao rosterDao(); + + public abstract ServiceRecordDao serviceRecordDao(); } diff --git a/app/src/main/java/im/conversations/android/database/Converters.java b/app/src/main/java/im/conversations/android/database/Converters.java index 9c6aee271..4988f6481 100644 --- a/app/src/main/java/im/conversations/android/database/Converters.java +++ b/app/src/main/java/im/conversations/android/database/Converters.java @@ -2,7 +2,10 @@ package im.conversations.android.database; import androidx.room.TypeConverter; import com.google.common.base.Strings; +import de.measite.minidns.DNSName; import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; import java.time.Instant; import org.jxmpp.jid.BareJid; import org.jxmpp.jid.Jid; @@ -145,4 +148,31 @@ public final class Converters { throw new RuntimeException(e); } } + + @TypeConverter + public static String fromDNSName(final DNSName dnsName) { + return dnsName == null ? null : dnsName.toString(); + } + + @TypeConverter + public static DNSName toDNSName(final String dnsName) { + return dnsName == null ? null : DNSName.from(dnsName); + } + + @TypeConverter + public static byte[] fromInetAddress(final InetAddress inetAddress) { + return inetAddress == null ? null : inetAddress.getAddress(); + } + + @TypeConverter + public static InetAddress toInetAddress(final byte[] address) { + if (address == null || address.length == 0) { + return null; + } + try { + return InetAddress.getByAddress(address); + } catch (final UnknownHostException e) { + return null; + } + } } diff --git a/app/src/main/java/im/conversations/android/database/dao/ServiceRecordDao.java b/app/src/main/java/im/conversations/android/database/dao/ServiceRecordDao.java new file mode 100644 index 000000000..fcb21a8aa --- /dev/null +++ b/app/src/main/java/im/conversations/android/database/dao/ServiceRecordDao.java @@ -0,0 +1,32 @@ +package im.conversations.android.database.dao; + +import androidx.room.Dao; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; +import im.conversations.android.database.entity.ServiceRecordCacheEntity; +import im.conversations.android.database.model.Account; +import im.conversations.android.dns.ServiceRecord; + +@Dao +public abstract class ServiceRecordDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + protected abstract void insert(ServiceRecordCacheEntity entity); + + public void insert(final Account account, final ServiceRecord serviceRecord) { + insert( + ServiceRecordCacheEntity.of( + account, account.address.getDomain().toString(), serviceRecord)); + } + + @Query( + "SELECT ip,hostname,port,directTls,priority,authenticated FROM service_record_cache" + + " WHERE accountId=:account AND domain=:domain LIMIT 1") + protected abstract ServiceRecord getCachedServiceRecord( + final long account, final String domain); + + public ServiceRecord getCachedServiceRecord(final Account account) { + return getCachedServiceRecord(account.id, account.address.getDomain().toString()); + } +} diff --git a/app/src/main/java/im/conversations/android/database/entity/ServiceRecordCacheEntity.java b/app/src/main/java/im/conversations/android/database/entity/ServiceRecordCacheEntity.java new file mode 100644 index 000000000..3151ffa81 --- /dev/null +++ b/app/src/main/java/im/conversations/android/database/entity/ServiceRecordCacheEntity.java @@ -0,0 +1,44 @@ +package im.conversations.android.database.entity; + +import androidx.annotation.NonNull; +import androidx.room.Embedded; +import androidx.room.Entity; +import androidx.room.ForeignKey; +import androidx.room.Index; +import androidx.room.PrimaryKey; +import im.conversations.android.database.model.Account; +import im.conversations.android.dns.ServiceRecord; + +@Entity( + tableName = "service_record_cache", + foreignKeys = + @ForeignKey( + entity = AccountEntity.class, + parentColumns = {"id"}, + childColumns = {"accountId"}, + onDelete = ForeignKey.CASCADE), + indices = { + @Index( + value = {"accountId", "domain"}, + unique = true) + }) +public class ServiceRecordCacheEntity { + + @PrimaryKey(autoGenerate = true) + public Long id; + + @NonNull public Long accountId; + + @NonNull public String domain; + + @Embedded @NonNull public ServiceRecord serviceRecord; + + public static ServiceRecordCacheEntity of( + final Account account, final String domain, final ServiceRecord serviceRecord) { + final var entity = new ServiceRecordCacheEntity(); + entity.accountId = account.id; + entity.domain = domain; + entity.serviceRecord = serviceRecord; + return entity; + } +} diff --git a/app/src/main/java/im/conversations/android/dns/Resolver.java b/app/src/main/java/im/conversations/android/dns/Resolver.java index 2415f3916..84417a408 100644 --- a/app/src/main/java/im/conversations/android/dns/Resolver.java +++ b/app/src/main/java/im/conversations/android/dns/Resolver.java @@ -41,6 +41,7 @@ import org.jxmpp.jid.DomainJid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +@SuppressWarnings("UnstableApiUsage") public class Resolver { private static final Logger LOGGER = LoggerFactory.getLogger(Resolver.class); diff --git a/app/src/main/java/im/conversations/android/dns/ServiceRecord.java b/app/src/main/java/im/conversations/android/dns/ServiceRecord.java index 7b3fcbfac..1ae18dd86 100644 --- a/app/src/main/java/im/conversations/android/dns/ServiceRecord.java +++ b/app/src/main/java/im/conversations/android/dns/ServiceRecord.java @@ -59,6 +59,10 @@ public class ServiceRecord implements Comparable { return port; } + public int getPriority() { + return this.priority; + } + public DNSName getHostname() { return hostname; } diff --git a/app/src/main/java/im/conversations/android/xmpp/XmppConnection.java b/app/src/main/java/im/conversations/android/xmpp/XmppConnection.java index 8eb1cbd55..c4531ca27 100644 --- a/app/src/main/java/im/conversations/android/xmpp/XmppConnection.java +++ b/app/src/main/java/im/conversations/android/xmpp/XmppConnection.java @@ -12,6 +12,7 @@ import androidx.annotation.Nullable; import com.google.common.base.Optional; import com.google.common.base.Strings; import com.google.common.collect.ClassToInstanceMap; +import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -338,22 +339,30 @@ public class XmppConnection implements Runnable { LOGGER.warn("Resolver results were empty"); return; } + final List resultsWithBackup; final ServiceRecord storedBackupResult; if (connection != null) { storedBackupResult = null; + resultsWithBackup = results; } else { - // TODO fix resolver result caching storedBackupResult = - null; // context.databaseBackend.findResolverResult(domain); + ConversationsDatabase.getInstance(context) + .serviceRecordDao() + .getCachedServiceRecord(account); if (storedBackupResult != null && !results.contains(storedBackupResult)) { - results.add(storedBackupResult); + resultsWithBackup = + new ImmutableList.Builder() + .addAll(results) + .add(storedBackupResult) + .build(); LOGGER.debug( - account.address - + ": loaded backup resolver result from db: " - + storedBackupResult); + "loaded backup resolver result from db {}", storedBackupResult); + } else { + resultsWithBackup = results; } } - for (Iterator iterator = results.iterator(); iterator.hasNext(); ) { + for (final Iterator iterator = resultsWithBackup.iterator(); + iterator.hasNext(); ) { final ServiceRecord result = iterator.next(); if (Thread.currentThread().isInterrupted()) { LOGGER.debug(account.address + ": Thread was interrupted"); @@ -362,9 +371,8 @@ public class XmppConnection implements Runnable { try { // if tls is true, encryption is implied and must not be started this.encryptionEnabled = result.isDirectTls(); - verifiedHostname = + this.verifiedHostname = result.isAuthenticated() ? result.getHostname().toString() : null; - LOGGER.debug("verified hostname " + verifiedHostname); final InetSocketAddress addr; if (result.getIp() != null) { addr = new InetSocketAddress(result.getIp(), result.getPort()); @@ -403,12 +411,11 @@ public class XmppConnection implements Runnable { localSocket.setSoTimeout(ConnectionPool.SOCKET_TIMEOUT * 1000); if (startXmpp(localSocket)) { - localSocket.setSoTimeout( - 0); // reset to 0; once the connection is established we don’t - // want this + localSocket.setSoTimeout(0); if (connection == null && !result.equals(storedBackupResult)) { - // TODO store resolver result - // context.databaseBackend.saveResolverResult(domain, result); + ConversationsDatabase.getInstance(context) + .serviceRecordDao() + .insert(account, result); } break; // successfully connected to server that speaks xmpp } else {