public class CompactHtmlRenderer extends CoreHtmlNodeRenderer {
    private final HtmlWriter html;

    public CompactHtmlRenderer(HtmlNodeRendererContext context) {
        super(context);
        this.html = context.getWriter();
    }

    @Override
    public void visit(Paragraph node) {
        // Replace paragraphs with line breaks to get a compact view.
        if (node.getNext() == null) {
            html.line();
            visitChildren(node);
            html.line();
        } else {
            super.visit(node);
        }
    }

} public interface Constants {
    String TAG = "Kandroid";
    int ResultChanged = 1;
    int RequestEditTask = 1;
    int FileSelectCode = 10;
    int RequestStoragePermission = 20;
    int[] minKanboardVersion = new int[] {1, 0, 38};
} public class DownloadIntentService extends IntentService {
    public class StopServiceIntent extends Activity {
        public StopServiceIntent() {
            super();
            Log.d(Constants.TAG, "DownloadService: Stop");
            stopService((Intent) getIntent().getSerializableExtra("serviceIntent"));
        }
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            Log.d(Constants.TAG, "DownloadService: Stop");
            stopService((Intent) getIntent().getSerializableExtra("serviceIntent"));
        }
    }
    public DownloadIntentService() {
        super("DownloadService");
    }

    @Override
    protected void onHandleIntent(@Nullable Intent intent) {
        Log.d(Constants.TAG, "Starting Download in Service");
        HttpURLConnection con = null;
        final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this.getBaseContext());

        Authenticator.setDefault(new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(preferences.getString("username", ""), preferences.getString("password", "").toCharArray());
            }

        });

        NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this);
        NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

        String serverURL = preferences.getString("serverurl", "");

        try {
            URL kanboardURL = KanboardAPI.sanitizeURL(serverURL.trim());

            String request = intent.getStringExtra("request");
            String filename = intent.getStringExtra("filename");
            if (BuildConfig.DEBUG) {
                Log.d(Constants.TAG, request);
                Log.d(Constants.TAG, filename);
            }

            Intent stopIntent = new Intent(this, DownloadIntentService.StopServiceIntent.class);
            stopIntent.putExtra("serviceIntent", intent);

            notificationBuilder.setContentTitle(getString(R.string.service_downloading))
                    .setContentText(filename)
                    .setSmallIcon(android.R.drawable.stat_sys_download)
                    .setAutoCancel(false)
                    .setOngoing(true)
                    .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
//                    .addAction(android.R.drawable.ic_menu_close_clear_cancel, getText(android.R.string.cancel), PendingIntent.getActivity(this, 0, stopIntent, PendingIntent.FLAG_UPDATE_CURRENT))
                    .setProgress(100, 0, true);

            mNotificationManager.notify(554, notificationBuilder.build());

            con = KanboardAPI.openConnection(kanboardURL, request);

            if (con == null) {
                Log.e(Constants.TAG, "DownloadService: Unable to connect to host");
                notificationBuilder.setContentTitle(getText(R.string.error_host_unknown))
                        .setOngoing(false)
                        .setSmallIcon(android.R.drawable.stat_notify_error)
                        .setProgress(0, 0, false);
                mNotificationManager.notify(554, notificationBuilder.build());
                return;
            }

            if (BuildConfig.DEBUG) {
                Log.d(Constants.TAG, Integer.toString(con.getResponseCode()));
            }

            DataInputStream in;
            if (con.getResponseCode() < 400)
                in = (new DataInputStream(con.getInputStream()));
            else
                in = (new DataInputStream(con.getErrorStream()));

            String contentStr = org.apache.commons.io.IOUtils.toString(in, org.apache.commons.io.Charsets.toCharset(con.getContentEncoding()));

            con.disconnect();

            JSONObject jsonData = new JSONObject(contentStr);
            String encodedData = jsonData.optString("result", "");
            if (encodedData.isEmpty()) {
                Log.d(Constants.TAG, "Ist leer");
                notificationBuilder.setContentTitle(getText(R.string.service_result_empty))
                        .setOngoing(false)
                        .setSmallIcon(android.R.drawable.stat_notify_error)
                        .setProgress(0, 0, false);
                mNotificationManager.notify(554, notificationBuilder.build());
                return;
            }
            byte[] decodedData = Base64.decode(encodedData, Base64.DEFAULT);

            File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), filename);
            FileOutputStream fs = new FileOutputStream(file);
            fs.write(decodedData);
            fs.close();

            Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).mkdirs();
            String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(MimeTypeMap.getFileExtensionFromUrl(Uri.fromFile(file).toString()));
            if (mime == null) {
                mime = "application/octet-stream";
            }
            if (BuildConfig.DEBUG) {
                Log.d(Constants.TAG, Uri.fromFile(file).toString());
                Log.d(Constants.TAG, mime);
            }

            DownloadManager dm = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
            dm.addCompletedDownload(file.getName(), getString(R.string.download_manager_name), false, mime, file.getPath(), file.length(), true);
            Thread.sleep(500);
            mNotificationManager.cancel(554);
        } catch (MalformedURLException e) {
            if (con != null)
                con.disconnect();

            // Do Something
        } catch (IllegalArgumentException e) {
            notificationBuilder.setContentTitle(getText(R.string.error_base64_decode))
                    .setOngoing(false)
                    .setSmallIcon(android.R.drawable.stat_notify_error)
                    .setProgress(0, 0, false);
            mNotificationManager.notify(554, notificationBuilder.build());

            if (con != null)
                con.disconnect();
        } catch (IOException e) {
            Log.d(Constants.TAG, e.toString());
            if (con != null)
                con.disconnect();
            // Do Something
        } catch (CertificateException e) {
            Log.d(Constants.TAG, e.toString());
            if (con != null)
                con.disconnect();
            // Do Something
        } catch (Exception e) {
            Log.d(Constants.TAG, e.toString());
            if (con != null)
                con.disconnect();
            // Do Something
        }

    }
} public class Utils {
    @SuppressWarnings("deprecation")
    public static Spanned fromHtml(String html){
        Spanned result;
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
            result = Html.fromHtml(html,Html.FROM_HTML_MODE_LEGACY);
        } else {
            result = Html.fromHtml(html);
        }
        return result;
    }
} @SuppressWarnings("unused")
public class KanboardAPI {

    public static HttpURLConnection openConnection(URL url, String data) throws IOException, CertificateException, NoSuchAlgorithmException, KeyManagementException, KeyStoreException {
        SSLContext sslcontext = null;
        HttpURLConnection con = null;
        URL tmpURL = url;

        do {
            con = (HttpURLConnection) tmpURL.openConnection();
            if (con == null)
                return null;

            if (url.getProtocol().equalsIgnoreCase("https")) {
                KeyStore keyStore = KeyStore.getInstance("AndroidCAStore");
                keyStore.load(null);
                TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
                tmf.init(keyStore);
                sslcontext = SSLContext.getInstance("TLS");
                sslcontext.init(null, tmf.getTrustManagers(), null);
                ((HttpsURLConnection) con).setSSLSocketFactory(sslcontext.getSocketFactory());
            }
            con.setConnectTimeout(120000);
            con.setReadTimeout(120000);
            con.setInstanceFollowRedirects(false);
            con.setRequestMethod("POST");
            con.setRequestProperty("Content-Type", "application/json");
            con.setRequestProperty("charset", "utf-8");
            con.setDoOutput(true);
            con.setDoInput(true);
            DataOutputStream out = new DataOutputStream(con.getOutputStream());
            out.writeBytes(data);
            out.flush();
            out.close();

            Log.d(Constants.TAG, String.format("API: Connected to %s", con.getURL().toString()));

            if (con.getResponseCode() == 301 || con.getResponseCode() == 302 || con.getResponseCode() == 307 || con.getResponseCode() == 308) {
                Log.i(Constants.TAG, "Follow URL redirect.");
                tmpURL = new URL(con.getHeaderField("Location"));
                Log.d(Constants.TAG, String.format("API: Redirect to %s", tmpURL.toString()));
                con.disconnect();
            } else {
                break;
            }
        } while (true);
        return con;
    }

    private static Pattern urlPattern = Pattern.compile("(?i)[^/]+?\\.(php|html|htm|asp){1}?$", Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);

    public static URL sanitizeURL(String url) throws MalformedURLException {
        Matcher regexMatcher = urlPattern.matcher(url);
        if (regexMatcher.find()) {
            //URL has filename, replace it
            return new URL(regexMatcher.replaceAll("jsonrpc.php"));
        } else {
            //URL hs no filename, add one
            if (!url.endsWith("/"))
                url += "/";
            url += "jsonrpc.php";
            return new URL(url);
        }
    }

