Commit abe34222 authored by Leonardo Aramaki's avatar Leonardo Aramaki

Fix reconnection loop issue when add new server

parent 65d5c3a7
...@@ -11,6 +11,10 @@ object ConnectionStatusManager { ...@@ -11,6 +11,10 @@ object ConnectionStatusManager {
} }
private const val DEBUG = false private const val DEBUG = false
private val DEFAULT_CALLBACK = object : TransitionCallback {
override fun onTransitioned(success: Boolean) {
}
}
private val stateMachine: EnumStateMachine<State> private val stateMachine: EnumStateMachine<State>
init { init {
...@@ -28,23 +32,46 @@ object ConnectionStatusManager { ...@@ -28,23 +32,46 @@ object ConnectionStatusManager {
fun currentState() = stateMachine.currentState() fun currentState() = stateMachine.currentState()
@Synchronized @Synchronized
fun setOnline(callback: TransitionCallback) { fun setOnline(callback: TransitionCallback = DEFAULT_CALLBACK) {
KeepAliveJob.cancelPendingJobRequests() KeepAliveJob.cancelPendingJobRequests()
tryTransitionTo(State.ONLINE, callback) tryTransitionTo(State.ONLINE, callback)
} }
@Synchronized @Synchronized
fun setConnecting(callback: TransitionCallback) { fun setOnline() {
KeepAliveJob.cancelPendingJobRequests()
tryTransitionTo(State.ONLINE, DEFAULT_CALLBACK)
}
@Synchronized
fun setConnecting(callback: TransitionCallback = DEFAULT_CALLBACK) {
KeepAliveJob.cancelPendingJobRequests() KeepAliveJob.cancelPendingJobRequests()
tryTransitionTo(State.CONNECTING, callback) tryTransitionTo(State.CONNECTING, callback)
} }
@Synchronized @Synchronized
fun setConnectionError(callback: TransitionCallback) { fun setConnecting() {
KeepAliveJob.cancelPendingJobRequests()
tryTransitionTo(State.CONNECTING, DEFAULT_CALLBACK)
}
@Synchronized
fun setConnectionError(callback: TransitionCallback = DEFAULT_CALLBACK) {
KeepAliveJob.schedule() KeepAliveJob.schedule()
tryTransitionTo(State.OFFLINE, callback) tryTransitionTo(State.OFFLINE, callback)
} }
@Synchronized
fun setConnectionError() {
KeepAliveJob.schedule()
tryTransitionTo(State.OFFLINE, DEFAULT_CALLBACK)
}
@Synchronized
fun setOffline() {
stateMachine.reset()
}
private fun tryTransitionTo(newState: State, callback: TransitionCallback) { private fun tryTransitionTo(newState: State, callback: TransitionCallback) {
try { try {
stateMachine.transition(newState) stateMachine.transition(newState)
......
...@@ -72,7 +72,7 @@ object RocketChatCache { ...@@ -72,7 +72,7 @@ object RocketChatCache {
return getString(KEY_SELECTED_SERVER_HOSTNAME, null) return getString(KEY_SELECTED_SERVER_HOSTNAME, null)
} }
fun setSelectedRoomId(roomId: String) { fun setSelectedRoomId(roomId: String?) {
try { try {
val jsonObject = getSelectedRoomIdJsonObject() val jsonObject = getSelectedRoomIdJsonObject()
jsonObject.put(getSelectedServerHostname(), roomId) jsonObject.put(getSelectedServerHostname(), roomId)
...@@ -81,7 +81,6 @@ object RocketChatCache { ...@@ -81,7 +81,6 @@ object RocketChatCache {
RCLog.e(e) RCLog.e(e)
Logger.report(e) Logger.report(e)
} }
} }
@Throws(JSONException::class) @Throws(JSONException::class)
...@@ -234,10 +233,11 @@ object RocketChatCache { ...@@ -234,10 +233,11 @@ object RocketChatCache {
fun clearSelectedHostnameReferences() { fun clearSelectedHostnameReferences() {
val hostname = getSelectedServerHostname() val hostname = getSelectedServerHostname()
if (hostname != null) { if (hostname != null) {
setString(KEY_OPENED_ROOMS, null)
removeSiteName(hostname) removeSiteName(hostname)
removeHostname(hostname) removeHostname(hostname)
removeSiteUrl(hostname) removeSiteUrl(hostname)
setSelectedServerHostname(null) setSelectedServerHostname(getFirstLoggedHostnameIfAny())
} }
} }
...@@ -308,7 +308,7 @@ object RocketChatCache { ...@@ -308,7 +308,7 @@ object RocketChatCache {
fun setSessionToken(sessionToken: String) { fun setSessionToken(sessionToken: String) {
val selectedServerHostname = getSelectedServerHostname() ?: val selectedServerHostname = getSelectedServerHostname() ?:
throw IllegalStateException("Trying to set sessionToken to null hostname") throw IllegalStateException("Trying to set sessionToken to null hostname")
val sessions = getSessionToken() val sessions = getString(KEY_SESSION_TOKEN, null)
try { try {
val jsonObject = if (sessions == null) JSONObject() else JSONObject(sessions) val jsonObject = if (sessions == null) JSONObject() else JSONObject(sessions)
jsonObject.put(selectedServerHostname, sessionToken) jsonObject.put(selectedServerHostname, sessionToken)
......
...@@ -118,7 +118,7 @@ public class MethodCallHelper { ...@@ -118,7 +118,7 @@ public class MethodCallHelper {
} else if (exception instanceof DDPClientCallback.RPC.Timeout) { } else if (exception instanceof DDPClientCallback.RPC.Timeout) {
return Task.forError(new MethodCall.Timeout()); return Task.forError(new MethodCall.Timeout());
} else if (exception instanceof DDPClientCallback.Closed) { } else if (exception instanceof DDPClientCallback.Closed) {
return Task.forError(new Exception("Oops, your connection seems off...")); return Task.forError(new Exception(exception.getMessage()));
} else { } else {
return Task.forError(exception); return Task.forError(exception);
} }
......
...@@ -17,6 +17,7 @@ import chat.rocket.core.models.PublicSetting ...@@ -17,6 +17,7 @@ import chat.rocket.core.models.PublicSetting
import chat.rocket.core.repositories.LoginServiceConfigurationRepository import chat.rocket.core.repositories.LoginServiceConfigurationRepository
import chat.rocket.core.repositories.PublicSettingRepository import chat.rocket.core.repositories.PublicSettingRepository
import com.hadisatrio.optional.Optional import com.hadisatrio.optional.Optional
import io.reactivex.Completable
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.rxkotlin.subscribeBy import io.reactivex.rxkotlin.subscribeBy
...@@ -74,25 +75,34 @@ class LoginPresenter(private val loginServiceConfigurationRepository: LoginServi ...@@ -74,25 +75,34 @@ class LoginPresenter(private val loginServiceConfigurationRepository: LoginServi
} }
private fun doLogin(username: String, password: String, optional: Optional<PublicSetting>) { private fun doLogin(username: String, password: String, optional: Optional<PublicSetting>) {
call(username, password, optional) addSubscription(
.continueWith(object : Continuation<Void, Any?> { Completable.create {
override fun then(task: Task<Void>?): Any? { call(username, password, optional)
if (task != null && task.isFaulted()) { .continueWith(object : Continuation<Void, Any?> {
view.hideLoader() override fun then(task: Task<Void>?): Any? {
if (task != null && task.isFaulted()) {
view.hideLoader()
val error = task.getError() val error = task.getError()
error?.let { error?.let {
if (error is TwoStepAuthException) { if (error is TwoStepAuthException) {
view.showTwoStepAuth() view.showTwoStepAuth()
} else { } else {
view.showError(error.message) view.showError(error.message)
}
}
return Completable.complete()
}
return null
} }
} }, Task.UI_THREAD_EXECUTOR)
}.subscribeBy(
onError = {
view.showError(it.message)
} }
return null )
} )
}, Task.UI_THREAD_EXECUTOR)
} }
private fun call(username: String, password: String, optional: Optional<PublicSetting>): Task<Void> { private fun call(username: String, password: String, optional: Optional<PublicSetting>): Task<Void> {
......
...@@ -14,6 +14,7 @@ import java.util.Map; ...@@ -14,6 +14,7 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import chat.rocket.android.ConnectionStatusManager;
import chat.rocket.android.RocketChatCache; import chat.rocket.android.RocketChatCache;
import chat.rocket.android.helper.RxHelper; import chat.rocket.android.helper.RxHelper;
import chat.rocket.android.log.RCLog; import chat.rocket.android.log.RCLog;
...@@ -260,6 +261,7 @@ import io.reactivex.subjects.BehaviorSubject; ...@@ -260,6 +261,7 @@ import io.reactivex.subjects.BehaviorSubject;
private Single<Boolean> connectToServer(String hostname) { private Single<Boolean> connectToServer(String hostname) {
return Single.defer(() -> { return Single.defer(() -> {
if (!serverConnectivityList.containsKey(hostname)) { if (!serverConnectivityList.containsKey(hostname)) {
ConnectionStatusManager.INSTANCE.setConnectionError();
return Single.error(new IllegalArgumentException("hostname not found")); return Single.error(new IllegalArgumentException("hostname not found"));
} }
...@@ -270,8 +272,10 @@ import io.reactivex.subjects.BehaviorSubject; ...@@ -270,8 +272,10 @@ import io.reactivex.subjects.BehaviorSubject;
} }
if (serviceInterface != null) { if (serviceInterface != null) {
ConnectionStatusManager.INSTANCE.setConnecting();
return serviceInterface.ensureConnectionToServer(hostname); return serviceInterface.ensureConnectionToServer(hostname);
} else { } else {
ConnectionStatusManager.INSTANCE.setConnectionError();
return Single.error(new ThreadLooperNotPreparedException("not prepared")); return Single.error(new ThreadLooperNotPreparedException("not prepared"));
} }
}); });
......
...@@ -38,7 +38,8 @@ public class RocketChatService extends Service implements ConnectivityServiceInt ...@@ -38,7 +38,8 @@ public class RocketChatService extends Service implements ConnectivityServiceInt
/** /**
* ensure RocketChatService alive. * ensure RocketChatService alive.
*/ */
/*package*/static void keepAlive(Context context) { /*package*/
static void keepAlive(Context context) {
context.startService(new Intent(context, RocketChatService.class)); context.startService(new Intent(context, RocketChatService.class));
} }
...@@ -99,31 +100,29 @@ public class RocketChatService extends Service implements ConnectivityServiceInt ...@@ -99,31 +100,29 @@ public class RocketChatService extends Service implements ConnectivityServiceInt
return Single.defer(() -> { return Single.defer(() -> {
webSocketThreadLock.acquire(); webSocketThreadLock.acquire();
int connectivityState = ConnectivityManager.getInstance(getApplicationContext()).getConnectivityState(hostname); int connectivityState = ConnectivityManager.getInstance(getApplicationContext()).getConnectivityState(hostname);
boolean isDisconnected = connectivityState < ServerConnectivity.STATE_CONNECTED ; boolean isDisconnected = connectivityState < ServerConnectivity.STATE_CONNECTED;
if (currentWebSocketThread != null && existsThreadForHostname(hostname) && !isDisconnected) { if (currentWebSocketThread != null && existsThreadForHostname(hostname) && !isDisconnected) {
webSocketThreadLock.release(); webSocketThreadLock.release();
return Single.just(currentWebSocketThread); return Single.just(currentWebSocketThread);
} }
if (currentWebSocketThread != null) { if (currentWebSocketThread != null) {
if (isDisconnected) { boolean hasFailed = existsThreadForHostname(hostname);
return currentWebSocketThread.terminate(true) return currentWebSocketThread.terminate(hasFailed)
.doAfterTerminate(() -> currentWebSocketThread = null) .doAfterTerminate(() -> currentWebSocketThread = null)
.flatMap(terminated -> .flatMap(terminated ->
RocketChatWebSocketThread.getStarted(getApplicationContext(), hostname) RocketChatWebSocketThread.getStarted(getApplicationContext(), hostname)
.doOnSuccess(thread -> { .doOnSuccess(thread -> {
currentWebSocketThread = thread; currentWebSocketThread = thread;
webSocketThreadLock.release(); webSocketThreadLock.release();
}) })
.doOnError(throwable -> { .doOnError(throwable -> {
currentWebSocketThread = null; currentWebSocketThread = null;
RCLog.e(throwable); RCLog.e(throwable);
Logger.INSTANCE.report(throwable); Logger.INSTANCE.report(throwable);
webSocketThreadLock.release(); webSocketThreadLock.release();
}) })
); );
}
return Single.just(currentWebSocketThread);
} }
return RocketChatWebSocketThread.getStarted(getApplicationContext(), hostname) return RocketChatWebSocketThread.getStarted(getApplicationContext(), hostname)
......
...@@ -14,6 +14,7 @@ import java.util.concurrent.TimeUnit; ...@@ -14,6 +14,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import bolts.Task; import bolts.Task;
import chat.rocket.android.ConnectionStatusManager;
import chat.rocket.android.api.MethodCallHelper; import chat.rocket.android.api.MethodCallHelper;
import chat.rocket.android.helper.LogIfError; import chat.rocket.android.helper.LogIfError;
import chat.rocket.android.helper.RxHelper; import chat.rocket.android.helper.RxHelper;
...@@ -123,26 +124,26 @@ public class RocketChatWebSocketThread extends HandlerThread { ...@@ -123,26 +124,26 @@ public class RocketChatWebSocketThread extends HandlerThread {
} }
/** /**
* terminate WebSocket thread. * Terminate WebSocket thread. If {@code hasFailed} is {@code true} it means that a connection was
* in progress but failed and got offline. If it's {@code false} means that the user explicitly
* disconnected from server either by logging out or by means of changing servers.
* *
* @param isDisconnected {@code true} If we're trying to terminate a disconnected a websocket * @param hasFailed {@code true} if the termination is due to a network error, otherwise
* thread which means it has failed. * return false
*/ */
@DebugLog @DebugLog
/* package */ Single<Boolean> terminate(boolean isDisconnected) { /* package */ Single<Boolean> terminate(boolean hasFailed) {
if (isAlive()) { if (isAlive()) {
return Single.create(emitter -> { return Single.create(emitter -> new Handler(getLooper()).post(() -> {
new Handler(getLooper()).post(() -> { RCLog.d("thread %s: terminated()", Thread.currentThread().getId());
RCLog.d("thread %s: terminated()", Thread.currentThread().getId()); int reason = (hasFailed) ?
int reason = (isDisconnected) ? DDPClient.REASON_NETWORK_ERROR : DDPClient.REASON_CLOSED_BY_USER;
DDPClient.REASON_NETWORK_ERROR : DDPClient.REASON_CLOSED_BY_USER; unregisterListenersAndClose(reason);
unregisterListenersAndClose(reason); connectivityManager.notifyConnectionLost(hostname, reason);
connectivityManager.notifyConnectionLost(hostname, RocketChatWebSocketThread.super.quit();
isDisconnected ? DDPClient.REASON_NETWORK_ERROR : DDPClient.REASON_CLOSED_BY_USER); ConnectionStatusManager.INSTANCE.setOffline();
RocketChatWebSocketThread.super.quit(); emitter.onSuccess(true);
emitter.onSuccess(true); }));
});
});
} else { } else {
connectivityManager.notifyConnectionLost(hostname, connectivityManager.notifyConnectionLost(hostname,
DDPClient.REASON_NETWORK_ERROR); DDPClient.REASON_NETWORK_ERROR);
...@@ -166,7 +167,7 @@ public class RocketChatWebSocketThread extends HandlerThread { ...@@ -166,7 +167,7 @@ public class RocketChatWebSocketThread extends HandlerThread {
@DebugLog @DebugLog
/* package */ Single<Boolean> keepAlive() { /* package */ Single<Boolean> keepAlive() {
return checkIfConnectionAlive() return checkIfConnectionAlive()
.flatMap(alive -> alive ? Single.just(true) : connectWithExponentialBackoff()); .flatMap(alive -> connectWithExponentialBackoff());
} }
@DebugLog @DebugLog
...@@ -227,7 +228,7 @@ public class RocketChatWebSocketThread extends HandlerThread { ...@@ -227,7 +228,7 @@ public class RocketChatWebSocketThread extends HandlerThread {
.onSuccessTask(task -> { .onSuccessTask(task -> {
final String newSession = task.getResult().session; final String newSession = task.getResult().session;
connectivityManager.notifyConnectionEstablished(hostname, newSession); connectivityManager.notifyConnectionEstablished(hostname, newSession);
// handling WebSocket#onClose() callback.
task.getResult().client.getOnCloseCallback().onSuccess(_task -> { task.getResult().client.getOnCloseCallback().onSuccess(_task -> {
RxWebSocketCallback.Close result = _task.getResult(); RxWebSocketCallback.Close result = _task.getResult();
if (result.code == DDPClient.REASON_NETWORK_ERROR) { if (result.code == DDPClient.REASON_NETWORK_ERROR) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment