com.mobicage.rpc.http.HttpCommunicator.java Source code

Java tutorial

Introduction

Here is the source code for com.mobicage.rpc.http.HttpCommunicator.java

Source

/*
 * Copyright 2016 Mobicage NV
 *
 * 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.
 *
 * @@license_version:1.1@@
 */

package com.mobicage.rpc.http;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.concurrent.CountDownLatch;

import javax.net.ssl.SSLException;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.HttpHostConnectException;
import org.apache.http.entity.StringEntity;
import org.jivesoftware.smack.util.Base64;
import org.json.JSONArray;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.PowerManager;

import com.mobicage.rogerthat.MainService;
import com.mobicage.rogerthat.config.ConfigurationProvider;
import com.mobicage.rogerthat.util.db.DatabaseManager;
import com.mobicage.rogerthat.util.logging.L;
import com.mobicage.rogerthat.util.pickle.PickleException;
import com.mobicage.rogerthat.util.system.SafeBroadcastReceiver;
import com.mobicage.rogerthat.util.system.SafeRunnable;
import com.mobicage.rogerthat.util.system.SystemUtils;
import com.mobicage.rogerthat.util.system.T;
import com.mobicage.rogerthat.util.system.TaggedWakeLock;
import com.mobicage.rpc.Credentials;
import com.mobicage.rpc.IResponseHandler;
import com.mobicage.rpc.RpcCall;
import com.mobicage.rpc.SDCardLogger;
import com.mobicage.rpc.config.CloudConstants;

public class HttpCommunicator {

    public static final String INTENT_HTTP_SUCCESSFUL = "com.mobicage.rpc.http.HttpCommunicator.SUCCESS";
    public static final String INTENT_HTTP_FAILURE = "com.mobicage.rpc.http.HttpCommunicator.FAILURE";
    public static final String INTENT_HTTP_START_OUTGOING_CALLS = "com.mobicage.rpc.http.HttpCommunicator.STARTOUTGOINGCALLS";
    public static final String INTENT_HTTP_START_OUTGOING_CALLS_WIFI = "filterOnWifiOnly";
    private static final String INTENT_SHOULD_RETRY_COMMUNICATION = "com.mobicage.rpc.http.SHOULD_RETRY_COMMUNICATION";
    private static final long COMMUNICATION_RETRY_INTERVAL = 300000; // 5 minutes

    private interface CommunicationResultHandler {

        void handle(final int resultCode);

    }

    private final static int STATUS_COMMUNICATION_FINISHED_WORK_DONE = 1; // Finished; we effectively did HTTP
    // communication
    private final static int STATUS_COMMUNICATION_CONTINUE = 2;
    private final static int STATUS_COMMUNICATION_SERVER_HAS_MORE = 3;
    private final static int STATUS_COMMUNICATION_ERROR = 4;
    private final static int STATUS_COMMUNICATION_FINISHED_NO_WORK_DONE = 5; // Finished; but we have not done HTTP
    // communication
    private final static String[] STATUS_COMMUNICATION_STRING_ARRAY = new String[] { "ILLEGAL STATUS",
            "STATUS_COMMUNICATION_FINISHED_WORK_DONE", "STATUS_COMMUNICATION_CONTINUE",
            "STATUS_COMMUNICATION_SERVER_HAS_MORE", "STATUS_COMMUNICATION_ERROR",
            "STATUS_COMMUNICATION_FINISHED_NO_WORK_DONE" };

    private final static int MAX_COMMUNICATION_CYCLES = 100; // Use -1 for infinite
    private final static int MAX_PACKET_SIZE = 200 * 1024;

    // Final
    private final Object mFinishedLock;
    private final Object mStateMachineLock;
    private final String mBase64Username;
    private final String mBase64Password;
    private final MainService mMainService;
    private final ConfigurationProvider mCfgProvider;
    private final SafeRunnable mStartStashingHandler;
    private final SafeRunnable mStopStashingHandler;
    private final SDCardLogger mSDCardLogger;
    private final AlarmManager mAlarmManager;

    // Use on HTTP thread
    private/* final */HttpBacklog mBacklog;

    // Stop doing work asap
    private volatile boolean mMustFinish = false;

    // New work is posted
    private volatile boolean mNewCallsInBacklog = false;