    private class KanboardAsync extends AsyncTask<KanboardRequest, Void, KanboardResult> {
        @Override
        protected KanboardResult doInBackground(KanboardRequest... params) {
            HttpURLConnection con = null;
            SSLContext sslcontext = null;
            int httpResponseCode = 0;
            List<JSONObject> responseList = new ArrayList<>();
            for (String s: params[0].JSON) {
                try {

                    Log.i(Constants.TAG, String.format("API: Send Request \"%s\"", params[0].Command));
                    if (BuildConfig.DEBUG) Log.v(Constants.TAG, String.format("API: Data:\n%s", s));
                    con = KanboardAPI.openConnection(kanboardURL, s);
                    if (con == null)
                        return new KanboardResult(params[0], new JSONObject[]{new JSONObject("{\"jsonrpc\":\"2.0\",\"error\":{\"code\":0,\"message\":\"Unable to open connection\"},\"id\":null}")}, 0);

                    Log.i(Constants.TAG, String.format("API: HTTP Return Code for \"%s\": %d", params[0].Command, con.getResponseCode()));


                    BufferedReader in;
                    if (con.getResponseCode() < 400)
                        in = new BufferedReader(new InputStreamReader(con.getInputStream()));
                    else
                        in = new BufferedReader(new InputStreamReader(con.getErrorStream()));
                    String line;
                    StringBuilder responseStr = new StringBuilder();
                    while ((line = in.readLine()) != null) {
                        responseStr.append(line);
                    }
                    in.close();
                    httpResponseCode = con.getResponseCode();

                    Log.i(Constants.TAG, String.format("API: Received Response \"%s\"", params[0].Command));
                    if (BuildConfig.DEBUG)
                        Log.v(Constants.TAG, String.format("API: Data:\n%s", responseStr.toString()));
                    JSONObject response;
                    try {
                        response = new JSONObject(responseStr.toString());
                    } catch (JSONException e) {
                        //server response is not a JSON string, print it for debugging
                        Log.d(Constants.TAG, String.format(Locale.getDefault(), "Erroneous response: %s", responseStr.toString()));
                        response = new JSONObject();
                        response.put("jsonrpc", "2.0");
                        response.put("id", null);
                        response.put("error", new JSONObject()
                                .put("code", -50)
                                .put("responseCode", con.getResponseCode())
                                .put("message", con.getResponseMessage()))
                                .put("data", responseStr.toString());
                    }
                    responseList.add(response);

                    con.disconnect();

                } catch (SSLException e) {
                    Log.e(Constants.TAG, "API: SSL Error.");
                    e.printStackTrace();
                    try {
                        responseList.add(new JSONObject("{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-40,\"message\":\"" + e.getLocalizedMessage() + "\"},\"id\":null}"));
                    } catch (JSONException e1) {
                        e1.printStackTrace();
                    }
                } catch (UnknownHostException e) {
                    Log.e(Constants.TAG, "API: Unknown Host.");
                    e.printStackTrace();
                    try {
                        responseList.add(new JSONObject("{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-10,\"message\":\"Unknown Host\"},\"id\":null}"));
                    } catch (JSONException e1) {
                        e1.printStackTrace();
                    }
                } catch (ProtocolException e) {
                    Log.e(Constants.TAG, "API: Protocol Exception.");
                    e.printStackTrace();
                    try {
                        responseList.add(new JSONObject("{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-20,\"message\":\"Protocol Exception\"},\"id\":null}"));
                    } catch (JSONException e1) {
                        e1.printStackTrace();
                    }
                } catch (SocketTimeoutException e) {
                    Log.e(Constants.TAG, String.format("API: Connection timed out.\tRequest: %s", params[0].Command));
                    e.printStackTrace();
                    try {
                        responseList.add(new JSONObject("{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-30,\"message\":\"Network Timeout\"},\"id\":null}"));
                    } catch (JSONException e1) {
                        e1.printStackTrace();
                    }
                } catch (Exception e) {
                    Log.e(Constants.TAG, String.format("API: CatchAll.\tRequest: %s", params[0].Command));
                    e.printStackTrace();
                    try {
                        responseList.add(new JSONObject("{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-1,\"message\":\"" + e.getMessage() + "\"},\"id\":null}\""));
                    } catch (JSONException e1) {
                        e1.printStackTrace();
                    }
                }
            }

            return new KanboardResult(params[0], responseList.toArray(new JSONObject[]{}), httpResponseCode);
        }
        @Override
        protected void onPostExecute(KanboardResult s) {
            // Handle Errors
            if (s == null || s.Result == null || s.Result[0] == null) {
                KanboardError res = new KanboardError(null, null, 0);
                for (OnErrorListener l: onErrorListeners)
                    l.onError(res);
                return;
            }
            if (s.Result[0].has("error") || s.ReturnCode >= 400) {
                Log.e(Constants.TAG, s.Result[0].toString());
                JSONObject err = s.Result[0].optJSONObject("error");
                KanboardError res = new KanboardError(s.Request, err, s.ReturnCode);
                for (OnErrorListener l: onErrorListeners)
                    l.onError(res);
                return;
            }

            // Handle Return Messages
            boolean success = false;
            if (s.Request.Command.equalsIgnoreCase("getMe")) {
                KanboardUserInfo res = null;
                try {
                    if (s.Result[0].has("result") && (s.ReturnCode < 400)) {
                        success = true;
                        JSONObject jso = s.Result[0].getJSONObject("result");
                        res = new KanboardUserInfo(jso);
                    }
                } catch (JSONException e) {
                    e.printStackTrace();
                }
                for (OnGetMeListener l: onGetMeListeners)
                    l.onGetMe(success, res);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("getVersion")) {
                String res;
                int[] version = new int[3];
                String tag = null;
                try {
                    if (s.Result[0].has("result")) {
                        success = true;
                        res = s.Result[0].getString("result");
                        if (res.startsWith("master")) {
                            Log.d(Constants.TAG, "Development Version");
                            version[0] = -1;
                            version[1] = -1;
                            version[2] = -1;
                            tag = res;
                        } else {
                            try {
                                Pattern regex = Pattern.compile("^(\\d+)\\.(\\d+)\\.(\\d+)(?:,(.*)){0,1}$", Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
                                Matcher regexMatcher = regex.matcher(res);
                                if (regexMatcher.find()) {
                                    version[0] = Integer.parseInt(regexMatcher.group(1));
                                    version[1] = Integer.parseInt(regexMatcher.group(2));
                                    version[2] = Integer.parseInt(regexMatcher.group(3));
                                    if (regexMatcher.groupCount() == 4 && regexMatcher.group(4) != null)
                                        tag = regexMatcher.group(4).trim();
                                }
                            } catch (PatternSyntaxException ex) {
                                ex.printStackTrace();
                                throw ex;
                                // Syntax error in the regular expression
                            }
                        }
                    }
                } catch (JSONException e) {
                    e.printStackTrace();
                }
                for (OnGetVersionListener l: onGetVersionListeners)
                    l.onGetVersion(success, version, tag);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("getDefaultTaskColor")) {
                String res = null;
                try {
                    if (s.Result[0].has("result")) {
                        success = true;
                        res = s.Result[0].getString("result");
                    }
                } catch (JSONException e) {
                    e.printStackTrace();
                }
                for (OnGetDefaultColorListener l: onGetDefaultColorListeners) {
                    l.onGetDefaultColor(success, res);
                }
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("getDefaultTaskColors")) {
                Dictionary<String, KanboardColor> res = null;
                try {
                    if (s.Result[0].has("result")) {
                        success = true;
                        res = new Hashtable<>();
                        JSONObject jso = s.Result[0].getJSONObject("result");
                        Iterator<String> iter = jso.keys();
                        while (iter.hasNext()) {
                            String key = iter.next();
                            res.put(key, new KanboardColor(key, jso.getJSONObject(key)));
                        }
                    }
                } catch (JSONException e) {
                    e.printStackTrace();
                }
                for (OnGetDefaultColorsListener l: onGetDefaultColorsListeners) {
                    l.onGetDefaultColors(success, res);
                }
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("getMyProjectsList")) {
                List<KanboardProjectInfo> res = null;
                try {
                    if (s.Result[0].has("result")) {
                        success = true;
                        res = new ArrayList<>();
                        JSONObject jso = s.Result[0].getJSONObject("result");
                        for (int i = 0; i < jso.names().length(); i++) {
                            String key = jso.names().getString(i);
                            res.add(new KanboardProjectInfo(Integer.parseInt(key), jso.optString(key, "")));
                        }
                    }
                } catch (JSONException e) {
                    e.printStackTrace();
                }
                    for (OnGetMyProjectsListListener l: onGetMyProjectsListListeners)
                        l.onGetMyProjectsList(success, res);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("getMyProjects")) {
                List<KanboardProject> res = null;
                try {
                    if (s.Result[0].has("result")) {
                        success = true;
                        res = new ArrayList<>();
                        JSONArray jsa = s.Result[0].getJSONArray("result");
                        for (int i = 0; i < jsa.length(); i++) {
                            res.add(new KanboardProject(jsa.getJSONObject(i)));
                        }
                    }
                } catch (JSONException | MalformedURLException e) {
                    e.printStackTrace();
                }
                    for (OnGetMyProjectsListener l: onGetMyProjectsListeners)
                        l.onGetMyProjects(success, res);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("getMyDashboard")) {
                KanboardDashboard res = null;
                try {
                    if (s.Result[0].has("result")) {
                        success = true;
                        Object dash = s.Result[0].get("result");
                        res = new KanboardDashboard(dash);
                    }
                } catch (JSONException | MalformedURLException e) {
                    e.printStackTrace();
                    for (OnErrorListener l: onErrorListeners) {
                        try {
                            l.onError(new KanboardError(s.Request, new JSONObject("{\"message\": \"\", \"code\": -50}"), 200));
                        } catch (JSONException e1) {
                            e1.printStackTrace();
                        }
                    }
                }
                for (OnGetMyDashboardListener l: onGetMyDashboardListeners)
                    l.onGetMyDashboard(success, res);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("getMyActivityStream")) {
                List<KanboardActivity> res = null;
                try {
                    if (s.Result[0].has("result")) {
                        success = true;
                        res = new ArrayList<>();
                        JSONArray jsa = s.Result[0].getJSONArray("result");
                        for (int i = 0; i < jsa.length(); i++) {
                            res.add(new KanboardActivity(jsa.getJSONObject(i)));
                        }
                    }
                } catch (JSONException e) {
                    e.printStackTrace();
                }
                for (OnGetMyActivityStreamListener l: onGetMyActivityStreamListeners)
                    l.onGetMyActivityStream(success, res);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("getMyOverdueTasks")) {
                List<KanboardTask> res = null;
                try {
                    if (s.Result[0].has("result")) {
                        success = true;
                        res = new ArrayList<>();
                        JSONArray jsa = s.Result[0].getJSONArray("result");
                        for (int i = 0; i < jsa.length(); i++) {
                            res.add(new KanboardTask(jsa.getJSONObject(i)));
                        }
                    }
                } catch (JSONException | MalformedURLException e) {
                    e.printStackTrace();
                }
                for (OnGetMyOverdueTasksListener l: onGetMyOverdueTasksListeners)
                    l.onGetMyOverdueTasks(success, res);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("getProjectById")) {
                KanboardProject res = null;
                try {
                    if (s.Result[0].has("result")) {
                        success = true;
                        res = new KanboardProject(s.Result[0].getJSONObject("result"));
                    }
                } catch (JSONException | MalformedURLException e) {
                    e.printStackTrace();
                }
                for (OnGetProjectByIdListener l: onGetProjectByIdListeners)
                    l.onGetProjectById(success, res);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("getColumns")) {
                List<KanboardColumn> res = null;
                try {
                    if (s.Result[0].has("result")) {
                        success = true;
                        res = new ArrayList<>();
                        JSONArray jsa = s.Result[0].getJSONArray("result");
                        for (int i = 0; i < jsa.length(); i++) {
                            res.add(new KanboardColumn(jsa.getJSONObject(i)));
                        }
                    }
                } catch (JSONException e) {
                    e.printStackTrace();
                }
                for (OnGetColumnsListener l: onGetColumnsListeners)
                    l.onGetColumns(success, res);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("getProjectUsers")) {
                Hashtable<Integer, String> res = null;
                try {
                    if (s.Result[0].has("result")) {
                        success = true;
                        res = new Hashtable<>();
                        JSONObject jso = s.Result[0].getJSONObject("result");
                        Iterator<String> iter = jso.keys();
                        while (iter.hasNext()) {
                            String key = iter.next();
                            res.put(Integer.parseInt(key), jso.getString(key));
                        }
                    }
                } catch (JSONException e) {
                    e.printStackTrace();
                }
                for (OnGetProjectUsersListener l: onGetProjectUsersListeners)
                    l.onGetProjectUsers(success, res);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("getAllTasks")) {
                List<KanboardTask> res = null;
                int status = 0;
                try {
                    if (s.Result[0].has("result")) {
                        success = true;
                        status = (new JSONObject(s.Request.JSON[0])).getJSONObject("params").getInt("status_id");
                        res = new ArrayList<>();
                        JSONArray jsa = s.Result[0].getJSONArray("result");
                        for (int i = 0; i < jsa.length(); i++) {
                            res.add(new KanboardTask(jsa.getJSONObject(i)));
                        }
                    }
                } catch (JSONException | MalformedURLException e) {
                    e.printStackTrace();
                }
                for (OnGetAllTasksListener l: onGetAllTasksListener)
                    l.onGetAllTasks(success, status, res);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("getOverdueTasksByProject")) {
                List<KanboardTask> res = null;
                try {
                    if (s.Result[0].has("result")) {
                        success = true;
                        res = new ArrayList<>();
                        JSONArray jsa = s.Result[0].getJSONArray("result");
                        for (int i = 0; i < jsa.length(); i++) {
                            res.add(new KanboardTask(jsa.getJSONObject(i)));
                        }
                    }
                } catch (JSONException | MalformedURLException e) {
                    e.printStackTrace();
                }
                for (OnGetOverdueTasksByProjectListener l: onGetOverdueTasksByProjectListeners)
                    l.onGetOverdueTasksByProject(success, res);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("createTask")) {
                Integer res = null;
                try {
                    if (!s.Result[0].getString("result").equalsIgnoreCase("false")) {
                        success = true;
                        res = s.Result[0].getInt("result");
                    }
                } catch (JSONException e) {
                    e.printStackTrace();
                }
                for (OnCreateTaskListener l: onCreateTaskListeners)
                    l.onCreateTask(success, res);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("updateTask")) {
                success = s.Result[0].optBoolean("result", false);
                for (OnUpdateTaskListener l: onUpdateTaskListeners)
                    l.onUpdateTask(success);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("getTask")) {
                KanboardTask res = null;
                try {
                    if (s.Result[0].has("result")) {
                        success = true;
                        res = new KanboardTask(s.Result[0].getJSONObject("result"));
                    }
                } catch (JSONException | MalformedURLException e) {
                    e.printStackTrace();
                }
                for (OnGetTaskListener l: onGetTaskListeners)
                    l.onGetTask(success, res);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("openTask")) {
                success = s.Result[0].optBoolean("result", false);
                for (OnOpenTaskListener l: onOpenTaskListeners)
                    l.onOpenTask(success);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("closeTask")) {
                success = s.Result[0].optBoolean("result", false);
                for (OnCloseTaskListener l: onCloseTaskListeners)
                    l.onCloseTask(success);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("removeTask")) {
                success = s.Result[0].optBoolean("result", false);
                for (OnRemoveTaskListener l: onRemoveTaskListeners)
                    l.onRemoveTask(success);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("moveTaskPosition")) {
                success = s.Result[0].optBoolean("result", false);
                for (OnMoveTaskPositionListener l: onMoveTaskPositionListeners)
                    l.onMoveTaskPosition(success);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("moveTaskToProject")) {
                success = s.Result[0].optBoolean("result", false);
                for (OnMoveTaskToProjectListener l: onMoveTaskToProjectListeners)
                    l.onMoveTaskToProject(success);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("duplicateTaskToProject")) {
                Integer res = null;
                if (s.Result[0].has("result")) {
                    success = true;
                    res = s.Result[0].optInt("result");
                }
                for (OnDuplicateTaskToProjectListener l: onDuplicateTaskToProjectListeners)
                    l.onDuplicateTaskToProject(success, res);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("getAllTaskFiles")) {
                List<KanboardTaskFile> res = null;
                try {
                    if (s.Result[0].has("result")) {
                        success = true;
                        res = new ArrayList<>();
                        JSONArray jsa = s.Result[0].getJSONArray("result");
                        for (int i = 0; i < jsa.length(); i++) {
                            res.add(new KanboardTaskFile(jsa.getJSONObject(i)));
                        }
                    }
                } catch (JSONException e) {
                    e.printStackTrace();
                }
                for (OnGetAllTaskFilesListener l: onGetAllTaskFilesListeners)
                    l.onGetAllTaskFiles(success, res);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("removeTaskFile")) {
                success = s.Result[0].optBoolean("result", false);
                for (OnRemoveTaskFileListener l: onRemoveTaskFileListeners)
                    l.onRemoveTaskFile(success);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("downloadTaskFile")) {
                int id = s.Result[0].optInt("id");
                String data = s.Result[0].optString("result", "");
                success = !data.isEmpty();
                for (OnDownloadTaskFileListener l: onDownloadTaskFileListeners)
                    l.onDownloadTaskFile(success, id, data);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("getActiveSwimlanes")) {
                List<KanboardSwimlane> res = null;
                try {
                    if (s.Result[0].has("result")) {
                        success = true;
                        res = new ArrayList<>();
                        JSONArray jsa = s.Result[0].getJSONArray("result");
                        for (int i = 0; i < jsa.length(); i++) {
                            res.add(new KanboardSwimlane(jsa.getJSONObject(i)));
                        }
                    }
                } catch (JSONException e) {
                    e.printStackTrace();
                }
                for (OnGetActiveSwimlanesListener l: onGetActiveSwimlanesListeners)
                    l.onGetActiveSwimlanes(success, res);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("getDefaultSwimlane")) {
                KanboardSwimlane res = null;
                try {
                    if (s.Result[0].has("result")) {
                        success = true;
                        JSONObject jso = s.Result[0].getJSONObject("result");
                        res = new KanboardSwimlane(jso);
                    }
                } catch (JSONException e) {
                    e.printStackTrace();
                }
                for (OnGetSwimlaneListener l: onGetDefaultSwimlaneListeners)
                    l.onGetSwimlane(success, res);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("getSwimlane")) {
                KanboardSwimlane res = null;
                try {
                    if (s.Result[0].has("result")) {
                        success = true;
                        res = new KanboardSwimlane(s.Result[0].getJSONObject("result"));
                    }
                } catch (JSONException e) {
                    e.printStackTrace();
                }
                for (OnGetSwimlaneListener l: onGetSwimlaneListeners)
                    l.onGetSwimlane(success, res);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("getAllCategories")) {
                List<KanboardCategory> res = null;
                try {
                    if (s.Result[0].has("result")) {
                        success = true;
                        res = new ArrayList<>();
                        JSONArray jsa = s.Result[0].getJSONArray("result");
                        for (int i = 0; i < jsa.length(); i++) {
                            res.add(new KanboardCategory(jsa.getJSONObject(i)));
                        }
                    }
                } catch (JSONException e) {
                    e.printStackTrace();
                }
                for (OnGetAllCategoriesListener l: onGetAllCategoriesListeners)
                    l.onGetAllCategories(success, res);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("getCategory")) {
                KanboardCategory res = null;
                try {
                    if (s.Result[0].has("result")) {
                        success = true;
                        res = new KanboardCategory(s.Result[0].getJSONObject("result"));
                    }
                } catch (JSONException e) {
                    e.printStackTrace();
                }
                for (OnGetCategoryListener l: onGetCategoryListeners)
                    l.onGetCategory(success, res);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("createComment")) {
                Integer res = null;
                try {
                    if (!s.Result[0].getString("result").equalsIgnoreCase("false")) {
                        success = true;
                        res = s.Result[0].getInt("result");
                    }
                } catch (JSONException e) {
                    e.printStackTrace();
                }
                for (OnCreateCommentListener l: onCreateCommentListeners)
                    l.onCreateComment(success, res);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("getAllComments")) {
                List<KanboardComment> res = null;
                try {
                    if (s.Result[0].has("result")) {
                        success = true;
                        res = new ArrayList<>();
                        JSONArray jsa = s.Result[0].getJSONArray("result");
                        for (int i = 0; i < jsa.length(); i++)
                            res.add(new KanboardComment(jsa.getJSONObject(i)));
                    }
                } catch (JSONException e) {
                    e.printStackTrace();
                }
                for (OnGetAllCommentsListener l: onGetAllCommentsListeners)
                    l.onGetAllComments(success, res);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("updateComment")) {
                success = s.Result[0].optBoolean("result", false);
                for (OnUpdateCommentListener l: onUpdateCommentListeners)
                    l.onUpdateComment(success);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("removeComment")) {
                success = s.Result[0].optBoolean("result", false);
                for (OnRemoveCommentListener l: onRemoveCommentListeners)
                    l.onRemoveComment(success);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("createSubtask")) {
                Integer res = null;
                try {
                    if (!s.Result[0].getString("result").equalsIgnoreCase("false")) {
                        success = true;
                        res = s.Result[0].getInt("result");
                    }
                } catch (JSONException e) {
                    e.printStackTrace();
                }
                for (OnCreateSubtaskListener l: onCreateSubtaskListeners)
                    l.onCreateSubtask(success, res);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("getAllSubtasks")) {
                List<KanboardSubtask> res = null;
                try {
                    if (s.Result[0].has("result")) {
                        success = true;
                        res = new ArrayList<>();
                        JSONArray jsa = s.Result[0].getJSONArray("result");
                        for (int i = 0; i < jsa.length(); i++)
                            res.add(new KanboardSubtask(jsa.getJSONObject(i)));
                    }
                } catch (JSONException e) {
                    e.printStackTrace();
                }
                for (OnGetAllSubtasksListener l: onGetAllSubtasksListeners)
                    l.onGetAllSubtasks(success, res);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("updateSubtask")) {
                success = s.Result[0].optBoolean("result", false);
                for (OnUpdateSubtaskListener l: onUpdateSubtaskListeners)
                    l.onUpdateSubtask(success);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("removeSubtask")) {
                success = s.Result[0].optBoolean("result", false);
                for (OnRemoveSubtaskListener l: onRemoveSubtaskListeners)
                    l.onRemoveSubtask(success);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("hasSubtaskTimer")) {
                try {
                    hasSubtaskTimerSet.remove(new JSONObject(s.Request.JSON[0]).getJSONArray("params").getInt(0));
                    success = s.Result[0].optBoolean("result", false);
                } catch (JSONException e) {
                    e.printStackTrace();
                }
                if (s.Request.Listener != null)
                    ((OnSubtaskTimetrackingListener) s.Request.Listener).onSubtaskTimetracking(success, 0);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("setSubtaskStartTime")) {
                success = s.Result[0].optBoolean("result", false);
                if (s.Request.Listener != null)
                    ((OnSubtaskTimetrackingListener) s.Request.Listener).onSubtaskTimetracking(success, 0);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("setSubtaskEndTime")) {
                success = s.Result[0].optBoolean("result", false);
                if (s.Request.Listener != null)
                    ((OnSubtaskTimetrackingListener) s.Request.Listener).onSubtaskTimetracking(success, 0);
                return;
            }

            if (s.Request.Command.equalsIgnoreCase("getSubtaskTimeSpent")) {
                double time;
                try {
                    getSubtaskTimeSpentSet.remove(new JSONObject(s.Request.JSON[0]).getJSONArray("params").getInt(0));
                    time = s.Result[0].getDouble("result");
                    success = true;
                } catch (JSONException e) {
                    time = 0;
                    success = false;
                }
                if (s.Request.Listener != null)
                    ((OnSubtaskTimetrackingListener) s.Request.Listener).onSubtaskTimetracking(success, time);
                return;
            }

//            if (s.Request.Command.equalsIgnoreCase("KD_getDashboard")) {
//                KanboardDashboard res = null;
//                try {
//                    if (s.Result[0].has("result") && s.Result[1].has("result") && s.Result[2].has("result")) {
//                        success = true;
//                        res = new KanboardDashboard(s.Result[0].getJSONObject("result"), s.Result[1].getJSONArray("result"), s.Result[2].getJSONArray("result"));
//                    }
//                } catch (JSONException | MalformedURLException e) {
//                    e.printStackTrace();
//                }
//                return;
//            }

//            if (s.Request.Command.equalsIgnoreCase("KD_getProjectById")) {
//                KanboardProject res = null;
//                try {
//                    if (s.Result[0].has("result") && s.Result[1].has("result") && s.Result[2].has("result") && s.Result[3].has("result") && s.Result[4].has("result") && s.Result[5].has("result") && s.Result[6].has("result")) {
//                        success = true;
//                        res = new KanboardProject(s.Result[0].optJSONObject("result"),
//                                                  s.Result[1].optJSONArray("result"),
//                                                  s.Result[2].optJSONArray("result"),
//                                                  s.Result[3].optJSONArray("result"),
//                                                  s.Result[4].optJSONArray("result"),
//                                                  s.Result[5].optJSONArray("result"),
//                                                  s.Result[6].optJSONArray("result"),
//                                                  s.Result[7].optJSONObject("result"));
//                    }
//                } catch (MalformedURLException e) {
//                    e.printStackTrace();
//                }
//                return;
//            }
        }
    }

