permanently cache last resolver result

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java |  49 
src/main/java/eu/siacs/conversations/utils/Resolver.java              | 518 
src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java         |  14 
3 files changed, 346 insertions(+), 235 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java 🔗

@@ -52,6 +52,7 @@ import eu.siacs.conversations.entities.ServiceDiscoveryResult;
 import eu.siacs.conversations.services.ShortcutService;
 import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.utils.MimeUtils;
+import eu.siacs.conversations.utils.Resolver;
 import eu.siacs.conversations.xmpp.jid.InvalidJidException;
 import eu.siacs.conversations.xmpp.jid.Jid;
 import eu.siacs.conversations.xmpp.mam.MamReference;
@@ -61,7 +62,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 	private static DatabaseBackend instance = null;
 
 	private static final String DATABASE_NAME = "history";
-	private static final int DATABASE_VERSION = 38;
+	private static final int DATABASE_VERSION = 39;
 
 	private static String CREATE_CONTATCS_STATEMENT = "create table "
 			+ Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "
@@ -148,9 +149,18 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 			+ ") ON CONFLICT IGNORE"
 			+ ");";
 
-	private static String START_TIMES_TABLE = "start_times";
-
-	private static String CREATE_START_TIMES_TABLE = "create table "+START_TIMES_TABLE+" (timestamp NUMBER);";
+	private static String RESOLVER_RESULTS_TABLENAME = "resolver_results";
+
+	private static String CREATE_RESOLVER_RESULTS_TABLE = "create table "+RESOLVER_RESULTS_TABLENAME+"("
+			+ Resolver.Result.DOMAIN + " TEXT,"
+			+ Resolver.Result.HOSTNAME + " TEXT,"
+			+ Resolver.Result.IP + " BLOB,"
+			+ Resolver.Result.PRIORITY + " NUMBER,"
+			+ Resolver.Result.DIRECT_TLS + " NUMBER,"
+			+ Resolver.Result.AUTHENTICATED + " NUMBER,"
+			+ Resolver.Result.PORT + " NUMBER,"
+			+ "UNIQUE("+Resolver.Result.DOMAIN+") ON CONFLICT REPLACE"
+			+ ");";
 
 	private static String CREATE_MESSAGE_TIME_INDEX = "create INDEX message_time_index ON "+Message.TABLENAME+"("+Message.TIME_SENT+")";
 	private static String CREATE_MESSAGE_CONVERSATION_INDEX = "create INDEX message_conversation_index ON "+Message.TABLENAME+"("+Message.CONVERSATION+")";
@@ -213,7 +223,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 		db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT);
 		db.execSQL(CREATE_IDENTITIES_STATEMENT);
 		db.execSQL(CREATE_PRESENCE_TEMPLATES_STATEMENT);
-		db.execSQL(CREATE_START_TIMES_TABLE);
+		db.execSQL(CREATE_RESOLVER_RESULTS_TABLE);
 	}
 
 	@Override
@@ -369,9 +379,6 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 		if (oldVersion < 29 && newVersion >= 29) {
 			db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.ERROR_MESSAGE + " TEXT");
 		}
