introduce connection timeout as its own state

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/entities/Account.java               |  3 
src/main/java/eu/siacs/conversations/services/CallIntegration.java       |  4 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java | 57 
src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java            | 51 
src/main/res/values/strings.xml                                          |  1 
5 files changed, 56 insertions(+), 60 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/entities/Account.java 🔗

@@ -779,6 +779,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
         CONNECTING(false),
         ONLINE(false),
         NO_INTERNET(false),
+        CONNECTION_TIMEOUT,
         UNAUTHORIZED,
         TEMPORARY_AUTH_FAILURE,
         SERVER_NOT_FOUND,
@@ -848,6 +849,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
                     return R.string.account_status_not_found;
                 case NO_INTERNET:
                     return R.string.account_status_no_internet;
+                case CONNECTION_TIMEOUT:
+                    return R.string.account_status_connection_timeout;
                 case REGISTRATION_FAILED:
                     return R.string.account_status_regis_fail;
                 case REGISTRATION_WEB:

src/main/java/eu/siacs/conversations/services/CallIntegration.java 🔗

@@ -2,7 +2,6 @@ package eu.siacs.conversations.services;
 
 import android.content.Context;
 import android.content.pm.PackageManager;
-import android.media.AudioAttributes;
 import android.media.AudioManager;
 import android.media.ToneGenerator;
 import android.net.Uri;
@@ -20,7 +19,6 @@ import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import eu.siacs.conversations.AppSettings;
 import eu.siacs.conversations.Config;
-import eu.siacs.conversations.R;
 import eu.siacs.conversations.ui.util.MainThreadExecutor;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
