diff --git a/app/src/main/java/com/github/gotify/service/WebSocketConnection.java b/app/src/main/java/com/github/gotify/service/WebSocketConnection.java index feea22aa..28101283 100644 --- a/app/src/main/java/com/github/gotify/service/WebSocketConnection.java +++ b/app/src/main/java/com/github/gotify/service/WebSocketConnection.java @@ -13,6 +13,7 @@ import com.github.gotify.log.Log; import java.util.Calendar; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -21,6 +22,7 @@ import okhttp3.WebSocketListener; class WebSocketConnection { + private static final AtomicLong ID = new AtomicLong(0); private final ConnectivityManager connectivityManager; private final AlarmManager alarmManager; private OkHttpClient client; @@ -38,7 +40,7 @@ class WebSocketConnection { private BadRequestRunnable onBadRequest; private OnNetworkFailureRunnable onNetworkFailure; private Runnable onReconnected; - private boolean isClosed; + private State state; private Runnable onDisconnect; WebSocketConnection( @@ -108,24 +110,33 @@ private Request request() { } public synchronized WebSocketConnection start() { + if (state == State.Connecting || state == State.Connected) { + return this; + } close(); - isClosed = false; - Log.i("WebSocket: starting..."); + state = State.Connecting; + long nextId = ID.incrementAndGet(); + Log.i("WebSocket(" + nextId + "): starting..."); - webSocket = client.newWebSocket(request(), new Listener()); + webSocket = client.newWebSocket(request(), new Listener(nextId)); return this; } public synchronized void close() { if (webSocket != null) { - Log.i("WebSocket: closing existing connection."); - isClosed = true; + Log.i("WebSocket(" + ID.get() + "): closing existing connection."); + state = State.Disconnected; webSocket.close(1000, ""); webSocket = null; } } public synchronized void scheduleReconnect(long seconds) { + if (state == State.Connecting || state == State.Connected) { + return; + } + state = State.Scheduled; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { Log.i( "WebSocket: scheduling a restart in " @@ -147,39 +158,49 @@ public synchronized void scheduleReconnect(long seconds) { } private class Listener extends WebSocketListener { + private final long id; + + public Listener(long id) { + this.id = id; + } @Override public void onOpen(WebSocket webSocket, Response response) { - Log.i("WebSocket: opened"); - synchronized (this) { - onOpen.run(); - - if (errorCount > 0) { - onReconnected.run(); - errorCount = 0; - } - } + syncExec( + () -> { + state = State.Connected; + Log.i("WebSocket(" + id + "): opened"); + onOpen.run(); + + if (errorCount > 0) { + onReconnected.run(); + errorCount = 0; + } + }); super.onOpen(webSocket, response); } @Override public void onMessage(WebSocket webSocket, String text) { - Log.i("WebSocket: received message " + text); - synchronized (this) { - Message message = Utils.JSON.fromJson(text, Message.class); - onMessage.onSuccess(message); - } + syncExec( + () -> { + Log.i("WebSocket(" + id + "): received message " + text); + Message message = Utils.JSON.fromJson(text, Message.class); + onMessage.onSuccess(message); + }); super.onMessage(webSocket, text); } @Override public void onClosed(WebSocket webSocket, int code, String reason) { - synchronized (this) { - if (!isClosed) { - Log.w("WebSocket: closed"); - onClose.run(); - } - } + syncExec( + () -> { + if (state == State.Connected) { + Log.w("WebSocket(" + id + "): closed"); + onClose.run(); + } + state = State.Disconnected; + }); super.onClosed(webSocket, code, reason); } @@ -188,30 +209,40 @@ public void onClosed(WebSocket webSocket, int code, String reason) { public void onFailure(WebSocket webSocket, Throwable t, Response response) { String code = response != null ? "StatusCode: " + response.code() : ""; String message = response != null ? response.message() : ""; - Log.e("WebSocket: failure " + code + " Message: " + message, t); - synchronized (this) { - if (response != null && response.code() >= 400 && response.code() <= 499) { - onBadRequest.execute(message); - close(); - return; - } + Log.e("WebSocket(" + id + "): failure " + code + " Message: " + message, t); + syncExec( + () -> { + state = State.Disconnected; + if (response != null && response.code() >= 400 && response.code() <= 499) { + onBadRequest.execute(message); + close(); + return; + } + + errorCount++; + + NetworkInfo network = connectivityManager.getActiveNetworkInfo(); + if (network == null || !network.isConnected()) { + Log.i("WebSocket(" + id + "): Network not connected"); + onDisconnect.run(); + return; + } + + int minutes = Math.min(errorCount * 2 - 1, 20); + + onNetworkFailure.execute(minutes); + scheduleReconnect(TimeUnit.MINUTES.toSeconds(minutes)); + }); - errorCount++; + super.onFailure(webSocket, t, response); + } - NetworkInfo network = connectivityManager.getActiveNetworkInfo(); - if (network == null || !network.isConnected()) { - Log.i("WebSocket: Network not connected"); - onDisconnect.run(); - return; + private void syncExec(Runnable runnable) { + synchronized (this) { + if (ID.get() == id) { + runnable.run(); } - - int minutes = Math.min(errorCount * 2 - 1, 20); - - onNetworkFailure.execute(minutes); - scheduleReconnect(TimeUnit.MINUTES.toSeconds(minutes)); } - - super.onFailure(webSocket, t, response); } } @@ -222,4 +253,11 @@ interface BadRequestRunnable { interface OnNetworkFailureRunnable { void execute(long millis); } + + enum State { + Scheduled, + Connecting, + Connected, + Disconnected + } } diff --git a/app/src/main/java/com/github/gotify/service/WebSocketService.java b/app/src/main/java/com/github/gotify/service/WebSocketService.java index fbce4195..e772d7c8 100644 --- a/app/src/main/java/com/github/gotify/service/WebSocketService.java +++ b/app/src/main/java/com/github/gotify/service/WebSocketService.java @@ -35,6 +35,8 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicLong; +import static com.github.gotify.api.Callback.call; + public class WebSocketService extends Service { public static final String NEW_MESSAGE_BROADCAST = @@ -105,7 +107,7 @@ private void startPushService() { cm, alarmManager) .onOpen(this::onOpen) - .onClose(() -> foreground(getString(R.string.websocket_closed))) + .onClose(this::onClose) .onBadRequest(this::onBadRequest) .onNetworkFailure( (min) -> foreground(getString(R.string.websocket_failed, min))) @@ -122,6 +124,24 @@ private void startPushService() { picassoHandler.updateAppIds(); } + private void onClose() { + foreground(getString(R.string.websocket_closed_try_reconnect)); + ClientFactory.userApiWithToken(settings) + .currentUser() + .enqueue( + call( + (ignored) -> this.doReconnect(), + (exception) -> { + if (exception.code() == 401) { + foreground(getString(R.string.websocket_closed_logout)); + } else { + Log.i( + "WebSocket closed but the user still authenticated, trying to reconnect"); + this.doReconnect(); + } + })); + } + private void onDisconnect() { foreground(getString(R.string.websocket_no_network)); } @@ -131,7 +151,7 @@ private void doReconnect() { return; } - connection.scheduleReconnect(5); + connection.scheduleReconnect(15); } private void onBadRequest(String message) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a52569e7..39c28489 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -21,7 +21,8 @@ Server returned 401 unauthorized while trying to get current user. This can be caused by deleting the client for this session. Could not get current user. Server (%s) responded with code %d: body (first 200 chars): %s Connection failed, trying again in %d minutes - WebSocket closed; The client-token could be invalidated, please re-login + User action required: Please login, the authentication token isn\'t valid anymore. + Connection closed, trying to establish a new one. Received %d messages while being disconnected Delete all Delete logs