com.chaosinmotion.securechat.network.SCNetwork.java Source code

Java tutorial

Introduction

Here is the source code for com.chaosinmotion.securechat.network.SCNetwork.java

Source

/*
 * Copyright (c) 2016. William Edward Woody
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation, either version 3 of the License, or (at your
 * option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program. If not, see <http://www.gnu.org/licenses/>
 *
 */

package com.chaosinmotion.securechat.network;

import android.text.TextUtils;
import android.util.Log;

import com.chaosinmotion.securechat.utils.ThreadPool;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.CookieManager;
import java.net.HttpCookie;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;

/**
 * Class wraper for handling network requests
 *
 * Created by woody on 4/9/16.
 */
public class SCNetwork {
    public static final int LOGIN_SUCCESS = 0;
    public static final int LOGIN_FAILURE = 1;
    public static final int LOGIN_SERVERERROR = 2;

    /**
     * Response interface used by callers to get a response from our
     * asynchronous request process
     */
    public interface ResponseInterface {
        void responseResult(Response response);
    }

    /**
     * Interface that must be called once the login dialog completes.
     */
    public interface LoginResponse {
        /**
         * Pass in true if the login dialog succeeded, and false if the
         * operation was canceled.
         * @param success True if logged in, false if canceled
         */
        void didLogin(boolean success);
    }

    public interface Delegate {
        void showServerError(Response response);

        SCNetworkCredentials credentials();

        void requestLoginDialog(LoginResponse response);
    }

    public interface WaitSpinner {
        void startWaitSpinner();

        void stopWaitSpinner();
    }

    /**
     * Interface which represents the results of attempting to log in.
     */
    public interface LoginCallback {
        void loginResult(int reason);
    }

    private static class Request {
        String requestURI;
        JSONObject params;
        boolean skipErrors;
        Object caller;
        long enqueueTime;
        ResponseInterface callback;

        Future<?> taskFuture; // generated if in progress call for cancel

        Response lastError;
        boolean waitFlag;
    }

    public static class Response {
        private int serverCode;
        private boolean success;
        private int error;
        private String errorMessage;
        private JSONArray exceptionStack;
        private JSONObject data;

        public JSONObject getData() {
            return data;
        }

        public int getError() {
            return error;
        }

        public String getErrorMessage() {
            return errorMessage;
        }

        public JSONArray getExceptionStack() {
            return exceptionStack;
        }

        public int getServerCode() {
            return serverCode;
        }

        public boolean isSuccess() {
            return success;
        }

        public boolean isServerError() {
            return serverCode != 200;
        }

        public boolean isAuthenticationError() {
            return !success && (error == 4);
        }
    }

    /*
     *  State
     */
    private static SCNetwork shared;

    private CookieManager cookies;
    private ArrayList<Request> callQueue;
    private ArrayList<Request> retryQueue;
    private String server;
    private boolean inLogin;
    private boolean networkError;
    private Delegate delegate;

    /**
     * Internal initialization
     */
    private SCNetwork() {
        cookies = new CookieManager();
        callQueue = new ArrayList<Request>();
        retryQueue = new ArrayList<Request>();
    }

    /**
     * Obtain the network object for network requests
     * @return
     */
    public static synchronized SCNetwork get() {
        if (shared == null)
            shared = new SCNetwork();
        return shared;
    }

    /**
     * Set the network delegate
     */

    public void setNetworkDelegate(Delegate del) {
        delegate = del;
    }

    /************************************************************************/
    /*                                                      */
    /*   Login Requests                                          */
    /*                                                      */
    /************************************************************************/

    /**
     * Attempt to log in. Returns the results in a callback run asynchronously
     * @param creds
     * @param callback
     */
    public void doLogin(final SCNetworkCredentials creds, final LoginCallback callback) {
        // Enqueue the login request
        request("login/token", null, this, new ResponseInterface() {
            @Override
            public void responseResult(Response response) {
                if (response.isSuccess()) {
                    try {
                        String token = response.getData().optString("token");

                        JSONObject params = new JSONObject();
                        params.putOpt("username", creds.getUsername());
                        params.putOpt("password", creds.hashPasswordWithToken(token));

                        request("login/login", params, this, new ResponseInterface() {
                            @Override
                            public void responseResult(Response response) {
                                if (response.isSuccess()) {
                                    callback.loginResult(LOGIN_SUCCESS);
                                } else if (response.getError() == 2) {
                                    callback.loginResult(LOGIN_FAILURE);
                                } else {
                                    callback.loginResult(LOGIN_SERVERERROR);
                                }
                            }
                        });
                    } catch (JSONException e) {
                        // I don't know a better way to handle this.
                        callback.loginResult(LOGIN_SERVERERROR);
                    }
                } else {
                    showError(response);
                    callback.loginResult(LOGIN_SERVERERROR);
                }
            }
        });
    }