-		if (oldVersion < 30 && newVersion >= 30) {
-			db.execSQL(CREATE_START_TIMES_TABLE);
-		}
 		if (oldVersion >= 15 && oldVersion < 31 && newVersion >= 31) {
 			db.execSQL("ALTER TABLE "+ SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN "+SQLiteAxolotlStore.TRUST + " TEXT");
 			db.execSQL("ALTER TABLE "+ SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN "+SQLiteAxolotlStore.ACTIVE + " NUMBER");
@@ -465,6 +472,10 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 		if (oldVersion < 38 && newVersion >= 38) {
 			db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.MARKABLE + " NUMBER DEFAULT 0");
 		}
+
+		if (oldVersion < 39 && newVersion >= 39) {
+			db.execSQL(CREATE_RESOLVER_RESULTS_TABLE);
+		}
 	}
 
 	private static ContentValues createFingerprintStatusContentValues(FingerprintStatus.Trust trust, boolean active) {
@@ -605,6 +616,28 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 		return result;
 	}
 
+	public void saveResolverResult(String domain, Resolver.Result result) {
+		SQLiteDatabase db = this.getWritableDatabase();
+		ContentValues contentValues = result.toContentValues();
+		contentValues.put(Resolver.Result.DOMAIN,domain);
+		db.insert(RESOLVER_RESULTS_TABLENAME, null, contentValues);
+	}
+
+	public Resolver.Result findResolverResult(String domain) {
+		SQLiteDatabase db = this.getReadableDatabase();
+		String where = Resolver.Result.DOMAIN+"=?";
+		String[] whereArgs = {domain};
+		Cursor cursor = db.query(RESOLVER_RESULTS_TABLENAME,null,where,whereArgs,null,null,null);
+		Resolver.Result result = null;
+		if (cursor != null) {
+			if (cursor.moveToFirst()) {
+				result = Resolver.Result.fromCursor(cursor);
+			}
+			cursor.close();
+		}
+		return result;
+	}
+
 	public void insertPresenceTemplate(PresenceTemplate template) {
 		SQLiteDatabase db = this.getWritableDatabase();
 		db.insert(PresenceTemplate.TABELNAME, null, template.getContentValues());

src/main/java/eu/siacs/conversations/utils/Resolver.java 🔗

@@ -1,12 +1,14 @@
 package eu.siacs.conversations.utils;
 
-import android.content.Context;
+import android.content.ContentValues;
+import android.database.Cursor;
 import android.support.annotation.NonNull;
 import android.util.Log;
 
 import java.io.IOException;
 import java.net.Inet4Address;
 import java.net.InetAddress;
+import java.net.UnknownHostException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
@@ -34,231 +36,293 @@ import eu.siacs.conversations.services.XmppConnectionService;
 
 public class Resolver {
 
-    private static final String DIRECT_TLS_SERVICE = "_xmpps-client";
-    private static final String STARTTLS_SERICE = "_xmpp-client";
-
-    private static final String NETWORK_IS_UNREACHABLE = "Network is unreachable";
-
-    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));
-    }
-
-    public static List<Result> resolve(String domain) throws NetworkIsUnreachableException {
-        List<Result> results = new ArrayList<>();
-        HashSet<String> messages = new HashSet<>();
-        try {
-            results.addAll(resolveSrv(domain, true));
-        } catch (MultipleIoException e) {
-            messages.addAll(extractMessages(e));
-        } catch (Throwable throwable) {
-            Log.d(Config.LOGTAG,Resolver.class.getSimpleName()+": error resolving SRV record (direct TLS)",throwable);
-        }
-        try {
-            results.addAll(resolveSrv(domain, false));
-        } catch (MultipleIoException e) {
-            messages.addAll(extractMessages(e));
-        } catch (Throwable throwable) {
-            Log.d(Config.LOGTAG,Resolver.class.getSimpleName()+": error resolving SRV record (STARTTLS)",throwable);
-        }
-        if (results.size() == 0) {
-            if (messages.size() == 1 && messages.contains(NETWORK_IS_UNREACHABLE)) {
-                throw new NetworkIsUnreachableException();
-            }
-            results.addAll(resolveNoSrvRecords(DNSName.from(domain),true));
-        }
-        Collections.sort(results);
-        Log.d(Config.LOGTAG,Resolver.class.getSimpleName()+": "+results.toString());
-        return results;
-    }
-
-    private static HashSet<String> extractMessages(MultipleIoException e) {
-        HashSet<String> messages = new HashSet<>();
-        for(Exception inner : e.getExceptions()) {
-            if (inner instanceof MultipleIoException) {
-                messages.addAll(extractMessages((MultipleIoException) inner));
-            } else {
-                messages.add(inner.getMessage());
-            }
-        }
-        return messages;
-    }
-
-    private static List<Result> resolveSrv(String domain, final boolean directTls) throws IOException {
-        if (Thread.currentThread().isInterrupted()) {
-            return Collections.emptyList();
-        }
-        DNSName dnsName = DNSName.from((directTls ? DIRECT_TLS_SERVICE : STARTTLS_SERICE)+"._tcp."+domain);
-        ResolverResult<SRV> result = resolveWithFallback(dnsName,SRV.class);
-        List<Result> results = new ArrayList<>();
-        for(SRV record : result.getAnswersOrEmptySet()) {
-            final boolean addedIPv4 = results.addAll(resolveIp(record,A.class,result.isAuthenticData(),directTls));
-            results.addAll(resolveIp(record,AAAA.class,result.isAuthenticData(),directTls));
-            if (!addedIPv4 && !Thread.currentThread().isInterrupted()) {
-                Result resolverResult = Result.fromRecord(record, directTls);
-                resolverResult.authenticated = resolverResult.isAuthenticated();
-                results.add(resolverResult);
-            }
-        }
-        return results;
-    }
-
-    private static <D extends InternetAddressRR> List<Result> resolveIp(SRV srv, Class<D> type, boolean authenticated, boolean directTls) {
-        if (Thread.currentThread().isInterrupted()) {
-            return Collections.emptyList();
-        }
-        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;
-                resolverResult.ip = record.getInetAddress();
-                list.add(resolverResult);
-            }
-        } catch (Throwable t) {
-            Log.d(Config.LOGTAG,Resolver.class.getSimpleName()+": error resolving "+type.getSimpleName()+" "+t.getMessage());
-        }
-        return list;
-    }
-
-    private static List<Result> resolveNoSrvRecords(DNSName dnsName, boolean withCnames) {
-        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 (Throwable throwable) {
-            Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + "error resolving fallback records",throwable);
-        }
-        results.add(Result.createDefault(dnsName));
-        return results;
-    }
-
-    private static <D extends Data> ResolverResult<D> resolveWithFallback(DNSName dnsName, Class<D> type) throws IOException {
-        return resolveWithFallback(dnsName,type,validateHostname());
-    }
-
-    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) {
-            return ResolverApi.INSTANCE.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);
-        }
-        return ResolverApi.INSTANCE.resolve(question);
-    }
-
-    private static boolean validateHostname() {
-        return SERVICE != null && SERVICE.getBooleanPreference("validate_hostname", R.bool.validate_hostname);
-    }
-
-    public static class Result implements Comparable<Result> {
-        private InetAddress ip;
-        private DNSName hostname;
-        private int port = 5222;
-        private boolean directTls = false;
-        private boolean authenticated =false;
-        private int priority;
-
-        public InetAddress getIp() {
-            return ip;
-        }
-
-        public int getPort() {
-            return port;
-        }
-
-        public DNSName getHostname() {
-            return hostname;
-        }
-
-        public boolean isDirectTls() {
-            return directTls;
-        }
-
-        public boolean isAuthenticated() {
-            return authenticated;
-        }
-
-        @Override
-        public String toString() {
-            return "Result{" +
-                    "ip='" + (ip==null?null:ip.getHostAddress()) + '\'' +
-                    ", hostame='" + 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;
-            }
-        }
-
-        public static Result fromRecord(SRV srv, boolean directTls) {
-            Result result = new Result();
-            result.port = srv.port;
-            result.hostname = srv.name;
-            result.directTls = directTls;
-            result.priority = srv.priority;
-            return result;
-        }
-
-        public static Result createDefault(DNSName hostname, InetAddress ip) {
-            Result result = new Result();
-            result.port = 5222;
-            result.hostname = hostname;
-            result.ip = ip;
-            return result;
-        }
-
-        public static Result createDefault(DNSName hostname) {
-            return createDefault(hostname,null);
-        }
-    }
-    public static class NetworkIsUnreachableException extends Exception {
-
-    }
+	private static final String DIRECT_TLS_SERVICE = "_xmpps-client";
+	private static final String STARTTLS_SERICE = "_xmpp-client";
+
+	private static final String NETWORK_IS_UNREACHABLE = "Network is unreachable";
+
+	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));
+	}
+
+	public static List<Result> resolve(String domain) throws NetworkIsUnreachableException {
+		List<Result> results = new ArrayList<>();
+		HashSet<String> messages = new HashSet<>();
+		try {
+			results.addAll(resolveSrv(domain, true));
+		} catch (MultipleIoException e) {
+			messages.addAll(extractMessages(e));
+		} catch (Throwable throwable) {
+			Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (direct TLS)", throwable);
+		}
+		try {
+			results.addAll(resolveSrv(domain, false));
+		} catch (MultipleIoException e) {
+			messages.addAll(extractMessages(e));
+		} catch (Throwable throwable) {
+			Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (STARTTLS)", throwable);
+		}
+		if (results.size() == 0) {
+			if (messages.size() == 1 && messages.contains(NETWORK_IS_UNREACHABLE)) {
+				throw new NetworkIsUnreachableException();
+			}
+			results.addAll(resolveNoSrvRecords(DNSName.from(domain), true));
+		}
+		Collections.sort(results);
+		Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + results.toString());
+		return results;
+	}
+
+	private static HashSet<String> extractMessages(MultipleIoException e) {
+		HashSet<String> messages = new HashSet<>();
+		for (Exception inner : e.getExceptions()) {
+			if (inner instanceof MultipleIoException) {
+				messages.addAll(extractMessages((MultipleIoException) inner));
+			} else {
+				messages.add(inner.getMessage());
+			}
+		}
+		return messages;
+	}
+
+	private static List<Result> resolveSrv(String domain, final boolean directTls) throws IOException {
+		if (Thread.currentThread().isInterrupted()) {
+			return Collections.emptyList();
+		}
+		DNSName dnsName = DNSName.from((directTls ? DIRECT_TLS_SERVICE : STARTTLS_SERICE) + "._tcp." + domain);
+		ResolverResult<SRV> result = resolveWithFallback(dnsName, SRV.class);
+		List<Result> results = new ArrayList<>();
+		for (SRV record : result.getAnswersOrEmptySet()) {
+			final boolean addedIPv4 = results.addAll(resolveIp(record, A.class, result.isAuthenticData(), directTls));
+			results.addAll(resolveIp(record, AAAA.class, result.isAuthenticData(), directTls));
+			if (!addedIPv4 && !Thread.currentThread().isInterrupted()) {
+				Result resolverResult = Result.fromRecord(record, directTls);
+				resolverResult.authenticated = resolverResult.isAuthenticated();
+				results.add(resolverResult);
+			}
+		}
+		return results;
+	}
+
+	private static <D extends InternetAddressRR> List<Result> resolveIp(SRV srv, Class<D> type, boolean authenticated, boolean directTls) {
+		if (Thread.currentThread().isInterrupted()) {
+			return Collections.emptyList();
+		}
+		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;
+				resolverResult.ip = record.getInetAddress();
+				list.add(resolverResult);
+			}
+		} catch (Throwable t) {
+			Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " " + t.getMessage());
+		}
+		return list;
+	}
+
+	private static List<Result> resolveNoSrvRecords(DNSName dnsName, boolean withCnames) {
+		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 (Throwable throwable) {
+			Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + "error resolving fallback records", throwable);
+		}
+		results.add(Result.createDefault(dnsName));
+		return results;
+	}
+
+	private static <D extends Data> ResolverResult<D> resolveWithFallback(DNSName dnsName, Class<D> type) throws IOException {
+		return resolveWithFallback(dnsName, type, validateHostname());
+	}
+
+	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) {
+			return ResolverApi.INSTANCE.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);
+		}
+		return ResolverApi.INSTANCE.resolve(question);
+	}
+
+	private static boolean validateHostname() {
+		return SERVICE != null && SERVICE.getBooleanPreference("validate_hostname", R.bool.validate_hostname);
+	}
+
+	public static class Result implements Comparable<Result> {
+		@Override
+		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;
+		}
+
+		@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;
+		}
+
+		public static final String DOMAIN = "domain";
+
+		public static final String IP = "ip";
+		public static final String HOSTNAME = "hostname";
+		public static final String PORT = "port";
+		public static final String PRIORITY = "priority";
+		public static final String DIRECT_TLS = "directTls";
+		public static final String AUTHENTICATED = "authenticated";
+
+		private InetAddress ip;
+		private DNSName hostname;
+		private int port = 5222;
+		private boolean directTls = false;
+		private boolean authenticated = false;
+		private int priority;
+
+		public InetAddress getIp() {
+			return ip;
+		}
+
+		public int getPort() {
+			return port;
+		}
+
+		public DNSName getHostname() {
+			return hostname;
+		}
+
+		public boolean isDirectTls() {
+			return directTls;
+		}
+
+		public boolean isAuthenticated() {
+			return authenticated;
+		}
+
+		@Override
+		public String toString() {
+			return "Result{" +
+					"ip='" + (ip == null ? null : ip.getHostAddress()) + '\'' +
+					", hostame='" + 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;
+			}
+		}
+
+		public static Result fromRecord(SRV srv, boolean directTls) {
+			Result result = new Result();
+			result.port = srv.port;
+			result.hostname = srv.name;
+			result.directTls = directTls;
+			result.priority = srv.priority;
+			return result;
+		}
+
+		public static Result createDefault(DNSName hostname, InetAddress ip) {
+			Result result = new Result();
+			result.port = 5222;
+			result.hostname = hostname;
+			result.ip = ip;
+			return result;
+		}
+
+		public 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)));
+			} catch (UnknownHostException e) {
+				result.ip = null;
+			}
+			result.hostname = DNSName.from(cursor.getString(cursor.getColumnIndex(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;
+			return result;
+		}
+
+		public ContentValues toContentValues() {
+			final ContentValues contentValues = new ContentValues();
+			contentValues.put(IP, ip.getAddress());
+			contentValues.put(HOSTNAME, hostname.toString());
+			contentValues.put(PORT, port);
+			contentValues.put(PRIORITY, priority);
+			contentValues.put(DIRECT_TLS, directTls ? 1 : 0);
+			contentValues.put(AUTHENTICATED, authenticated ? 1 : 0);
+			return contentValues;
+		}
+	}
+
+	public static class NetworkIsUnreachableException extends Exception {
+
+	}
 
 }