    private URL kanboardURL;
    private ThreadPoolExecutor threadPoolExecutor;
    private HashSet<OnGetMyDashboardListener> onGetMyDashboardListeners = new HashSet<>();
    private HashSet<OnGetMyActivityStreamListener> onGetMyActivityStreamListeners = new HashSet<>();
    private HashSet<OnGetMyOverdueTasksListener> onGetMyOverdueTasksListeners = new HashSet<>();
    private HashSet<OnGetProjectByIdListener> onGetProjectByIdListeners = new HashSet<>();
    private HashSet<OnGetColumnsListener> onGetColumnsListeners = new HashSet<>();
    private HashSet<OnGetAllCommentsListener> onGetAllCommentsListeners = new HashSet<>();
    private HashSet<OnGetAllTasksListener> onGetAllTasksListener = new HashSet<>();
    private HashSet<OnGetOverdueTasksByProjectListener> onGetOverdueTasksByProjectListeners = new HashSet<>();
    private HashSet<OnGetTaskListener> onGetTaskListeners = new HashSet<>();
    private HashSet<OnGetAllCategoriesListener> onGetAllCategoriesListeners = new HashSet<>();
    private HashSet<OnGetCategoryListener> onGetCategoryListeners = new HashSet<>();
    private HashSet<OnGetProjectUsersListener> onGetProjectUsersListeners = new HashSet<>();
    private HashSet<OnGetSwimlaneListener> onGetSwimlaneListeners = new HashSet<>();
    private HashSet<OnGetActiveSwimlanesListener> onGetActiveSwimlanesListeners = new HashSet<>();
    private HashSet<OnGetSwimlaneListener> onGetDefaultSwimlaneListeners = new HashSet<>();
    private HashSet<OnGetAllSubtasksListener> onGetAllSubtasksListeners = new HashSet<>();
    private HashSet<OnGetMeListener> onGetMeListeners = new HashSet<>();
    private HashSet<OnCreateCommentListener> onCreateCommentListeners = new HashSet<>();
    private HashSet<OnUpdateCommentListener> onUpdateCommentListeners = new HashSet<>();
    private HashSet<OnRemoveCommentListener> onRemoveCommentListeners = new HashSet<>();
    private HashSet<OnCreateSubtaskListener> onCreateSubtaskListeners = new HashSet<>();
    private HashSet<OnUpdateSubtaskListener> onUpdateSubtaskListeners = new HashSet<>();
    private HashSet<OnRemoveSubtaskListener> onRemoveSubtaskListeners = new HashSet<>();
    private HashSet<OnOpenTaskListener> onOpenTaskListeners = new HashSet<>();
    private HashSet<OnCloseTaskListener> onCloseTaskListeners = new HashSet<>();
    private HashSet<OnRemoveTaskListener> onRemoveTaskListeners = new HashSet<>();
    private HashSet<OnCreateTaskListener> onCreateTaskListeners = new HashSet<>();
    private HashSet<OnUpdateTaskListener> onUpdateTaskListeners = new HashSet<>();
    private HashSet<OnMoveTaskPositionListener> onMoveTaskPositionListeners = new HashSet<>();
    private HashSet<OnMoveTaskToProjectListener> onMoveTaskToProjectListeners = new HashSet<>();
    private HashSet<OnDuplicateTaskToProjectListener> onDuplicateTaskToProjectListeners = new HashSet<>();
    private HashSet<OnGetVersionListener> onGetVersionListeners = new HashSet<>();
    private HashSet<OnErrorListener> onErrorListeners = new HashSet<>();
    private HashSet<OnGetDefaultColorListener> onGetDefaultColorListeners = new HashSet<>();
    private HashSet<OnGetDefaultColorsListener> onGetDefaultColorsListeners = new HashSet<>();
    private HashSet<OnGetMyProjectsListener> onGetMyProjectsListeners = new HashSet<>();
    private HashSet<OnGetMyProjectsListListener> onGetMyProjectsListListeners = new HashSet<>();
    private HashSet<OnGetAllTaskFilesListener> onGetAllTaskFilesListeners = new HashSet<>();
    private HashSet<OnRemoveTaskFileListener> onRemoveTaskFileListeners = new HashSet<>();
    private HashSet<OnDownloadTaskFileListener> onDownloadTaskFileListeners = new HashSet<>();

    //HashSets for AsyncTask limiting
    private HashSet<Integer> hasSubtaskTimerSet = new HashSet<>();
    private HashSet<Integer> getSubtaskTimeSpentSet = new HashSet<>();