    /**
     *   We failed to login. Cancel all failed operations. Call on main thread
     */

    private void failedLogin() {
        inLogin = false;

        for (Request req : retryQueue) {
            req.callback.responseResult(req.lastError);
        }
        retryQueue.clear();
    }

    /**
     *   Succeeded logging in; retry all the requests that need to be retried
     */

    private void successfulLogin() {
        inLogin = false;

        ArrayList<Request> tmp = new ArrayList<Request>(retryQueue);
        retryQueue.clear();
        ;

        for (Request req : tmp) {
            sendRequest(req);
        }
    }

    /**
     *   Login request
     */

    private void loginRequest() {
        if (inLogin)
            return;
        inLogin = true;

        // Perform login request if we have credentials. Otherwise run dialog
        SCNetworkCredentials creds = delegate.credentials();
        if (creds != null) {
            doLogin(creds, new LoginCallback() {
                @Override
                public void loginResult(int reason) {
                    if (reason == LOGIN_SUCCESS) {
                        successfulLogin();
                    } else if (reason == LOGIN_FAILURE) {
                        runLoginDialog();
                    } else {
                        failedLogin();
                    }
                }
            });
        } else {
            runLoginDialog();
        }
    }

    /**
     *   Run the login dialog. The login dialog will handle the login process
     *   itself. We presume the dialog will actually handle the login
     *  process by calling doLogin, and if successful this will return
     *  true. We also presume the login dialog will set the credentials
     *  returned by our delegate.
     */

    private void runLoginDialog() {
        delegate.requestLoginDialog(new LoginResponse() {
            @Override
            public void didLogin(boolean success) {
                if (success) {
                    successfulLogin();
                } else {
                    failedLogin();
                }
            }
        });
    }

    /************************************************************************/
    /*                                                      */
    /*   Request                                                */
    /*                                                      */
    /************************************************************************/

    /**
     *   Iterate through all queued calls forcing them to be canceled
     */

    public void cancelRequestsWithCaller(Object caller) {
        ArrayList<Request> remove = new ArrayList<Request>();
        for (Request req : callQueue) {
            if (req.caller == caller) {
                if (req.taskFuture != null) {
                    req.taskFuture.cancel(true);
                }
                remove.add(req);

                if (req.waitFlag && ((req.caller instanceof WaitSpinner))) {
                    ((WaitSpinner) req.caller).stopWaitSpinner();
                    req.waitFlag = false;
                }
            }
        }
        callQueue.removeAll(remove);

        remove.clear();
        for (Request req : retryQueue) {
            if (req.caller == caller) {
                if (req.taskFuture != null) {
                    req.taskFuture.cancel(true);
                }
                remove.add(req);

                if (req.waitFlag && ((req.caller instanceof WaitSpinner))) {
                    ((WaitSpinner) req.caller).stopWaitSpinner();
                    req.waitFlag = false;
                }
            }
        }
        retryQueue.removeAll(remove);
    }

    /*
     *   Set the server prefix
     */

    public void setServerPrefix(String prefix) {
        if ((prefix == null) || (prefix.length() == 0))
            return; // empty

        if (prefix.charAt(prefix.length() - 1) == '/') {
            prefix = prefix.substring(0, prefix.length() - 1); // trim off trailing '/'
        }

        int index = prefix.indexOf("://");
        if (index == -1) {
            prefix = "https://" + prefix; // prepend https:// if not otherwise specified
        }
        server = prefix + "/ss/api/1/"; // full url prefix
    }

    /*
     *   Convert network request to a request URL
     */

    private HttpURLConnection requestWith(Request req) throws IOException {
        String path = server + req.requestURI;
        URL url = new URL(path);

        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("POST");

        if (cookies.getCookieStore().getCookies().size() > 0) {
            conn.setRequestProperty("Cookie", TextUtils.join(";", cookies.getCookieStore().getCookies()));
        }

        conn.setDoInput(true);

        if (req.params != null) {
            String jsonString = req.params.toString();
            conn.setDoOutput(true);
            OutputStream os = conn.getOutputStream();
            os.write(jsonString.getBytes("UTF-8"));
            os.close();
        }

        return conn;
    }

