Unverified Commit 92ee59e8 authored by Rafael Kellermann Streit's avatar Rafael Kellermann Streit Committed by GitHub

Merge pull request #586 from RocketChat/fix/indefinite-status-ticker

[FIX] Infinite status ticker
parents 61707e2d cfa5323f
......@@ -50,30 +50,30 @@ public class DDPClient {
impl = new DDPClientImpl(this, client);
}
public Task<DDPClientCallback.Connect> connect(String url, String session) {
private Task<DDPClientCallback.Connect> connect(String url, String session) {
hostname.set(url);
TaskCompletionSource<DDPClientCallback.Connect> task = new TaskCompletionSource<>();
impl.connect(task, url, session);
return task.getTask();
}
public Task<DDPClientCallback.Ping> ping(@Nullable String id) {
private Task<DDPClientCallback.Ping> ping(@Nullable String id) {
TaskCompletionSource<DDPClientCallback.Ping> task = new TaskCompletionSource<>();
impl.ping(task, id);
return task.getTask();
}
public Maybe<DDPClientCallback.Base> doPing(@Nullable String id) {
private Maybe<DDPClientCallback.Base> doPing(@Nullable String id) {
return impl.ping(id);
}
public Task<DDPSubscription.Ready> sub(String id, String name, JSONArray params) {
private Task<DDPSubscription.Ready> sub(String id, String name, JSONArray params) {
TaskCompletionSource<DDPSubscription.Ready> task = new TaskCompletionSource<>();
impl.sub(task, name, params, id);
return task.getTask();
}
public Task<DDPSubscription.NoSub> unsub(String id) {
private Task<DDPSubscription.NoSub> unsub(String id) {
TaskCompletionSource<DDPSubscription.NoSub> task = new TaskCompletionSource<>();
impl.unsub(task, id);
return task.getTask();
......
......@@ -52,7 +52,7 @@ public class DDPClientImpl {
}
}
public void connect(final TaskCompletionSource<DDPClientCallback.Connect> task, final String url,
/* package */ void connect(final TaskCompletionSource<DDPClientCallback.Connect> task, final String url,
String session) {
try {
flowable = websocket.connect(url).autoConnect(2);
......
......@@ -47,9 +47,9 @@ import hugo.weaving.DebugLog;
*/
public class MainActivity extends AbstractAuthedActivity implements MainContract.View {
private RoomToolbar toolbar;
private StatusTicker statusTicker;
private SlidingPaneLayout pane;
private MainContract.Presenter presenter;
private volatile Snackbar statusTicker;
@Override
public int getLayoutContainerForFragment() {
......@@ -61,7 +61,6 @@ public class MainActivity extends AbstractAuthedActivity implements MainContract
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
toolbar = findViewById(R.id.activity_main_toolbar);
statusTicker = new StatusTicker();
pane = findViewById(R.id.sliding_pane);
setupToolbar();
}
......@@ -95,6 +94,8 @@ public class MainActivity extends AbstractAuthedActivity implements MainContract
if (presenter != null) {
presenter.release();
}
// Dismiss any status ticker
if (statusTicker != null) statusTicker.dismiss();
super.onPause();
}
......@@ -245,28 +246,36 @@ public class MainActivity extends AbstractAuthedActivity implements MainContract
@Override
public void showLoginScreen() {
LaunchUtil.showLoginActivity(this, hostname);
statusTicker.updateStatus(StatusTicker.STATUS_DISMISS, null);
showConnectionOk();
}
@Override
public void showConnectionError() {
statusTicker.updateStatus(StatusTicker.STATUS_CONNECTION_ERROR,
Snackbar.make(findViewById(getLayoutContainerForFragment()),
R.string.fragment_retry_login_error_title, Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.fragment_retry_login_retry_title, view ->
ConnectivityManager.getInstance(getApplicationContext()).keepAliveServer()));
public synchronized void showConnectionError() {
dismissStatusTickerIfShowing();
statusTicker = Snackbar.make(findViewById(getLayoutContainerForFragment()),
R.string.fragment_retry_login_error_title, Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.fragment_retry_login_retry_title, view ->
ConnectivityManager.getInstance(getApplicationContext()).keepAliveServer());
statusTicker.show();
}
@Override
public void showConnecting() {
statusTicker.updateStatus(StatusTicker.STATUS_TOKEN_LOGIN,
Snackbar.make(findViewById(getLayoutContainerForFragment()),
R.string.server_config_activity_authenticating, Snackbar.LENGTH_INDEFINITE));
public synchronized void showConnecting() {
dismissStatusTickerIfShowing();
statusTicker = Snackbar.make(findViewById(getLayoutContainerForFragment()),
R.string.server_config_activity_authenticating, Snackbar.LENGTH_INDEFINITE);
statusTicker.show();
}
@Override
public void showConnectionOk() {
statusTicker.updateStatus(StatusTicker.STATUS_DISMISS, null);
public synchronized void showConnectionOk() {
dismissStatusTickerIfShowing();
}
private void dismissStatusTickerIfShowing() {
if (statusTicker != null) {
statusTicker.dismiss();
}
}
@Override
......@@ -349,34 +358,4 @@ public class MainActivity extends AbstractAuthedActivity implements MainContract
public void beforeLogoutCleanUp() {
presenter.beforeLogoutCleanUp();
}
//TODO: consider this class to define in layouthelper for more complicated operation.
private static class StatusTicker {
static final int STATUS_DISMISS = 0;
static final int STATUS_CONNECTION_ERROR = 1;
static final int STATUS_TOKEN_LOGIN = 2;
private int status;
private Snackbar snackbar;
StatusTicker() {
status = STATUS_DISMISS;
}
void updateStatus(int status, Snackbar snackbar) {
if (status == this.status) {
return;
}
this.status = status;
if (this.snackbar != null) {
this.snackbar.dismiss();
}
if (status != STATUS_DISMISS) {
this.snackbar = snackbar;
if (this.snackbar != null) {
this.snackbar.show();
}
}
}
}
}
......@@ -6,26 +6,26 @@ import android.support.annotation.Nullable;
import java.util.List;
import chat.rocket.core.models.ServerInfo;
import io.reactivex.Observable;
import io.reactivex.Flowable;
import io.reactivex.Single;
/**
* interfaces used for Activity/Fragment and other UI-related logic.
*/
public interface ConnectivityManagerApi {
void keepAliveServer();
void keepAliveServer();
void addOrUpdateServer(String hostname, @Nullable String name, boolean insecure);
void addOrUpdateServer(String hostname, @Nullable String name, boolean insecure);
void removeServer(String hostname);
void removeServer(String hostname);
Single<Boolean> connect(String hostname);
Single<Boolean> connect(String hostname);
List<ServerInfo> getServerList();
List<ServerInfo> getServerList();
Observable<ServerConnectivity> getServerConnectivityAsObservable();
Flowable<ServerConnectivity> getServerConnectivityAsObservable();
int getConnectivityState(@NonNull String hostname);
int getConnectivityState(@NonNull String hostname);
void resetConnectivityStateList();
void resetConnectivityStateList();
}
......@@ -8,10 +8,6 @@ import chat.rocket.core.models.ServerInfo;
* interfaces used for RocketChatService and RocketChatwebSocketThread.
*/
/*package*/ interface ConnectivityManagerInternal {
int REASON_CLOSED_BY_USER = 101;
int REASON_NETWORK_ERROR = 102;
int REASON_SERVER_ERROR = 103;
int REASON_UNKNOWN = 104;
void resetConnectivityStateList();
......
......@@ -23,137 +23,130 @@ import io.reactivex.Single;
*/
public class RocketChatService extends Service implements ConnectivityServiceInterface {
private ConnectivityManagerInternal connectivityManager;
private static volatile Semaphore webSocketThreadLock = new Semaphore(1);
private static volatile RocketChatWebSocketThread currentWebSocketThread;
private ConnectivityManagerInternal connectivityManager;
private static volatile Semaphore webSocketThreadLock = new Semaphore(1);
private static volatile RocketChatWebSocketThread currentWebSocketThread;
public class LocalBinder extends Binder {
ConnectivityServiceInterface getServiceInterface() {
return RocketChatService.this;
}
}
private final LocalBinder localBinder = new LocalBinder();
/**
* ensure RocketChatService alive.
*/
/*package*/static void keepAlive(Context context) {
context.startService(new Intent(context, RocketChatService.class));
}
public static void bind(Context context, ServiceConnection serviceConnection) {
context.bindService(
new Intent(context, RocketChatService.class), serviceConnection, Context.BIND_AUTO_CREATE);
}
public static void unbind(Context context, ServiceConnection serviceConnection) {
context.unbindService(serviceConnection);
}
@DebugLog
@Override
public void onCreate() {
super.onCreate();
connectivityManager = ConnectivityManager.getInstanceForInternal(getApplicationContext());
connectivityManager.resetConnectivityStateList();
}
public class LocalBinder extends Binder {
ConnectivityServiceInterface getServiceInterface() {
return RocketChatService.this;
@DebugLog
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
connectivityManager.ensureConnections();
return START_NOT_STICKY;
}
}
private final LocalBinder localBinder = new LocalBinder();
/**
* ensure RocketChatService alive.
*/
/*package*/ static void keepAlive(Context context) {
context.startService(new Intent(context, RocketChatService.class));
}
public static void bind(Context context, ServiceConnection serviceConnection) {
context.bindService(
new Intent(context, RocketChatService.class), serviceConnection, Context.BIND_AUTO_CREATE);
}
public static void unbind(Context context, ServiceConnection serviceConnection) {
context.unbindService(serviceConnection);
}
@DebugLog
@Override
public void onCreate() {
super.onCreate();
connectivityManager = ConnectivityManager.getInstanceForInternal(getApplicationContext());
connectivityManager.resetConnectivityStateList();
}
@DebugLog
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
connectivityManager.ensureConnections();
return START_NOT_STICKY;
}
@Override
public Single<Boolean> ensureConnectionToServer(String hostname) { //called via binder.
return getOrCreateWebSocketThread(hostname)
.doOnError(err -> {
err.printStackTrace();
currentWebSocketThread = null;
})
.flatMap(webSocketThreads -> webSocketThreads.keepAlive());
}
@Override
public Single<Boolean> disconnectFromServer(String hostname) { //called via binder.
return Single.defer(() -> {
if (!existsThreadForHostname(hostname)) {
return Single.just(true);
}
if (currentWebSocketThread != null) {
return currentWebSocketThread.terminate()
// after disconnection from server
.doAfterTerminate(() -> {
currentWebSocketThread = null;
// remove RealmConfiguration key from HashMap
RealmStore.sStore.remove(hostname);
});
} else {
return Observable.timer(1, TimeUnit.SECONDS).singleOrError()
.flatMap(_val -> disconnectFromServer(hostname));
}
});
}
@DebugLog
private Single<RocketChatWebSocketThread> getOrCreateWebSocketThread(String hostname) {
return Single.defer(() -> {
webSocketThreadLock.acquire();
int connectivityState = ConnectivityManager.getInstance(getApplicationContext()).getConnectivityState(hostname);
boolean isDisconnected = connectivityState != ServerConnectivity.STATE_CONNECTED;
if (currentWebSocketThread != null && existsThreadForHostname(hostname) && !isDisconnected) {
webSocketThreadLock.release();
return Single.just(currentWebSocketThread);
}
connectivityManager.notifyConnecting(hostname);
if (currentWebSocketThread != null) {
return currentWebSocketThread.terminate()
.doAfterTerminate(() -> currentWebSocketThread = null)
.doOnError(RCLog::e)
.flatMap(terminated ->
RocketChatWebSocketThread.getStarted(getApplicationContext(), hostname)
.doOnSuccess(thread -> {
currentWebSocketThread = thread;
webSocketThreadLock.release();
})
.doOnError(throwable -> {
@Override
public Single<Boolean> ensureConnectionToServer(String hostname) { //called via binder.
return getOrCreateWebSocketThread(hostname)
.flatMap(RocketChatWebSocketThread::keepAlive);
}
@Override
public Single<Boolean> disconnectFromServer(String hostname) { //called via binder.
return Single.defer(() -> {
if (!existsThreadForHostname(hostname)) {
return Single.just(true);
}
if (currentWebSocketThread != null) {
return currentWebSocketThread.terminate()
// after disconnection from server
.doAfterTerminate(() -> {
currentWebSocketThread = null;
RCLog.e(throwable);
Logger.report(throwable);
webSocketThreadLock.release();
})
);
}
return RocketChatWebSocketThread.getStarted(getApplicationContext(), hostname)
.doOnSuccess(thread -> {
currentWebSocketThread = thread;
webSocketThreadLock.release();
})
.doOnError(throwable -> {
currentWebSocketThread = null;
RCLog.e(throwable);
Logger.report(throwable);
webSocketThreadLock.release();
});
});
}
private boolean existsThreadForHostname(String hostname) {
if (hostname == null || currentWebSocketThread == null) {
return false;
// remove RealmConfiguration key from HashMap
RealmStore.sStore.remove(hostname);
});
} else {
return Observable.timer(1, TimeUnit.SECONDS).singleOrError()
.flatMap(_val -> disconnectFromServer(hostname));
}
});
}
@DebugLog
private Single<RocketChatWebSocketThread> getOrCreateWebSocketThread(String hostname) {
return Single.defer(() -> {
webSocketThreadLock.acquire();
int connectivityState = ConnectivityManager.getInstance(getApplicationContext()).getConnectivityState(hostname);
boolean isDisconnected = connectivityState != ServerConnectivity.STATE_CONNECTED;
if (currentWebSocketThread != null && existsThreadForHostname(hostname) && !isDisconnected) {
webSocketThreadLock.release();
return Single.just(currentWebSocketThread);
}
if (currentWebSocketThread != null) {
return currentWebSocketThread.terminate()
.doAfterTerminate(() -> currentWebSocketThread = null)
.flatMap(terminated ->
RocketChatWebSocketThread.getStarted(getApplicationContext(), hostname)
.doOnSuccess(thread -> {
currentWebSocketThread = thread;
webSocketThreadLock.release();
})
.doOnError(throwable -> {
currentWebSocketThread = null;
RCLog.e(throwable);
Logger.report(throwable);
webSocketThreadLock.release();
})
);
}
return RocketChatWebSocketThread.getStarted(getApplicationContext(), hostname)
.doOnSuccess(thread -> {
currentWebSocketThread = thread;
webSocketThreadLock.release();
})
.doOnError(throwable -> {
currentWebSocketThread = null;
RCLog.e(throwable);
Logger.report(throwable);
webSocketThreadLock.release();
});
});
}
private boolean existsThreadForHostname(String hostname) {
if (hostname == null || currentWebSocketThread == null) {
return false;
}
return currentWebSocketThread.getName().equals("RC_thread_" + hostname);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return localBinder;
}
return currentWebSocketThread.getName().equals("RC_thread_" + hostname);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return localBinder;
}
}
......@@ -76,26 +76,6 @@ public class RocketChatWebSocketThread extends HandlerThread {
private final CompositeDisposable reconnectDisposable = new CompositeDisposable();
private boolean listenersRegistered;
private static class KeepAliveTimer {
private long lastTime;
private final long thresholdMs;
public KeepAliveTimer(long thresholdMs) {
this.thresholdMs = thresholdMs;
lastTime = System.currentTimeMillis();
}
public boolean shouldCheckPrecisely() {
return lastTime + thresholdMs < System.currentTimeMillis();
}
public void update() {
lastTime = System.currentTimeMillis();
}
}
private final KeepAliveTimer keepAliveTimer = new KeepAliveTimer(20000);
private RocketChatWebSocketThread(Context appContext, String hostname) {
super("RC_thread_" + hostname);
this.appContext = appContext;
......@@ -108,7 +88,7 @@ public class RocketChatWebSocketThread extends HandlerThread {
* build new Thread.
*/
@DebugLog
public static Single<RocketChatWebSocketThread> getStarted(Context appContext, String hostname) {
/* package */ static Single<RocketChatWebSocketThread> getStarted(Context appContext, String hostname) {
return Single.<RocketChatWebSocketThread>create(objectSingleEmitter -> {
new RocketChatWebSocketThread(appContext, hostname) {
@Override
......@@ -148,7 +128,7 @@ public class RocketChatWebSocketThread extends HandlerThread {
* terminate WebSocket thread.
*/
@DebugLog
public Single<Boolean> terminate() {
/* package */ Single<Boolean> terminate() {
if (isAlive()) {
return Single.create(emitter -> {
new Handler(getLooper()).post(() -> {
......@@ -181,7 +161,7 @@ public class RocketChatWebSocketThread extends HandlerThread {
* synchronize the state of the thread with ServerConfig.
*/
@DebugLog
public Single<Boolean> keepAlive() {
/* package */ Single<Boolean> keepAlive() {
return checkIfConnectionAlive()
.flatMap(alive -> alive ? Single.just(true) : connectWithExponentialBackoff());
}
......@@ -192,11 +172,6 @@ public class RocketChatWebSocketThread extends HandlerThread {
return Single.just(false);
}
if (!keepAliveTimer.shouldCheckPrecisely()) {
return Single.just(true);
}
keepAliveTimer.update();
return Single.create(emitter -> {
new Thread() {
@Override
......@@ -207,9 +182,8 @@ public class RocketChatWebSocketThread extends HandlerThread {
RCLog.e(error);
connectivityManager.notifyConnectionLost(
hostname, DDPClient.REASON_CLOSED_BY_USER);
emitter.onError(error);
emitter.onSuccess(false);
} else {
keepAliveTimer.update();
emitter.onSuccess(true);
}
return null;
......@@ -245,11 +219,11 @@ public class RocketChatWebSocketThread extends HandlerThread {
return;
}
RCLog.d("DDPClient#connect");
connectivityManager.notifyConnecting(hostname);
DDPClient.get().connect(hostname, info.getSession(), info.isSecure())
.onSuccessTask(task -> {
final String newSession = task.getResult().session;
connectivityManager.notifyConnectionEstablished(hostname, newSession);
// handling WebSocket#onClose() callback.
task.getResult().client.getOnCloseCallback().onSuccess(_task -> {
RxWebSocketCallback.Close result = _task.getResult();
......@@ -292,18 +266,18 @@ public class RocketChatWebSocketThread extends HandlerThread {
return;
}
forceInvalidateTokens();
connectivityManager.notifyConnecting(hostname);
reconnectDisposable.add(
connectWithExponentialBackoff()
.subscribe(connected -> {
if (!connected) {
connectivityManager.notifyConnecting(hostname);
connectivityManager.notifyConnectionLost(hostname,
DDPClient.REASON_NETWORK_ERROR);
}
reconnectDisposable.clear();
}, error -> {
logErrorAndUnsubscribe(reconnectDisposable, error);
connectivityManager.notifyConnectionLost(hostname,
DDPClient.REASON_NETWORK_ERROR);
logErrorAndUnsubscribe(reconnectDisposable, error);
}
)
);
......@@ -315,7 +289,9 @@ public class RocketChatWebSocketThread extends HandlerThread {
}
private Single<Boolean> connectWithExponentialBackoff() {
return connect().retryWhen(RxHelper.exponentialBackoff(3, 500, TimeUnit.MILLISECONDS));
return connect()
.retryWhen(RxHelper.exponentialBackoff(1, 250, TimeUnit.MILLISECONDS))
.onErrorResumeNext(Single.just(false));
}
@DebugLog
......
......@@ -4,10 +4,13 @@ package chat.rocket.android.service;
* pair with server's hostname and its connectivity state.
*/
public class ServerConnectivity {
public static final int STATE_CONNECTED = 1;
public static final int STATE_DISCONNECTED = 2;
public static final int STATE_CONNECTING = 3;
/*package*/ static final int STATE_DISCONNECTING = 4;
public static final ServerConnectivity CONNECTED = new ServerConnectivity(null, STATE_CONNECTED);
public final String hostname;
public final int state;
......@@ -25,6 +28,21 @@ public class ServerConnectivity {
this.code = code;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ServerConnectivity that = (ServerConnectivity) o;
return state == that.state;
}
@Override
public int hashCode() {
return state;
}
/**
* This exception should be thrown when connection is lost during waiting for CONNECTED.
*/
......
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