get rid of ResolverApi and use DNS client directly

Daniel Gultsch created

Change summary

build.gradle                                                                |   3 
src/main/java/de/gultsch/minidns/AndroidDNSClient.java                      |  64 
src/main/java/de/gultsch/minidns/DNSSocket.java                             |  36 
src/main/java/de/gultsch/minidns/NetworkDataSource.java                     |  66 
src/main/java/de/gultsch/minidns/ResolutionUnsuccessfulException.java       |  35 
src/main/java/de/gultsch/minidns/ResolverResult.java                        | 178 
src/main/java/eu/siacs/conversations/Conversations.java                     |   9 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java    |   1 
src/main/java/eu/siacs/conversations/utils/AndroidUsingExecLowPriority.java |  92 
src/main/java/eu/siacs/conversations/utils/AndroidUsingLinkProperties.java  |  90 
src/main/java/eu/siacs/conversations/utils/Resolver.java                    | 565 
src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java               |   2 
12 files changed, 590 insertions(+), 551 deletions(-)

Detailed changes

build.gradle πŸ”—

@@ -59,7 +59,8 @@ dependencies {
     //zxing stopped supporting Java 7 so we have to stick with 3.3.3
     //https://github.com/zxing/zxing/issues/1170
     implementation 'com.google.zxing:core:3.3.3'
-    implementation 'de.measite.minidns:minidns-hla:0.2.4'
+    implementation 'org.minidns:minidns-client:1.0.4'
+    implementation 'org.minidns:minidns-dnssec:1.0.4'
     implementation 'me.leolin:ShortcutBadger:1.1.22@aar'
     implementation 'org.whispersystems:signal-protocol-java:2.6.2'
     implementation 'com.makeramen:roundedimageview:2.3.0'

src/main/java/de/gultsch/minidns/AndroidDNSClient.java πŸ”—

@@ -14,10 +14,15 @@ import com.google.common.base.Strings;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableList;
 
-import de.measite.minidns.AbstractDNSClient;
-import de.measite.minidns.DNSMessage;
-import de.measite.minidns.Record;
-import de.measite.minidns.record.Data;
+import org.minidns.AbstractDnsClient;
+import org.minidns.dnsmessage.DnsMessage;
+import org.minidns.dnsqueryresult.DirectCachedDnsQueryResult;
+import org.minidns.dnsqueryresult.DnsQueryResult;
+import org.minidns.dnsqueryresult.StandardDnsQueryResult;
+import org.minidns.dnsqueryresult.SynthesizedCachedDnsQueryResult;
+import org.minidns.record.Data;
+
+import org.minidns.record.Record;
 
 import eu.siacs.conversations.Config;
 
@@ -27,11 +32,11 @@ import java.time.Duration;
 import java.util.Collections;
 import java.util.List;
 
-public class AndroidDNSClient extends AbstractDNSClient {
+public class AndroidDNSClient extends AbstractDnsClient {
 
     private static final long DNS_MAX_TTL = 86_400L;
 
-    private static final LruCache<QuestionServerTuple, DNSMessage> QUERY_CACHE =
+    private static final LruCache<QuestionServerTuple, DnsMessage> QUERY_CACHE =
             new LruCache<>(1024);
     private final Context context;
     private final NetworkDataSource networkDataSource = new NetworkDataSource();
@@ -60,7 +65,7 @@ public class AndroidDNSClient extends AbstractDNSClient {
     }
 
     @Override
-    protected DNSMessage.Builder newQuestion(final DNSMessage.Builder message) {
+    protected DnsMessage.Builder newQuestion(final DnsMessage.Builder message) {
         message.setRecursionDesired(true);
         message.getEdnsBuilder()
                 .setUdpPayloadSize(networkDataSource.getUdpPayloadSize())
@@ -69,15 +74,16 @@ public class AndroidDNSClient extends AbstractDNSClient {
     }
 
     @Override
-    protected DNSMessage query(final DNSMessage.Builder queryBuilder) throws IOException {
-        final DNSMessage question = newQuestion(queryBuilder).build();
+    protected DnsQueryResult query(final DnsMessage.Builder queryBuilder) throws IOException {
+        final DnsMessage question = newQuestion(queryBuilder).build();
         for (final DNSServer dnsServer : getDNSServers()) {
             final QuestionServerTuple cacheKey = new QuestionServerTuple(dnsServer, question);
-            final DNSMessage cachedResponse = queryCache(cacheKey);
+            final DnsMessage cachedResponse = queryCache(cacheKey);
             if (cachedResponse != null) {
-                return cachedResponse;
+                return new CachedDnsQueryResult(question, cachedResponse);
             }
-            final DNSMessage response = this.networkDataSource.query(question, dnsServer);
+            final DnsQueryResult result = this.networkDataSource.query(question, dnsServer);
+            final var response = result.response;
             if (response == null) {
                 continue;
             }
@@ -89,7 +95,7 @@ public class AndroidDNSClient extends AbstractDNSClient {
                     continue;
             }
             cacheQuery(cacheKey, response);
-            return response;
+            return new StandardDnsQueryResult(dnsServer.inetAddress, dnsServer.port,result.queryMethod,question,response);
         }
         return null;
     }
@@ -104,8 +110,7 @@ public class AndroidDNSClient extends AbstractDNSClient {
 
     private List<DNSServer> getDNSServers() {
         final ImmutableList.Builder<DNSServer> dnsServerBuilder = new ImmutableList.Builder<>();
-        final ConnectivityManager connectivityManager =
-                (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+        final ConnectivityManager connectivityManager = context.getSystemService(ConnectivityManager.class);
         final Network[] networks = getActiveNetworks(connectivityManager);
         for (final Network network : networks) {
             final LinkProperties linkProperties = connectivityManager.getLinkProperties(network);
@@ -133,17 +138,15 @@ public class AndroidDNSClient extends AbstractDNSClient {
         if (connectivityManager == null) {
             return new Network[0];
         }
-        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
-            final Network activeNetwork = connectivityManager.getActiveNetwork();
-            if (activeNetwork != null) {
+        final Network activeNetwork = connectivityManager.getActiveNetwork();
+        if (activeNetwork != null) {
                 return new Network[] {activeNetwork};
-            }
         }
         return connectivityManager.getAllNetworks();
     }
 
-    private DNSMessage queryCache(final QuestionServerTuple key) {
-        final DNSMessage cachedResponse;
+    private DnsMessage queryCache(final QuestionServerTuple key) {
+        final DnsMessage cachedResponse;
         synchronized (QUERY_CACHE) {
             cachedResponse = QUERY_CACHE.get(key);
             if (cachedResponse == null) {
@@ -163,7 +166,7 @@ public class AndroidDNSClient extends AbstractDNSClient {
         return cachedResponse;
     }
 
-    private void cacheQuery(final QuestionServerTuple key, final DNSMessage response) {
+    private void cacheQuery(final QuestionServerTuple key, final DnsMessage response) {
         if (response.receiveTimestamp <= 0) {
             return;
         }
@@ -172,7 +175,7 @@ public class AndroidDNSClient extends AbstractDNSClient {
         }
     }
 
-    private static long ttl(final DNSMessage dnsMessage) {
+    private static long ttl(final DnsMessage dnsMessage) {
         final List<Record<? extends Data>> answerSection = dnsMessage.answerSection;
         if (answerSection == null || answerSection.isEmpty()) {
             final List<Record<? extends Data>> authoritySection = dnsMessage.authoritySection;
@@ -187,19 +190,19 @@ public class AndroidDNSClient extends AbstractDNSClient {
         }
     }
 
-    private static long expiresAt(final DNSMessage dnsMessage) {
+    private static long expiresAt(final DnsMessage dnsMessage) {
         return dnsMessage.receiveTimestamp + (Math.min(DNS_MAX_TTL, ttl(dnsMessage)) * 1000L);
     }
 
-    private static long expiresIn(final DNSMessage dnsMessage) {
+    private static long expiresIn(final DnsMessage dnsMessage) {
         return expiresAt(dnsMessage) - System.currentTimeMillis();
     }
 
     private static class QuestionServerTuple {
         private final DNSServer dnsServer;
-        private final DNSMessage question;
+        private final DnsMessage question;
 
-        private QuestionServerTuple(final DNSServer dnsServer, final DNSMessage question) {
+        private QuestionServerTuple(final DNSServer dnsServer, final DnsMessage question) {
             this.dnsServer = dnsServer;
             this.question = question.asNormalizedVersion();
         }
@@ -218,4 +221,11 @@ public class AndroidDNSClient extends AbstractDNSClient {
             return Objects.hashCode(dnsServer, question);
         }
     }
+
+    public static class CachedDnsQueryResult extends DnsQueryResult {
+
+        private CachedDnsQueryResult(final DnsMessage query, final DnsMessage response) {
+            super(QueryMethod.cachedDirect, query, response);
+        }
+    }
 }

src/main/java/de/gultsch/minidns/DNSSocket.java πŸ”—

@@ -7,11 +7,11 @@ import com.google.common.base.Strings;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.SettableFuture;
 
-import de.measite.minidns.DNSMessage;
 
 import eu.siacs.conversations.Config;
 
 import org.conscrypt.OkHostnameVerifier;
+import org.minidns.dnsmessage.DnsMessage;
 
 import java.io.Closeable;
 import java.io.DataInputStream;
@@ -41,7 +41,7 @@ final class DNSSocket implements Closeable {
     public static final int QUERY_TIMEOUT = 5_000;
 
     private final Semaphore semaphore = new Semaphore(1);
-    private final Map<Integer, SettableFuture<DNSMessage>> inFlightQueries = new HashMap<>();
+    private final Map<Integer, SettableFuture<DnsMessage>> inFlightQueries = new HashMap<>();
     private final Socket socket;
     private final DataInputStream dataInputStream;
     private final DataOutputStream dataOutputStream;
@@ -59,8 +59,8 @@ final class DNSSocket implements Closeable {
     private void readDNSMessages() {
         try {
             while (socket.isConnected()) {
-                final DNSMessage response = readDNSMessage();
-                final SettableFuture<DNSMessage> future;
+                final DnsMessage response = readDNSMessage();
+                final SettableFuture<DnsMessage> future;
                 synchronized (inFlightQueries) {
                     future = inFlightQueries.remove(response.id);
                 }
@@ -78,10 +78,10 @@ final class DNSSocket implements Closeable {
 
     private void evictInFlightQueries(final Exception e) {
         synchronized (inFlightQueries) {
-            final Iterator<Map.Entry<Integer, SettableFuture<DNSMessage>>> iterator =
+            final Iterator<Map.Entry<Integer, SettableFuture<DnsMessage>>> iterator =
                     inFlightQueries.entrySet().iterator();
             while (iterator.hasNext()) {
-                final Map.Entry<Integer, SettableFuture<DNSMessage>> entry = iterator.next();
+                final Map.Entry<Integer, SettableFuture<DnsMessage>> entry = iterator.next();
                 entry.getValue().setException(e);
                 iterator.remove();
             }
@@ -95,14 +95,11 @@ final class DNSSocket implements Closeable {
     }
 
     public static DNSSocket connect(final DNSServer dnsServer) throws IOException {
-        switch (dnsServer.uniqueTransport()) {
-            case TCP:
-                return connectTcpSocket(dnsServer);
-            case TLS:
-                return connectTlsSocket(dnsServer);
-            default:
-                throw new IllegalStateException("This is not a socket based transport");
-        }
+        return switch (dnsServer.uniqueTransport()) {
+            case TCP -> connectTcpSocket(dnsServer);
+            case TLS -> connectTlsSocket(dnsServer);
+            default -> throw new IllegalStateException("This is not a socket based transport");
+        };
     }
 
     private static DNSSocket connectTcpSocket(final DNSServer dnsServer) throws IOException {
@@ -133,10 +130,9 @@ final class DNSSocket implements Closeable {
             sslSocket.startHandshake();
             final SSLSession session = sslSocket.getSession();
             final Certificate[] peerCertificates = session.getPeerCertificates();
-            if (peerCertificates.length == 0 || !(peerCertificates[0] instanceof X509Certificate)) {
+            if (peerCertificates.length == 0 || !(peerCertificates[0] instanceof X509Certificate certificate)) {
                 throw new IOException("Peer did not provide X509 certificates");
             }
-            final X509Certificate certificate = (X509Certificate) peerCertificates[0];
             if (!OkHostnameVerifier.strictInstance().verify(dnsServer.hostname, certificate)) {
                 throw new SSLPeerUnverifiedException("Peer did not provide valid certificates");
             }
@@ -144,7 +140,7 @@ final class DNSSocket implements Closeable {
         return DNSSocket.of(sslSocket);
     }
 
-    public DNSMessage query(final DNSMessage query) throws IOException, InterruptedException {
+    public DnsMessage query(final DnsMessage query) throws IOException, InterruptedException {
         try {
             return queryAsync(query).get(QUERY_TIMEOUT, TimeUnit.MILLISECONDS);
         } catch (final ExecutionException e) {
@@ -159,9 +155,9 @@ final class DNSSocket implements Closeable {
         }
     }
 
-    public ListenableFuture<DNSMessage> queryAsync(final DNSMessage query)
+    public ListenableFuture<DnsMessage> queryAsync(final DnsMessage query)
             throws InterruptedException, IOException {
-        final SettableFuture<DNSMessage> responseFuture = SettableFuture.create();
+        final SettableFuture<DnsMessage> responseFuture = SettableFuture.create();
         synchronized (this.inFlightQueries) {
             this.inFlightQueries.put(query.id, responseFuture);
         }
@@ -175,7 +171,7 @@ final class DNSSocket implements Closeable {
         return responseFuture;
     }
 
-    private DNSMessage readDNSMessage() throws IOException {
+    private DnsMessage readDNSMessage() throws IOException {
         final int length = this.dataInputStream.readUnsignedShort();
         byte[] data = new byte[length];
         int read = 0;

src/main/java/de/gultsch/minidns/NetworkDataSource.java πŸ”—

@@ -11,13 +11,14 @@ import com.google.common.cache.LoadingCache;
 import com.google.common.cache.RemovalListener;
 import com.google.common.collect.ImmutableList;
 
-import de.measite.minidns.DNSMessage;
-import de.measite.minidns.MiniDNSException;
-import de.measite.minidns.source.DNSDataSource;
-import de.measite.minidns.util.MultipleIoException;
-
 import eu.siacs.conversations.Config;
 
+import org.minidns.MiniDnsException;
+import org.minidns.dnsmessage.DnsMessage;
+import org.minidns.dnsqueryresult.DnsQueryResult;
+import org.minidns.dnsqueryresult.StandardDnsQueryResult;
+import org.minidns.util.MultipleIoException;
+
 import java.io.IOException;
 import java.net.DatagramPacket;
 import java.net.DatagramSocket;
@@ -28,7 +29,7 @@ import java.util.Map;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 
-public class NetworkDataSource extends DNSDataSource {
+public class NetworkDataSource extends org.minidns.source.NetworkDataSource {
 
     private static final LoadingCache<DNSServer, DNSSocket> socketCache =
             CacheBuilder.newBuilder()
@@ -66,7 +67,8 @@ public class NetworkDataSource extends DNSDataSource {
     }
 
     @Override
-    public DNSMessage query(final DNSMessage message, final InetAddress address, final int port)
+    public StandardDnsQueryResult query(
+            final DnsMessage message, final InetAddress address, final int port)
             throws IOException {
         final List<Transport> transports = transportsForPort(port);
         Log.w(
@@ -80,16 +82,21 @@ public class NetworkDataSource extends DNSDataSource {
         return query(message, new DNSServer(address, port, transports));
     }
 
-    public DNSMessage query(final DNSMessage message, final DNSServer dnsServer)
+    public StandardDnsQueryResult query(final DnsMessage message, final DNSServer dnsServer)
             throws IOException {
         Log.d(Config.LOGTAG, "using " + dnsServer);
         final List<IOException> ioExceptions = new ArrayList<>();
         for (final Transport transport : dnsServer.transports) {
             try {
-                final DNSMessage response =
+                final DnsMessage response =
                         queryWithUniqueTransport(message, dnsServer.asUniqueTransport(transport));
                 if (response != null && !response.truncated) {
-                    return response;
+                    return new StandardDnsQueryResult(
+                            dnsServer.inetAddress,
+                            dnsServer.port,
+                            transportToMethod(transport),
+                            message,
+                            response);
                 }
             } catch (final IOException e) {
                 ioExceptions.add(e);
@@ -101,23 +108,26 @@ public class NetworkDataSource extends DNSDataSource {
         return null;
     }
 
-    private DNSMessage queryWithUniqueTransport(final DNSMessage message, final DNSServer dnsServer)
+    private static DnsQueryResult.QueryMethod transportToMethod(final Transport transport) {
+        return switch (transport) {
+            case UDP -> DnsQueryResult.QueryMethod.udp;
+            default -> DnsQueryResult.QueryMethod.tcp;
+        };
+    }
+
+    private DnsMessage queryWithUniqueTransport(final DnsMessage message, final DNSServer dnsServer)
             throws IOException, InterruptedException {
         final Transport transport = dnsServer.uniqueTransport();
-        switch (transport) {
-            case UDP:
-                return queryUdp(message, dnsServer.inetAddress, dnsServer.port);
-            case TCP:
-            case TLS:
-                return queryDnsSocket(message, dnsServer);
-            default:
-                throw new IOException(
-                        String.format("Transport %s has not been implemented", transport));
-        }
+        return switch (transport) {
+            case UDP -> queryUdp(message, dnsServer.inetAddress, dnsServer.port);
+            case TCP, TLS -> queryDnsSocket(message, dnsServer);
+            default -> throw new IOException(
+                    String.format("Transport %s has not been implemented", transport));
+        };
     }
 
-    protected DNSMessage queryUdp(
-            final DNSMessage message, final InetAddress address, final int port)
+    protected DnsMessage queryUdp(
+            final DnsMessage message, final InetAddress address, final int port)
             throws IOException {
         final DatagramPacket request = message.asDatagram(address, port);
         final byte[] buffer = new byte[udpPayloadSize];
@@ -126,15 +136,15 @@ public class NetworkDataSource extends DNSDataSource {
             socket.send(request);
             final DatagramPacket response = new DatagramPacket(buffer, buffer.length);
             socket.receive(response);
-            final DNSMessage dnsMessage = readDNSMessage(response.getData());
+            final DnsMessage dnsMessage = readDNSMessage(response.getData());
             if (dnsMessage.id != message.id) {
-                throw new MiniDNSException.IdMismatch(message, dnsMessage);
+                throw new MiniDnsException.IdMismatch(message, dnsMessage);
             }
             return dnsMessage;
         }
     }
 
-    protected DNSMessage queryDnsSocket(final DNSMessage message, final DNSServer dnsServer)
+    protected DnsMessage queryDnsSocket(final DnsMessage message, final DNSServer dnsServer)
             throws IOException, InterruptedException {
         final DNSSocket cachedDnsSocket = socketCache.getIfPresent(dnsServer);
         if (cachedDnsSocket != null) {
@@ -159,9 +169,9 @@ public class NetworkDataSource extends DNSDataSource {
         }
     }
 
-    public static DNSMessage readDNSMessage(final byte[] bytes) throws IOException {
+    public static DnsMessage readDNSMessage(final byte[] bytes) throws IOException {
         try {
-            return new DNSMessage(bytes);
+            return new DnsMessage(bytes);
         } catch (final IllegalArgumentException e) {
             throw new IOException(Throwables.getRootCause(e));
         }

src/main/java/de/gultsch/minidns/ResolutionUnsuccessfulException.java πŸ”—

@@ -0,0 +1,35 @@
+/*
+ * Copyright 2015-2022 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package de.gultsch.minidns;
+
+import org.minidns.MiniDnsException;
+import org.minidns.dnsmessage.Question;
+import org.minidns.dnsmessage.DnsMessage.RESPONSE_CODE;
+
+import java.io.Serial;
+
+public class ResolutionUnsuccessfulException extends MiniDnsException {
+
+    /**
+     * 
+     */
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    public final Question question;
+    public final RESPONSE_CODE responseCode;
+
+    public ResolutionUnsuccessfulException(Question question, RESPONSE_CODE responseCode) {
+        super("Asking for " + question + " yielded an error response " + responseCode);
+        this.question = question;
+        this.responseCode = responseCode;
+    }
+}

src/main/java/de/gultsch/minidns/ResolverResult.java πŸ”—

@@ -0,0 +1,178 @@
+/*
+ * Copyright 2015-2022 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package de.gultsch.minidns;
+
+import java.util.Collections;
+import java.util.Set;
+
+import org.minidns.MiniDnsException;
+import org.minidns.MiniDnsException.NullResultException;
+import org.minidns.dnsmessage.DnsMessage;
+import org.minidns.dnsmessage.Question;
+import org.minidns.dnsqueryresult.DnsQueryResult;
+import org.minidns.dnsmessage.DnsMessage.RESPONSE_CODE;
+import org.minidns.dnssec.DnssecResultNotAuthenticException;
+import org.minidns.dnssec.DnssecUnverifiedReason;
+import org.minidns.record.Data;
+
+public class ResolverResult<D extends Data> {
+
+    protected final Question question;
+    private final RESPONSE_CODE responseCode;
+    private final Set<D> data;
+    private final boolean isAuthenticData;
+    protected final Set<DnssecUnverifiedReason> unverifiedReasons;
+    protected final DnsMessage answer;
+    protected final DnsQueryResult result;
+
+    public ResolverResult(Question question, DnsQueryResult result, Set<DnssecUnverifiedReason> unverifiedReasons) throws NullResultException {
+        // TODO: Is this null check still needed?
+        if (result == null) {
+            throw new MiniDnsException.NullResultException(question.asMessageBuilder().build());
+        }
+
+        this.result = result;
+
+        DnsMessage answer = result.response;
+        this.question = question;
+        this.responseCode = answer.responseCode;
+        this.answer = answer;
+
+        Set<D> r = answer.getAnswersFor(question);
+        if (r == null) {
+            this.data = Collections.emptySet();
+        } else {
+            this.data = Collections.unmodifiableSet(r);
+        }
+
+        if (unverifiedReasons == null) {
+            this.unverifiedReasons = null;
+            isAuthenticData = false;
+        } else {
+            this.unverifiedReasons = Collections.unmodifiableSet(unverifiedReasons);
+            isAuthenticData = this.unverifiedReasons.isEmpty();
+        }
+    }
+
+    public boolean wasSuccessful() {
+        return responseCode == RESPONSE_CODE.NO_ERROR;
+    }
+
+    public Set<D> getAnswers() {
+        throwIseIfErrorResponse();
+        return data;
+    }
+
+    public Set<D> getAnswersOrEmptySet() {
+        return data;
+    }
+
+    public RESPONSE_CODE getResponseCode() {
+        return responseCode;
+    }
+
+    public boolean isAuthenticData() {
+        throwIseIfErrorResponse();
+        return isAuthenticData;
+    }
+
+    /**
+     * Get the reasons the result could not be verified if any exists.
+     *
+     * @return The reasons the result could not be verified or <code>null</code>.
+     */
+    public Set<DnssecUnverifiedReason> getUnverifiedReasons() {
+        throwIseIfErrorResponse();
+        return unverifiedReasons;
+    }
+
+    public Question getQuestion() {
+        return question;
+    }
+
+    public void throwIfErrorResponse() throws ResolutionUnsuccessfulException {
+        ResolutionUnsuccessfulException resolutionUnsuccessfulException = getResolutionUnsuccessfulException();
+        if (resolutionUnsuccessfulException != null) throw resolutionUnsuccessfulException;
+    }
+
+    private ResolutionUnsuccessfulException resolutionUnsuccessfulException;
+
+    public ResolutionUnsuccessfulException getResolutionUnsuccessfulException() {
+        if (wasSuccessful()) return null;
+
+        if (resolutionUnsuccessfulException == null) {
+            resolutionUnsuccessfulException = new ResolutionUnsuccessfulException(question, responseCode);
+        }
+
+        return resolutionUnsuccessfulException;
+    }
+
+    private DnssecResultNotAuthenticException dnssecResultNotAuthenticException;
+
+    public DnssecResultNotAuthenticException getDnssecResultNotAuthenticException() {
+        if (!wasSuccessful())
+            return null;
+        if (isAuthenticData)
+            return null;
+
+        if (dnssecResultNotAuthenticException == null) {
+            dnssecResultNotAuthenticException = DnssecResultNotAuthenticException.from(getUnverifiedReasons());
+        }
+
+        return dnssecResultNotAuthenticException;
+    }
+
+    /**
+     * Get the raw answer DNS message we received. <b>This is likely not what you want</b>, try {@link #getAnswers()} instead.
+     *
+     * @return the raw answer DNS Message.
+     * @see #getAnswers()
+     */
+    public DnsMessage getRawAnswer() {
+        return answer;
+    }
+
+    public DnsQueryResult getDnsQueryResult() {
+        return result;
+    }
+
+    @Override
+    public final String toString() {
+        StringBuilder sb = new StringBuilder();
+
+        sb.append(getClass().getName()).append('\n')
+               .append("Question: ").append(question).append('\n')
+               .append("Response Code: ").append(responseCode).append('\n');
+
+        if (responseCode == RESPONSE_CODE.NO_ERROR) {
+            if (isAuthenticData) {
+                sb.append("Results verified via DNSSEC\n");
+            }
+            if (hasUnverifiedReasons()) {
+                sb.append(unverifiedReasons).append('\n');
+            }
+            sb.append(answer.answerSection);
+        }
+
+        return sb.toString();
+    }
+
+    boolean hasUnverifiedReasons() {
+        return unverifiedReasons != null && !unverifiedReasons.isEmpty();
+    }
+
+    protected void throwIseIfErrorResponse() {
+        ResolutionUnsuccessfulException resolutionUnsuccessfulException = getResolutionUnsuccessfulException();
+        if (resolutionUnsuccessfulException != null)
+            throw new IllegalStateException("Can not perform operation because the DNS resolution was unsuccessful",
+                    resolutionUnsuccessfulException);
+    }
+}

src/main/java/eu/siacs/conversations/Conversations.java πŸ”—

@@ -1,5 +1,6 @@
 package eu.siacs.conversations;
 
+import android.annotation.SuppressLint;
 import android.app.Application;
 import android.content.Context;
 import android.content.SharedPreferences;
@@ -14,9 +15,17 @@ import eu.siacs.conversations.utils.ExceptionHelper;
 
 public class Conversations extends Application {
 
+    @SuppressLint("StaticFieldLeak")
+    private static Context CONTEXT;
+
+    public static Context getContext() {
+        return Conversations.CONTEXT;
+    }
+
     @Override
     public void onCreate() {
         super.onCreate();
+        CONTEXT = this.getApplicationContext();
         ExceptionHelper.init(getApplicationContext());
         applyThemeSettings();
     }

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java πŸ”—

@@ -1267,7 +1267,6 @@ public class XmppConnectionService extends Service {
         } catch (Throwable throwable) {
             Log.e(Config.LOGTAG, "unable to initialize security provider", throwable);
         }
-        Resolver.init(this);
         updateMemorizingTrustManager();
         final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
         final int cacheSize = maxMemory / 8;

src/main/java/eu/siacs/conversations/utils/AndroidUsingExecLowPriority.java πŸ”—

@@ -1,92 +0,0 @@
-/*
- * Copyright 2015-2016 the original author or authors
- *
- * This software is licensed under the Apache License, Version 2.0,
- * the GNU Lesser General Public License version 2 or later ("LGPL")
- * and the WTFPL.
- * You may choose either license to govern your use of this software only
- * upon the condition that you accept all of the terms of either
- * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
- */
-package eu.siacs.conversations.utils;
-
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.LineNumberReader;
-import java.net.InetAddress;
-import java.util.HashSet;
-import java.util.logging.Level;
-
-import de.measite.minidns.dnsserverlookup.AbstractDNSServerLookupMechanism;
-import de.measite.minidns.dnsserverlookup.AndroidUsingReflection;
-import de.measite.minidns.dnsserverlookup.DNSServerLookupMechanism;
-import de.measite.minidns.util.PlatformDetection;
-
-/**
- * Try to retrieve the list of DNS server by executing getprop.
- */
-public class AndroidUsingExecLowPriority extends AbstractDNSServerLookupMechanism {
-
-	public static final DNSServerLookupMechanism INSTANCE = new AndroidUsingExecLowPriority();
-	public static final int PRIORITY = AndroidUsingReflection.PRIORITY + 1;
-
-	private AndroidUsingExecLowPriority() {
-		super(AndroidUsingExecLowPriority.class.getSimpleName(), PRIORITY);
-	}
-
-	@Override
-	public String[] getDnsServerAddresses() {
-		try {
-			Process process = Runtime.getRuntime().exec("getprop");
-			InputStream inputStream = process.getInputStream();
-			LineNumberReader lnr = new LineNumberReader(
-					new InputStreamReader(inputStream));
-			String line;
-			HashSet<String> server = new HashSet<>(6);
-			while ((line = lnr.readLine()) != null) {
-				int split = line.indexOf("]: [");
-				if (split == -1) {
-					continue;
-				}
-				String property = line.substring(1, split);
-				String value = line.substring(split + 4, line.length() - 1);
-
-				if (value.isEmpty()) {
-					continue;
-				}
-
-				if (property.endsWith(".dns") || property.endsWith(".dns1") ||
-						property.endsWith(".dns2") || property.endsWith(".dns3") ||
-						property.endsWith(".dns4")) {
-
-					// normalize the address
-
-					InetAddress ip = InetAddress.getByName(value);
-
-					if (ip == null) continue;
-
-					value = ip.getHostAddress();
-
-					if (value == null) continue;
-					if (value.length() == 0) continue;
-
-					server.add(value);
-				}
-			}
-			if (server.size() > 0) {
-				return server.toArray(new String[server.size()]);
-			}
-		} catch (IOException e) {
-			LOGGER.log(Level.WARNING, "Exception in findDNSByExec", e);
-		}
-		return null;
-	}
-
-	@Override
-	public boolean isAvailable() {
-		return PlatformDetection.isAndroid();
-	}
-
-}

src/main/java/eu/siacs/conversations/utils/AndroidUsingLinkProperties.java πŸ”—

@@ -1,90 +0,0 @@
-package eu.siacs.conversations.utils;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.net.ConnectivityManager;
-import android.net.LinkProperties;
-import android.net.Network;
-import android.net.NetworkInfo;
-import android.net.RouteInfo;
-import android.os.Build;
-
-import java.net.Inet4Address;
-import java.net.InetAddress;
-import java.util.ArrayList;
-import java.util.List;
-
-import de.measite.minidns.dnsserverlookup.AbstractDNSServerLookupMechanism;
-import de.measite.minidns.dnsserverlookup.AndroidUsingExec;
-
-public class AndroidUsingLinkProperties extends AbstractDNSServerLookupMechanism {
-
-    private final Context context;
-
-    AndroidUsingLinkProperties(Context context) {
-        super(AndroidUsingLinkProperties.class.getSimpleName(), AndroidUsingExec.PRIORITY - 1);
-        this.context = context;
-    }
-
-    @Override
-    public boolean isAvailable() {
-        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
-    }
-
-    @Override
-    @TargetApi(21)
-    public String[] getDnsServerAddresses() {
-        final ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
-        final Network[] networks = connectivityManager == null ? null : connectivityManager.getAllNetworks();
-        if (networks == null) {
-            return new String[0];
-        }
-        final Network activeNetwork = getActiveNetwork(connectivityManager);
-        final List<String> servers = new ArrayList<>();
-        int vpnOffset = 0;
-        for(Network network : networks) {
-            LinkProperties linkProperties = connectivityManager.getLinkProperties(network);
-            if (linkProperties == null) {
-                continue;
-            }
-            final NetworkInfo networkInfo = connectivityManager.getNetworkInfo(network);
-            final boolean isActiveNetwork = network.equals(activeNetwork);
-            final boolean isVpn = networkInfo != null && networkInfo.getType() == ConnectivityManager.TYPE_VPN;
-            if (isActiveNetwork && isVpn) {
-                final List<String> tmp = getIPv4First(linkProperties.getDnsServers());
-                servers.addAll(0, tmp);
-                vpnOffset += tmp.size();
-            } else if (hasDefaultRoute(linkProperties) || isActiveNetwork || activeNetwork == null || isVpn) {
-                servers.addAll(vpnOffset, getIPv4First(linkProperties.getDnsServers()));
-            }
-        }
-        return servers.toArray(new String[0]);
-    }
-
-    @TargetApi(23)
-    private static Network getActiveNetwork(ConnectivityManager cm) {
-        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? cm.getActiveNetwork() : null;
-    }
-
-    private static List<String> getIPv4First(List<InetAddress> in) {
-        List<String> out = new ArrayList<>();
-        for(InetAddress address : in) {
-            if (address instanceof Inet4Address) {
-                out.add(0, address.getHostAddress());
-            } else {
-                out.add(address.getHostAddress());
-            }
-        }
-        return out;
-    }
-
-    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
-    private static boolean hasDefaultRoute(LinkProperties linkProperties) {
-        for(RouteInfo route: linkProperties.getRoutes()) {
-            if (route.isDefaultRoute()) {
-                return true;
-            }
-        }
-        return false;
-    }
-}

src/main/java/eu/siacs/conversations/utils/Resolver.java πŸ”—

@@ -6,85 +6,83 @@ import android.util.Log;
 
 import androidx.annotation.NonNull;
 
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Objects;
 import com.google.common.base.Strings;
-import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
 import com.google.common.net.InetAddresses;
 import com.google.common.primitives.Ints;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import de.gultsch.minidns.AndroidDNSClient;
+import de.gultsch.minidns.ResolverResult;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.Conversations;
+import eu.siacs.conversations.xmpp.Jid;
+
+import org.minidns.dnsmessage.Question;
+import org.minidns.dnsname.DnsName;
+import org.minidns.dnsqueryresult.DnsQueryResult;
+import org.minidns.record.A;
+import org.minidns.record.AAAA;
+import org.minidns.record.CNAME;
+import org.minidns.record.Data;
+import org.minidns.record.InternetAddressRR;
+import org.minidns.record.Record;
+import org.minidns.record.SRV;
 
-import java.io.IOException;
-import java.lang.reflect.Field;
 import java.net.Inet4Address;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
-import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
-
-import de.gultsch.minidns.AndroidDNSClient;
-import de.measite.minidns.AbstractDNSClient;
-import de.measite.minidns.DNSCache;
-import de.measite.minidns.DNSClient;
-import de.measite.minidns.DNSName;
-import de.measite.minidns.Question;
-import de.measite.minidns.Record;
-import de.measite.minidns.cache.LRUCache;
-import de.measite.minidns.dnssec.DNSSECResultNotAuthenticException;
-import de.measite.minidns.dnsserverlookup.AndroidUsingExec;
-import de.measite.minidns.hla.DnssecResolverApi;
-import de.measite.minidns.hla.ResolverApi;
-import de.measite.minidns.hla.ResolverResult;
-import de.measite.minidns.iterative.ReliableDNSClient;
-import de.measite.minidns.record.A;
-import de.measite.minidns.record.AAAA;
-import de.measite.minidns.record.CNAME;
-import de.measite.minidns.record.Data;
-import de.measite.minidns.record.InternetAddressRR;
-import de.measite.minidns.record.SRV;
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.R;
-import eu.siacs.conversations.services.XmppConnectionService;
-import eu.siacs.conversations.xmpp.Jid;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 
 public class Resolver {
 
+    private static final Comparator<Result> RESULT_COMPARATOR =
+            (left, right) -> {
+                if (left.priority == right.priority) {
+                    if (left.directTls == right.directTls) {
+                        if (left.ip == null && right.ip == null) {
+                            return 0;
+                        } else if (left.ip != null && right.ip != null) {
+                            if (left.ip instanceof Inet4Address
+                                    && right.ip instanceof Inet4Address) {
+                                return 0;
+                            } else {
+                                return left.ip instanceof Inet4Address ? -1 : 1;
+                            }
+                        } else {
+                            return left.ip != null ? -1 : 1;
+                        }
+                    } else {
+                        return left.directTls ? -1 : 1;
+                    }
+                } else {
+                    return left.priority - right.priority;
+                }
+            };
+
+    private static final ExecutorService DNS_QUERY_EXECUTOR = Executors.newFixedThreadPool(12);
+
     public static final int DEFAULT_PORT_XMPP = 5222;
 
     private static final String DIRECT_TLS_SERVICE = "_xmpps-client";
     private static final String STARTTLS_SERVICE = "_xmpp-client";
 
-    private static XmppConnectionService SERVICE = null;
-
-
-    public static void init(XmppConnectionService service) {
-        Resolver.SERVICE = service;
-        DNSClient.removeDNSServerLookupMechanism(AndroidUsingExec.INSTANCE);
-        DNSClient.addDnsServerLookupMechanism(AndroidUsingExecLowPriority.INSTANCE);
-        DNSClient.addDnsServerLookupMechanism(new AndroidUsingLinkProperties(service));
-        final AbstractDNSClient client = ResolverApi.INSTANCE.getClient();
-        if (client instanceof ReliableDNSClient) {
-            disableHardcodedDnsServers((ReliableDNSClient) client);
-        }
-    }
-
-    private static void disableHardcodedDnsServers(ReliableDNSClient reliableDNSClient) {
-        try {
-            final Field dnsClientField = ReliableDNSClient.class.getDeclaredField("dnsClient");
-            dnsClientField.setAccessible(true);
-            final DNSClient dnsClient = (DNSClient) dnsClientField.get(reliableDNSClient);
-            if (dnsClient != null) {
-                dnsClient.getDataSource().setTimeout(3000);
-            }
-            final Field useHardcodedDnsServers = DNSClient.class.getDeclaredField("useHardcodedDnsServers");
-            useHardcodedDnsServers.setAccessible(true);
-            useHardcodedDnsServers.setBoolean(dnsClient, false);
-        } catch (NoSuchFieldException | IllegalAccessException e) {
-            Log.e(Config.LOGTAG, "Unable to disable hardcoded DNS servers", e);
-        }
-    }
-
     public static List<Result> fromHardCoded(final String hostname, final int port) {
         final Result result = new Result();
-        result.hostname = DNSName.from(hostname);
+        result.hostname = DnsName.from(hostname);
         result.port = port;
         result.directTls = useDirectTls(port);
         result.authenticated = true;
@@ -92,221 +90,236 @@ public class Resolver {
     }
 
     public static void checkDomain(final Jid jid) {
-        DNSName.from(jid.getDomain());
+        DnsName.from(jid.getDomain());
     }
 
     public static boolean invalidHostname(final String hostname) {
         try {
-            DNSName.from(hostname);
+            DnsName.from(hostname);
             return false;
         } catch (IllegalArgumentException e) {
             return true;
         }
     }
 
-    public static void clearCache() {
-        final AbstractDNSClient client = ResolverApi.INSTANCE.getClient();
-        final DNSCache dnsCache = client.getCache();
-        if (dnsCache instanceof LRUCache) {
-            Log.d(Config.LOGTAG,"clearing DNS cache");
-            ((LRUCache) dnsCache).clear();
-        }
-    }
-
+    public static void clearCache() {}
 
     public static boolean useDirectTls(final int port) {
         return port == 443 || port == 5223;
     }
 
     public static List<Result> resolve(final String domain) {
-        final  List<Result> ipResults = fromIpAddress(domain);
-        if (ipResults.size() > 0) {
+        final List<Result> ipResults = fromIpAddress(domain);
+        if (!ipResults.isEmpty()) {
             return ipResults;
         }
-        final List<Result> results = new ArrayList<>();
-        final List<Result> fallbackResults = new ArrayList<>();
-        final Thread[] threads = new Thread[3];
-        threads[0] = new Thread(() -> {
-            try {
-                final List<Result> list = resolveSrv(domain, true);
-                synchronized (results) {
-                    results.addAll(list);
-                }
-            } catch (final Throwable throwable) {
-                if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) {
-                    Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (direct TLS)", throwable);
-                }
-            }
-        });
-        threads[1] = new Thread(() -> {
-            try {
-                final List<Result> list = resolveSrv(domain, false);
-                synchronized (results) {
-                    results.addAll(list);
-                }
-            } catch (final Throwable throwable) {
-                if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) {
-                    Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (STARTTLS)", throwable);
-                }
-            }
-        });
-        threads[2] = new Thread(() -> {
-            List<Result> list = resolveNoSrvRecords(DNSName.from(domain), true);
-            synchronized (fallbackResults) {
-                fallbackResults.addAll(list);
-            }
-        });
-        for (final Thread thread : threads) {
-            thread.start();
-        }
+
+        final var startTls = resolveSrvAsFuture(domain, false);
+        final var directTls = resolveSrvAsFuture(domain, true);
+
+        final var combined = merge(ImmutableList.of(startTls, directTls));
+
+        final var combinedWithFallback =
+                Futures.transformAsync(
+                        combined,
+                        results -> {
+                            if (results.isEmpty()) {
+                                return resolveNoSrvAsFuture(DnsName.from(domain), true);
+                            } else {
+                                return Futures.immediateFuture(results);
+                            }
+                        },
+                        MoreExecutors.directExecutor());
+        final var orderedFuture =
+                Futures.transform(
+                        combinedWithFallback,
+                        all -> Ordering.from(RESULT_COMPARATOR).immutableSortedCopy(all),
+                        MoreExecutors.directExecutor());
         try {
-            threads[0].join();
-            threads[1].join();
-            if (results.size() > 0) {
-                threads[2].interrupt();
-                synchronized (results) {
-                    Collections.sort(results);
-                    Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + results);
-                    return results;
-                }
-            } else {
-                threads[2].join();
-                synchronized (fallbackResults) {
-                    Collections.sort(fallbackResults);
-                    Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + fallbackResults);
-                    return fallbackResults;
-                }
-            }
-        } catch (InterruptedException e) {
-            for (Thread thread : threads) {
-                thread.interrupt();
-            }
+            final var ordered = orderedFuture.get();
+            Log.d(Config.LOGTAG, "Resolver (" + ordered.size() + "): " + ordered);
+            return ordered;
+        } catch (final ExecutionException e) {
+            Log.d(Config.LOGTAG, "error resolving DNS", e);
+            return Collections.emptyList();
+        } catch (final InterruptedException e) {
+            Log.d(Config.LOGTAG, "DNS resolution interrupted");
             return Collections.emptyList();
         }
     }
 
-    private static List<Result> fromIpAddress(String domain) {
-        if (!IP.matches(domain)) {
-            return Collections.emptyList();
-        }
-        try {
-            Result result = new Result();
-            result.ip = InetAddress.getByName(domain);
+    private static List<Result> fromIpAddress(final String domain) {
+        if (IP.matches(domain)) {
+            final InetAddress inetAddress;
+            try {
+                inetAddress = InetAddress.getByName(domain);
+            } catch (final UnknownHostException e) {
+                return Collections.emptyList();
+            }
+            final Result result = new Result();
+            result.ip = inetAddress;
             result.port = DEFAULT_PORT_XMPP;
             return Collections.singletonList(result);
-        } catch (UnknownHostException e) {
+        } else {
             return Collections.emptyList();
         }
     }
 
-    private static List<Result> resolveSrv(String domain, final boolean directTls) throws IOException {
-        DNSName dnsName = DNSName.from((directTls ? DIRECT_TLS_SERVICE : STARTTLS_SERVICE) + "._tcp." + domain);
-        ResolverResult<SRV> result = resolveWithFallback(dnsName, SRV.class);
-        final List<Result> results = new ArrayList<>();
-        final List<Thread> threads = new ArrayList<>();
-        for (SRV record : result.getAnswersOrEmptySet()) {
-            if (record.name.length() == 0 && record.priority == 0) {
-                continue;
-            }
-            threads.add(new Thread(() -> {
-                final List<Result> ipv4s = resolveIp(record, A.class, result.isAuthenticData(), directTls);
-                if (ipv4s.size() == 0) {
-                    Result resolverResult = Result.fromRecord(record, directTls);
-                    resolverResult.authenticated = result.isAuthenticData();
-                    ipv4s.add(resolverResult);
-                }
-                synchronized (results) {
-                    results.addAll(ipv4s);
-                }
-
-            }));
-            threads.add(new Thread(() -> {
-                final List<Result> ipv6s = resolveIp(record, AAAA.class, result.isAuthenticData(), directTls);
-                synchronized (results) {
-                    results.addAll(ipv6s);
-                }
-            }));
-        }
-        for (Thread thread : threads) {
-            thread.start();
-        }
-        for (Thread thread : threads) {
-            try {
-                thread.join();
-            } catch (InterruptedException e) {
-                return Collections.emptyList();
-            }
-        }
-        return results;
+    private static ListenableFuture<List<Result>> resolveSrvAsFuture(
+            final String domain, final boolean directTls) {
+        final DnsName dnsName =
+                DnsName.from(
+                        (directTls ? DIRECT_TLS_SERVICE : STARTTLS_SERVICE) + "._tcp." + domain);
+        final var resultFuture = resolveAsFuture(dnsName, SRV.class);
+        return Futures.transformAsync(
+                resultFuture,
+                result -> resolveIpsAsFuture(result, directTls),
+                MoreExecutors.directExecutor());
     }
 
-    private static <D extends InternetAddressRR> List<Result> resolveIp(SRV srv, Class<D> type, boolean authenticated, boolean directTls) {
-        List<Result> list = new ArrayList<>();
-        try {
-            ResolverResult<D> results = resolveWithFallback(srv.name, type, authenticated);
-            for (D record : results.getAnswersOrEmptySet()) {
-                Result resolverResult = Result.fromRecord(srv, directTls);
-                resolverResult.authenticated = results.isAuthenticData() && authenticated; //TODO technically it doesn’t matter if the IP was authenticated
-                resolverResult.ip = record.getInetAddress();
-                list.add(resolverResult);
+    @NonNull
+    private static ListenableFuture<List<Result>> resolveIpsAsFuture(
+            final ResolverResult<SRV> srvResolverResult, final boolean directTls) {
+        final ImmutableList.Builder<ListenableFuture<List<Result>>> futuresBuilder =
+                new ImmutableList.Builder<>();
+        for (final SRV record : srvResolverResult.getAnswersOrEmptySet()) {
+            if (record.target.length() == 0 && record.priority == 0) {
+                continue;
             }
-        } catch (Throwable t) {
-            Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " " + t.getMessage());
+            final var ipv4sRaw =
+                    resolveIpsAsFuture(
+                            record, A.class, srvResolverResult.isAuthenticData(), directTls);
+            final var ipv4s =
+                    Futures.transform(
+                            ipv4sRaw,
+                            results -> {
+                                if (results.isEmpty()) {
+                                    final Result resolverResult =
+                                            Result.fromRecord(record, directTls);
+                                    resolverResult.authenticated =
+                                            srvResolverResult.isAuthenticData();
+                                    return Collections.singletonList(resolverResult);
+                                } else {
+                                    return results;
+                                }
+                            },
+                            MoreExecutors.directExecutor());
+            final var ipv6s =
+                    resolveIpsAsFuture(
+                            record, AAAA.class, srvResolverResult.isAuthenticData(), directTls);
+            futuresBuilder.add(ipv4s);
+            futuresBuilder.add(ipv6s);
         }
-        return list;
+        final ImmutableList<ListenableFuture<List<Result>>> futures = futuresBuilder.build();
+        return merge(futures);
     }
 
-    private static List<Result> resolveNoSrvRecords(DNSName dnsName, boolean withCnames) {
-        final List<Result> results = new ArrayList<>();
-        try {
-            for (A a : resolveWithFallback(dnsName, A.class, false).getAnswersOrEmptySet()) {
-                results.add(Result.createDefault(dnsName, a.getInetAddress()));
-            }
-            for (AAAA aaaa : resolveWithFallback(dnsName, AAAA.class, false).getAnswersOrEmptySet()) {
-                results.add(Result.createDefault(dnsName, aaaa.getInetAddress()));
-            }
-            if (results.size() == 0 && withCnames) {
-                for (CNAME cname : resolveWithFallback(dnsName, CNAME.class, false).getAnswersOrEmptySet()) {
-                    results.addAll(resolveNoSrvRecords(cname.name, false));
-                }
-            }
-        } catch (final Throwable throwable) {
-            if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) {
-                Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + "error resolving fallback records", throwable);
-            }
-        }
-        results.add(Result.createDefault(dnsName));
-        return results;
+    private static ListenableFuture<List<Result>> merge(
+            final Collection<ListenableFuture<List<Result>>> futures) {
+        return Futures.transform(
+                Futures.successfulAsList(futures),
+                lists -> {
+                    final var builder = new ImmutableList.Builder<Result>();
+                    for (final Collection<Result> list : lists) {
+                        if (list == null) {
+                            continue;
+                        }
+                        builder.addAll(list);
+                    }
+                    return builder.build();
+                },
+                MoreExecutors.directExecutor());
     }
 
-    private static <D extends Data> ResolverResult<D> resolveWithFallback(DNSName dnsName, Class<D> type) throws IOException {
-        return resolveWithFallback(dnsName, type, validateHostname());
+    private static <D extends InternetAddressRR<?>>
+            ListenableFuture<List<Result>> resolveIpsAsFuture(
+                    final SRV srv, Class<D> type, boolean authenticated, boolean directTls) {
+        final var resultFuture = resolveAsFuture(srv.target, type);
+        return Futures.transform(
+                resultFuture,
+                result -> {
+                    final var builder = new ImmutableList.Builder<Result>();
+                    for (D record : result.getAnswersOrEmptySet()) {
+                        Result resolverResult = Result.fromRecord(srv, directTls);
+                        resolverResult.authenticated =
+                                result.isAuthenticData()
+                                        && authenticated; // TODO technically it does not matter if
+                        // the IP
+                        // was authenticated
+                        resolverResult.ip = record.getInetAddress();
+                        builder.add(resolverResult);
+                    }
+                    return builder.build();
+                },
+                MoreExecutors.directExecutor());
     }
 
-    private static <D extends Data> ResolverResult<D> resolveWithFallback(DNSName dnsName, Class<D> type, boolean validateHostname) throws IOException {
-        final Question question = new Question(dnsName, Record.TYPE.getType(type));
-        if (!validateHostname) {
-            final AndroidDNSClient androidDNSClient = new AndroidDNSClient(SERVICE);
-            final ResolverApi resolverApi = new ResolverApi(androidDNSClient);
-            return resolverApi.resolve(question);
-        }
-        try {
-            return DnssecResolverApi.INSTANCE.resolveDnssecReliable(question);
-        } catch (DNSSECResultNotAuthenticException e) {
-            Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", e);
-        } catch (IOException e) {
-            throw e;
-        } catch (Throwable throwable) {
-            Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", throwable);
+    private static ListenableFuture<List<Result>> resolveNoSrvAsFuture(
+            final DnsName dnsName, boolean cName) {
+        final ImmutableList.Builder<ListenableFuture<List<Result>>> futuresBuilder =
+                new ImmutableList.Builder<>();
+        ListenableFuture<List<Result>> aRecordResults =
+                Futures.transform(
+                        resolveAsFuture(dnsName, A.class),
+                        result ->
+                                Lists.transform(
+                                        ImmutableList.copyOf(result.getAnswersOrEmptySet()),
+                                        a -> Result.createDefault(dnsName, a.getInetAddress())),
+                        MoreExecutors.directExecutor());
+        futuresBuilder.add(aRecordResults);
+        ListenableFuture<List<Result>> aaaaRecordResults =
+                Futures.transform(
+                        resolveAsFuture(dnsName, AAAA.class),
+                        result ->
+                                Lists.transform(
+                                        ImmutableList.copyOf(result.getAnswersOrEmptySet()),
+                                        aaaa ->
+                                                Result.createDefault(
+                                                        dnsName, aaaa.getInetAddress())),
+                        MoreExecutors.directExecutor());
+        futuresBuilder.add(aaaaRecordResults);
+        if (cName) {
+            ListenableFuture<List<Result>> cNameRecordResults =
+                    Futures.transformAsync(
+                            resolveAsFuture(dnsName, CNAME.class),
+                            result -> {
+                                Collection<ListenableFuture<List<Result>>> test =
+                                        Lists.transform(
+                                                ImmutableList.copyOf(result.getAnswersOrEmptySet()),
+                                                cname -> resolveNoSrvAsFuture(cname.target, false));
+                                return merge(test);
+                            },
+                            MoreExecutors.directExecutor());
+            futuresBuilder.add(cNameRecordResults);
         }
-        return ResolverApi.INSTANCE.resolve(question);
+        final ImmutableList<ListenableFuture<List<Result>>> futures = futuresBuilder.build();
+        final var noSrvFallbacks = merge(futures);
+        return Futures.transform(
+                noSrvFallbacks,
+                results -> {
+                    if (results.isEmpty()) {
+                        return Collections.singletonList(Result.createDefault(dnsName));
+                    } else {
+                        return results;
+                    }
+                },
+                MoreExecutors.directExecutor());
     }
 
-    private static boolean validateHostname() {
-        return SERVICE != null && SERVICE.getBooleanPreference("validate_hostname", R.bool.validate_hostname);
+    private static <D extends Data> ListenableFuture<ResolverResult<D>> resolveAsFuture(
+            final DnsName dnsName, final Class<D> type) {
+        return Futures.submit(
+                () -> {
+                    final Question question = new Question(dnsName, Record.TYPE.getType(type));
+                    final AndroidDNSClient androidDNSClient =
+                            new AndroidDNSClient(Conversations.getContext());
+                    final DnsQueryResult dnsQueryResult = androidDNSClient.query(question);
+                    return new ResolverResult<D>(question, dnsQueryResult, null);
+                },
+                DNS_QUERY_EXECUTOR);
     }
 
-    public static class Result implements Comparable<Result> {
+    public static class Result {
         public static final String DOMAIN = "domain";
         public static final String IP = "ip";
         public static final String HOSTNAME = "hostname";
@@ -315,7 +328,7 @@ public class Resolver {
         public static final String DIRECT_TLS = "directTls";
         public static final String AUTHENTICATED = "authenticated";
         private InetAddress ip;
-        private DNSName hostname;
+        private DnsName hostname;
         private int port = DEFAULT_PORT_XMPP;
         private boolean directTls = false;
         private boolean authenticated = false;
@@ -330,7 +343,7 @@ public class Resolver {
             return result;
         }
 
-        static Result createDefault(DNSName hostname, InetAddress ip) {
+        static Result createDefault(DnsName hostname, InetAddress ip) {
             Result result = new Result();
             result.port = DEFAULT_PORT_XMPP;
             result.hostname = hostname;
@@ -338,23 +351,24 @@ public class Resolver {
             return result;
         }
 
-        static Result createDefault(DNSName hostname) {
+        static Result createDefault(DnsName hostname) {
             return createDefault(hostname, null);
         }
 
         public static Result fromCursor(Cursor cursor) {
             final Result result = new Result();
             try {
-                result.ip = InetAddress.getByAddress(cursor.getBlob(cursor.getColumnIndex(IP)));
+                result.ip =
+                        InetAddress.getByAddress(cursor.getBlob(cursor.getColumnIndexOrThrow(IP)));
             } catch (UnknownHostException e) {
                 result.ip = null;
             }
-            final String hostname = cursor.getString(cursor.getColumnIndex(HOSTNAME));
-            result.hostname = hostname == null ? null : DNSName.from(hostname);
-            result.port = cursor.getInt(cursor.getColumnIndex(PORT));
-            result.priority = cursor.getInt(cursor.getColumnIndex(PRIORITY));
-            result.authenticated = cursor.getInt(cursor.getColumnIndex(AUTHENTICATED)) > 0;
-            result.directTls = cursor.getInt(cursor.getColumnIndex(DIRECT_TLS)) > 0;
+            final String hostname = cursor.getString(cursor.getColumnIndexOrThrow(HOSTNAME));
+            result.hostname = hostname == null ? null : DnsName.from(hostname);
+            result.port = cursor.getInt(cursor.getColumnIndexOrThrow(PORT));
+            result.priority = cursor.getInt(cursor.getColumnIndexOrThrow(PRIORITY));
+            result.authenticated = cursor.getInt(cursor.getColumnIndexOrThrow(AUTHENTICATED)) > 0;
+            result.directTls = cursor.getInt(cursor.getColumnIndexOrThrow(DIRECT_TLS)) > 0;
             return result;
         }
 
@@ -362,26 +376,18 @@ public class Resolver {
         public boolean equals(Object o) {
             if (this == o) return true;
             if (o == null || getClass() != o.getClass()) return false;
-
             Result result = (Result) o;
-
-            if (port != result.port) return false;
-            if (directTls != result.directTls) return false;
-            if (authenticated != result.authenticated) return false;
-            if (priority != result.priority) return false;
-            if (ip != null ? !ip.equals(result.ip) : result.ip != null) return false;
-            return hostname != null ? hostname.equals(result.hostname) : result.hostname == null;
+            return port == result.port
+                    && directTls == result.directTls
+                    && authenticated == result.authenticated
+                    && priority == result.priority
+                    && Objects.equal(ip, result.ip)
+                    && Objects.equal(hostname, result.hostname);
         }
 
         @Override
         public int hashCode() {
-            int result = ip != null ? ip.hashCode() : 0;
-            result = 31 * result + (hostname != null ? hostname.hashCode() : 0);
-            result = 31 * result + port;
-            result = 31 * result + (directTls ? 1 : 0);
-            result = 31 * result + (authenticated ? 1 : 0);
-            result = 31 * result + priority;
-            return result;
+            return Objects.hashCode(ip, hostname, port, directTls, authenticated, priority);
         }
 
         public InetAddress getIp() {
@@ -392,7 +398,7 @@ public class Resolver {
             return port;
         }
 
-        public DNSName getHostname() {
+        public DnsName getHostname() {
             return hostname;
         }
 
@@ -405,38 +411,16 @@ public class Resolver {
         }
 
         @Override
+        @NonNull
         public String toString() {
-            return "Result{" +
-                    "ip='" + (ip == null ? null : ip.getHostAddress()) + '\'' +
-                    ", hostame='" + (hostname == null ? null : hostname.toString()) + '\'' +
-                    ", port=" + port +
-                    ", directTls=" + directTls +
-                    ", authenticated=" + authenticated +
-                    ", priority=" + priority +
-                    '}';
-        }
-
-        @Override
-        public int compareTo(@NonNull Result result) {
-            if (result.priority == priority) {
-                if (directTls == result.directTls) {
-                    if (ip == null && result.ip == null) {
-                        return 0;
-                    } else if (ip != null && result.ip != null) {
-                        if (ip instanceof Inet4Address && result.ip instanceof Inet4Address) {
-                            return 0;
-                        } else {
-                            return ip instanceof Inet4Address ? -1 : 1;
-                        }
-                    } else {
-                        return ip != null ? -1 : 1;
-                    }
-                } else {
-                    return directTls ? -1 : 1;
-                }
-            } else {
-                return priority - result.priority;
-            }
+            return MoreObjects.toStringHelper(this)
+                    .add("ip", ip)
+                    .add("hostname", hostname)
+                    .add("port", port)
+                    .add("directTls", directTls)
+                    .add("authenticated", authenticated)
+                    .add("priority", priority)
+                    .toString();
         }
 
         public ContentValues toContentValues() {
@@ -482,7 +466,7 @@ public class Resolver {
                         return null;
                     }
                     try {
-                        result.hostname = DNSName.from(hostPart.trim());
+                        result.hostname = DnsName.from(hostPart.trim());
                     } catch (final Exception e) {
                         return null;
                     }
@@ -499,7 +483,7 @@ public class Resolver {
                     result.ip = inetAddress;
                 } else {
                     try {
-                        result.hostname = DNSName.from(hostname);
+                        result.hostname = DnsName.from(hostname);
                     } catch (final Exception e) {
                         return null;
                     }
@@ -509,5 +493,4 @@ public class Resolver {
             return result;
         }
     }
-
 }

src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java πŸ”—

@@ -354,7 +354,7 @@ public class XmppConnection implements Runnable {
                     Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": Thread was interrupted");
                     return;
                 }
-                if (results.size() == 0) {
+                if (results.isEmpty()) {
                     Log.e(
                             Config.LOGTAG,
                             account.getJid().asBareJid() + ": Resolver results were empty");