    public KanboardAPI(String serverURL, final String username, final String password) throws IOException {
        Authenticator.setDefault(new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(username, password.toCharArray());
            }

        });
        kanboardURL = KanboardAPI.sanitizeURL(serverURL.trim());
        Log.i(Constants.TAG, String.format("Host uses %s", kanboardURL.getProtocol()));
//        threadPoolExecutor = new ThreadPoolExecutor(12, 12, 20, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(256));
        threadPoolExecutor = (ThreadPoolExecutor) AsyncTask.THREAD_POOL_EXECUTOR;
        threadPoolExecutor.setCorePoolSize(12);
        threadPoolExecutor.setMaximumPoolSize(12);
    }

    // Event Listeners

    public void addOnGetMyDashboardListener(@NonNull OnGetMyDashboardListener listener) {
        onGetMyDashboardListeners.add(listener);
    }

    public void removeOnGetMyDashboardListener(@NonNull OnGetMyDashboardListener listener) {
        onGetMyDashboardListeners.remove(listener);
    }

    public void addOnGetMyActivityStreamListener(@NonNull OnGetMyActivityStreamListener listener) {
        onGetMyActivityStreamListeners.add(listener);
    }

    public void removeOnGetMyActivityStreamListener(@NonNull OnGetMyActivityStreamListener listener) {
        onGetMyActivityStreamListeners.remove(listener);
    }

    public void addOnGetMyOverdueTasksListener(@NonNull OnGetMyOverdueTasksListener listener) {
        onGetMyOverdueTasksListeners.add(listener);
    }

    public void removeOnGetMyOverdueTasksListener(@NonNull OnGetMyOverdueTasksListener listener) {
        onGetMyOverdueTasksListeners.remove(listener);
    }

    public void addOnGetProjectByIdListener(@NonNull OnGetProjectByIdListener listener) {
        onGetProjectByIdListeners.add(listener);
    }

    public void removeOnGetProjectByIdListener(@NonNull OnGetProjectByIdListener listener) {
        onGetProjectByIdListeners.remove(listener);
    }

    public void addOnGetColumnsListener(@NonNull OnGetColumnsListener listener) {
        onGetColumnsListeners.add(listener);
    }

    public void removeOnGetColumnsListener(@NonNull OnGetColumnsListener listener) {
        onGetColumnsListeners.remove(listener);
    }

    public void addOnGetAllCommentsListener(@NonNull OnGetAllCommentsListener listener) {
        onGetAllCommentsListeners.add(listener);
    }

    public void removeOnGetAllCommentsListener(@NonNull OnGetAllCommentsListener listener) {
        onGetAllCommentsListeners.remove(listener);
    }

    public void addOnGetActiveSwimlanesListener(@NonNull OnGetActiveSwimlanesListener listener) {
        onGetActiveSwimlanesListeners.add(listener);
    }

    public void removeOnGetActiveSwimlanesListener(@NonNull OnGetActiveSwimlanesListener listener) {
        onGetActiveSwimlanesListeners.remove(listener);
    }

    public void addOnGetSwimlaneListener(@NonNull OnGetSwimlaneListener listener) {
        onGetSwimlaneListeners.add(listener);
    }

    public void removeOnGetSwimlaneListener(@NonNull OnGetSwimlaneListener listener) {
        onGetSwimlaneListeners.remove(listener);
    }

    public void addOnGetDefaultSwimlaneListener(@NonNull OnGetSwimlaneListener listener) {
        onGetDefaultSwimlaneListeners.add(listener);
    }

    public void removeOnGetDefaultSwimlaneListener(@NonNull OnGetSwimlaneListener listener) {
        onGetDefaultSwimlaneListeners.remove(listener);
    }

    public void addOnGetAllTasksListener(@NonNull OnGetAllTasksListener listener) {
        onGetAllTasksListener.add(listener);
    }

    public void removeGetAllTasksListener(@NonNull OnGetAllTasksListener listener) {
        onGetAllTasksListener.remove(listener);
    }

    public void addOnGetOverdueTasksByProjectListener(@NonNull OnGetOverdueTasksByProjectListener listener) {
        onGetOverdueTasksByProjectListeners.add(listener);
    }

    public void removeGetOverdueTasksByProjectListener(@NonNull OnGetOverdueTasksByProjectListener listener) {
        onGetOverdueTasksByProjectListeners.remove(listener);
    }

    public void addOnGetTaskListener(@NonNull OnGetTaskListener listener) {
        onGetTaskListeners.add(listener);
    }

    public void removeGetTaskListener(@NonNull OnGetTaskListener listener) {
        onGetTaskListeners.remove(listener);
    }

    public void addOnOpenTaskListener(@NonNull OnOpenTaskListener listener) {
        onOpenTaskListeners.add(listener);
    }

    public void removeOnOpenTaskListener(@NonNull OnOpenTaskListener listener) {
        onOpenTaskListeners.remove(listener);
    }

    public void addOnCloseTaskListener(@NonNull OnCloseTaskListener listener) {
        onCloseTaskListeners.add(listener);
    }

    public void removeOnCloseTaskListener(@NonNull OnCloseTaskListener listener) {
        onCloseTaskListeners.remove(listener);
    }

    public void addOnRemoveTaskListener(@NonNull OnRemoveTaskListener listener) {
        onRemoveTaskListeners.add(listener);
    }

    public void removeOnRemoveTaskListener(@NonNull OnRemoveTaskListener listener) {
        onRemoveTaskListeners.remove(listener);
    }

    public void addOnMoveTaskPositionListener(@NonNull OnMoveTaskPositionListener listener) {
        onMoveTaskPositionListeners.add(listener);
    }

    public void removeOnMoveTaskPositionListener(@NonNull OnMoveTaskPositionListener listener) {
        onMoveTaskPositionListeners.remove(listener);
    }

    public void addOnMoveTaskToProjectListener(@NonNull OnMoveTaskToProjectListener listener) {
        onMoveTaskToProjectListeners.add(listener);
    }

    public void removeOnMoveTaskToProjectListener(@NonNull OnMoveTaskToProjectListener listener) {
        onMoveTaskToProjectListeners.remove(listener);
    }

    public void addOnDuplicateTaskToProjectListener(@NonNull OnDuplicateTaskToProjectListener listener) {
        onDuplicateTaskToProjectListeners.add(listener);
    }

    public void removeOnDuplicateTaskToProjectListener(@NonNull OnDuplicateTaskToProjectListener listener) {
        onDuplicateTaskToProjectListeners.remove(listener);
    }

    public void addOnGetAllCategoriesListener(@NonNull OnGetAllCategoriesListener listener) {
        onGetAllCategoriesListeners.add(listener);
    }

    public void removeOnGetAllCategoriesListener(@NonNull OnGetAllCategoriesListener listener) {
        onGetAllCategoriesListeners.remove(listener);
    }

    public void addOnGetCategoryListener(@NonNull OnGetCategoryListener listener) {
        onGetCategoryListeners.add(listener);
    }

    public void removeOnGetCategoryListener(@NonNull OnGetCategoryListener listener) {
        onGetCategoryListeners.remove(listener);
    }

    public void addOnGetProjectUsersListener(@NonNull OnGetProjectUsersListener listener) {
        onGetProjectUsersListeners.add(listener);
    }

    public void removeOnGetProjectUsersListener(@NonNull OnGetProjectUsersListener listener) {
        onGetProjectUsersListeners.remove(listener);
    }

    public void addOnGetAllSubtasksListener(@NonNull OnGetAllSubtasksListener listener) {
        onGetAllSubtasksListeners.add(listener);
    }

    public void removeOnGetAllSubtasksListener(@NonNull OnGetAllSubtasksListener listener) {
        onGetAllSubtasksListeners.remove(listener);
    }

    public void addOnGetMeListener(@NonNull OnGetMeListener listener) {
        onGetMeListeners.add(listener);
    }

    public void removeOnGetMeListener(@NonNull OnGetMeListener listener) {
        onGetMeListeners.remove(listener);
    }

    public void addOnCreateCommentListener(@NonNull OnCreateCommentListener listener) {
        onCreateCommentListeners.add(listener);
    }

    public void removeOnCreateCommentListener(@NonNull OnCreateCommentListener listener) {
        onCreateCommentListeners.remove(listener);
    }

    public void addOnUpdateCommentListener(@NonNull OnUpdateCommentListener listener) {
        onUpdateCommentListeners.add(listener);
    }

    public void removeOnUpdateCommentListener(@NonNull OnUpdateCommentListener listener) {
        onUpdateCommentListeners.remove(listener);
    }

    public void addOnRemoveCommentListener(@NonNull OnRemoveCommentListener listener) {
        onRemoveCommentListeners.add(listener);
    }

    public void removeOnRemoveCommentListener(@NonNull OnRemoveCommentListener listener) {
        onRemoveCommentListeners.remove(listener);
    }

    public void addOnCreateSubtaskListener(@NonNull OnCreateSubtaskListener listener) {
        onCreateSubtaskListeners.add(listener);
    }

    public void removeOnCreateSubtaskListener(@NonNull OnCreateSubtaskListener listener) {
        onCreateSubtaskListeners.remove(listener);
    }

    public void addOnUpdateSubtaskListener(@NonNull OnUpdateSubtaskListener listener) {
        onUpdateSubtaskListeners.add(listener);
    }

    public void removeOnUpdateSubtaskListener(@NonNull OnUpdateSubtaskListener listener) {
        onUpdateSubtaskListeners.remove(listener);
    }

    public void addOnRemoveSubtaskListener(@NonNull OnRemoveSubtaskListener listener) {
        onRemoveSubtaskListeners.add(listener);
    }

    public void removeOnRemoveSubtaskListener(@NonNull OnRemoveSubtaskListener listener) {
        onRemoveSubtaskListeners.remove(listener);
    }

    public void addOnCreateTaskListener(@NonNull OnCreateTaskListener listener) {
        onCreateTaskListeners.add(listener);
    }

    public void removeOnCreateTaskListener(@NonNull OnCreateTaskListener listener) {
        onCreateTaskListeners.remove(listener);
    }

    public void addOnUpdateTaskListener(@NonNull OnUpdateTaskListener listener) {
        onUpdateTaskListeners.add(listener);
    }

    public void removeOnUpdateTaskListener(@NonNull OnUpdateTaskListener listener) {
        onUpdateTaskListeners.remove(listener);
    }

    public void addOnGetVersionListener(@NonNull OnGetVersionListener listener) {
        onGetVersionListeners.add(listener);
    }

    public void removeOnGetVersionListener(@NonNull OnGetVersionListener listener) {
        onGetVersionListeners.remove(listener);
    }

    public void addOnGetDefaultColorListener(@NonNull OnGetDefaultColorListener listener) {
        onGetDefaultColorListeners.add(listener);
    }

    public void removeOnGetDefaultColorListener(@NonNull OnGetDefaultColorListener listener) {
        onGetDefaultColorListeners.remove(listener);
    }

    public void addOnGetDefaultColorsListener(@NonNull OnGetDefaultColorsListener listener) {
        onGetDefaultColorsListeners.add(listener);
    }

    public void removeOnGetDefaultColorsListener(@NonNull OnGetDefaultColorsListener listener) {
        onGetDefaultColorsListeners.remove(listener);
    }

    public void addOnGetMyProjectsListener(@NonNull OnGetMyProjectsListener listener) {
        onGetMyProjectsListeners.add(listener);
    }

    public void removeOnGetMyProjectsListener(@NonNull OnGetMyProjectsListener listener) {
        onGetMyProjectsListeners.remove(listener);
    }

    public void addErrorListener(@NonNull OnErrorListener listener) {
        onErrorListeners.add(listener);
    }

    public void removeErrorListener(@NonNull OnErrorListener listener) {
        onErrorListeners.remove(listener);
    }

    public void addOnGetMyProjectsListListeners(@NonNull OnGetMyProjectsListListener listener) {
        onGetMyProjectsListListeners.add(listener);
    }

    public void removeOnGetMyProjectsListListeners(@NonNull OnGetMyProjectsListListener listener) {
        onGetMyProjectsListListeners.remove(listener);
    }

    public void addOnGetAllTaskFilesListListeners(@NonNull OnGetAllTaskFilesListener listener) {
        onGetAllTaskFilesListeners.add(listener);
    }

    public void removeOnGetAllTaskFilesListeners(@NonNull OnGetAllTaskFilesListener listener) {
        onGetAllTaskFilesListeners.remove(listener);
    }

    public void addOnRemoveTaskFileListeners(@NonNull OnRemoveTaskFileListener listener) {
        onRemoveTaskFileListeners.add(listener);
    }

    public void removeRemoveTaskFileListeners(@NonNull OnRemoveTaskFileListener listener) {
        onRemoveTaskFileListeners.remove(listener);
    }

    public void addOnDownloadTaskFileListeners(@NonNull OnDownloadTaskFileListener listener) {
        onDownloadTaskFileListeners.add(listener);
    }

    public void removeDownloadTaskFileListeners(@NonNull OnDownloadTaskFileListener listener) {
        onDownloadTaskFileListeners.remove(listener);
    }

    // API Calls

    public void getMe() {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.getMe());
    }

    public void getVersion() {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.getVersion());
    }

    public void getDefaultTaskColors() {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.getDefaultTaskColors());
    }

    public void getDefaultTaskColor() {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.getDefaultTaskColor());
    }

    public void getMyProjectsList() {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.getMyProjectsList());
    }

    public void getMyProjects() {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.getMyProjects());
    }

    public void getMyDashboard() {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.getMyDashboard());
    }

    public void getMyActivityStream() {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.getMyActivityStream());
    }

    public void getMyOverdueTasks() {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.getMyOverdueTasks());
    }

    public void getProjectById(int projectid) {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.getProjectById(projectid));
    }

    public void getProjectUsers(int projectid) {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.getProjectUsers(projectid));
    }

    public void getColumns(int projectid) {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.getColumns(projectid));
    }

    public void getAllTasks(int projectid, int status) {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.getAllTasks(projectid, status));
    }

    public void getOverdueTasksByProject(int projectid) {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.getOverdueTasksByProject(projectid));
    }

    public void createTask(@NonNull String title, int projectid, @Nullable String colorid,
                           @Nullable Integer columnid, @Nullable Integer ownerid,
                           @Nullable Integer creatorid, @Nullable Date duedate,
                           @Nullable String description, @Nullable Integer categoryid,
                           @Nullable Integer score, @Nullable Integer swimlaneid,
                           @Nullable Integer priority, @Nullable Integer recurrencestatus,
                           @Nullable Integer recurrencetrigger, @Nullable Integer recurrencefactor,
                           @Nullable Integer recurrencetimeframe, @Nullable Integer recurrencebasedate,
                           @Nullable String[] tags, @Nullable Date starteddate) {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.createTask(title,
                projectid, colorid, columnid, ownerid, creatorid, duedate, description, categoryid,
                score, swimlaneid, priority, recurrencestatus, recurrencetrigger, recurrencefactor,
                recurrencetimeframe, recurrencebasedate, tags, starteddate));
    }

    public void updateTask(int taskid, @NonNull String title, @Nullable String colorid,
                           @Nullable Integer ownerid,
                           @Nullable Date duedate,
                           @Nullable String description, @Nullable Integer categoryid,
                           @Nullable Integer score,
                           @Nullable Integer priority, @Nullable Integer recurrencestatus,
                           @Nullable Integer recurrencetrigger, @Nullable Integer recurrencefactor,
                           @Nullable Integer recurrencetimeframe, @Nullable Integer recurrencebasedate,
                           @Nullable String[] tags, @Nullable Date starteddate) {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.updateTask(taskid,
                title, colorid, ownerid, duedate, description, categoryid, score,
                priority, recurrencestatus, recurrencetrigger, recurrencefactor,
                recurrencetimeframe, recurrencebasedate, tags, starteddate));
    }

    public void getTask(int taskid) {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.getTask(taskid));
    }

    public void openTask(int taskid) {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.openTask(taskid));
    }

    public void closeTask(int taskid) {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.closeTask(taskid));
    }

    public void removeTask(int taskid) {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.removeTask(taskid));
    }

    public void moveTaskPosition(int projectid, int taskid, int columnid, int position,
                                 int swimlaneid) {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor,
                KanboardRequest.moveTaskPosition(projectid, taskid, columnid, position, swimlaneid));
    }

    public void moveTaskToProject(int projectid, int taskid, @Nullable Integer columnid,
                                  @Nullable Integer position, @Nullable Integer swimlaneid) {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor,
                KanboardRequest.moveTaskToProject(projectid, taskid, columnid, position, swimlaneid));
    }

    public void duplicateTaskToProject(int projectid, int taskid, @Nullable Integer columnid,
                                  @Nullable Integer position, @Nullable Integer swimlaneid) {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor,
                KanboardRequest.duplicateTaskToProject(projectid, taskid, columnid, position, swimlaneid));
    }

    public void getAllComments(int taskid) {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.getAllComments(taskid));
    }

    public void createComment(int taskid, int userid, String comment) {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.createComment(taskid, userid, comment));
    }

    public void updateComment(int commentid, @NonNull String comment) {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.updateComment(commentid, comment));
    }

    public void removeComment(int commentid) {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.removeComment(commentid));
    }

    public void getActiveSwimlanes(int projectid) {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.getActiveSwimlanes(projectid));
    }

    public void getDefaultSwimlane(int projectid) {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.getDefaultSwimlane(projectid));
    }

    public void getSwimlane(int swimlaneid) {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.getSwimlane(swimlaneid));
    }

    public void getAllCategories(int projectid) {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.getAllCategories(projectid));
    }

    public void getCategory(int categoryid) {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.getCategrory(categoryid));
    }

    public void getAllSubtasks(int taskid) {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.getAllSubtasks(taskid));
    }

    public void createSubtask(int taskid, @NonNull String title, @Nullable Integer userid,
                              @Nullable Integer timeestimated, @Nullable Integer timespent, @Nullable Integer status) {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.createSubtask(taskid, title, userid, timeestimated, timespent, status));
    }

    public void updateSubtask(int subtaskid, int taskid, @Nullable String title, @Nullable Integer userid,
                              @Nullable Integer timeestimated, @Nullable Integer timespent, @Nullable Integer status) {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.updateSubtask(subtaskid, taskid, title, userid, timeestimated, timespent, status));
    }

    public void removeSubtask(int subtaskid) {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.removeSubtask(subtaskid));
    }

    public void hasSubtaskTimer(int subtaskid, int userid, @NonNull OnSubtaskTimetrackingListener listener) {
        if (!hasSubtaskTimerSet.contains(subtaskid)) {
            hasSubtaskTimerSet.add(subtaskid);
            new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.hasSubtaskTimer(subtaskid, userid, listener));
        }
    }

    public void setSubtaskStartTime(int subtaskid, int userid, @NonNull OnSubtaskTimetrackingListener listener) {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.setSubtaskStartTime(subtaskid, userid, listener));
    }

    public void setSubtaskEndTime(int subtaskid, int userid, @NonNull OnSubtaskTimetrackingListener listener) {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.setSubtaskEndTime(subtaskid, userid, listener));
    }

    public void getSubtaskTimeSpent(int subtaskid, int userid, @NonNull OnSubtaskTimetrackingListener listener) {
        if (!getSubtaskTimeSpentSet.contains(subtaskid)) {
            getSubtaskTimeSpentSet.add(subtaskid);
            new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.getSubtaskTimeSpent(subtaskid, userid, listener));
        }
    }

    public void getAllTaskFiles(int taskid) {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.getAllTaskFiles(taskid));
    }

    public void removeTaskFile(int fileid) {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.removeTaskFile(fileid));
    }

    public void downloadTaskFile(int fileid) {
        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.downloadTaskFile(fileid));
    }