@@ -374,7 +372,7 @@ public class CallIntegration extends Connection {
             }
         }
         if (state == STATE_ACTIVE) {
-            startTone(DEFAULT_TONE_VOLUME, ToneGenerator.TONE_CDMA_ANSWER, 100 );
+            startTone(DEFAULT_TONE_VOLUME, ToneGenerator.TONE_CDMA_ANSWER, 100);
         } else if (state == STATE_DISCONNECTED) {
             final var audioManager = this.appRTCAudioManager;
             if (audioManager != null) {

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -5,7 +5,6 @@ import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
 
 import android.Manifest;
 import android.annotation.SuppressLint;
-import android.annotation.TargetApi;
 import android.app.AlarmManager;
 import android.app.KeyguardManager;
 import android.app.Notification;
@@ -1036,6 +1035,7 @@ public class XmppConnectionService extends Service {
         if (!account.getStatus().isAttemptReconnect()) {
             return false;
         }
+        final var requestCode = account.getUuid().hashCode();
         if (!hasInternetConnection()) {
             account.setStatus(Account.State.NO_INTERNET);
             statusListener.onStatusChanged(account);
@@ -1065,8 +1065,7 @@ public class XmppConnectionService extends Service {
                             Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ping timeout");
                             this.reconnectAccount(account, true, interactive);
                         } else {
-                            int secs = (int) (pingTimeoutIn / 1000);
-                            this.scheduleWakeUpCall(secs, account.getUuid().hashCode());
+                            this.scheduleWakeUpCall(pingTimeoutIn, requestCode);
                         }
                     } else {
                         pingCandidates.add(account);
@@ -1081,8 +1080,7 @@ public class XmppConnectionService extends Service {
                         } else if (msToNextPing <= 0) {
                             return true;
                         } else {
-                            this.scheduleWakeUpCall(
-                                    (int) (msToNextPing / 1000), account.getUuid().hashCode());
+                            this.scheduleWakeUpCall(msToNextPing, requestCode);
                             if (mLowPingTimeoutMode.remove(account.getJid().asBareJid())) {
                                 Log.d(
                                         Config.LOGTAG,
@@ -1095,32 +1093,18 @@ public class XmppConnectionService extends Service {
             } else if (account.getStatus() == Account.State.OFFLINE) {
                 reconnectAccount(account, true, interactive);
             } else if (account.getStatus() == Account.State.CONNECTING) {
-                long secondsSinceLastConnect =
-                        (SystemClock.elapsedRealtime()
-                                        - account.getXmppConnection().getLastConnect())
-                                / 1000;
-                long secondsSinceLastDisco =
-                        (SystemClock.elapsedRealtime()
-                                        - account.getXmppConnection().getLastDiscoStarted())
-                                / 1000;
-                long discoTimeout = Config.CONNECT_DISCO_TIMEOUT - secondsSinceLastDisco;
-                long timeout = Config.CONNECT_TIMEOUT - secondsSinceLastConnect;
-                if (timeout < 0) {
-                    Log.d(
-                            Config.LOGTAG,
-                            account.getJid()
-                                    + ": time out during connect reconnecting (secondsSinceLast="
-                                    + secondsSinceLastConnect
-                                    + ")");
-                    account.getXmppConnection().resetAttemptCount(false);
-                    reconnectAccount(account, true, interactive);
+                final var connection = account.getXmppConnection();
+                final var connectionDuration = connection.getConnectionDuration();
+                final var discoDuration = connection.getDiscoDuration();
+                final var connectionTimeout = Config.CONNECT_TIMEOUT * 1000L - connectionDuration;
+                final var discoTimeout = Config.CONNECT_DISCO_TIMEOUT * 1000L - discoDuration;
+                if (connectionTimeout < 0) {
+                    connection.triggerConnectionTimeout();
                 } else if (discoTimeout < 0) {
-                    account.getXmppConnection().sendDiscoTimeout();
-                    scheduleWakeUpCall(
-                            (int) Math.min(timeout, discoTimeout), account.getUuid().hashCode());
+                    connection.sendDiscoTimeout();
+                    scheduleWakeUpCall(discoTimeout, requestCode);
                 } else {
-                    scheduleWakeUpCall(
-                            (int) Math.min(timeout, discoTimeout), account.getUuid().hashCode());
+                    scheduleWakeUpCall(Math.min(connectionTimeout, discoTimeout), requestCode);
                 }
             } else {
                 final boolean aggressive =
@@ -1768,12 +1752,12 @@ public class XmppConnectionService extends Service {
     }
 
     public void scheduleWakeUpCall(final int seconds, final int requestCode) {
-        final long timeToWake =
-                SystemClock.elapsedRealtime() + (seconds < 0 ? 1 : seconds + 1) * 1000L;
-        final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
-        if (alarmManager == null) {
-            return;
-        }
+        scheduleWakeUpCall((long) (seconds < 0 ? 1 : seconds + 1), requestCode);
+    }
+
+    private void scheduleWakeUpCall(final long milliSeconds, final int requestCode) {
+        final var timeToWake = SystemClock.elapsedRealtime() + milliSeconds;
+        final var alarmManager = getSystemService(AlarmManager.class);
         final Intent intent = new Intent(this, SystemEventReceiver.class);
         intent.setAction(ACTION_PING);
         try {
@@ -1781,12 +1765,11 @@ public class XmppConnectionService extends Service {
                     PendingIntent.getBroadcast(
                             this, requestCode, intent, PendingIntent.FLAG_IMMUTABLE);
             alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingIntent);
-        } catch (RuntimeException e) {
+        } catch (final RuntimeException e) {
             Log.e(Config.LOGTAG, "unable to schedule alarm for ping", e);
         }
     }
 
-    @TargetApi(Build.VERSION_CODES.M)
     private void scheduleNextIdlePing() {
         final long timeToWake = SystemClock.elapsedRealtime() + (Config.IDLE_PING_INTERVAL * 1000);
         final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);

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

@@ -20,6 +20,7 @@ import com.google.common.base.Preconditions;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
+import com.google.common.primitives.Ints;
 import eu.siacs.conversations.AppSettings;
 import eu.siacs.conversations.BuildConfig;
 import eu.siacs.conversations.Config;
@@ -166,7 +167,7 @@ public class XmppConnection implements Runnable {
     private int stanzasSentBeforeAuthentication;
     private long lastPacketReceived = 0;
     private long lastPingSent = 0;
-    private long lastConnect = 0;
+    private long lastConnectionStarted = 0;
     private long lastSessionStarted = 0;
     private long lastDiscoStarted = 0;
     private boolean isMamPreferenceAlways = false;
@@ -264,7 +265,7 @@ public class XmppConnection implements Runnable {
     }
 
     public void prepareNewConnection() {
-        this.lastConnect = SystemClock.elapsedRealtime();
+        this.lastConnectionStarted = SystemClock.elapsedRealtime();
         this.lastPingSent = SystemClock.elapsedRealtime();
         this.lastDiscoStarted = Long.MAX_VALUE;
         this.mWaitingForSmCatchup.set(false);
@@ -526,15 +527,17 @@ public class XmppConnection implements Runnable {
         if (Thread.currentThread().isInterrupted()) {
             throw new InterruptedException();
         }
+        // this means we have at least found a socket to connect to. give the connection another 90s
+        this.lastConnectionStarted = SystemClock.elapsedRealtime();
         this.socket = socket;
-        tagReader = new XmlReader();
+        this.tagReader = new XmlReader();
         if (tagWriter != null) {
             tagWriter.forceClose();
         }
-        tagWriter = new TagWriter();
-        tagWriter.setOutputStream(socket.getOutputStream());
-        tagReader.setInputStream(socket.getInputStream());
-        tagWriter.beginDocument();
+        this.tagWriter = new TagWriter();
+        this.tagWriter.setOutputStream(socket.getOutputStream());
+        this.tagReader.setInputStream(socket.getInputStream());
+        this.tagWriter.beginDocument();
         final boolean quickStart;
         if (socket instanceof SSLSocket sslSocket) {
             SSLSockets.log(account, sslSocket);
@@ -2372,7 +2375,7 @@ public class XmppConnection implements Runnable {
         } else if (streamError.hasChild("host-unknown")) {
             throw new StateChangingException(Account.State.HOST_UNKNOWN);
         } else if (streamError.hasChild("policy-violation")) {
-            this.lastConnect = SystemClock.elapsedRealtime();
+            this.lastConnectionStarted = SystemClock.elapsedRealtime();
             final String text = streamError.findChildContent("text");
             Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": policy violation. " + text);
             if (isSecureLoggedIn) {
@@ -2712,8 +2715,7 @@ public class XmppConnection implements Runnable {
     }
 
     public String getMucServer() {
-        List<String> servers = getMucServers();
-        return servers.size() > 0 ? servers.get(0) : null;
+        return Iterables.getFirst(getMucServers(), null);
     }
 
     public int getTimeToNextAttempt(final boolean aggressive) {
@@ -2725,9 +2727,8 @@ public class XmppConnection implements Runnable {
                     account.getLastErrorStatus() == Account.State.POLICY_VIOLATION ? 3 : 0;
             interval = Math.min((int) (25 * Math.pow(1.3, (additionalTime + attempt))), 300);
         }
-        final int secondsSinceLast =
-                (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
-        return interval - secondsSinceLast;
+        final var connectionDuration = Ints.saturatedCast(getConnectionDuration() / 1000);
+        return interval - connectionDuration;
     }
 
     public int getAttempt() {
@@ -2743,16 +2744,16 @@ public class XmppConnection implements Runnable {
         return System.currentTimeMillis() - diff;
     }
 
-    public long getLastConnect() {
-        return this.lastConnect;
+    public long getConnectionDuration() {
+        return SystemClock.elapsedRealtime() - this.lastConnectionStarted;
     }
 
-    public long getLastPingSent() {
-        return this.lastPingSent;
+    public long getDiscoDuration() {
+        return SystemClock.elapsedRealtime() - this.lastDiscoStarted;
     }
 
-    public long getLastDiscoStarted() {
-        return this.lastDiscoStarted;
+    public long getLastPingSent() {
+        return this.lastPingSent;
     }
 
     public long getLastPacketReceived() {
@@ -2770,7 +2771,7 @@ public class XmppConnection implements Runnable {
     public void resetAttemptCount(boolean resetConnectTime) {
         this.attempt = 0;
         if (resetConnectTime) {
-            this.lastConnect = 0;
+            this.lastConnectionStarted = 0;
         }
     }
 
@@ -2818,6 +2819,16 @@ public class XmppConnection implements Runnable {
         sendIqPacket(iqPacket, unregisteredIqListener);
     }
 
+    public void triggerConnectionTimeout() {
+        final var duration = getConnectionDuration();
+        Log.d(
+                Config.LOGTAG,
+                account.getJid().asBareJid() + ": connection timeout after " + duration + "ms");
+        this.changeStatus(Account.State.CONNECTION_TIMEOUT);
+        this.interrupt();
+        this.forceCloseSocket();
+    }
+
     private class MyKeyManager implements X509KeyManager {
         @Override
         public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {

src/main/res/values/strings.xml 🔗

@@ -158,6 +158,7 @@
     <string name="account_status_unauthorized">Unauthorized</string>
     <string name="account_status_not_found">Server not found</string>
     <string name="account_status_no_internet">No connectivity</string>
+    <string name="account_status_connection_timeout">Connection timeout</string>
     <string name="account_status_regis_fail">Registration failed</string>
     <string name="account_status_regis_conflict">Username already in use</string>
     <string name="account_status_regis_success">Registration completed</string>