/* * Copyright 2014 OpenMarket 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.util; import android.content.Context; import android.text.TextUtils; import org.matrix.androidsdk.util.Log; import org.matrix.androidsdk.MXDataHandler; import org.matrix.androidsdk.listeners.IMXNetworkEventListener; import org.matrix.androidsdk.network.NetworkConnectivityReceiver; import org.matrix.androidsdk.rest.callback.ApiCallback; import org.matrix.androidsdk.rest.callback.RestAdapterCallback; import org.matrix.androidsdk.rest.model.MatrixError; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Random; import java.util.Timer; import java.util.TimerTask; import retrofit.RetrofitError; /** * unsent matrix events manager * This manager schedules the unsent events sending. * 1 - it keeps the unsent events order (i.e. wait that the first event is resent before sending the second one) * 2 - Apply the retry rules (event time life, 3 tries...) */ public class UnsentEventsManager { private static final String LOG_TAG = "UnsentEventsManager"; // 3 minutes private static final int MAX_MESSAGE_LIFETIME_MS = 180000; // perform only MAX_RETRIES retries private static final int MAX_RETRIES = 4; // The jitter value to apply to compute a random retry time. private static final int RETRY_JITTER_MS = 3000; // the network receiver private final NetworkConnectivityReceiver mNetworkConnectivityReceiver; // faster way to check if the event is already sent private final HashMap<Object, UnsentEventSnapshot> mUnsentEventsMap = new HashMap<>(); // get the sending order private final ArrayList<UnsentEventSnapshot> mUnsentEvents = new ArrayList<>(); // true of the device is connected to a data network private boolean mbIsConnected = false; // matrix error management private final MXDataHandler mDataHandler; /** * storage class */ private class UnsentEventSnapshot { // first time the message has been sent // -1 to ignore age test private long mAge; // the number of retries // it should be limited private int mRetryCount; // retry callback. private RestAdapterCallback.RequestRetryCallBack mRequestRetryCallBack; // retry timer private Timer mAutoResendTimer = null; public Timer mLifeTimeTimer = null; // the retry is in progress public boolean mIsResending = false; // human description of the event // The snapshot creator can hide some fields public String mEventDescription = null; /** * */ public boolean waitToBeResent() { return (null != mAutoResendTimer); } /** * Resend the event after a delay. * @param delayMs the delay in milliseconds. */ public void resendEventAfter(int delayMs) { stopTimer(); if (null != mEventDescription) { Log.d(LOG_TAG, "Resend after " + delayMs + " [" + mEventDescription + "]"); } mAutoResendTimer = new Timer(); mAutoResendTimer.schedule(new TimerTask() { @Override public void run() { try { UnsentEventSnapshot.this.mIsResending = true; if (null != mEventDescription) { Log.d(LOG_TAG, "Resend [" + mEventDescription + "]"); } mRequestRetryCallBack.onRetry(); } catch (Exception e) { Log.e(LOG_TAG, "## resendEventAfter() : onRetry failed " + e.getMessage()); } } }, delayMs); } /** * Stop any pending resending timer. */ public void stopTimer() { if (null != mAutoResendTimer) { mAutoResendTimer.cancel(); mAutoResendTimer = null; } } /** * Stop timers. */ public void stopTimers() { if (null != mAutoResendTimer) { mAutoResendTimer.cancel(); mAutoResendTimer = null; } if (null != mLifeTimeTimer) { mLifeTimeTimer.cancel(); mLifeTimeTimer = null; } } } /** * Constructor * @param networkConnectivityReceiver the network received * @param dataHandler the data handler */ public UnsentEventsManager(NetworkConnectivityReceiver networkConnectivityReceiver, MXDataHandler dataHandler) { mNetworkConnectivityReceiver = networkConnectivityReceiver; // add a default listener // to resend the unsent messages mNetworkConnectivityReceiver.addEventListener(new IMXNetworkEventListener() { @Override public void onNetworkConnectionUpdate(boolean isConnected) { mbIsConnected = isConnected; if (isConnected) { resentUnsents(); } } }); mbIsConnected = mNetworkConnectivityReceiver.isConnected(); mDataHandler = dataHandler; } /** * Warn that the apiCallback has been called * @param apiCallback the called apiCallback */ public void onEventSent(ApiCallback apiCallback) { if (null != apiCallback) { UnsentEventSnapshot snapshot = null; synchronized (mUnsentEventsMap) { if (mUnsentEventsMap.containsKey(apiCallback)) { snapshot = mUnsentEventsMap.get(apiCallback); } } if (null != snapshot) { if (null != snapshot.mEventDescription) { Log.d(LOG_TAG, "Resend Succeeded [" + snapshot.mEventDescription + "]"); } snapshot.stopTimers(); synchronized (mUnsentEventsMap) { mUnsentEventsMap.remove(apiCallback); mUnsentEvents.remove(snapshot); } resentUnsents(); } } } /** * Clear the session data */ public void clear() { synchronized (mUnsentEventsMap) { for(UnsentEventSnapshot snapshot : mUnsentEvents) { snapshot.stopTimers(); } mUnsentEvents.clear(); mUnsentEventsMap.clear(); } } /** * @return the network connectivity receiver */ public NetworkConnectivityReceiver getNetworkConnectivityReceiver() { return mNetworkConnectivityReceiver; } /** * @return the context */ public Context getContext() { return mDataHandler.getStore().getContext(); } /** * The event failed to be sent and cannot be resent. * It triggers the error callbacks. * @param eventDescription the event description * @param error the retrofit error * @param callback the callback. */ private static void triggerErrorCallback(MXDataHandler dataHandler, String eventDescription, RetrofitError error, ApiCallback callback) { if ((null != error) && !TextUtils.isEmpty(error.getMessage())) { // privacy //Log.e(LOG_TAG, error.getMessage() + " url=" + error.getUrl()); Log.e(LOG_TAG, error.getLocalizedMessage()); } if (null == error) { try { if (null != eventDescription) { Log.e(LOG_TAG, "Unexpected Error " + eventDescription); } if (null != callback) { callback.onUnexpectedError(null); } } catch (Exception e) { // privacy //Log.e(LOG_TAG, "Exception UnexpectedError " + e.getMessage() + " while managing " + error.getUrl()); Log.e(LOG_TAG, "Exception UnexpectedError " + e.getLocalizedMessage()); } } else if (error.isNetworkError()) { try { if (null != eventDescription) { Log.e(LOG_TAG, "Network Error " + eventDescription); } if (null != callback) { callback.onNetworkError(error); } } catch (Exception e) { // privacy //Log.e(LOG_TAG, "Exception NetworkError " + e.getMessage() + " while managing " + error.getUrl()); Log.e(LOG_TAG, "Exception NetworkError " + e.getLocalizedMessage()); } } else { // Try to convert this into a Matrix error MatrixError mxError; try { mxError = (MatrixError) error.getBodyAs(MatrixError.class); } catch (Exception e) { mxError = null; } if (mxError != null) { try { if (null != eventDescription) { Log.e(LOG_TAG, "Matrix Error " + mxError + " " + eventDescription); } if (TextUtils.equals(MatrixError.UNKNOWN_TOKEN, mxError.errcode)) { dataHandler.onInvalidToken(); } else if (null != callback) { callback.onMatrixError(mxError); } } catch (Exception e) { // privacy //Log.e(LOG_TAG, "Exception MatrixError " + e.getMessage() + " while managing " + error.getUrl()); Log.e(LOG_TAG, "Exception MatrixError " + e.getLocalizedMessage()); } } else { try { if (null != eventDescription) { Log.e(LOG_TAG, "Unexpected Error " + eventDescription); } if (null != callback) { callback.onUnexpectedError(error); } } catch (Exception e) { // privacy //Log.e(LOG_TAG, "Exception UnexpectedError " + e.getMessage() + " while managing " + error.getUrl()); Log.e(LOG_TAG, "Exception UnexpectedError " + e.getLocalizedMessage()); } } } } /** * warns that an event failed to be sent. * @param eventDescription the event description * @param ignoreEventTimeLifeInOffline tell if the event timelife is ignored in offline mode * @param retrofitError the retrofit error . * @param apiCallback the apiCallback. * @param requestRetryCallBack requestRetryCallBack. */ public void onEventSendingFailed(final String eventDescription, final boolean ignoreEventTimeLifeInOffline, final RetrofitError retrofitError, final ApiCallback apiCallback, final RestAdapterCallback.RequestRetryCallBack requestRetryCallBack) { boolean isManaged = false; if (null != eventDescription) { Log.d(LOG_TAG, "Fail to send [" + eventDescription + "]"); } if ((null != requestRetryCallBack) && (null != apiCallback)) { synchronized (mUnsentEventsMap) { UnsentEventSnapshot snapshot; // Try to convert this into a Matrix error MatrixError mxError = null; if (null != retrofitError) { try { mxError = (MatrixError) retrofitError.getBodyAs(MatrixError.class); } catch (Exception e) { mxError = null; } } // trace the matrix error. if ((null != eventDescription) && (null != mxError)) { Log.d(LOG_TAG, "Matrix error " + mxError.errcode + " " + mxError.getLocalizedMessage() + " [" + eventDescription + "]"); } int matrixRetryTimeout = -1; if ((null != mxError) && MatrixError.LIMIT_EXCEEDED.equals(mxError.errcode) && (null != mxError.retry_after_ms)) { matrixRetryTimeout = mxError.retry_after_ms + 200; } // some matrix errors are not trapped. if ((null == mxError) || !mxError.isSupportedErrorCode() || MatrixError.LIMIT_EXCEEDED.equals(mxError.errcode)) { // is it the first time that the event has been sent ? if (mUnsentEventsMap.containsKey(apiCallback)) { snapshot = mUnsentEventsMap.get(apiCallback); snapshot.mIsResending = false; snapshot.stopTimer(); // assume that LIMIT_EXCEEDED error is not a default retry if (matrixRetryTimeout < 0) { snapshot.mRetryCount++; } // any event has a time life to avoid very old messages long timeLife = 0; // age < 0 means that the event time life is ignored if (snapshot.mAge > 0) { timeLife = System.currentTimeMillis() - snapshot.mAge; } if ((timeLife > MAX_MESSAGE_LIFETIME_MS) || (snapshot.mRetryCount > MAX_RETRIES)) { snapshot.stopTimers(); mUnsentEventsMap.remove(apiCallback); mUnsentEvents.remove(snapshot); if (null != eventDescription) { Log.d(LOG_TAG, "Cancel [" + eventDescription + "]"); } isManaged = false; } else { isManaged = true; } } else { snapshot = new UnsentEventSnapshot(); snapshot.mAge = ignoreEventTimeLifeInOffline ? -1 : System.currentTimeMillis(); snapshot.mRequestRetryCallBack = requestRetryCallBack; snapshot.mRetryCount = 1; snapshot.mEventDescription = eventDescription; mUnsentEventsMap.put(apiCallback, snapshot); mUnsentEvents.add(snapshot); if (mbIsConnected || !ignoreEventTimeLifeInOffline) { // the event has a life time final UnsentEventSnapshot fSnapshot = snapshot; fSnapshot.mLifeTimeTimer = new Timer(); fSnapshot.mLifeTimeTimer.schedule(new TimerTask() { @Override public void run() { try { if (null != eventDescription) { Log.d(LOG_TAG, "Cancel to send [" + eventDescription + "]"); } fSnapshot.stopTimers(); synchronized (mUnsentEventsMap) { mUnsentEventsMap.remove(apiCallback); mUnsentEvents.remove(fSnapshot); } triggerErrorCallback(mDataHandler, eventDescription, retrofitError, apiCallback); } catch (Exception e) { Log.e(LOG_TAG, "## onEventSendingFailed() : failure Msg=" + e.getMessage()); } } }, MAX_MESSAGE_LIFETIME_MS); } else if (ignoreEventTimeLifeInOffline) { Log.d(LOG_TAG, "The request " + eventDescription + " will be sent when a network will be available"); } isManaged = true; } // retry to send the message ? if (isManaged) { // resend the event only if there is an available network // retrofitError.isNetworkError() does not provide a valid description of the failure // 1- there is no available network / the connection is lost. (what we could expect) // 2- the server did not response after 15s : the client would wrongly behave, it would wait until to switch to a valid network // It never happens, so the message is never resent. // if (mbIsConnected) { int jitterTime = ((int)Math.pow(2, snapshot.mRetryCount)) + (Math.abs(new Random(System.currentTimeMillis()).nextInt()) % RETRY_JITTER_MS); snapshot.resendEventAfter((matrixRetryTimeout > 0) ? matrixRetryTimeout : jitterTime); } } } } } if (!isManaged) { Log.d(LOG_TAG, "Cannot resend it"); triggerErrorCallback(mDataHandler, eventDescription, retrofitError, apiCallback); } } /** * check if some messages must be resent */ private void resentUnsents() { Log.d(LOG_TAG, "resentUnsents"); synchronized (mUnsentEventsMap) { if (mUnsentEvents.size() > 0) { try { // retry the first for(int index = 0; index < mUnsentEvents.size(); index++) { UnsentEventSnapshot unsentEventSnapshot = mUnsentEvents.get(index); // check if there is no required delay to resend the message if (!unsentEventSnapshot.waitToBeResent()) { // if the message is already resending, if (unsentEventSnapshot.mIsResending) { // do not resend any other one to try to keep the messages sending order. return; } else { if (null != unsentEventSnapshot.mEventDescription) { Log.d(LOG_TAG, "Automatically resend " + unsentEventSnapshot.mEventDescription); } unsentEventSnapshot.mIsResending = true; unsentEventSnapshot.mRequestRetryCallBack.onRetry(); } break; } } } catch (Exception e) { Log.e(LOG_TAG, "## resentUnsents() : failure Msg=" + e.getMessage()); } } } } }