//    public void KB_getDashboard() {
//        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.KD_getDashboard());
//    }
//
//    public void KB_getProjectById(int projectid) {
//        new KanboardAsync().executeOnExecutor(threadPoolExecutor, KanboardRequest.KD_getProjectById(projectid));
//    }

    // TODO: add API calls

    public static boolean StringToBoolean(String s) {
        return s.equalsIgnoreCase("true") || s.equalsIgnoreCase("yes") || s.equalsIgnoreCase("1");
    }

    @Nullable
    public static Integer parseColorString(String colorstring) {
        try {
            return Color.parseColor(colorstring);
        } catch (IllegalArgumentException e) {
            Pattern c = Pattern.compile("rgb *\\( *([0-9]+), *([0-9]+), *([0-9]+) *\\)");
            Matcher m = c.matcher(colorstring);

            if (m.matches())
            {
                return Color.rgb(Integer.valueOf(m.group(1)),  // r
                        Integer.valueOf(m.group(2)),  // g
                        Integer.valueOf(m.group(3))); // b
            }

        }
        return null;
    }
} @SuppressWarnings("unused")
public class KanboardCategory implements Serializable {
    private int Id;
    private String Name;
    private int ProjectId;

    public KanboardCategory(JSONObject category) {
        Id = category.optInt("id");
        Name = category.optString("name");
        ProjectId = category.optInt("project_id");
    }

    public int getId() {
        return Id;
    }

    public String getName() {
        return Name;
    }

    public int getProjectId() {
        return ProjectId;
    }

    @Override
    public String toString() {
        return this.Name;
    }
} @SuppressWarnings("unused")
public class KanboardColumn implements Comparable<KanboardColumn>, Serializable {
    private int Id;
    private int Position;
    private int TaskLimit;
    private int NumberTasks;
    private String Title;
    private String Description;
    private int ProjectId;
    private boolean HideInDashboard;

    public int getId() {
        return Id;
    }

    public int getPosition() {
        return Position;
    }

    public int getTaskLimit() {
        return TaskLimit;
    }

    public int getNumberTasks() {
        return NumberTasks;
    }

    public String getTitle() {
        return Title;
    }

    public String getDescription() {
        return Description;
    }

    public int getProjectId() {
        return ProjectId;
    }

    public boolean getHideInDashboard() {
        return HideInDashboard;
    }

    public KanboardColumn(JSONObject json) {
        Id = json.optInt("id");
        Position = json.optInt("position");
        TaskLimit = json.optInt("task_limit");
        NumberTasks = json.optInt("nb_tasks");
        Title = json.optString("title");
        Description = json.optString("description");
        ProjectId = json.optInt("project_id");
        HideInDashboard = KanboardAPI.StringToBoolean(json.optString("hide_in_dashboard"));
    }

    @Override
    public int compareTo(@NonNull KanboardColumn o) {
        return this.Title.compareTo(o.Title);
    }

    @Override
    public String toString() {
        return this.Title;
    }
} @SuppressWarnings("unused")
public class KanboardComment implements Serializable {
    private int id;
    private Date date_creation;
    private Date date_modification;
    private int task_id;
    private int user_id;
    private String content;
    private String username;
    private String name;
    private String email;
    private String avatar_path;

    public KanboardComment() {}

    public KanboardComment(@NonNull JSONObject comment) {
        id = comment.optInt("id", -1);
        long tmpTime = comment.optLong("date_creation");
        if (tmpTime > 0)
            date_creation = new Date(tmpTime * 1000);
        else
            date_creation = null;
        tmpTime = comment.optLong("date_modification");
        if (tmpTime > 0)
            date_modification = new Date(tmpTime * 1000);
        else
            date_modification = null;
        task_id = comment.optInt("task_id");
        user_id = comment.optInt("user_id");
        content = comment.optString("comment");
        username = comment.optString("username");
        name = comment.optString("name");
        email = comment.optString("email");
        avatar_path = comment.optString("avatar_path");
    }

    public int getId() {
        return this.id;
    }

    @Nullable
    public Date getDateCreation() {
        return this.date_creation;
    }

    @Nullable
    public Date getDateModification() {
        return date_modification;
    }

    public int getTaskId() {
        return this.task_id;
    }

    public void setTaskID(int taskid) {
        this.task_id = taskid;
    }

    public int getUserId() {
        return this.user_id;
    }

    public void setUserId(int userid) {
        this.user_id = userid;
    }

    @Nullable
    public String getContent() {
        return this.content;
    }

    public void setContent(@NonNull String content) {
        this.content = content;
    }

    @Nullable
    public String getUsername() {
        return this.username;
    }

    @Nullable
    public String getName() {
        return this.name;
    }

    @Nullable
    public String getEmail() {
        return this.email;
    }

    @Nullable
    public String getAvatarPath() {
        return this.avatar_path;
    }

    @Override
    public String toString() {
        return this.content;
    }
} @SuppressWarnings("unused")
public class KanboardRequest {
    public final String Command;
    public final String[] JSON;
    public final Object Listener;

    private KanboardRequest(@NonNull String command, @NonNull String[] json) {
        this(command, json, null);
    }

    private KanboardRequest(@NonNull String command, @NonNull String[] json, @Nullable Object listener) {
        this.Command = command;
        this.JSON = json;
        this.Listener = listener;
    }

    //region Kanboard API
    @NonNull
    public static KanboardRequest getMe() {
        return new KanboardRequest("getMe", new String[] {"{\"jsonrpc\": \"2.0\", \"method\": \"getMe\", \"id\": 1}"});
    }

    public static KanboardRequest getVersion() {
        return new KanboardRequest("getVersion", new String[] {"{\n" +
                "    \"jsonrpc\": \"2.0\",\n" +
                "    \"method\": \"getVersion\",\n" +
                "    \"id\": 1661138292\n" +
                "}"});
    }

    public static KanboardRequest getDefaultTaskColors() {
        return new KanboardRequest("getDefaultTaskColors", new String[] {"{\n" +
                "    \"jsonrpc\": \"2.0\",\n" +
                "    \"method\": \"getDefaultTaskColors\",\n" +
                "    \"id\": 2108929212\n" +
                "}"});
    }

    public static KanboardRequest getDefaultTaskColor() {
        return new KanboardRequest("getDefaultTaskColor", new String[] {"{\n" +
                "    \"jsonrpc\": \"2.0\",\n" +
                "    \"method\": \"getDefaultTaskColor\",\n" +
                "    \"id\": 1144775215\n" +
                "}"});
    }

    @NonNull
    public static KanboardRequest getMyProjectsList() {
        return new KanboardRequest("getMyProjectsList", new String[] {"{\"jsonrpc\": \"2.0\", \"method\": \"getMyProjectsList\", \"id\": 1}"});
    }

    @NonNull
    public static KanboardRequest getMyProjects() {
        return new KanboardRequest("getMyProjects", new String[] {"{\"jsonrpc\": \"2.0\", \"method\": \"getMyProjects\", \"id\": 2134420212}"});
    }

    @NonNull
    public static KanboardRequest getMyDashboard() {
        return new KanboardRequest("getMyDashboard", new  String[] {"{\"jsonrpc\": \"2.0\", \"method\": \"getMyDashboard\", \"id\": 1}"});
    }

    @NonNull
    public static KanboardRequest getMyOverdueTasks() {
        return new KanboardRequest("getMyOverdueTasks", new String[] {"{\"jsonrpc\": \"2.0\", \"method\": \"getMyOverdueTasks\", \"id\": 1}"});
    }

    @NonNull
    public static KanboardRequest getMyActivityStream() {
        return new KanboardRequest("getMyActivityStream", new String[] {"{\"jsonrpc\": \"2.0\", \"method\": \"getMyActivityStream\", \"id\": 1}"});
    }

    @NonNull
    public static KanboardRequest getProjectById(int projectid) {
        return new KanboardRequest("getProjectById", new String[] {String.format(
                "{\n" +
                "    \"jsonrpc\": \"2.0\",\n" +
                "    \"method\": \"getProjectById\",\n" +
                "    \"id\": 1,\n" +
                "    \"params\": {\n" +
                "        \"project_id\": %d\n" +
                "    }\n" +
                "}", projectid)});
    }

    @NonNull
    public static KanboardRequest getProjectUsers(int projectid) {
        return new KanboardRequest("getProjectUsers", new String[] {String.format(
                "{\n" +
                "    \"jsonrpc\": \"2.0\",\n" +
                "    \"method\": \"getProjectUsers\",\n" +
                "    \"id\": 1601016721,\n" +
                "    \"params\": [\n" +
                "        \"%d\"\n" +
                "    ]\n" +
                "}", projectid)});
    }

    @NonNull
    public static KanboardRequest getTask(int taskid) {
        return new KanboardRequest("getTask", new String[] {String.format(
                "{\n" +
                "    \"jsonrpc\": \"2.0\",\n" +
                "    \"method\": \"getTask\",\n" +
                "    \"id\": 700738119,\n" +
                "    \"params\": {\n" +
                "        \"task_id\": %d\n" +
                "    }\n" +
                "}", taskid)});
    }

    @NonNull
    public static KanboardRequest getAllTasks(int projectid, int status) {
        return new KanboardRequest("getAllTasks", new String[] {String.format(
                "{\n" +
                "    \"jsonrpc\": \"2.0\",\n" +
                "    \"method\": \"getAllTasks\",\n" +
                "    \"id\": 1,\n" +
                "    \"params\": {\n" +
                "        \"project_id\": %d,\n" +
                "        \"status_id\": %d\n" +
                "    }\n" +
                "}", projectid, status)});
    }

    @NonNull
    public static KanboardRequest getOverdueTasksByProject(int projectid) {
        return new KanboardRequest("getOverdueTasksByProject", new String[] {String.format(
                "{\n" +
                "    \"jsonrpc\": \"2.0\",\n" +
                "    \"method\": \"getOverdueTasksByProject\",\n" +
                "    \"id\": 133280317,\n" +
                "    \"params\": {\n" +
                "        \"project_id\": %d\n" +
                "    }\n" +
                "}", projectid)});
    }

    @NonNull
    public static KanboardRequest getSwimlane(int swimlaneid) {
        return new KanboardRequest("getSwimlane", new String[] {String.format(
                "{\n" +
                "    \"jsonrpc\": \"2.0\",\n" +
                "    \"method\": \"getSwimlane\",\n" +
                "    \"id\": 131071870,\n" +
                "    \"params\": [\n" +
                "        %d\n" +
                "    ]\n" +
                "}", swimlaneid)});
    }

    @NonNull
    public static KanboardRequest getDefaultSwimlane(int projectid) {
        return new KanboardRequest("getDefaultSwimlane", new String[] {String.format(
                "{\n" +
                        "    \"jsonrpc\": \"2.0\",\n" +
                        "    \"method\": \"getDefaultSwimlane\",\n" +
                        "    \"id\": 131071870,\n" +
                        "    \"params\": [\n" +
                        "        %d\n" +
                        "    ]\n" +
                        "}", projectid)});
    }

    @NonNull
    public static KanboardRequest getCategrory(int categoryid) {
        return new KanboardRequest("getCategory", new String[] {String.format(
                "{\n" +
                "    \"jsonrpc\": \"2.0\",\n" +
                "    \"method\": \"getCategory\",\n" +
                "    \"id\": 203539163,\n" +
                "    \"params\": {\n" +
                "        \"category_id\": %d\n" +
                "    }\n" +
                "}", categoryid)});
    }

    @NonNull
    public static KanboardRequest getColumns(int projectid) {
        return new KanboardRequest("getColumns", new String[] {String.format(
                "{\n" +
                "    \"jsonrpc\": \"2.0\",\n" +
                "    \"method\": \"getColumns\",\n" +
                "    \"id\": 1,\n" +
                "    \"params\": [\n" +
                "        %d\n" +
                "    ]\n" +
                "}", projectid)});
    }

    @NonNull
    public static KanboardRequest getActiveSwimlanes(int projectid) {
        return new KanboardRequest("getActiveSwimlanes", new String[] {String.format(
                "{\n" +
                "    \"jsonrpc\": \"2.0\",\n" +
                "    \"method\": \"getActiveSwimlanes\",\n" +
                "    \"id\": 934789422,\n" +
                "    \"params\": [\n" +
                "        %d\n" +
                "    ]\n" +
                "}", projectid)});
    }

    @NonNull
    public static KanboardRequest getAllCategories(int projectid) {
        return new KanboardRequest("getAllCategories", new String[] {String.format(
                "{\n" +
                "    \"jsonrpc\": \"2.0\",\n" +
                "    \"method\": \"getAllCategories\",\n" +
                "    \"id\": 1,\n" +
                "    \"params\": {\n" +
                "        \"project_id\": %d\n" +
                "    }\n" +
                "}", projectid)});
    }

    @NonNull
    public static KanboardRequest getAllComments(int projectid) {
        return new KanboardRequest("getAllComments", new String[] {String.format(
                "{\n" +
                "    \"jsonrpc\": \"2.0\",\n" +
                "    \"method\": \"getAllComments\",\n" +
                "    \"id\": 1,\n" +
                "    \"params\": {\n" +
                "        \"task_id\": %d\n" +
                "    }\n" +
                "}", projectid)});
    }

    @NonNull
    public static KanboardRequest createComment(int taskid, int userid, @NonNull String comment) {
        return new KanboardRequest("createComment", new String[] {String.format("" +
                "{\n" +
                "    \"jsonrpc\": \"2.0\",\n" +
                "    \"method\": \"createComment\",\n" +
                "    \"id\": 1580417921,\n" +
                "    \"params\": {\n" +
                "        \"task_id\": %d,\n" +
                "        \"user_id\": %d,\n" +
                "        \"content\": \"%s\"\n" +
                "    }\n" +
                "}", taskid, userid, StringEscapeUtils.escapeJson(comment))});
    }