    // Current http request; we can abort it
    private volatile HttpPost mHttpPostRequest;

    // Protected by mStateMachineLock
    private boolean mIsCommunicating = false;
    private boolean mKickReceived = false;
    private boolean mLastCycleSuccess = true;

    // Owned by HTTP thread
    private String mCurrentServerUrl = null;
    private HandlerThread mNetworkWorkerThread;
    private Handler mNetworkHandler;

    private boolean mWifiOnlyEnabled;

    private final SafeBroadcastReceiver mBroadcastReceiver = new SafeBroadcastReceiver() {
        @Override
        public String[] onSafeReceive(Context context, Intent intent) {
            final String action = intent.getAction();
            if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
                // check if items in backlog
                scheduleCommunication(true, "Network connectivity changed");
            } else {
                scheduleCommunication(true, "Previous communication attempt failed");
            }
            return null;
        }
    };

    @SuppressWarnings("serial")
    private final class ServerRespondedWrongHTTPCodeException extends Exception {
        public ServerRespondedWrongHTTPCodeException(String string) {
            super(string);
        }
    };

    private abstract class LoopCompleteHandlerRunnable extends SafeRunnable {

        // Owned by BIZZ thread
        private boolean mSuccess = false;

        public void run(boolean success) {
            T.BIZZ();
            mSuccess = success;
            run();
        }

        public boolean getSuccess() {
            T.BIZZ();
            return mSuccess;
        }

    }

    private abstract class WakeLockReleaseRunnable extends SafeRunnable {
    }

    public HttpCommunicator(final MainService mainService, final Context context,
            final DatabaseManager databaseManager, final Credentials credentials,
            final ConfigurationProvider cfgProvider, final SafeRunnable onStartStashingHandler,
            final SafeRunnable onStopStashingHandler, final SDCardLogger sdcardLogger,
            final boolean wifiOnlySettingEnabled) {
        T.UI();

        mMainService = mainService;
        mCfgProvider = cfgProvider;
        mFinishedLock = new Object();
        mStateMachineLock = new Object();
        mStartStashingHandler = onStartStashingHandler;
        mStopStashingHandler = onStopStashingHandler;
        mWifiOnlyEnabled = wifiOnlySettingEnabled;

        mSDCardLogger = sdcardLogger;

        mAlarmManager = (AlarmManager) mMainService.getSystemService(Context.ALARM_SERVICE);

        // Create network thread for actual network communication
        mNetworkWorkerThread = new HandlerThread("rogerthat_net_worker");
        mNetworkWorkerThread.start();
        final Looper looper = mNetworkWorkerThread.getLooper();
        mNetworkHandler = new Handler(looper);
        mNetworkHandler.post(new SafeRunnable() {
            @Override
            protected void safeRun() throws Exception {
                debugLog("HTTP network thread id is " + android.os.Process.myTid());
            }
        });

        final CountDownLatch latch = new CountDownLatch(1);
        mMainService.postAtFrontOfBIZZHandler(new SafeRunnable() {
            @Override
            public void safeRun() {
                T.BIZZ();
                // For simplicity, I want to construct backlog on HTTP thread
                // This way backlog is 100% on HTTP thread
                mBacklog = new HttpBacklog(context, databaseManager);
                latch.countDown();
            }
        });
        try {
            latch.await();
        } catch (InterruptedException e) {
            bugLog(e);
        }

        mBase64Username = Base64.encodeBytes(credentials.getUsername().getBytes(), Base64.DONT_BREAK_LINES);
        mBase64Password = Base64.encodeBytes(credentials.getPassword().getBytes(), Base64.DONT_BREAK_LINES);

        mMainService.addHighPriorityIntent(HttpCommunicator.INTENT_HTTP_START_OUTGOING_CALLS);

        if (CloudConstants.USE_GCM_KICK_CHANNEL) {
            final IntentFilter filter = new IntentFilter();
            filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
            filter.addAction(INTENT_SHOULD_RETRY_COMMUNICATION);
            mMainService.registerReceiver(mBroadcastReceiver, filter);
        }
    }

    public void setWifiOnlyEnabled(boolean wifiOnlyEnabled) {
        this.mWifiOnlyEnabled = wifiOnlyEnabled;
    }

    public boolean isBusy() {
        synchronized (mStateMachineLock) {
            return !mLastCycleSuccess || mIsCommunicating;
        }
    }

    protected void bugLog(String s) {
        if (mSDCardLogger == null) {
            L.bug(s);
        } else {
            mSDCardLogger.bug(s);
        }
    }

    protected void bugLog(Exception e) {
        if (mSDCardLogger == null) {
            L.bug(e);
        } else {
            mSDCardLogger.bug(e);
        }
    }

    protected void bugLog(String s, Exception e) {
        if (mSDCardLogger == null) {
            L.bug(s, e);
        } else {
            mSDCardLogger.bug(s, e);
        }
    }

    protected void debugLog(String s, Exception e) {
        if (mSDCardLogger == null) {
            L.d(s, e);
        } else {
            mSDCardLogger.d(s, e);
        }
    }

    protected void debugLog(String s) {
        if (mSDCardLogger == null) {
            L.d(s);
        } else {
            mSDCardLogger.d(s);
        }
    }

    protected void debugLog(Exception e) {
        if (mSDCardLogger == null) {
            L.d(e);
        } else {
            mSDCardLogger.d(e);
        }
    }

    public void close() {
        T.UI();

        mMustFinish = true;

        // XXX: here we could be nicer e.g. wait 2 seconds for clean finish
        // e.g. synchronized(mFinishedLock) { try { mFinishedLock.wait(); } ...

        final Looper looper = mNetworkWorkerThread.getLooper();
        if (looper != null) {
            looper.quit();
        }
        if (mHttpPostRequest != null)
            mHttpPostRequest.abort();
        try {
            // XXX: can this cause ANR?
            mNetworkWorkerThread.join();
        } catch (InterruptedException e) {
            bugLog(e);
        }
        mNetworkHandler = null;
        mNetworkWorkerThread = null;

        final CountDownLatch latch = new CountDownLatch(1);
        mMainService.postAtFrontOfBIZZHandler(new SafeRunnable() {
            @Override
            public void safeRun() {
                T.BIZZ();
                mBacklog.close();
                latch.countDown();
            }
        });

        if (CloudConstants.USE_GCM_KICK_CHANNEL) {
            mMainService.unregisterReceiver(mBroadcastReceiver);
        }

        // XXX: can cause ANR
        try {
            latch.await();
        } catch (InterruptedException e) {
            bugLog(e);
        }

    }

    // ///////////////////////////////////////////////////////////////////

    public void scheduleCommunication(final boolean force, final String reason) {
        T.dontCare();
        debugLog("Schedule HTTP communication force: " + force + " - reason: " + reason);
        synchronized (mStateMachineLock) {
            if (mIsCommunicating) {
                // Already communicating. Work will be picked up automatically
                debugLog("Skipping duplicate communication in scheduleCommunication()");
                mKickReceived = true;
                return;
            } else {
                mStartStashingHandler.run();
            }
        }
        // Must start new communication cycle
        final TaggedWakeLock wakeLock = newWakeLock();
        debugLog("Acquiring wakelock " + wakeLock.hashCode());
        wakeLock.acquire();
        mMainService.postOnBIZZHandler(new SafeRunnable() {
            @Override
            protected void safeRun() throws Exception {
                T.BIZZ();
                communicate(force, new WakeLockReleaseRunnable() {

                    private boolean mHasRun = false;

                    @Override
                    protected void safeRun() throws Exception {
                        T.BIZZ();
                        if (!mHasRun) {
                            debugLog("Releasing wakelock " + wakeLock.hashCode());
                            mHasRun = true;
                            wakeLock.release();
                        } else {
                            debugLog("Skipping duplicate release of wakelock " + wakeLock.hashCode());
                        }
                    }
                });
            }
        });
    }

    /*
     * Execute HTTP request/response/request/response/... communication cycle
     * 
     * force == true if we have to do at least 1 communication because we know that there is or might be data ready on
     * the server
     * 
     * force == false if the initiative to kick communication is caused by a client call. It might have been processed
     * by a previous communication run. The volatile flag newCallsInBacklog will tell us
     */
    private void communicate(final boolean force, final WakeLockReleaseRunnable wakeLockReleaseRunnable) {
        T.BIZZ();
        synchronized (mStateMachineLock) {
            if (mIsCommunicating) {
                debugLog("Skipping duplicate communcation in communicate() method");
                mKickReceived = true;
                wakeLockReleaseRunnable.run();
                return;
            }
        }
        try {
            if (mMustFinish) {
                debugLog("Leaving HTTP communicate(.) because mustFinish==true");
                wakeLockReleaseRunnable.run();
                return;
            }
            if (!force && !mNewCallsInBacklog) {
                debugLog("Leaving HTTP communicate(.) because force==false and newCallsInBacklog==false");
                wakeLockReleaseRunnable.run();
                return;
            }
            synchronized (mStateMachineLock) {
                mLastCycleSuccess = false;
            }
            if (!mMainService.getNetworkConnectivityManager().isConnected()) {
                debugLog("Leaving HTTP communicate(.) because network is not connected");
                if (CloudConstants.USE_GCM_KICK_CHANNEL)
                    scheduleRetryCommunication();
                wakeLockReleaseRunnable.run();
                return;
            }
        } catch (Exception e) {
            bugLog("Exception caught while investigating if we need to communicate!", e);
            wakeLockReleaseRunnable.run();
            return;
        }

        final SafeRunnable finallyHandler = new SafeRunnable() {
            private boolean mHasRun = false;

            @Override
            protected void safeRun() throws Exception {
                T.BIZZ();
                if (!mHasRun) {
                    mHasRun = true;
                    try {
                        synchronized (mStateMachineLock) {
                            debugLog("Setting mIsCommunicating from " + mIsCommunicating + " to false");
                            mIsCommunicating = false;
                            mStopStashingHandler.run();
                            if (mKickReceived) {
                                // There might be a last kick which we have not processed yet
                                scheduleCommunication(true, "Kick received during busy HTTP communication cycle.");
                            }
                        }
                        synchronized (mFinishedLock) {
                            mFinishedLock.notifyAll();
                        }
                    } finally {
                        wakeLockReleaseRunnable.run();
                    }
                } else {
                    debugLog("finallyHandler: not running because mHasRun==true");
                }
            }
        };

        final LoopCompleteHandlerRunnable loopCompleteHandler = new LoopCompleteHandlerRunnable() {

            @Override
            protected void safeRun() throws Exception {
                T.BIZZ();
                try {
                    if (!mMustFinish)
                        mBacklog.doRetentionCleanup();

                    final boolean success = getSuccess();
                    synchronized (mStateMachineLock) {
                        mLastCycleSuccess = success;
                    }
                    if (success)
                        broadcastHttpSuccess();
                } finally {
                    finallyHandler.run();
                }
            }
        };

        try {
            final HttpProtocol protocol = new HttpProtocol(mMainService, mBacklog, mCfgProvider, mSDCardLogger);

            if (protocol.getAlternativeUrl() == null) {
                mCurrentServerUrl = CloudConstants.JSON_RPC_URL;
            } else {
                mCurrentServerUrl = protocol.getAlternativeUrl();
            }

            startCommunicationCycle(protocol, 1, STATUS_COMMUNICATION_SERVER_HAS_MORE, loopCompleteHandler,
                    finallyHandler, wakeLockReleaseRunnable); // force first round-trip

        } catch (Exception e) {
            finallyHandler.run();
            bugLog("Exception in communicate.", e);
        }

    }

    private void startCommunicationCycle(final HttpProtocol protocol, final int loopCount, final int status,
            final LoopCompleteHandlerRunnable loopCompleteHandler, final SafeRunnable finallyRunnable,
            final WakeLockReleaseRunnable wakeLockReleaseRunnable) {
        try {
            debugLog("Starting HTTP communication loop " + loopCount);
            boolean mustActOnKick;
            synchronized (mStateMachineLock) {
                // No matter what the current backlog state is, we MUST communicate if one
                // of the following conditions is met:
                // * server responded that there is more
                // * we received a kick after the start of the last communication cycle
                mustActOnKick = mKickReceived;
                mKickReceived = false;
            }

            final boolean cachedNewCallsInBacklog = mNewCallsInBacklog;
            mNewCallsInBacklog = false; // we know that at least all work that is in the backlog *now*, will be
            // processed
            doCommunication(protocol, (status == STATUS_COMMUNICATION_SERVER_HAS_MORE) || mustActOnKick, loopCount,
                    wakeLockReleaseRunnable, new CommunicationResultHandler() {
                        @Override
                        public void handle(final int newStatus) {
                            T.BIZZ();

                            debugLog("CommunicationResultHandler received status " + newStatus + " = "
                                    + STATUS_COMMUNICATION_STRING_ARRAY[newStatus]);

                            if (newStatus == STATUS_COMMUNICATION_ERROR) {
                                debugLog("Error communicating to server");
                                if (cachedNewCallsInBacklog)
                                    mNewCallsInBacklog = true;
                                broadcastHttpError();
                                loopCompleteHandler.run(false);

                                if (CloudConstants.USE_GCM_KICK_CHANNEL) {
                                    scheduleRetryCommunication();
                                }

                                // Note: if we have received a kick in the meantime, we will actually retry
                                // in the finallyHandler

                                // TODO: improvement would be to set mKickReceived to true in case we have
                                // network connectivity at this point in time. However, a backoff scheme is needed

                                return;
                            }

                            if ((newStatus == STATUS_COMMUNICATION_FINISHED_NO_WORK_DONE
                                    || newStatus == STATUS_COMMUNICATION_FINISHED_WORK_DONE)
                                    && !mNewCallsInBacklog) {
                                // We can finish communication cycle unless someone has kicked us during
                                // the last communication step
                                final boolean exitLoop;
                                synchronized (mStateMachineLock) {
                                    if (!mKickReceived) {
                                        debugLog("Successfully finished HTTP communication cycle with server");
                                        exitLoop = true;
                                    } else {
                                        exitLoop = false;
                                    }
                                }
                                if (exitLoop) {
                                    loopCompleteHandler.run(newStatus == STATUS_COMMUNICATION_FINISHED_WORK_DONE);
                                    return;
                                }

                            }

                            final int newLoopCount = loopCount + 1;

                            if (!((MAX_COMMUNICATION_CYCLES < 0) || (newLoopCount <= MAX_COMMUNICATION_CYCLES))) {
                                bugLog("Reached max amount of HTTP communication cycles: "
                                        + MAX_COMMUNICATION_CYCLES);
                                loopCompleteHandler.run(true);
                                return;
                            }

                            if (mMustFinish) {
                                debugLog("Forced finish communication loop");
                                loopCompleteHandler.run(false);
                                return;
                            }

                            startCommunicationCycle(protocol, newLoopCount, newStatus, loopCompleteHandler,
                                    finallyRunnable, wakeLockReleaseRunnable);
                        }
                    });
        } catch (Exception e) {
            // XXX: should we run finallyRunnable here ? It will set mIsCommunicating:=false
            finallyRunnable.run();
            bugLog("Exception in startCommunicationCycle", e);
        }
    }

    private void broadcastHttpSuccess() {
        debugLog("Broadcast HTTP success");
        final Intent intent = new Intent(INTENT_HTTP_SUCCESSFUL);
        mMainService.sendBroadcast(intent);
    }

    private void broadcastHttpError() {
        debugLog("Broadcast HTTP error");
        final Intent intent = new Intent(INTENT_HTTP_FAILURE);
        mMainService.sendBroadcast(intent);
    }

    private void broadcastHtppStartOutgoingCalls(boolean filterOnWifiOnly) {
        debugLog("Broadcast HTTP start outgoing calls");
        final Intent intent = new Intent(INTENT_HTTP_START_OUTGOING_CALLS);
        intent.putExtra(INTENT_HTTP_START_OUTGOING_CALLS_WIFI, filterOnWifiOnly);
        mMainService.sendBroadcast(intent);

    }

    private void doCommunication(final HttpProtocol protocol, final boolean force, final int loopCount,
            final WakeLockReleaseRunnable wakeLockReleaseRunnable, final CommunicationResultHandler resultHandler) {
        T.BIZZ();

        debugLog("doCommunication - force = " + force + " - loopCount = " + loopCount);
        boolean filterOnWifiOnly = mWifiOnlyEnabled
                && !mMainService.getNetworkConnectivityManager().isWifiConnected();
        broadcastHtppStartOutgoingCalls(filterOnWifiOnly);
        final String callsJSONString = getJSONRepresentationForOutgoingCalls(filterOnWifiOnly);
        final boolean hasNoOutgoingCallsInBacklog = callsJSONString.equals("[]");

        if (!force && protocol.getAckIDsToSend().size() == 0 && protocol.getResponseIDsToSend().size() == 0
                && hasNoOutgoingCallsInBacklog) {
            resultHandler.handle(STATUS_COMMUNICATION_FINISHED_NO_WORK_DONE);
            return;
        }

        synchronized (mStateMachineLock) {
            if (mIsCommunicating && loopCount == 1) {
                debugLog("Skipping duplicate first loop of doCommunication()");
                mKickReceived = true;
                wakeLockReleaseRunnable.run();
                return;
            } else {
                debugLog("Setting mIsCommunicating from " + mIsCommunicating + " to true for loop " + loopCount);
                mIsCommunicating = true;
            }
        }

        final StringBuilder sb = new StringBuilder("{\"av\":1, \"c\":");
        sb.append(getJSONRepresentationForOutgoingCalls(filterOnWifiOnly));
        sb.append(", \"r\":");
        sb.append(getJSONRepresentationForOutgoingResponses(protocol));
        sb.append(", \"a\":");
        sb.append(getJSONRepresentationForOutgoingAcks(protocol));
        sb.append("}");

        protocol.getAckIDsToSend().clear();
        protocol.getResponseIDsToSend().clear();

        mNetworkHandler.post(new Runnable() {
            @Override
            public void run() {
                String tmpResponse = null;
                long before = 0;
                long after = 0;
                try {
                    before = System.currentTimeMillis();
                    tmpResponse = doSynchronousRequest(protocol.getHttpClient(), sb.toString());
                    after = System.currentTimeMillis();
                    debugLog("HTTP request (loop=" + loopCount + ") finished in " + (after - before) + " millis");
                } catch (UnknownHostException e) {
                    debugLog(e.getMessage());
                } catch (HttpHostConnectException e) {
                    debugLog(e.getMessage());
                } catch (ServerRespondedWrongHTTPCodeException e) {
                    debugLog(e.getMessage(), e);
                } catch (SSLException e) {
                    debugLog(e);
                } catch (SocketTimeoutException e) {
                    debugLog(e);
                } catch (IOException e) {
                    debugLog(e);
                } catch (Exception e) {
                    bugLog(e);
                }
                final String response = tmpResponse;
                final long fbefore = before;
                final long fafter = after;
                mMainService.postOnBIZZHandler(new SafeRunnable() {
                    @Override
                    protected void safeRun() throws Exception {
                        T.BIZZ();
                        if (response == null) {
                            redoCommunicationForError(protocol, force, loopCount, wakeLockReleaseRunnable,
                                    resultHandler);
                            return;
                        }
                        HttpProtocol.ProtocolDetails pd = protocol.processIncomingMessagesString(response);
                        if (pd.serverTime != 0) {
                            final long serverTimestamp = pd.serverTime * 1000;
                            if (fafter - fbefore < 5000) {
                                final long localCorrelationTimestamp = fafter - (fafter - fbefore) / 2;
                                final long adjustedTimeDiff = serverTimestamp - localCorrelationTimestamp;
                                mMainService.setAdjustedTimeDiff(adjustedTimeDiff);
                            }
                        }
                        if (pd.more) {
                            resultHandler.handle(STATUS_COMMUNICATION_SERVER_HAS_MORE);
                            return;
                        }
                        if (hasNoOutgoingCallsInBacklog && protocol.getAckIDsToSend().size() == 0
                                && protocol.getResponseIDsToSend().size() == 0) {
                            resultHandler.handle(STATUS_COMMUNICATION_FINISHED_WORK_DONE);
                            return;
                        }
                        resultHandler.handle(STATUS_COMMUNICATION_CONTINUE);
                    }
                });
            }
        });
    }

    private void redoCommunicationForError(final HttpProtocol protocol, final boolean force, final int loopCount,
            final WakeLockReleaseRunnable wakeLockReleaseRunnable, final CommunicationResultHandler resultHandler) {
        if (!mCurrentServerUrl.equals(CloudConstants.JSON_RPC_URL)) {
            debugLog("Failover from " + mCurrentServerUrl + " to " + CloudConstants.JSON_RPC_URL);
            protocol.clearAlternativeUrl();
            mCurrentServerUrl = CloudConstants.JSON_RPC_URL;

            // loopCount + 1 otherwise failover won't work since doCommunication will see that mIsCommunicating == true
            doCommunication(protocol, force, loopCount + 1, wakeLockReleaseRunnable, resultHandler);
        } else {
            resultHandler.handle(STATUS_COMMUNICATION_ERROR);
        }
    }

    private String getJSONRepresentationForOutgoingCalls(boolean filterOnWifiOnly) {
        T.BIZZ();
        StringBuilder sb = new StringBuilder("[");
        HttpBacklogStreamer streamer = mBacklog.getStreamer(filterOnWifiOnly);
        boolean needComma = false;
        try {
            HttpBacklogItem item;
            while ((item = streamer.next()) != null) {
                if (needComma)
                    sb.append(", ");
                else
                    needComma = true;
                sb.append(item.body);
                if (sb.length() > MAX_PACKET_SIZE)
                    break;
            }
        } finally {
            streamer.close();
        }
        sb.append("]");
        return sb.toString();
    }

    private String getJSONRepresentationForOutgoingResponses(HttpProtocol protocol) {
        T.BIZZ();
        StringBuilder sb = new StringBuilder("[");
        boolean needComma = false;
        for (String callid : protocol.getResponseIDsToSend()) {
            String body = mBacklog.getBodyForCallId(callid);
            if (body != null) {
                if (needComma)
                    sb.append(", ");
                else
                    needComma = true;
                sb.append(body);

            } else {
                bugLog("Could not find body for backlog response item " + callid);
            }

        }
        sb.append("]");
        return sb.toString();
    }

    private String getJSONRepresentationForOutgoingAcks(HttpProtocol protocol) {
        T.BIZZ();
        return new JSONArray(protocol.getAckIDsToSend()).toString();
    }

    private String doSynchronousRequest(HttpClient httpClient, String requestString)
            throws ServerRespondedWrongHTTPCodeException, ClientProtocolException, UnsupportedEncodingException,
            IOException {

        L.d("Sending HTTP request to " + mCurrentServerUrl + " : " + requestString);
        mHttpPostRequest = new HttpPost(mCurrentServerUrl);
        mHttpPostRequest.setHeader("Content-type", "application/json-rpc; charset=\"utf-8\"");
        mHttpPostRequest.setHeader("X-MCTracker-User", mBase64Username);
        mHttpPostRequest.setHeader("X-MCTracker-Pass", mBase64Password);

        // XXX: improve performance - accept zipped encoding

        mHttpPostRequest.setEntity(new StringEntity(requestString, "UTF-8"));

        final HttpResponse response = httpClient.execute(mHttpPostRequest);

        final int responseCode = response.getStatusLine().getStatusCode();

        if (responseCode == HttpStatus.SC_OK) {
            HttpEntity responseEntity = response.getEntity();
            InputStream is = responseEntity.getContent();
            return SystemUtils.convertStreamToString(is);
        } else {
            HttpEntity responseEntity = response.getEntity();
            InputStream is = responseEntity.getContent();
            debugLog("Error - server responded HTTP code: " + responseCode + "\n"
                    + SystemUtils.convertStreamToString(is));
            throw new ServerRespondedWrongHTTPCodeException("HTTP response code: " + responseCode);
        }

    }

    public void addOutgoingCall(final HttpBacklogItem item, final boolean priority, final String function,
            final IResponseHandler<?> responseHandler) throws PickleException {
        T.BIZZ();
        mBacklog.addOutgoingCall(item, priority, function, responseHandler,
                RpcCall.WIFI_ONLY_FUNCTIONS.contains(function));
        mNewCallsInBacklog = true;
    }

    private TaggedWakeLock newWakeLock() {
        return TaggedWakeLock.newTaggedWakeLock(mMainService, PowerManager.PARTIAL_WAKE_LOCK, "HTTP WakeLock");
    }

    private void scheduleRetryCommunication() {
        final Intent intent = new Intent(INTENT_SHOULD_RETRY_COMMUNICATION);
        long triggerAtMillis = System.currentTimeMillis() + COMMUNICATION_RETRY_INTERVAL;
        mAlarmManager.set(AlarmManager.RTC_WAKEUP, triggerAtMillis,
                PendingIntent.getBroadcast(mMainService, 0, intent, 0));
    }

}