src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java 🔗

@@ -351,7 +351,18 @@ public class XmppConnection implements Runnable {
 					throw new IOException(e.getMessage());
 				}
 			} else {
+				final String domain = account.getJid().getDomainpart();
 				List<Resolver.Result> results = Resolver.resolve(account.getJid().getDomainpart());
+				Resolver.Result storedBackupResult;
+				if (!Thread.currentThread().isInterrupted()) {
+					storedBackupResult = mXmppConnectionService.databaseBackend.findResolverResult(domain);
+					if (storedBackupResult != null && !results.contains(storedBackupResult)) {
+						results.add(storedBackupResult);
+						Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": loaded backup resolver result from db: " + storedBackupResult);
+					}
+				} else {
+					storedBackupResult = null;
+				}
 				for (Iterator<Resolver.Result> iterator = results.iterator(); iterator.hasNext(); ) {
 					final Resolver.Result result = iterator.next();
 					if (Thread.currentThread().isInterrupted()) {
@@ -400,6 +411,9 @@ public class XmppConnection implements Runnable {
 							}
 						}
 						if (startXmpp(localSocket)) {
+							if (!result.equals(storedBackupResult)) {
+								mXmppConnectionService.databaseBackend.saveResolverResult(domain, result);
+							}
 							break; // successfully connected to server that speaks xmpp
 						} else {
 							localSocket.close();