    @NonNull
    public static KanboardRequest updateComment(int commentid, @NonNull String comment) {
        return new KanboardRequest("updateComment", new String[] {String.format(
                "{\n" +
                "    \"jsonrpc\": \"2.0\",\n" +
                "    \"method\": \"updateComment\",\n" +
                "    \"id\": 496470023,\n" +
                "    \"params\": {\n" +
                "        \"id\": %d,\n" +
                "        \"content\": \"%s\"\n" +
                "    }\n" +
                "}", commentid, StringEscapeUtils.escapeJson(comment))});
    }

    @NonNull
    public static KanboardRequest removeComment(int commentid) {
        return new KanboardRequest("removeComment", new String[] {String.format(
                "{\n" +
                "    \"jsonrpc\": \"2.0\",\n" +
                "    \"method\": \"removeComment\",\n" +
                "    \"id\": 328836871,\n" +
                "    \"params\": {\n" +
                "        \"comment_id\": %d\n" +
                "    }\n" +
                "}", commentid)});
    }

    @NonNull
    public static KanboardRequest createTask(@NonNull String title, int projectid, @Nullable String colorid,
                                             @Nullable Integer columnid, @Nullable Integer ownerid,
                                             @Nullable Integer creatorid, @Nullable Date duedate,
                                             @Nullable String description, @Nullable Integer categoryid,
                                             @Nullable Integer score, @Nullable Integer swimlaneid,
                                             @Nullable Integer priority, @Nullable Integer recurrencestatus,
                                             @Nullable Integer recurrencetrigger, @Nullable Integer recurrencefactor,
                                             @Nullable Integer recurrencetimeframe, @Nullable Integer recurrencebasedate,
                                             @Nullable String[] tags, @Nullable Date starteddate) {
        String content = String.format("" +
                "   \"title\": \"%s\",\n" +
                "   \"project_id\": %d", StringEscapeUtils.escapeJson(title), projectid);
        if (colorid != null)
            content += String.format(", \n \"color_id\": \"%s\"", colorid);
        if (columnid != null)
            content += String.format(", \n \"column_id\": %d", columnid);
        if (ownerid != null)
            content += String.format(", \n \"owner_id\": %d", ownerid);
        if (creatorid != null)
            content += String.format(", \n \"creator_id\": %d", creatorid);
        if (duedate != null)
            content += String.format(", \n \"date_due\": \"%tF\"", duedate);
        if (description != null)
            content += String.format(", \n \"description\": \"%s\"", StringEscapeUtils.escapeJson(description));
        if (categoryid != null)
            content += String.format(", \n \"category_id\": %d", categoryid);
        if (score != null)
            content += String.format(", \n \"score\": %d", score);
        if (swimlaneid != null)
            content += String.format(", \n \"swimlane_id\": %d", swimlaneid);
        if (priority != null)
            content += String.format(", \n \"priority\": %d", priority);
        if (recurrencestatus != null)
            content += String.format(", \n \"recurrence_status\": %d", recurrencestatus);
        if (recurrencetrigger != null)
            content += String.format(", \n \"recurrence_trigger\": %d", recurrencetrigger);
        if (recurrencefactor != null)
            content += String.format(", \n \"recurrence_factor\": %d", recurrencefactor);
        if (recurrencetimeframe != null)
            content += String.format(", \n \"recurrence_timeframe\": %d", recurrencetimeframe);
        if (recurrencebasedate != null)
            content += String.format(", \n \"recurrence_basedate\": %d", recurrencebasedate);
        if (starteddate != null)
            content += String.format(", \n \"date_started\": \"%1$tF %1$tH:%1$tM\"", starteddate);
        //TODO: Add tags
        return new KanboardRequest("createTask", new String[] {String.format(
                "{\n" +
                        "    \"jsonrpc\": \"2.0\",\n" +
                        "    \"method\": \"createTask\",\n" +
                        "    \"id\": 1406803059,\n" +
                        "    \"params\": {\n" +
                        "%s\n" +
                        "    }\n" +
                        "}", content)});
    }

    @NonNull
    public static KanboardRequest updateTask(int taskid, @NonNull String title, @Nullable String colorid,
                                             @Nullable Integer ownerid,
                                             @Nullable Date duedate,
                                             @Nullable String description, @Nullable Integer categoryid,
                                             @Nullable Integer score,
                                             @Nullable Integer priority, @Nullable Integer recurrencestatus,
                                             @Nullable Integer recurrencetrigger, @Nullable Integer recurrencefactor,
                                             @Nullable Integer recurrencetimeframe, @Nullable Integer recurrencebasedate,
                                             @Nullable String[] tags, @Nullable Date starteddate) {
        String content = String.format("" +
                "   \"title\": \"%s\",\n" +
                "   \"id\": %d", StringEscapeUtils.escapeJson(title), taskid);
        if (colorid != null)
            content += String.format(", \n \"color_id\": \"%s\"", colorid);
        if (ownerid != null)
            content += String.format(", \n \"owner_id\": %d", ownerid);
        if (duedate != null)
            content += String.format(", \n \"date_due\": \"%tF\"", duedate);
        else
            content += ", \n \"date_due\": \"\"";
        if (description != null)
            content += String.format(", \n \"description\": \"%s\"", StringEscapeUtils.escapeJson(description));
        if (categoryid != null)
            content += String.format(", \n \"category_id\": %d", categoryid);
        if (score != null)
            content += String.format(", \n \"score\": %d", score);
        if (priority != null)
            content += String.format(", \n \"priority\": %d", priority);
        if (recurrencestatus != null)
            content += String.format(", \n \"recurrence_status\": %d", recurrencestatus);
        if (recurrencetrigger != null)
            content += String.format(", \n \"recurrence_trigger\": %d", recurrencetrigger);
        if (recurrencefactor != null)
            content += String.format(", \n \"recurrence_factor\": %d", recurrencefactor);
        if (recurrencetimeframe != null)
            content += String.format(", \n \"recurrence_timeframe\": %d", recurrencetimeframe);
        if (recurrencebasedate != null)
            content += String.format(", \n \"recurrence_basedate\": %d", recurrencebasedate);
        if (starteddate != null)
            content += String.format(", \n \"date_started\": \"%1$tF %1$tH:%1$tM\"", starteddate);
        else
            content += ", \n \"date_started\": \"\"";
        //TODO: Add tags
        return new KanboardRequest("updateTask", new String[] {String.format(
                "{\n" +
                        "    \"jsonrpc\": \"2.0\",\n" +
                        "    \"method\": \"updateTask\",\n" +
                        "    \"id\": 1406803059,\n" +
                        "    \"params\": {\n" +
                        "%s\n" +
                        "    }\n" +
                        "}", content)});
    }

    @NonNull
    public static KanboardRequest openTask(int taskid) {
        return new KanboardRequest("openTask", new String[] {String.format(
                "{\n" +
                "    \"jsonrpc\": \"2.0\",\n" +
                "    \"method\": \"openTask\",\n" +
                "    \"id\": 1888531925,\n" +
                "    \"params\": {\n" +
                "        \"task_id\": %d\n" +
                "    }\n" +
                "}", taskid)});
    }

    @NonNull
    public static KanboardRequest closeTask(int taskid) {
        return new KanboardRequest("closeTask", new String[] {String.format(
                "{\n" +
                "    \"jsonrpc\": \"2.0\",\n" +
                "    \"method\": \"closeTask\",\n" +
                "    \"id\": 1654396960,\n" +
                "    \"params\": {\n" +
                "        \"task_id\": %d\n" +
                "    }\n" +
                "}", taskid)});
    }

    @NonNull
    public static KanboardRequest removeTask(int taskid) {
        return new KanboardRequest("removeTask", new String[] {String.format(
                "{\n" +
                "    \"jsonrpc\": \"2.0\",\n" +
                "    \"method\": \"removeTask\",\n" +
                "    \"id\": 1423501287,\n" +
                "    \"params\": {\n" +
                "        \"task_id\": %d\n" +
                "    }\n" +
                "}", taskid)});
    }

    @NonNull
    public static KanboardRequest moveTaskPosition(int projectid, int taskid, int columnid, int position,
                                                   int swimlaneid) {
        return new KanboardRequest("moveTaskPosition", new String[] {String.format(
                "{\n" +
                "    \"jsonrpc\": \"2.0\",\n" +
                "    \"method\": \"moveTaskPosition\",\n" +
                "    \"id\": 117211800,\n" +
                "    \"params\": {\n" +
                "        \"project_id\": %d,\n" +
                "        \"task_id\": %d,\n" +
                "        \"column_id\": %d,\n" +
                "        \"position\": %d,\n" +
                "        \"swimlane_id\": %d\n" +
                "    }\n" +
                "}", projectid, taskid, columnid, position, swimlaneid)});
    }

    @NonNull
    public static KanboardRequest moveTaskToProject(int projectid, int taskid, @Nullable Integer columnid,
                                                    @Nullable Integer position, @Nullable Integer swimlaneid) {
        String content = String.format("" +
                "        \"project_id\": %d,\n" +
                "        \"task_id\": %d", projectid, taskid);
        if (columnid != null)
            content += String.format("" + ",\n        \"column_id\": %d", columnid);
        if (position != null)
            content += String.format("" + ",\n        \"position\": %d", position);
        if (swimlaneid != null)
            content += String.format("" + ",\n        \"swimlane_id\": %d", swimlaneid);
        return new KanboardRequest("moveTaskToProject", new String[] {String.format(
                "{\n" +
                "    \"jsonrpc\": \"2.0\",\n" +
                "    \"method\": \"moveTaskToProject\",\n" +
                "    \"id\": 15775829,\n" +
                "    \"params\": {\n" +
                "%s\n" +
                "    }\n" +
                "}", content)});
    }

    @NonNull
    public static KanboardRequest duplicateTaskToProject(int projectid, int taskid, @Nullable Integer columnid,
                                                    @Nullable Integer position, @Nullable Integer swimlaneid) {
        String content = String.format("" +
                "        \"project_id\": %d,\n" +
                "        \"task_id\": %d", projectid, taskid);
        if (columnid != null)
            content += String.format("" + ",\n        \"column_id\": %d", columnid);
        if (position != null)
            content += String.format("" + ",\n        \"position\": %d", position);
        if (swimlaneid != null)
            content += String.format("" + ",\n        \"swimlane_id\": %d", swimlaneid);
        return new KanboardRequest("duplicateTaskToProject", new String[] {String.format(
                "{\n" +
                "    \"jsonrpc\": \"2.0\",\n" +
                "    \"method\": \"duplicateTaskToProject\",\n" +
                "    \"id\": 1662458687,\n" +
                "    \"params\": {\n" +
                "%s\n" +
                "    }\n" +
                "}", content)});
    }

    @NonNull
    public static KanboardRequest createSubtask(int taskid, @NonNull String title, @Nullable Integer userid,
                                                @Nullable Integer timeestimated, @Nullable Integer timespent, @Nullable Integer status) {
        String content = String.format("" +
                "        \"task_id\": %d,\n" +
                "        \"title\": \"%s\"", taskid, StringEscapeUtils.escapeJson(title));
        if (userid != null)
            content += String.format("" + ",\n" +
                    "        \"user_id\": %d", userid);
        if (timeestimated != null)
            content += String.format("" + ",\n" +
                    "        \"time_estimated\": %d", timeestimated);
        if (timespent != null)
            content += String.format("" + ",\n" +
                    "        \"time_spent\": %d", timespent);
        if (status != null)
            content += String.format("" + ",\n" +
                    "        \"status\": %d", status);
        return new KanboardRequest("createSubtask", new String[] {String.format(
                "{\n" +
                        "    \"jsonrpc\": \"2.0\",\n" +
                        "    \"method\": \"createSubtask\",\n" +
                        "    \"id\": 2041554661,\n" +
                        "    \"params\": {\n" +
                        "%s\n"+
                        "    }\n" +
                        "}", content)});
    }

    @NonNull
    public static KanboardRequest updateSubtask(int subtaskid, int taskid, @Nullable String title, @Nullable Integer userid,
                                                @Nullable Integer timeestimated, @Nullable Integer timespent, @Nullable Integer status) {
        String content = String.format("" +
                "        \"id\": %d,\n" +
                "        \"task_id\": %d", subtaskid, taskid);
        if (title != null)
            content += String.format("" + ",\n" +
                    "        \"title\": \"%s\"", StringEscapeUtils.escapeJson(title));
        if (userid != null)
            content += String.format("" + ",\n" +
                    "        \"user_id\": %d", userid);
        if (timeestimated != null)
            content += String.format("" + ",\n" +
                    "        \"time_estimated\": %d", timeestimated);
        if (timespent != null)
            content += String.format("" + ",\n" +
                    "        \"time_spent\": %d", timespent);
        if (status != null)
            content += String.format("" + ",\n" +
                    "        \"status\": %d", status);
        return new KanboardRequest("updateSubtask", new String[] {String.format(
                "{\n" +
                        "    \"jsonrpc\": \"2.0\",\n" +
                        "    \"method\": \"updateSubtask\",\n" +
                        "    \"id\": 191749979,\n" +
                        "    \"params\": {\n" +
                        "%s\n"+
                        "    }\n" +
                        "}", content)});
    }

    @NonNull
    public static KanboardRequest getAllSubtasks(int taskid) {
        return new KanboardRequest("getAllSubtasks", new String[] {String.format(
                "{\n" +
                "    \"jsonrpc\": \"2.0\",\n" +
                "    \"method\": \"getAllSubtasks\",\n" +
                "    \"id\": 2087700490,\n" +
                "    \"params\": {\n" +
                "        \"task_id\":%d\n" +
                "    }\n" +
                "}", taskid)});
    }

    @NonNull
    public static KanboardRequest removeSubtask(int subtaskid) {
        return new KanboardRequest("removeSubtask", new String[] {String.format(
                "{\n" +
                "    \"jsonrpc\": \"2.0\",\n" +
                "    \"method\": \"removeSubtask\",\n" +
                "    \"id\": 1382487306,\n" +
                "    \"params\": {\n" +
                "        \"subtask_id\": %d\n" +
                "    }\n" +
                "}", subtaskid)});
    }

    @NonNull
    public static KanboardRequest hasSubtaskTimer(int subtaskid, int userid, @NonNull OnSubtaskTimetrackingListener listener) {
        return new KanboardRequest("hasSubtaskTimer", new String[] {String.format(
                "{\n" +
                "    \"jsonrpc\": \"2.0\",\n" +
                "    \"method\": \"hasSubtaskTimer\"," +
                "    \"id\": 1786995697,\n" +
                "    \"params\": [%d, %d]\n" +
                "}", subtaskid, userid)}, listener);
    }