    /**
     * Internal method to parse result from server
     * @param is Input stream from server
     * @return JSON parsed result
     */
    private static JSONObject parseResult(InputStream is) throws IOException, JSONException {
        ByteArrayOutputStream bais = new ByteArrayOutputStream();
        byte[] buffer = new byte[512];
        int len;

        while (0 < (len = is.read(buffer))) {
            bais.write(buffer, 0, len);
        }
        is.close();

        byte[] data = bais.toByteArray();
        if (data.length < 2)
            return new JSONObject(); // no return result
        String json = new String(data, "UTF-8");
        JSONTokener tokener = new JSONTokener(json);
        return (JSONObject) tokener.nextValue();
    }

    /*
     *   Internal process request. This enqueues the request and parses the
     *   response.
     */

    private synchronized void sendRequest(final Request request) {
        callQueue.add(request);

        // If not in background, spin the spinner
        if (request.caller instanceof WaitSpinner) {
            ((WaitSpinner) request.caller).startWaitSpinner();
            request.waitFlag = true;
        }

        request.taskFuture = ThreadPool.get().enqueueAsync(new Runnable() {
            @Override
            public void run() {
                try {
                    HttpURLConnection conn = requestWith(request);

                    conn.connect();
                    Map<String, List<String>> headers = conn.getHeaderFields();
                    List<String> clist = headers.get("Set-Cookie");
                    if (clist != null) {
                        for (String cookie : clist) {
                            cookies.getCookieStore().add(null, HttpCookie.parse(cookie).get(0));
                        }
                    }

                    InputStream is = conn.getInputStream();
                    JSONObject d = parseResult(is);
                    conn.disconnect();

                    final Response response = new Response();
                    response.serverCode = conn.getResponseCode();
                    if (d != null) {
                        response.success = d.optBoolean("success");
                        response.error = d.optInt("error");
                        response.errorMessage = d.optString("message");
                        response.exceptionStack = d.optJSONArray("exception");
                        response.data = d.optJSONObject("data");
                    }

                    ThreadPool.get().enqueueMain(new Runnable() {
                        @Override
                        public void run() {
                            if (request.waitFlag && ((request.caller instanceof WaitSpinner))) {
                                ((WaitSpinner) request.caller).stopWaitSpinner();
                                request.waitFlag = false;
                            }
                            callQueue.remove(request);
                            handleResponse(response, request);
                        }
                    });
                } catch (Exception ex) {
                    /*
                     *  This happens if there is a connection error.
                     */
                    ThreadPool.get().enqueueMain(new Runnable() {
                        @Override
                        public void run() {
                            if (request.waitFlag && ((request.caller instanceof WaitSpinner))) {
                                ((WaitSpinner) request.caller).stopWaitSpinner();
                                request.waitFlag = false;
                            }
                            callQueue.remove(request);
                            handleIOError(request);
                        }
                    });
                }
            }
        });
    }

    /*
     *   Handle errors
     */

    private void showError(Response response) {
        if (response.isServerError()) {
            if (networkError)
                return;
            networkError = true; // if server error, do only once
        }
        delegate.showServerError(response);
    }

    /*
     *   Handle the request
     */

    private void handleResponse(Response response, Request request) {
        if (response.isAuthenticationError()) {
            /*
             *  If authentication error, queue for retry and try to log in
             */

            retryQueue.add(request);
            request.lastError = response;
            loginRequest();
            return;
        }

        /*
         *  Process error notifications
         */

        if (!request.skipErrors) {
            /*
             *  Handle errors via delegate. If we are skipping error processing
             *  we simply pass the results up
             */

            if (response.isServerError()) {
                showError(response);
            } else if (!response.success) {
                /*
                 *  For random errors, display them. Skip login errors.
                 */

                if (response.error != 2) {
                    showError(response);
                }
            }
        }

        /*
         *  Reset our network error flag if the request succeeded.
         */

        if (!response.isServerError()) {
            networkError = false;
        }

        /*
         *  Now pass the response upwards.
         */
        request.callback.responseResult(response);
    }

    /**
     * This shouldn't happen normally, as it occurs while we are
     * constructing the request. But handle it anyway.
     * @param request
     */
    private void handleIOError(Request request) {
        Response response = new Response();
        response.serverCode = 0;
        response.success = false;
        response.error = 3; // internal error?
        response.errorMessage = "Client internal error";
        response.exceptionStack = null;
        response.data = null;
        handleResponse(response, request);
    }

    /*
     *   Enqueue request
     */

    public void request(String requestUri, JSONObject params, Object caller, ResponseInterface callback) {
        request(requestUri, params, false, caller, callback);
    }

    public void request(String requestUri, JSONObject params, boolean skipErrors, Object caller,
            ResponseInterface callback) {
        Request request = new Request();

        request.requestURI = requestUri;
        request.params = params;
        request.skipErrors = skipErrors;
        request.caller = caller;
        request.callback = callback;
        request.enqueueTime = System.currentTimeMillis();

        sendRequest(request);
    }
}