/* * Copyright 2014 OpenMarket Ltd * Copyright 2017 Vector Creations Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.matrix.androidsdk.sync; import android.os.Handler; import android.os.Looper; import android.text.TextUtils; import org.matrix.androidsdk.util.Log; import org.matrix.androidsdk.listeners.IMXNetworkEventListener; import org.matrix.androidsdk.network.NetworkConnectivityReceiver; import org.matrix.androidsdk.rest.callback.ApiFailureCallback; import org.matrix.androidsdk.rest.callback.SimpleApiCallback; import org.matrix.androidsdk.rest.client.EventsRestClient; import org.matrix.androidsdk.rest.model.MatrixError; import org.matrix.androidsdk.rest.model.Sync.SyncResponse; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.CountDownLatch; /** * Thread that continually watches the event stream and sends events to its listener. */ public class EventsThread extends Thread { private static final String LOG_TAG = "EventsThread"; private static final int RETRY_WAIT_TIME_MS = 10000; private static final int DEFAULT_SERVER_TIMEOUT_MS = 30000; private static final int DEFAULT_CLIENT_TIMEOUT_MS = 120000; private EventsRestClient mEventsRestClient = null; private EventsThreadListener mListener = null; private String mCurrentToken = null; private boolean mInitialSyncDone = false; private boolean mPaused = true; private boolean mIsNetworkSuspended = false; private boolean mIsCatchingUp = false; private boolean mIsOnline = true; private boolean mKilling = false; private int mDefaultServerTimeoutms = DEFAULT_SERVER_TIMEOUT_MS; private int mNextServerTimeoutms = DEFAULT_SERVER_TIMEOUT_MS; // add a delay between two sync requests private int mRequestDelayMs = 0; private Timer mSyncDelayTimer = null; // avoid sync on "this" because it might differ if there is a timer. private final Object mSyncObject = new Object(); // Custom Retrofit error callback that will convert Retrofit errors into our own error callback private ApiFailureCallback mFailureCallback; // avoid restarting the listener if there is no network. // wait that there is an available network. private NetworkConnectivityReceiver mNetworkConnectivityReceiver; private boolean mbIsConnected = true; private final IMXNetworkEventListener mNetworkListener = new IMXNetworkEventListener() { @Override public void onNetworkConnectionUpdate(boolean isConnected) { Log.d(LOG_TAG, "onNetworkConnectionUpdate : before " + mbIsConnected + " now " + isConnected); synchronized (mSyncObject) { mbIsConnected = isConnected; } // the thread has been suspended and there is an available network if (isConnected && !mKilling) { Log.d(LOG_TAG, "onNetworkConnectionUpdate : call onNetworkAvailable"); onNetworkAvailable(); } } }; /** * Default constructor. * @param apiClient API client to make the events API calls * @param listener a listener to inform * @param initialToken the sync initial token. */ public EventsThread(EventsRestClient apiClient, EventsThreadListener listener, String initialToken) { super("Events thread"); mEventsRestClient = apiClient; mListener = listener; mCurrentToken = initialToken; } /** * Update the long poll timeout. * @param ms the timeout in ms */ public void setServerLongPollTimeout(int ms) { mDefaultServerTimeoutms = Math.max(ms, DEFAULT_SERVER_TIMEOUT_MS); Log.d(LOG_TAG, "setServerLongPollTimeout : " + mDefaultServerTimeoutms); } /** * @return the long poll timeout */ public int getServerLongPollTimeout() { return mDefaultServerTimeoutms; } /** * Set a delay between two sync requests. * @param ms the delay in ms */ public void setSyncDelay(int ms) { mRequestDelayMs = Math.max(0, ms); Log.d(LOG_TAG, "setSyncDelay : " + mRequestDelayMs); // cancel any pending delay timer if (null != mSyncDelayTimer) { Log.d(LOG_TAG, "setSyncDelay : cancel the delay timer"); mSyncDelayTimer.cancel(); // and sync asap synchronized (mSyncObject) { mSyncObject.notify(); } } } /** * @return the delay between two sync requests. */ public int getSyncDelay() { return mRequestDelayMs; } /** * Set the network connectivity listener. * It is used to avoid restarting the events threads each 10 seconds when there is no available network. * @param networkConnectivityReceiver the network receiver */ public void setNetworkConnectivityReceiver(NetworkConnectivityReceiver networkConnectivityReceiver) { mNetworkConnectivityReceiver = networkConnectivityReceiver; } /** * Set the failure callback. * @param failureCallback the failure callback. */ public void setFailureCallback(ApiFailureCallback failureCallback) { mFailureCallback = failureCallback; } /** * Pause the thread. It will resume where it left off when unpause()d. */ public void pause() { Log.d(LOG_TAG, "pause()"); mPaused = true; mIsCatchingUp = false; } /** * A network connection has been retrieved. */ private void onNetworkAvailable() { Log.d(LOG_TAG, "onNetWorkAvailable()"); if (mIsNetworkSuspended) { mIsNetworkSuspended = false; if (mPaused) { Log.d(LOG_TAG, "the event thread is still suspended"); } else { Log.d(LOG_TAG, "Resume the thread"); // cancel any catchup process. mIsCatchingUp = false; synchronized (mSyncObject) { mSyncObject.notify(); } } } else { Log.d(LOG_TAG, "onNetWorkAvailable() : nothing to do"); } } /** * Unpause the thread if it had previously been paused. If not, this does nothing. */ public void unpause() { Log.d(LOG_TAG, "unpause()"); if (mPaused) { Log.d(LOG_TAG, "unpause : the thread was paused so resume it."); mPaused = false; synchronized (mSyncObject) { mSyncObject.notify(); } } // cancel any catchup process. mIsCatchingUp = false; } /** * Catchup until some events are retrieved. */ public void catchup() { Log.d(LOG_TAG, "catchup()"); if (mPaused) { Log.d(LOG_TAG, "unpause : the thread was paused so wake it up"); mPaused = false; synchronized (mSyncObject) { mSyncObject.notify(); } } mIsCatchingUp = true; } /** * Allow the thread to finish its current processing, then permanently stop. */ public void kill() { Log.d(LOG_TAG, "killing ..."); mKilling = true; if (mPaused) { Log.d(LOG_TAG, "killing : the thread was pause so wake it up"); mPaused = false; synchronized (mSyncObject) { mSyncObject.notify(); } Log.d(LOG_TAG, "Resume the thread to kill it."); } } /** * Update the online status * @param isOnline true if the client must be seen as online */ public void setIsOnline(boolean isOnline) { Log.d(LOG_TAG, "setIsOnline to " + isOnline); mIsOnline = isOnline; } @Override public void run() { startSync(); } /** * Tells if a sync request contains some changed devices. * @param syncResponse the sync response * @return true if the response contains some changed devices. */ private static boolean hasDevicesChanged(SyncResponse syncResponse) { return (null != syncResponse.deviceLists) && (null != syncResponse.deviceLists.changed) && (syncResponse.deviceLists.changed.size() > 0); } /** * Start the events sync */ private void startSync() { if (null != mCurrentToken) { Log.d(LOG_TAG, "Resuming initial sync from " + mCurrentToken); } else { Log.d(LOG_TAG, "Requesting initial sync..."); } int serverTimeout; mPaused = false; // mInitialSyncDone = null != mCurrentToken; if (mInitialSyncDone) { // get the latest events asap serverTimeout = 0; // dummy initial sync // to hide the splash screen SyncResponse dummySyncResponse = new SyncResponse(); dummySyncResponse.nextBatch = mCurrentToken; mListener.onSyncResponse(dummySyncResponse, null, true); } else { // Start with initial sync while (!mInitialSyncDone) { final CountDownLatch latch = new CountDownLatch(1); mEventsRestClient.syncFromToken(null, 0, DEFAULT_CLIENT_TIMEOUT_MS, null, null, new SimpleApiCallback<SyncResponse>(mFailureCallback) { @Override public void onSuccess(SyncResponse syncResponse) { Log.d(LOG_TAG, "Received initial sync response."); mNextServerTimeoutms = hasDevicesChanged(syncResponse) ? 0 : mDefaultServerTimeoutms; mListener.onSyncResponse(syncResponse, null, (0 == mNextServerTimeoutms)); mCurrentToken = syncResponse.nextBatch; mInitialSyncDone = true; // unblock the events thread latch.countDown(); } private void sleepAndUnblock() { Log.i(LOG_TAG, "Waiting a bit before retrying"); new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { public void run() { latch.countDown(); } }, RETRY_WAIT_TIME_MS); } @Override public void onNetworkError(Exception e) { if (null != mCurrentToken) { onSuccess(null); } else { Log.e(LOG_TAG, "Sync V2 onNetworkError " + e.getLocalizedMessage()); super.onNetworkError(e); sleepAndUnblock(); } } @Override public void onMatrixError(MatrixError e) { super.onMatrixError(e); if (TextUtils.equals(MatrixError.UNKNOWN_TOKEN, e.errcode)) { mListener.onInvalidToken(); } else { sleepAndUnblock(); } } @Override public void onUnexpectedError(Exception e) { super.onUnexpectedError(e); Log.e(LOG_TAG, "Sync V2 onUnexpectedError " + e.getLocalizedMessage()); sleepAndUnblock(); } }); // block until the initial sync callback is invoked. try { latch.await(); } catch (InterruptedException e) { Log.e(LOG_TAG, "Interrupted whilst performing initial sync."); } } serverTimeout = mNextServerTimeoutms; } Log.d(LOG_TAG, "Starting event stream from token " + mCurrentToken); // sanity check if (null != mNetworkConnectivityReceiver) { mNetworkConnectivityReceiver.addEventListener(mNetworkListener); // mbIsConnected = mNetworkConnectivityReceiver.isConnected(); mIsNetworkSuspended = !mbIsConnected; } // Then repeatedly long-poll for events while (!mKilling) { // test if a delay between two syncs if ((!mPaused && !mIsNetworkSuspended) && (0 != mRequestDelayMs)) { mSyncDelayTimer = new Timer(); Log.d(LOG_TAG, "startSync : start a delay timer"); mSyncDelayTimer.schedule(new TimerTask() { @Override public void run() { Log.d(LOG_TAG, "start a sync after " + mRequestDelayMs + " ms"); synchronized (mSyncObject) { mSyncObject.notify(); } } }, mRequestDelayMs); } if (mPaused || mIsNetworkSuspended || (null != mSyncDelayTimer)) { if (null != mSyncDelayTimer) { Log.d(LOG_TAG, "Event stream is paused because there is a timer delay."); } else if (mIsNetworkSuspended) { Log.d(LOG_TAG, "Event stream is paused because there is no available network."); } else { Log.d(LOG_TAG, "Event stream is paused. Waiting."); } try { Log.d(LOG_TAG, "startSync : wait ..."); synchronized (mSyncObject) { mSyncObject.wait(); } if (null != mSyncDelayTimer) { mSyncDelayTimer.cancel(); mSyncDelayTimer = null; } Log.d(LOG_TAG, "Event stream woken from pause."); // perform a catchup asap serverTimeout = 0; } catch (InterruptedException e) { Log.e(LOG_TAG, "Unexpected interruption while paused: " + e.getMessage()); } } // the service could have been killed while being paused. if (!mKilling) { String inlineFilter = null; //"{\"room\":{\"timeline\":{\"limit\":250}}}"; final CountDownLatch latch = new CountDownLatch(1); Log.d(LOG_TAG, "Get events from token " + mCurrentToken); final int fServerTimeout = serverTimeout; mNextServerTimeoutms = mDefaultServerTimeoutms; mEventsRestClient.syncFromToken(mCurrentToken, serverTimeout, DEFAULT_CLIENT_TIMEOUT_MS, (mIsCatchingUp && mIsOnline) ? "offline" : null, inlineFilter, new SimpleApiCallback<SyncResponse>(mFailureCallback) { @Override public void onSuccess(SyncResponse syncResponse) { if (!mKilling) { // poll /sync with timeout=0 until // we get no to_device messages back. if (0 == fServerTimeout) { if (hasDevicesChanged(syncResponse)) { mNextServerTimeoutms = 0; } } // the catchup request is suspended when there is no need // to loop again if (mIsCatchingUp && (0 != mNextServerTimeoutms)) { Log.e(LOG_TAG, "Stop the catchup"); // stop any catch up mIsCatchingUp = false; mPaused = true; } Log.d(LOG_TAG, "Got event response"); mListener.onSyncResponse(syncResponse, mCurrentToken, (0 == mNextServerTimeoutms)); mCurrentToken = syncResponse.nextBatch; Log.d(LOG_TAG, "mCurrentToken is now set to " + mCurrentToken); } // unblock the events thread latch.countDown(); } private void onError(String description) { boolean isConnected; Log.d(LOG_TAG, "Got an error while polling events " + description); synchronized (mSyncObject) { isConnected = mbIsConnected; } // detected if the device is connected before trying again if (isConnected) { new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { public void run() { latch.countDown(); } }, RETRY_WAIT_TIME_MS); } else { // no network -> wait that a network connection comes back. mIsNetworkSuspended = true; latch.countDown(); } } @Override public void onNetworkError(Exception e) { onError(e.getLocalizedMessage()); } @Override public void onMatrixError(MatrixError e) { if (TextUtils.equals(MatrixError.UNKNOWN_TOKEN, e.errcode)) { mListener.onInvalidToken(); } else { onError(e.getLocalizedMessage()); } } @Override public void onUnexpectedError(Exception e) { onError(e.getLocalizedMessage()); } }); // block until the sync callback is invoked. try { latch.await(); } catch (InterruptedException e) { Log.e(LOG_TAG, "Interrupted whilst polling message"); } } serverTimeout = mNextServerTimeoutms; } if (null != mNetworkConnectivityReceiver) { mNetworkConnectivityReceiver.removeEventListener(mNetworkListener); } Log.d(LOG_TAG, "Event stream terminating."); } }