    @NonNull
    public static KanboardRequest setSubtaskStartTime(int subtaskid, int userid, @NonNull OnSubtaskTimetrackingListener listener) {
        return new KanboardRequest("setSubtaskStartTime", new String[] {String.format(
                "{\n" +
                "    \"jsonrpc\": \"2.0\",\n" +
                "    \"method\": \"setSubtaskStartTime\"," +
                "    \"id\": 1168991769,\n" +
                "    \"params\": [%d, %d]\n" +
                "}", subtaskid, userid)}, listener);
    }

    @NonNull
    public static KanboardRequest setSubtaskEndTime(int subtaskid, int userid, @NonNull OnSubtaskTimetrackingListener listener) {
        return new KanboardRequest("setSubtaskEndTime", new String[] {String.format(
                "{\n" +
                "    \"jsonrpc\": \"2.0\",\n" +
                "    \"method\": \"setSubtaskEndTime\"," +
                "    \"id\": 1026607603,\n" +
                "    \"params\": [%d, %d]\n" +
                "}", subtaskid, userid)}, listener);
    }

    @NonNull
    public static KanboardRequest getSubtaskTimeSpent(int subtaskid, int userid, @NonNull OnSubtaskTimetrackingListener listener) {
        return new KanboardRequest("getSubtaskTimeSpent", new String[] {String.format(
                "{\n" +
                "    \"jsonrpc\": \"2.0\",\n" +
                "    \"method\": \"getSubtaskTimeSpent\"," +
                "    \"id\": 738527378,\n" +
                "    \"params\": [%d, %d]\n" +
                "}", subtaskid, userid)}, listener);
    }

    @NonNull
    public static KanboardRequest getAllTaskFiles(int taskid) {
        return new KanboardRequest("getAllTaskFiles", new String[] {String.format(
                "{\n" +
                        "    \"jsonrpc\": \"2.0\",\n" +
                        "    \"method\": \"getAllTaskFiles\",\n" +
                        "    \"id\": 1880662820,\n" +
                        "    \"params\": {\n" +
                        "        \"task_id\": %d\n" +
                        "    }\n" +
                        "}", taskid)});
    }

    @NonNull
    public static KanboardRequest removeTaskFile(int fileid) {
        return new KanboardRequest("removeTaskFile", new String[] {String.format(
                "{\n" +
                        "    \"jsonrpc\": \"2.0\",\n" +
                        "    \"method\": \"removeTaskFile\",\n" +
                        "    \"id\": 447036524,\n" +
                        "    \"params\": [\n" +
                        "        \"%d\"\n" +
                        "    ]" +
                        "}", fileid)});
    }

    @NonNull
    public static KanboardRequest downloadTaskFile(int fileid) {
        return new KanboardRequest("downloadTaskFile", new String[] {String.format(
                "{\n" +
                        "    \"jsonrpc\": \"2.0\",\n" +
                        "    \"method\": \"downloadTaskFile\",\n" +
                        "    \"id\": %1$d,\n" +
                        "    \"params\": [\n" +
                        "        \"%1$d\"\n" +
                        "    ]" +
                        "}", fileid)});
    }
    //endregion

    //region Custom API
    @NonNull
    public static KanboardRequest KD_getDashboard() {
        return new KanboardRequest("KD_getDashboard", new String[] {KanboardRequest.getMyDashboard().JSON[0],
                                                                    KanboardRequest.getMyOverdueTasks().JSON[0],
                                                                    KanboardRequest.getMyActivityStream().JSON[0]});
    }

    @NonNull
    public static KanboardRequest KD_getProjectById(int projectid) {
        return new KanboardRequest("KD_getProjectById", new String[] {KanboardRequest.getProjectById(projectid).JSON[0],
                                                                      KanboardRequest.getColumns(projectid).JSON[0],
                                                                      KanboardRequest.getActiveSwimlanes(projectid).JSON[0],
                                                                      KanboardRequest.getAllCategories(projectid).JSON[0],
                                                                      KanboardRequest.getAllTasks(projectid, 1).JSON[0],
                                                                      KanboardRequest.getAllTasks(projectid, 0).JSON[0],
                                                                      KanboardRequest.getOverdueTasksByProject(projectid).JSON[0],
                                                                      KanboardRequest.getProjectUsers(projectid).JSON[0]});
    }
    //endregion
} @SuppressWarnings("unused")
public class KanboardSubtask implements Comparable<KanboardSubtask>, Serializable {
    private int Id;
    private int TaskId;
    private int Position;
    private int UserId;
    private int Status;
    private String Title;
    private double TimeEstimated;
    private double TimeSpent;
    // Dashboard properties
    private int ProjectId;
    private String TaskName;
    private String ProjectName;
    private String StatusName;
    private boolean IsTimerStarted;
    private int TimerStartDate;
    private String ColorName;
    // TODO: What is timer_start_date?

    public KanboardSubtask(JSONObject json) {
        Id = json.optInt("id", -1);
        TaskId = json.optInt("task_id", -1);
        Position = json.optInt("position", -1);
        UserId = json.optInt("user_id", -1);
        Status = json.optInt("status");
        Title = json.optString("title");
        try {
            TimeEstimated = Double.parseDouble(json.optString("time_estimated"));
        } catch (NumberFormatException e) {
            TimeEstimated = 0;
        }
        try {
            TimeSpent = Double.parseDouble(json.optString("time_spent"));
        } catch (NumberFormatException e) {
            TimeSpent = 0;
        }
        // Dashboard properties
        ProjectId = json.optInt("project_id", -1);
        TaskName = json.optString("task_name");
        ProjectName = json.optString("project_name");
        StatusName = json.optString("status_name");
        IsTimerStarted = KanboardAPI.StringToBoolean(json.optString("is_timer_started"));
        TimerStartDate = json.optInt("timer_start_date");
        ColorName = json.optString("color_id");
    }

    public int getId() {
        return Id;
    }

    public int getTaskId() {
        return TaskId;
    }

    public int getPosition() {
        return Position;
    }

    public int getUserId() {
        return UserId;
    }

    public int getStatus() {
        return Status;
    }

    public String getTitle() {
        return Title;
    }

    public double getTimeEstimated() {
        return TimeEstimated;
    }

    public double getTimeSpent() {
        return TimeSpent;
    }

    public int getProjectId() {
        return ProjectId;
    }

    public String getTaskName() {
        return TaskName;
    }

    public String getProjectName() {
        return ProjectName;
    }

    public String getStatusName() {
        return StatusName;
    }

    public boolean getIsTimerStarted() {
        return IsTimerStarted;
    }

    public int getTimerStartDate() {
        return TimerStartDate;
    }

    public String getColorName() {
        return ColorName;
    }

    @Override
    public int compareTo(@NonNull KanboardSubtask o) {
        return this.Title.compareTo(o.Title);
    }

    @Override
    public String toString() {
        return this.Title;
    }
} @SuppressWarnings("unused")
public class KanboardSwimlane implements Serializable {
    private int Id;
    private String Name;
    private String Description;
    private int Position;
    private int ProjectId;
    private boolean IsActive;
    private boolean IsDefault = false;

    public KanboardSwimlane(JSONObject swimlane) {
        if (swimlane.has("default_swimlane")) {
            Id = 0;
            Name = swimlane.optString("default_swimlane");
            IsActive = KanboardAPI.StringToBoolean(swimlane.optString("show_default_swimlane"));
            IsDefault = true;
        } else {
            Id = swimlane.optInt("id");
            Name = swimlane.optString("name");
            Description = swimlane.optString("description");
            Position = swimlane.optInt("position");
            ProjectId = swimlane.optInt("project_id");
            IsActive = KanboardAPI.StringToBoolean(swimlane.optString("is_active"));
        }
    }

    public int getId() {
        return Id;
    }

    public String getName() {
        return Name;
    }

    public String getDescription() {
        return Description.contentEquals("null") ? null : Description;
    }

    public int getPosition() {
        return Position;
    }

    public int getProjectId() {
        return ProjectId;
    }

    public boolean getIsActive() {
        return IsActive;
    }

    public boolean getIsDefault() {
        return IsDefault;
    }

    @Override
    public String toString() {
        return this.Name;
    }
} @SuppressWarnings("unused")
public class KanboardTask implements Comparable<KanboardTask>, Serializable {
    private int ID;
    private int ProjectID;
    private int ColumnID;
    private int SwimlaneID;
    private int CategoryID;
    private int CreatorID;
    private int OwnerID;
    private int Priority;
    private int Position;
    private boolean IsActive;
    private String Title;
    private String Description;
    private URL Url;
    private double TimeEstimated;
    private double TimeSpent;
    private String ColorId;
    private Integer ColorBackground = null;
    // Dashboard properties
    private String ProjectName;
    private String ColumnTitle;
    private Date DueDate;
    private Date CompletedDate;
    private Date StartedDate;
    private Date CreationDate;
    private Date ModificationDate;
    private Date MovedDate;
    // TODO: getTaskByID has additional properties
    // TODO: add color

    public KanboardTask(JSONObject json) throws MalformedURLException {
        ID = json.optInt("id", -1);
        ProjectID = json.optInt("project_id", -1);
        ColumnID = json.optInt("column_id", -1);
        SwimlaneID = json.optInt("swimlane_id", -1);
        CategoryID = json.optInt("category_id", -1);
        CreatorID = json.optInt("creator_id", -1);
        OwnerID = json.optInt("owner_id", -1);
        Priority = json.optInt("priority", -1);
        Position = json.optInt("position", -1);
        IsActive = KanboardAPI.StringToBoolean(json.optString("is_active"));
        Title = json.optString("title");
        Description = json.optString("description");
        if (json.has("url"))
            Url = new URL(json.optString("url"));
        else
            Url = null;
        TimeEstimated = json.optDouble("time_estimated");
        TimeSpent = json.optDouble("time_spent");
        // Dashboard properties
        ProjectName = json.optString("project_name");
        ColumnTitle = json.optString("column_title");
        long tmpTime = json.optLong("date_due");
        // Kanboard returns 0 when there was no time set
        if (tmpTime > 0)
            DueDate = new Date(tmpTime * 1000);
        else
            DueDate = null;
        tmpTime = json.optLong("date_completed");
        if (tmpTime > 0)
            CompletedDate = new Date(tmpTime * 1000);
        else
            CompletedDate = null;
        tmpTime = json.optLong("date_started");
        if (tmpTime > 0)
            StartedDate = new Date(tmpTime * 1000);
        else
            StartedDate = null;
        tmpTime = json.optLong("date_creation");
        if (tmpTime > 0)
            CreationDate = new Date(tmpTime * 1000);
        else
            CreationDate = null;
        tmpTime = json.optLong("date_modification");
        if (tmpTime > 0)
            ModificationDate = new Date(tmpTime * 1000);
        else
            ModificationDate = null;
        tmpTime = json.optLong("date_moved");
        if (tmpTime > 0)
            MovedDate = new Date(tmpTime * 1000);
        else
            MovedDate = null;

        ColorId = json.optString("color_id", "");

        if (json.has("color"))
            ColorBackground = KanboardAPI.parseColorString(json.optJSONObject("color").optString("background"));
    }

    public int getId() {
        return ID;
    }

    public int getProjectId() {
        return ProjectID;
    }

    public int getColumnId() {
        return ColumnID;
    }

    public int getSwimlaneId() {
        return SwimlaneID;
    }

    public int getCategoryId() {
        return CategoryID;
    }

    public int getOwnerId() {
        return OwnerID;
    }

    public int getCreatorId() {
        return CreatorID;
    }

    public int getPriority() {
        return Priority;
    }

    public int getPosition() {
        return Position;
    }

    public boolean getIsActive() {
        return IsActive;
    }

    public String getDescription() {
        return Description;
    }

    public String getTitle() {
        return Title;
    }

    public URL getUrl() {
        return Url;
    }

    public double getTimeEstimated() {
        return TimeEstimated;
    }

    public double getTimeSpent() {
        return TimeSpent;
    }

    public String getProjectName() {
        return ProjectName;
    }

    public String getColumnTitle() {
        return ColumnTitle;
    }

    public Date getDateCompleted() {
        return CompletedDate;
    }

    public Date getDateStarted() {
        return StartedDate;
    }

    public Date getDateDue() {
        return DueDate;
    }

    public Date getDateCreation() {
        return CreationDate;
    }

    public Date getDateModification() {
        return ModificationDate;
    }

    public Date getDateMoved() {
        return MovedDate;
    }

    public String getColorId() {
        return ColorId;
    }

    public Integer getColorBackground() {
        return ColorBackground;
    }

    @Override
    public int compareTo(@NonNull KanboardTask o) {
        return this.getPosition() - o.getPosition();
//        return this.Title.compareTo(o.Title);
    }

    @Override
    public String toString() {
        return this.Title;
    }
} @SuppressWarnings("unused")
public class KanboardTaskFile implements Serializable{
    private int id;
    private String name;
    private String path;
    private int taskId;
    private Date fileDate;
    private int userId;
    private long size;
    private String username;

    public KanboardTaskFile(JSONObject json) {
        id = json.optInt("id", -1);
        name = json.optString("name");
        path = json.optString("path");
        taskId = json.optInt("task_id", -1);
        userId = json.optInt("user_id", -1);
        size = json.optLong("size", 0);
        username = json.optString("username");
        long tmpTime = json.optLong("date");
        if (tmpTime > 0)
            fileDate = new Date(tmpTime * 1000);
        else
            fileDate = null;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getPath() {
        return path;
    }

    public int getTaskId() {
        return taskId;
    }

    public int getUserId() {
        return userId;
    }

    public String getUsername() {
        return username;
    }

    public long getSize() {
        return size;
    }

    public Date getFileDate() {
        return fileDate;
    }
} @SuppressWarnings("unused")
public class KanboardUserInfo implements Comparable<KanboardUserInfo>, Serializable {
    private int Id;
    private String Username;
    private String Role;
    private boolean IsLDAPUser;
    private String Name;
    private String Email;
    private String GoogleId;
    private String GithubId;
    private String GitlabId;
    private int NotificationsEnabled;
    private String Timezone;
    private String Language;
    private int DisableLoginForm;
    private boolean TwofactorActivated;
    private String TwofactorSecret;
    private String Token;
    private int NotificationsFilter;
    private int NumberFailedLogin;
    private int LockExpirationDate;
    private boolean IsActive;
    private String AvatarPath;

    KanboardUserInfo(int id, String username, String role, boolean isLDAPUser, String name,
                     String email, String googleId, String githubId, String gitlabId,
                     int notificationsEnabled, String timezone, String language,
                     int disableLoginForm, boolean twofactorActivated, String twofactorSecret,
                     String token, int notificationsFilter, int numberFailedLogin,
                     int lockExpirationDate, boolean isActive, String avatarPath) {
        Id = id;
        Username = username;
        Role = role;
        IsLDAPUser = isLDAPUser;
        Name = name;
        Email = email;
        GoogleId = googleId;
        GithubId = githubId;
        GitlabId = gitlabId;
        NotificationsEnabled = notificationsEnabled;
        Timezone = timezone;
        Language = language;
        DisableLoginForm = disableLoginForm;
        TwofactorActivated = twofactorActivated;
        TwofactorSecret = twofactorSecret;
        Token = token;
        NotificationsFilter = notificationsFilter;
        NumberFailedLogin = numberFailedLogin;
        LockExpirationDate = lockExpirationDate;
        IsActive = isActive;
        AvatarPath = avatarPath;
    }

    KanboardUserInfo(JSONObject json) {
        Id = json.optInt("id", -1);
        Username = json.optString("username", "");
        Role = json.optString("role", "");
        IsLDAPUser = json.optBoolean("is_ldap_user", false);
        Name = json.optString("name", "");
        Email = json.optString("email", "");
        GoogleId = json.optString("google_id", "");
        GithubId = json.optString("github_id", "");
        GitlabId = json.optString("gitlab_id", "");
        NotificationsEnabled = json.optInt("notifications_enabled", -1);
        Timezone = json.optString("timezone", "");
        Language = json.optString("language", "");
        DisableLoginForm = json.optInt("disable_login_form", -1);
        TwofactorActivated = json.optBoolean("twofactor_activated", false);
        TwofactorSecret = json.optString("twofactor_secret", "");
        Token = json.optString("token", "");
        NotificationsFilter = json.optInt("notifications_filter", -1);
        NumberFailedLogin = json.optInt("nb_failed_login", -1);
        LockExpirationDate = json.optInt("lock_expiration_date", -1);
        IsActive = json.optBoolean("is_active", false);
        AvatarPath = json.optString("avatar_path", "");

    }

    public int getId() {
        return Id;
    }

    public String getUsername() {
        return Username;
    }

    public String getRole() {
        return Role;
    }

    public boolean getIsLDAPUser() {
        return IsLDAPUser;
    }

    public String getName() {
        return Name;
    }

    public String getEmail() {
        return Email;
    }

    public String getGoogleId() {
        return GoogleId;
    }

    public String getGithubId() {
        return GithubId;
    }

    public String getGitlabId() {
        return GitlabId;
    }

    public int getNotificationsEnabled() {
        return NotificationsEnabled;
    }

    public String getTimezone() {
        return Timezone;
    }

    public String getLanguage() {
        return Language;
    }

    public int getDisableLoginForm() {
        return DisableLoginForm;
    }

    public boolean getTwofactorActivated() {
        return TwofactorActivated;
    }

    public String getTwofactorSecret() {
        return TwofactorSecret;
    }

    public String getToken() {
        return Token;
    }

    public int getNotificationsFilter() {
        return NotificationsFilter;
    }

    public int getNumberFailedLogin() {
        return NumberFailedLogin;
    }

    public int getLockExpirationDate() {
        return LockExpirationDate;
    }

    public boolean getIsActive() {
        return IsActive;
    }

    public String getAvatarPath() {
        return AvatarPath;
    }

    @Override
    public int compareTo(@NonNull KanboardUserInfo o) {
        return this.Username.compareTo(o.Username);
    }
} public interface OnCloseTaskListener {
    void onCloseTask(boolean success);
} public interface OnCreateCommentListener {
    void onCreateComment(boolean success, Integer commentid);
} public interface OnCreateSubtaskListener {
    void onCreateSubtask(boolean success, Integer result);
} public interface OnDownloadTaskFileListener {
    void onDownloadTaskFile(boolean success, int id, String data);
} public interface OnGetActiveSwimlanesListener {
    void onGetActiveSwimlanes(boolean success, List<KanboardSwimlane> result);
} public interface OnGetAllCommentsListener {
    void onGetAllComments(boolean success, List<KanboardComment> comments);
} public interface OnGetAllSubtasksListener {
    void onGetAllSubtasks(boolean success, List<KanboardSubtask> result);
} public interface OnGetAllTaskFilesListener {
    void onGetAllTaskFiles(boolean success, List<KanboardTaskFile> result);
} public interface OnGetCategoryListener {
    void onGetCategory(boolean success, KanboardCategory result);
} public interface OnGetColumnsListener {
    void onGetColumns(boolean success, List<KanboardColumn> result);
} public interface OnGetProjectUsersListener {
    void onGetProjectUsers(boolean success, Dictionary<Integer, String> result);
} public interface OnGetSwimlaneListener {
    void onGetSwimlane(boolean success, KanboardSwimlane result);
} public interface OnGetTaskListener {
    void onGetTask(boolean success, KanboardTask result);
} public interface OnMoveTaskPositionListener {
    void onMoveTaskPosition(boolean success);
} public interface OnOpenTaskListener {
    void onOpenTask(boolean success);
} public interface OnRemoveCommentListener {
    void onRemoveComment(boolean success);
} public interface OnRemoveSubtaskListener {
    void onRemoveSubtask(boolean success);
} public interface OnRemoveTaskFileListener extends Serializable {
    void onRemoveTaskFile(boolean success);
} public interface OnRemoveTaskListener {
    void onRemoveTask(boolean success);
} public interface OnSubtaskTimetrackingListener {
    void onSubtaskTimetracking(boolean result, double time);
} public interface OnUpdateCommentListener {
    void onUpdateComment(boolean success);
} public interface OnUpdateSubtaskListener {
    void onUpdateSubtask(boolean success);
}
import android.Manifest;
import android.app.Activity;
import android.app.DownloadManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.res.ResourcesCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.NestedScrollView;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.text.method.LinkMovementMethod;
import android.util.Base64;
import android.util.Log;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.OvershootInterpolator;
import android.webkit.MimeTypeMap;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.Spinner;
import android.widget.Switch;
import android.widget.TextView;
import android.widget.Toast;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.NodeRenderer;
import org.commonmark.renderer.html.HtmlNodeRendererContext;
import org.commonmark.renderer.html.HtmlNodeRendererFactory;
import org.commonmark.renderer.html.HtmlRenderer;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Locale;
import in.andres.kandroid.BuildConfig;
import in.andres.kandroid.CompactHtmlRenderer;
import in.andres.kandroid.Constants;
import in.andres.kandroid.DownloadIntentService;
import in.andres.kandroid.R;
import in.andres.kandroid.Utils;
import in.andres.kandroid.kanboard.KanboardAPI;
import in.andres.kandroid.kanboard.KanboardCategory;
import in.andres.kandroid.kanboard.KanboardColumn;
import in.andres.kandroid.kanboard.KanboardComment;
import in.andres.kandroid.kanboard.KanboardRequest;
import in.andres.kandroid.kanboard.KanboardSubtask;
import in.andres.kandroid.kanboard.KanboardSwimlane;
import in.andres.kandroid.kanboard.KanboardTask;
import in.andres.kandroid.kanboard.KanboardTaskFile;
import in.andres.kandroid.kanboard.KanboardUserInfo;
import in.andres.kandroid.kanboard.events.OnCloseTaskListener;
import in.andres.kandroid.kanboard.events.OnCreateCommentListener;
import in.andres.kandroid.kanboard.events.OnCreateSubtaskListener;
import in.andres.kandroid.kanboard.events.OnDownloadTaskFileListener;
import in.andres.kandroid.kanboard.events.OnGetActiveSwimlanesListener;
import in.andres.kandroid.kanboard.events.OnGetAllCommentsListener;
import in.andres.kandroid.kanboard.events.OnGetAllSubtasksListener;
import in.andres.kandroid.kanboard.events.OnGetAllTaskFilesListener;
import in.andres.kandroid.kanboard.events.OnGetCategoryListener;
import in.andres.kandroid.kanboard.events.OnGetColumnsListener;
import in.andres.kandroid.kanboard.events.OnGetProjectUsersListener;
import in.andres.kandroid.kanboard.events.OnGetSwimlaneListener;
import in.andres.kandroid.kanboard.events.OnGetTaskListener;
import in.andres.kandroid.kanboard.events.OnMoveTaskPositionListener;
import in.andres.kandroid.kanboard.events.OnOpenTaskListener;
import in.andres.kandroid.kanboard.events.OnRemoveCommentListener;
import in.andres.kandroid.kanboard.events.OnRemoveSubtaskListener;
import in.andres.kandroid.kanboard.events.OnRemoveTaskFileListener;
import in.andres.kandroid.kanboard.events.OnRemoveTaskListener;
import in.andres.kandroid.kanboard.events.OnSubtaskTimetrackingListener;
import in.andres.kandroid.kanboard.events.OnUpdateCommentListener;
import in.andres.kandroid.kanboard.events.OnUpdateSubtaskListener;
        @Override
        public void onGetSwimlane(boolean success, KanboardSwimlane result) {
            hideProgress();
            if (success) {
                swimlane = result;
                setSwimlaneDetails(swimlane.getName());
            }
        }
    };
    private OnGetAllSubtasksListener allSubtasksListener = new OnGetAllSubtasksListener() {
        @Override
        public void onGetAllSubtasks(boolean success, List<KanboardSubtask> result) {
            hideProgress();
            hideSubtaskProgress();
            if (success && result.size() > 0) {
                subtasks = result;
                subtaskListview.setAdapter(new SubtaskAdapter(getBaseContext(), subtasks, true));
//                findViewById(R.id.card_subtasks).setVisibility(View.VISIBLE);
                for (final KanboardSubtask sub: subtasks) {
                    kanboardAPI.hasSubtaskTimer(sub.getId(), me.getId(), new OnSubtaskTimetrackingListener() {
                        @Override
                        public void onSubtaskTimetracking(boolean result, double time) {
                            if (result && !hasTimer.containsKey(sub.getId())) {
                                hasTimer.put(sub.getId(), 0d);
                                ((SubtaskAdapter) subtaskListview.getAdapter()).notifyDataSetChanged();
                                kanboardAPI.getSubtaskTimeSpent(sub.getId(), me.getId(), new OnSubtaskTimetrackingListener() {
                                    @Override
                                    public void onSubtaskTimetracking(boolean result, double time) {
                                        hasTimer.put(sub.getId(), time);
                                        ((SubtaskAdapter) subtaskListview.getAdapter()).notifyDataSetChanged();
                                    }
                                });
                            } else if (!result && hasTimer.containsKey(sub.getId())){
                                hasTimer.remove(sub.getId());
                                ((SubtaskAdapter) subtaskListview.getAdapter()).notifyDataSetChanged();
                            }
                        }
                    });
                }
            } else {
//                findViewById(R.id.card_subtasks).setVisibility(View.GONE);
            }
        }
    };
    private OnCreateCommentListener createCommentListener = new OnCreateCommentListener() {
        @Override
        public void onCreateComment(boolean success, Integer commentid) {
            if (success) {
                showProgress();
                kanboardAPI.getAllComments(task.getId());
            } else
                Snackbar.make(findViewById(R.id.root_layout), getString(R.string.error_msg_create_comment), Snackbar.LENGTH_LONG).show();
        }
    };
    private OnUpdateCommentListener updateCommentListener = new OnUpdateCommentListener() {
        @Override
        public void onUpdateComment(boolean success) {
            if (success) {
                showProgress();
                kanboardAPI.getAllComments(task.getId());
            } else
                Snackbar.make(findViewById(R.id.root_layout), getString(R.string.error_msg_update_comment), Snackbar.LENGTH_LONG).show();
        }
    };
    private OnRemoveCommentListener removeCommentListener = new OnRemoveCommentListener() {
        @Override
        public void onRemoveComment(boolean success) {
            if (success) {
                showProgress();
                kanboardAPI.getAllComments(task.getId());
            } else
                Snackbar.make(findViewById(R.id.root_layout), getString(R.string.error_msg_remove_comment), Snackbar.LENGTH_LONG).show();
        }
    };
    private OnCreateSubtaskListener createSubtaskListener = new OnCreateSubtaskListener() {
        @Override
        public void onCreateSubtask(boolean success, Integer result) {
            if (success) {
                showProgress();
                kanboardAPI.getAllSubtasks(task.getId());
            } else
                Snackbar.make(findViewById(R.id.root_layout), getString(R.string.error_msg_create_subtask), Snackbar.LENGTH_LONG).show();
        }
    };
    private OnUpdateSubtaskListener updateSubtaskListener = new OnUpdateSubtaskListener() {
        @Override
        public void onUpdateSubtask(boolean success) {
            if (success) {
                showProgress();
                kanboardAPI.getAllSubtasks(task.getId());
            } else
                Snackbar.make(findViewById(R.id.root_layout), getString(R.string.error_msg_update_subtask), Snackbar.LENGTH_LONG).show();
        }
    };
    private OnRemoveSubtaskListener removeSubtaskListener = new OnRemoveSubtaskListener() {
        @Override
        public void onRemoveSubtask(boolean success) {
            if (success) {
                showProgress();
                kanboardAPI.getAllSubtasks(task.getId());
            } else
                Snackbar.make(findViewById(R.id.root_layout), getString(R.string.error_msg_remove_subtask), Snackbar.LENGTH_LONG).show();
        }
    };
    private OnOpenTaskListener openTaskListener = new OnOpenTaskListener() {
        @Override
        public void onOpenTask(boolean success) {
            if (success) {
                textStatus.setText(Utils.fromHtml(getString(R.string.taskview_status, getString(R.string.taskview_status_open))));
                fabMenuButtonOpenCloseTask.setImageDrawable(ResourcesCompat.getDrawable(getResources(), R.drawable.task_close, null));
                fabMenuLabelOpenCloseTask.setText(getString(R.string.taskview_fab_close_task));
                if (opencloseAction != null) {
                    opencloseAction.setTitle(R.string.taskview_fab_close_task);
                }
                showProgress();
                kanboardAPI.getTask(task.getId());
            } else
                Snackbar.make(findViewById(R.id.root_layout), getString(R.string.error_msg_open_task), Snackbar.LENGTH_LONG).show();
        }
    };
    private OnCloseTaskListener closeTaskListener = new OnCloseTaskListener() {