public abstract class AsyncTaskEx<Params, Progress, Result> {
    private static final String LOG_TAG = "AsyncTaskEx";

    private static final int CORE_POOL_SIZE = 10;
    private static final int MAXIMUM_POOL_SIZE = 50;
    private static final int KEEP_ALIVE = 10;

    private static final LinkedBlockingQueue<Runnable> sWorkQueue =
            new LinkedBlockingQueue<Runnable>();

    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);

        public Thread newThread(Runnable r) {
            return new Thread(r, "AsyncTaskEx #" + mCount.getAndIncrement());
        }
    };

    private static final ThreadPoolExecutor sExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE,
            MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, sWorkQueue, sThreadFactory);

    private static final int MESSAGE_POST_RESULT = 0x1;
    private static final int MESSAGE_POST_PROGRESS = 0x2;
    private static final int MESSAGE_POST_CANCEL = 0x3;

    private static final InternalHandler sHandler = new InternalHandler();

    private final WorkerRunnable<Params, Result> mWorker;
    private final FutureTask<Result> mFuture;

    private volatile Status mStatus = Status.PENDING;

    /**
     * Indicates the current status of the task. Each status will be set only once
     * during the lifetime of a task.
     */
    public enum Status {
        /**
         * Indicates that the task has not been executed yet.
         */
        PENDING,
        /**
         * Indicates that the task is running.
         */
        RUNNING,
        /**
         * Indicates that {@link AsyncTaskEx#onPostExecute} has finished.
         */
        FINISHED,
    }

    public static void clearQueue() {
        sWorkQueue.clear();
    }

    /**
     * Creates a new asynchronous task. This constructor must be invoked on the UI thread.
     */
    public AsyncTaskEx() {
        mWorker = new WorkerRunnable<Params, Result>() {
            public Result call() throws Exception {
                Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                return doInBackground(mParams);
            }
        };

        mFuture = new FutureTask<Result>(mWorker) {
            @SuppressWarnings("unchecked")
            @Override
            protected void done() {
                Message message;
                Result result = null;

                try {
                    result = get();
                } catch (InterruptedException e) {
                    android.util.Log.w(LOG_TAG, e);
                } catch (ExecutionException e) {
                    throw new RuntimeException("An error occured while executing doInBackground()",
                            e.getCause());
                } catch (CancellationException e) {
                    message = sHandler.obtainMessage(MESSAGE_POST_CANCEL,
                            new AsyncTaskExResult<Result>(AsyncTaskEx.this, (Result[]) null));
                    message.sendToTarget();
                    return;
                } catch (Throwable t) {
                    throw new RuntimeException("An error occured while executing "
                            + "doInBackground()", t);
                }

                message = sHandler.obtainMessage(MESSAGE_POST_RESULT,
                        new AsyncTaskExResult<Result>(AsyncTaskEx.this, result));
                message.sendToTarget();
            }
        };
    }

    /**
     * Returns the current status of this task.
     *
     * @return The current status.
     */
    public final Status getStatus() {
        return mStatus;
    }

    /**
     * Override this method to perform a computation on a background thread. The
     * specified parameters are the parameters passed to {@link #execute}
     * by the caller of this task.
     * <p/>
     * This method can call {@link #publishProgress} to publish updates
     * on the UI thread.
     *
     * @param params The parameters of the task.
     * @return A result, defined by the subclass of this task.
     * @see #onPreExecute()
     * @see #onPostExecute
     * @see #publishProgress
     */
    protected abstract Result doInBackground(Params... params);

    /**
     * Runs on the UI thread before {@link #doInBackground}.
     *
     * @see #onPostExecute
     * @see #doInBackground
     */
    protected void onPreExecute() {
    }

    /**
     * Runs on the UI thread after {@link #doInBackground}. The
     * specified result is the value returned by {@link #doInBackground}
     * or null if the task was cancelled or an exception occured.
     *
     * @param result The result of the operation computed by {@link #doInBackground}.
     * @see #onPreExecute
     * @see #doInBackground
     */
    protected void onPostExecute(Result result) {
    }

    /**
     * Runs on the UI thread after {@link #publishProgress} is invoked.
     * The specified values are the values passed to {@link #publishProgress}.
     *
     * @param values The values indicating progress.
     * @see #publishProgress
     * @see #doInBackground
     */
    protected void onProgressUpdate(Progress... values) {
    }

    /**
     * Runs on the UI thread after {@link #cancel(boolean)} is invoked.
     *
     * @see #cancel(boolean)
     * @see #isCancelled()
     */
    protected void onCancelled() {
    }

    /**
     * Returns <tt>true</tt> if this task was cancelled before it completed
     * normally.
     *
     * @return <tt>true</tt> if task was cancelled before it completed
     * @see #cancel(boolean)
     */
    public final boolean isCancelled() {
        return mFuture.isCancelled();
    }

    /**
     * Attempts to cancel execution of this task.  This attempt will
     * fail if the task has already completed, already been cancelled,
     * or could not be cancelled for some other reason. If successful,
     * and this task has not started when <tt>cancel</tt> is called,
     * this task should never run.  If the task has already started,
     * then the <tt>mayInterruptIfRunning</tt> parameter determines
     * whether the thread executing this task should be interrupted in
     * an attempt to stop the task.
     *
     * @param mayInterruptIfRunning <tt>true</tt> if the thread executing this
     *                              task should be interrupted; otherwise, in-progress tasks are allowed
     *                              to complete.
     * @return <tt>false</tt> if the task could not be cancelled,
     * typically because it has already completed normally;
     * <tt>true</tt> otherwise
     * @see #isCancelled()
     * @see #onCancelled()
     */
    public final boolean cancel(boolean mayInterruptIfRunning) {
        return mFuture.cancel(mayInterruptIfRunning);
    }

    /**
     * Waits if necessary for the computation to complete, and then
     * retrieves its result.
     *
     * @return The computed result.
     * @throws CancellationException If the computation was cancelled.
     * @throws ExecutionException    If the computation threw an exception.
     * @throws InterruptedException  If the current thread was interrupted
     *                               while waiting.
     */
    public final Result get() throws InterruptedException, ExecutionException {
        return mFuture.get();
    }

    /**
     * Waits if necessary for at most the given time for the computation
     * to complete, and then retrieves its result.
     *
     * @param timeout Time to wait before cancelling the operation.
     * @param unit    The time unit for the timeout.
     * @return The computed result.
     * @throws CancellationException If the computation was cancelled.
     * @throws ExecutionException    If the computation threw an exception.
     * @throws InterruptedException  If the current thread was interrupted
     *                               while waiting.
     * @throws TimeoutException      If the wait timed out.
     */
    public final Result get(long timeout, TimeUnit unit) throws InterruptedException,
            ExecutionException, TimeoutException {
        return mFuture.get(timeout, unit);
    }

    /**
     * Executes the task with the specified parameters. The task returns
     * itself (this) so that the caller can keep a reference to it.
     * <p/>
     * This method must be invoked on the UI thread.
     *
     * @param params The parameters of the task.
     * @return This instance of AsyncTaskEx.
     * @throws IllegalStateException If {@link #getStatus()} returns either
     *                               {@link AsyncTaskEx.Status#RUNNING} or {@link AsyncTaskEx.Status#FINISHED}.
     */
    public final AsyncTaskEx<Params, Progress, Result> execute(Params... params) {
        if (mStatus != Status.PENDING) {
            switch (mStatus) {
                case RUNNING:
                    throw new IllegalStateException("Cannot execute task:"
                            + " the task is already running.");
                case FINISHED:
                    throw new IllegalStateException("Cannot execute task:"
                            + " the task has already been executed "
                            + "(a task can be executed only once)");
            }
        }

        mStatus = Status.RUNNING;

        onPreExecute();

        mWorker.mParams = params;
        sExecutor.execute(mFuture);

        return this;
    }

    /**
     * This method can be invoked from {@link #doInBackground} to
     * publish updates on the UI thread while the background computation is
     * still running. Each call to this method will trigger the execution of
     * {@link #onProgressUpdate} on the UI thread.
     *
     * @param values The progress values to update the UI with.
     * @see #onProgressUpdate
     * @see #doInBackground
     */
    protected final void publishProgress(Progress... values) {
        sHandler.obtainMessage(MESSAGE_POST_PROGRESS,
                new AsyncTaskExResult<Progress>(this, values)).sendToTarget();
    }

    private void finish(Result result) {
        onPostExecute(result);
        mStatus = Status.FINISHED;
    }

    private static class InternalHandler extends Handler {
        public InternalHandler() {
            super(Looper.getMainLooper());
        }

        @SuppressWarnings({"rawtypes", "unchecked"})
        @Override
        public void handleMessage(Message msg) {
            AsyncTaskExResult result = (AsyncTaskExResult) msg.obj;
            switch (msg.what) {
                case MESSAGE_POST_RESULT:
                    // There is only one result
                    result.mTask.finish(result.mData[0]);
                    break;
                case MESSAGE_POST_PROGRESS:
                    result.mTask.onProgressUpdate(result.mData);
                    break;
                case MESSAGE_POST_CANCEL:
                    result.mTask.onCancelled();
                    break;
            }
        }
    }

    private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> {
        Params[] mParams;
    }

    private static class AsyncTaskExResult<Data> {
        @SuppressWarnings("rawtypes")
        final AsyncTaskEx mTask;
        final Data[] mData;

        @SuppressWarnings("rawtypes")
        AsyncTaskExResult(AsyncTaskEx task, Data... data) {
            mTask = task;
            mData = data;
        }
    }
} public class ColorScheme {
    private static ColorScheme instance = new ColorScheme();

    public static ColorScheme getInstance() {
        return instance;
    }

    public static String getUserTheme() {
        String theme = PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext()).getString("theme", ColorScheme.defaultTheme());
        if(theme.equals("auto"))
            return getSystemDarkMode() ? "midnight" : "dawn";
        return theme;
    }

    public static int getIRCColor(int color, boolean background) {
        String s = ColorFormatter.COLOR_MAP[color];

        if(getInstance().isDarkTheme && !background) {
            if(ColorFormatter.DARK_FG_SUBSTITUTIONS.containsKey(s))
                s = ColorFormatter.DARK_FG_SUBSTITUTIONS.get(s);
        }

        return 0xff000000 + Integer.parseInt(s, 16);
    }

    public static int getTheme(String theme, boolean actionbar) {
        switch(theme) {
            case "dawn":
                return actionbar?R.style.dawn:R.style.dawnNoActionBar;
            case "dusk":
                return actionbar?R.style.dusk:R.style.duskNoActionBar;
            case "tropic":
                return actionbar?R.style.tropic:R.style.tropicNoActionBar;
            case "emerald":
                return actionbar?R.style.emerald:R.style.emeraldNoActionBar;
            case "sand":
                return actionbar?R.style.sand:R.style.sandNoActionBar;
            case "rust":
                return actionbar?R.style.rust:R.style.rustNoActionBar;
            case "orchid":
                return actionbar?R.style.orchid:R.style.orchidNoActionBar;
            case "ash":
                return actionbar?R.style.ash:R.style.ashNoActionBar;
            case "midnight":
                return actionbar?R.style.midnight:R.style.midnightNoActionBar;
            default:
                return getSystemDarkMode()?(actionbar?R.style.midnight:R.style.midnightNoActionBar):(actionbar?R.style.dawn:R.style.dawnNoActionBar);
        }
    }

    public static boolean getSystemDarkMode() {
        if(Build.VERSION.SDK_INT < 29) {
            PowerManager pm = (PowerManager)IRCCloudApplication.getInstance().getApplicationContext().getSystemService(Context.POWER_SERVICE);
            return pm.isPowerSaveMode();
        } else {
            int currentNightMode = IRCCloudApplication.getInstance().getApplicationContext().getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
            return currentNightMode == Configuration.UI_MODE_NIGHT_YES;
        }
    }

    public static String defaultTheme() {
        return Build.VERSION.SDK_INT >= 29 ? "auto" : "dawn";
    }

    public static int getDialogTheme(String theme) {
        switch(theme) {
            case "dawn":
                return R.style.dawnDialog;
            case "dusk":
                return R.style.duskDialog;
            case "tropic":
                return R.style.tropicDialog;
            case "emerald":
                return R.style.emeraldDialog;
            case "sand":
                return R.style.sandDialog;
            case "rust":
                return R.style.rustDialog;
            case "orchid":
                return R.style.orchidDialog;
            case "ash":
                return R.style.ashDialog;
            case "midnight":
                return R.style.midnightDialog;
            default:
                return getSystemDarkMode()?R.style.midnightDialog:R.style.dawnDialog;
        }
    }

    public static int getDialogWhenLargeTheme(String theme) {
        switch(theme) {
            case "dawn":
                return R.style.dawnDialogWhenLarge;
            case "dusk":
                return R.style.duskDialogWhenLarge;
            case "tropic":
                return R.style.tropicDialogWhenLarge;
            case "emerald":
                return R.style.emeraldDialogWhenLarge;
            case "sand":
                return R.style.sandDialogWhenLarge;
            case "rust":
                return R.style.rustDialogWhenLarge;
            case "orchid":
                return R.style.orchidDialogWhenLarge;
            case "ash":
                return R.style.ashDialogWhenLarge;
            case "midnight":
                return R.style.midnightDialogWhenLarge;
            default:
                return getSystemDarkMode()?R.style.midnightDialogWhenLarge:R.style.dawnDialogWhenLarge;
        }
    }

    private static String[] light_nick_colors = {
            "b22222",
            "d2691e",
            "ff9166",
            "fa8072",
            "ff8c00",
            "228b22",
            "808000",
            "b7b05d",
            "8ebd2e",
            "2ebd2e",
            "82b482",
            "37a467",
            "57c8a1",
            "1da199",
            "579193",
            "008b8b",
            "00bfff",
            "4682b4",
            "1e90ff",
            "4169e1",
            "6a5acd",
            "7b68ee",
            "9400d3",
            "8b008b",
            "ba55d3",
            "ff00ff",
            "ff1493"
    };

    private static String dark_nick_colors[] = {
            "deb887",
            "ffd700",
            "ff9166",
            "fa8072",
            "ff8c00",
            "00ff00",
            "ffff00",
            "bdb76b",
            "9acd32",
            "32cd32",
            "8fbc8f",
            "3cb371",
            "66cdaa",
            "20b2aa",
            "40e0d0",
            "00ffff",
            "00bfff",
            "87ceeb",
            "339cff",
            "6495ed",
            "b2a9e5",
            "ff69b4",
            "da70d6",
            "ee82ee",
            "d68fff",
            "ff00ff",
            "ffb6c1"
    };

    public static String colorForNick(String nick, boolean isDarkTheme) {
        String colors[] = isDarkTheme ? dark_nick_colors : light_nick_colors;
        // Normalise a bit
        // typically ` and _ are used on the end alone
        String normalizedNick = nick.toLowerCase().replaceAll("[`_]+$", "");
        //remove |<anything> from the end
        normalizedNick = normalizedNick.replaceAll("\\|.*$", "");

        Double hash = 0.0;

        for (int i = 0; i < normalizedNick.length(); i++) {
            hash = ((int) normalizedNick.charAt(i)) + (double) ((int) (hash.longValue()) << 6) + (double) ((int) (hash.longValue()) << 16) - hash;
        }

        return colors[(int) Math.abs(hash.longValue() % colors.length)];
    }

    public void setThemeFromContext(Context ctx, String theme_name) {
        theme = theme_name;
        contentBackgroundColor = colorForAttribute(ctx, R.attr.contentBackgroundColor);
        messageTextColor = colorForAttribute(ctx, R.attr.messageTextColor);
        opersGroupColor = colorForAttribute(ctx, R.attr.opersGroupColor);
        ownersGroupColor = colorForAttribute(ctx, R.attr.ownersGroupColor);
        adminsGroupColor = colorForAttribute(ctx, R.attr.adminsGroupColor);
        opsGroupColor = colorForAttribute(ctx, R.attr.opsGroupColor);
        halfopsGroupColor = colorForAttribute(ctx, R.attr.halfopsGroupColor);
        voicedGroupColor = colorForAttribute(ctx, R.attr.voicedGroupColor);
        membersGroupColor = colorForAttribute(ctx, R.attr.membersGroupColor);
        opersHeadingColor = colorForAttribute(ctx, R.attr.opersHeadingColor);
        ownersHeadingColor = colorForAttribute(ctx, R.attr.ownersHeadingColor);
        adminsHeadingColor = colorForAttribute(ctx, R.attr.adminsHeadingColor);
        opsHeadingColor = colorForAttribute(ctx, R.attr.opsHeadingColor);
        halfopsHeadingColor = colorForAttribute(ctx, R.attr.halfopsHeadingColor);
        voicedHeadingColor = colorForAttribute(ctx, R.attr.voicedHeadingColor);
        membersHeadingColor = colorForAttribute(ctx, R.attr.membersHeadingColor);
        memberListTextColor = colorForAttribute(ctx, R.attr.memberListTextColor);
        memberListAwayTextColor = colorForAttribute(ctx, R.attr.memberListAwayTextColor);
        timestampColor = colorForAttribute(ctx, R.attr.timestampColor);
        darkBlueColor = colorForAttribute(ctx, R.attr.darkBlueColor);
        networkErrorBackgroundColor = colorForAttribute(ctx, R.attr.networkErrorBackgroundColor);
        networkErrorColor = colorForAttribute(ctx, R.attr.networkErrorColor);
        errorBackgroundColor = colorForAttribute(ctx, R.attr.errorBackgroundColor);
        statusBackgroundColor = colorForAttribute(ctx, R.attr.statusBackgroundColor);
        selfBackgroundColor = colorForAttribute(ctx, R.attr.selfBackgroundColor);
        highlightBackgroundColor = colorForAttribute(ctx, R.attr.highlightBackgroundColor);
        highlightTimestampColor = colorForAttribute(ctx, R.attr.highlightTimestampColor);
        noticeBackgroundColor = colorForAttribute(ctx, R.attr.noticeBackgroundColor);
        timestampBackgroundColor = colorForAttribute(ctx, R.attr.timestampBackgroundColor);
        newMsgsBackgroundColor = colorForAttribute(ctx, R.attr.newMsgsBackgroundColor);
        collapsedRowTextColor = colorForAttribute(ctx, R.attr.collapsedRowTextColor);
        collapsedRowNickColor = colorForAttribute(ctx, R.attr.collapsedRowNickColor);
        collapsedHeadingBackgroundColor = colorForAttribute(ctx, R.attr.collapsedHeadingBackgroundColor);
        navBarColor = colorForAttribute(ctx, R.attr.navBarColor);
        navBarHeadingColor = colorForAttribute(ctx, R.attr.navBarHeadingColor);
        navBarSubheadingColor = colorForAttribute(ctx, R.attr.navBarSubheadingColor);
        navBarBorderColor = colorForAttribute(ctx, R.attr.navBarBorderColor);
        textareaTextColor = colorForAttribute(ctx, R.attr.textareaTextColor);
        textareaBackgroundColor = colorForAttribute(ctx, R.attr.textareaBackgroundColor);
        linkColor = colorForAttribute(ctx, R.attr.linkColor);
        lightLinkColor = colorForAttribute(ctx, R.attr.lightLinkColor);
        serverBackgroundColor = colorForAttribute(ctx, R.attr.serverBackgroundColor);
        bufferBackgroundColor = colorForAttribute(ctx, R.attr.bufferBackgroundColor);
        unreadBorderColor = colorForAttribute(ctx, R.attr.unreadBorderColor);
        highlightBorderColor = colorForAttribute(ctx, R.attr.highlightBorderColor);
        networkErrorBorderColor = colorForAttribute(ctx, R.attr.networkErrorBorderColor);
        bufferTextColor = colorForAttribute(ctx, R.attr.bufferTextColor);
        inactiveBufferTextColor = colorForAttribute(ctx, R.attr.inactiveBufferTextColor);
        unreadBufferTextColor = colorForAttribute(ctx, R.attr.unreadBufferTextColor);
        selectedBufferTextColor = colorForAttribute(ctx, R.attr.selectedBufferTextColor);
        selectedBufferBackgroundColor = colorForAttribute(ctx, R.attr.selectedBufferBackgroundColor);
        bufferBorderColor = colorForAttribute(ctx, R.attr.bufferBorderColor);
        selectedBufferBorderColor = colorForAttribute(ctx, R.attr.selectedBufferBorderColor);
        backlogDividerColor = colorForAttribute(ctx, R.attr.backlogDividerColor);
        chatterBarTextColor = colorForAttribute(ctx, R.attr.chatterBarTextColor);
        chatterBarColor = colorForAttribute(ctx, R.attr.chatterBarColor);
        awayBarTextColor = colorForAttribute(ctx, R.attr.awayBarTextColor);
        awayBarColor = colorForAttribute(ctx, R.attr.awayBarColor);
        connectionBarTextColor = colorForAttribute(ctx, R.attr.connectionBarTextColor);
        connectionBarColor = colorForAttribute(ctx, R.attr.connectionBarColor);
        placeholderColor = colorForAttribute(ctx, R.attr.placeholderColor);
        unreadBlueColor = colorForAttribute(ctx, R.attr.unreadBlueColor);
        serverBorderColor = colorForAttribute(ctx, R.attr.serverBorderColor);
        failedServerBorderColor = colorForAttribute(ctx, R.attr.failedServerBorderColor);
        archivesHeadingTextColor = colorForAttribute(ctx, R.attr.archivesHeadingTextColor);
        archivedChannelTextColor = colorForAttribute(ctx, R.attr.archivedChannelTextColor);
        archivedBufferTextColor = colorForAttribute(ctx, R.attr.archivedBufferTextColor);
        selectedArchivesHeadingColor = colorForAttribute(ctx, R.attr.selectedArchivesHeadingColor);
        timestampTopBorderColor = colorForAttribute(ctx, R.attr.timestampTopBorderColor);
        timestampBottomBorderColor = colorForAttribute(ctx, R.attr.timestampBottomBorderColor);
        expandCollapseIndicatorColor = colorForAttribute(ctx, R.attr.expandCollapseIndicatorColor);
        bufferHighlightColor = colorForAttribute(ctx, R.attr.bufferHighlightColor);
        selectedBufferHighlightColor = colorForAttribute(ctx, R.attr.selectedBufferHighlightColor);
        archivedBufferHighlightColor = colorForAttribute(ctx, R.attr.archivedBufferHighlightColor);
        selectedArchivedBufferHighlightColor = colorForAttribute(ctx, R.attr.selectedArchivedBufferHighlightColor);
        selectedArchivedBufferBackgroundColor = colorForAttribute(ctx, R.attr.selectedArchivedBufferBackgroundColor);
        contentBorderColor = colorForAttribute(ctx, R.attr.contentBorderColor);
        bufferBorderDrawable = resourceForAttribute(ctx, R.attr.bufferBorderDrawable);
        serverBorderDrawable = resourceForAttribute(ctx, R.attr.serverBorderDrawable);
        selectedBorderDrawable = resourceForAttribute(ctx, R.attr.selectedBorderDrawable);
        collapsedBorderDrawable = resourceForAttribute(ctx, R.attr.collapsedBorderDrawable);
        bufferBackgroundDrawable = resourceForAttribute(ctx, R.attr.bufferBackgroundDrawable);
        serverBackgroundDrawable = resourceForAttribute(ctx, R.attr.serverBackgroundDrawable);
        selectedBackgroundDrawable = resourceForAttribute(ctx, R.attr.selectedBackgroundDrawable);
        lastSeenEIDBackgroundDrawable = resourceForAttribute(ctx, R.attr.lastSeenEIDBackgroundDrawable);
        socketclosedBackgroundDrawable = resourceForAttribute(ctx, R.attr.socketclosedBackgroundDrawable);
        timestampBackgroundDrawable = resourceForAttribute(ctx, R.attr.timestampBackgroundDrawable);
        actionBarDrawable = resourceForAttribute(ctx, R.attr.actionbarDrawable);
        colorControlNormal = colorForAttribute(ctx, R.attr.colorControlNormal);
        dialogBackgroundColor = colorForAttribute(ctx, R.attr.dialogBackgroundColor);
        windowBackgroundDrawable = resourceForAttribute(ctx, R.attr.windowBackgroundDrawable);
        row_opers_bg_drawable = resourceForAttribute(ctx, R.attr.row_opers_bg_drawable);
        row_owners_bg_drawable = resourceForAttribute(ctx, R.attr.row_owners_bg_drawable);
        row_admins_bg_drawable = resourceForAttribute(ctx, R.attr.row_admins_bg_drawable);
        row_ops_bg_drawable = resourceForAttribute(ctx, R.attr.row_ops_bg_drawable);
        row_halfops_bg_drawable = resourceForAttribute(ctx, R.attr.row_halfops_bg_drawable);
        row_voiced_bg_drawable = resourceForAttribute(ctx, R.attr.row_voiced_bg_drawable);
        row_members_bg_drawable = resourceForAttribute(ctx, R.attr.row_members_bg_drawable);
        codeSpanForegroundColor = colorForAttribute(ctx, R.attr.codeSpanForegroundColor);
        codeSpanBackgroundColor = colorForAttribute(ctx, R.attr.codeSpanBackgroundColor);
        statusBarColor = colorForAttribute(ctx, android.R.attr.statusBarColor);
        isDarkTheme = !theme.equals("dawn");
        selfTextColor = isDarkTheme?"ffffff":"142b43";
    }

    private int colorForAttribute(Context ctx, int attribute) {
        TypedValue v = new TypedValue();
        ctx.getTheme().resolveAttribute(attribute, v, true);
        return v.data;
    }

    private int resourceForAttribute(Context ctx, int attribute) {
        TypedValue v = new TypedValue();
        ctx.getTheme().resolveAttribute(attribute, v, true);
        return v.resourceId;
    }

    public int contentBackgroundColor;
    public int messageTextColor;
    public int opersGroupColor;
    public int ownersGroupColor;
    public int adminsGroupColor;
    public int opsGroupColor;
    public int halfopsGroupColor;
    public int voicedGroupColor;
    public int membersGroupColor;
    public int opersHeadingColor;
    public int ownersHeadingColor;
    public int adminsHeadingColor;
    public int opsHeadingColor;
    public int halfopsHeadingColor;
    public int voicedHeadingColor;
    public int membersHeadingColor;
    public int memberListTextColor;
    public int memberListAwayTextColor;
    public int timestampColor;
    public int darkBlueColor;
    public int networkErrorBackgroundColor;
    public int networkErrorColor;
    public int errorBackgroundColor;
    public int statusBackgroundColor;
    public int selfBackgroundColor;
    public int highlightBackgroundColor;
    public int highlightTimestampColor;
    public int noticeBackgroundColor;
    public int timestampBackgroundColor;
    public int newMsgsBackgroundColor;
    public int collapsedRowTextColor;
    public int collapsedRowNickColor;
    public int collapsedHeadingBackgroundColor;
    public int navBarColor;
    public int navBarHeadingColor;
    public int navBarSubheadingColor;
    public int navBarBorderColor;
    public int textareaTextColor;
    public int textareaBackgroundColor;
    public int linkColor;
    public int lightLinkColor;
    public int serverBackgroundColor;
    public int bufferBackgroundColor;
    public int unreadBorderColor;
    public int highlightBorderColor;
    public int networkErrorBorderColor;
    public int bufferTextColor;
    public int inactiveBufferTextColor;
    public int unreadBufferTextColor;
    public int selectedBufferTextColor;
    public int selectedBufferBackgroundColor;
    public int bufferBorderColor;
    public int selectedBufferBorderColor;
    public int backlogDividerColor;
    public int chatterBarTextColor;
    public int chatterBarColor;
    public int awayBarTextColor;
    public int awayBarColor;
    public int connectionBarTextColor;
    public int connectionBarColor;
    public int placeholderColor;
    public int unreadBlueColor;
    public int serverBorderColor;
    public int failedServerBorderColor;
    public int archivesHeadingTextColor;
    public int archivedChannelTextColor;
    public int archivedBufferTextColor;
    public int selectedArchivesHeadingColor;
    public int timestampTopBorderColor;
    public int timestampBottomBorderColor;
    public int expandCollapseIndicatorColor;
    public int bufferHighlightColor;
    public int selectedBufferHighlightColor;
    public int archivedBufferHighlightColor;
    public int selectedArchivedBufferHighlightColor;
    public int selectedArchivedBufferBackgroundColor;
    public int contentBorderColor;
    public int bufferBorderDrawable;
    public int serverBorderDrawable;
    public int selectedBorderDrawable;
    public int collapsedBorderDrawable;
    public int bufferBackgroundDrawable;
    public int serverBackgroundDrawable;
    public int selectedBackgroundDrawable;
    public int lastSeenEIDBackgroundDrawable;
    public int socketclosedBackgroundDrawable;
    public int timestampBackgroundDrawable;
    public int colorControlNormal;
    public int actionBarDrawable;
    public int dialogBackgroundColor;
    public int windowBackgroundDrawable;
    public int statusBarColor;
    public int row_opers_bg_drawable;
    public int row_owners_bg_drawable;
    public int row_admins_bg_drawable;
    public int row_ops_bg_drawable;
    public int row_halfops_bg_drawable;
    public int row_voiced_bg_drawable;
    public int row_members_bg_drawable;
    public String theme;
    public boolean isDarkTheme;
    public String selfTextColor;
    public int codeSpanForegroundColor;
    public int codeSpanBackgroundColor;
} public class IRCCloudJSONObject {
    private JsonNode o;
    private int cid = -1;
    private int bid = -1;
    private long eid = -1;
    private String type = null;

    public IRCCloudJSONObject() {
        o = new ObjectMapper().createObjectNode();
    }

    public IRCCloudJSONObject(JsonNode object) {
        o = object;
    }

    public IRCCloudJSONObject(String message) {
        try {
            ObjectMapper mapper = new ObjectMapper();
            o = mapper.readValue(message, JsonNode.class);
        } catch (IOException e) {
            NetworkConnection.printStackTraceToCrashlytics(e);
        }
    }

    public IRCCloudJSONObject(JSONObject object) {
        try {
            ObjectMapper mapper = new ObjectMapper();
            o = mapper.readValue(object.toString(), JsonNode.class);
        } catch (IOException e) {
            NetworkConnection.printStackTraceToCrashlytics(e);
        }
    }

    public int cid() {
        if (cid == -1)
            cid = getInt("cid");
        return cid;
    }

    public int bid() {
        if (bid == -1)
            bid = getInt("bid");
        return bid;
    }

    public long eid() {
        if (eid == -1)
            eid = getLong("eid");
        return eid;
    }

    public void setEid(long e) {
        eid = e;
    }

    public String type() {
        if (type == null) {
            if (o.has("type"))
                type = o.get("type").asText();
            else
                type = "undefined";
        }
        return type;
    }

    public boolean has(String name) {
        return o.has(name) && !o.get(name).isNull();
    }

    public boolean getBoolean(String name) {
        JsonNode node = o.get(name);
        if(node != null)
            return node.asBoolean(false);
        else
            return false;
    }

    public int getInt(String name) {
        JsonNode node = o.get(name);
        if(node != null)
            return node.asInt(-1);
        else
            return -1;
    }

    public long getLong(String name) {
        JsonNode node = o.get(name);
        if(node != null)
            return node.asLong(-1);
        else
            return -1;
    }

    public String getString(String name) {
        JsonNode node = o.get(name);
        if(node == null || node.isNull())
            return null;
        else
            return node.asText();
    }

    public JsonNode getJsonNode(String name) {
        return o.get(name);
    }

    public ObjectNode getJsonObject(String name) {
        try {
            return (ObjectNode) getJsonNode(name);
        } catch (ClassCastException e) {
            return null;
        }
    }

    public JsonNode getObject() {
        return o;
    }

    public String toString() {
        ObjectMapper mapper = new ObjectMapper();
        StringWriter writer = new StringWriter();
        try {
            mapper.writeValue(writer, o);
        } catch (IOException e) {
            NetworkConnection.printStackTraceToCrashlytics(e);
        }
        return writer.toString();
    }

    public void remove(String key) {
        if(o instanceof ObjectNode) {
            ((ObjectNode)o).remove(key);
        }
    }
} public class NetworkConnection {
    private static final String TAG = "IRCCloud";
    private static NetworkConnection instance = null;

    private static final ServersList mServers = ServersList.getInstance();
    private static final BuffersList mBuffers = BuffersList.getInstance();
    private static final ChannelsList mChannels = ChannelsList.getInstance();
    private static final UsersList mUsers = UsersList.getInstance();
    private static final EventsList mEvents = EventsList.getInstance();
    private static final RecentConversationsList mRecentConversations = RecentConversationsList.getInstance();

    public static final int STATE_DISCONNECTED = 0;
    public static final int STATE_CONNECTING = 1;
    public static final int STATE_CONNECTED = 2;
    public static final int STATE_DISCONNECTING = 3;
    private int state = STATE_DISCONNECTED;
    private long highest_eid = -1;

    public interface IRCEventHandler {
        void onIRCEvent(int message, Object object);
    }

    public interface IRCResultCallback {
        void onIRCResult(IRCCloudJSONObject result);
    }

    private WebSocketClient client = null;
    private UserInfo userInfo = null;
    private final ArrayList<IRCEventHandler> handlers = new ArrayList<IRCEventHandler>();
    private final HashMap<Integer, IRCResultCallback> resultCallbacks = new HashMap<>();
    public String session = null;
    private volatile int last_reqid = 0;
    private static final Timer idleTimer = new Timer("websocket-idle-timer");
    //private static final Timer saveTimer = new Timer("backlog-save-timer");
    private TimerTask idleTimerTask = null;
    private TimerTask saveTimerTask = null;
    private TimerTask disconnectSockerTimerTask = null;
    public long idle_interval = 1000;
    private volatile int failCount = 0;
    private long reconnect_timestamp = 0;
    public String useragent = null;
    private String streamId = null;
    private int accrued = 0;
    int currentBid = -1;
    long firstEid = -1;
    public JSONObject config = null;
    public boolean notifier;
    public static String file_uri_template;
    public static String pastebin_uri_template;
    public static String avatar_uri_template;
    public static String avatar_redirect_uri_template;

    private ObjectMapper mapper = new ObjectMapper();

    public static final int EVENT_CONNECTIVITY = 0;
    public static final int EVENT_USERINFO = 1;
    public static final int EVENT_MAKESERVER = 2;
    public static final int EVENT_MAKEBUFFER = 3;
    public static final int EVENT_DELETEBUFFER = 4;
    public static final int EVENT_BUFFERMSG = 5;
    public static final int EVENT_HEARTBEATECHO = 6;
    public static final int EVENT_CHANNELINIT = 7;
    public static final int EVENT_CHANNELTOPIC = 8;
    public static final int EVENT_JOIN = 9;
    public static final int EVENT_PART = 10;
    public static final int EVENT_NICKCHANGE = 11;
    public static final int EVENT_QUIT = 12;
    public static final int EVENT_MEMBERUPDATES = 13;
    public static final int EVENT_USERCHANNELMODE = 14;
    public static final int EVENT_BUFFERARCHIVED = 15;
    public static final int EVENT_BUFFERUNARCHIVED = 16;
    public static final int EVENT_RENAMECONVERSATION = 17;
    public static final int EVENT_STATUSCHANGED = 18;
    public static final int EVENT_CONNECTIONDELETED = 19;
    public static final int EVENT_AWAY = 20;
    public static final int EVENT_SELFBACK = 21;
    public static final int EVENT_KICK = 22;
    public static final int EVENT_CHANNELMODE = 23;
    public static final int EVENT_CHANNELTIMESTAMP = 24;
    public static final int EVENT_SELFDETAILS = 25;
    public static final int EVENT_USERMODE = 26;
    public static final int EVENT_SETIGNORES = 27;
    public static final int EVENT_BADCHANNELKEY = 28;
    public static final int EVENT_OPENBUFFER = 29;
    public static final int EVENT_BANLIST = 31;
    public static final int EVENT_WHOLIST = 32;
    public static final int EVENT_WHOIS = 33;
    public static final int EVENT_LINKCHANNEL = 34;
    public static final int EVENT_LISTRESPONSEFETCHING = 35;
    public static final int EVENT_LISTRESPONSE = 36;
    public static final int EVENT_LISTRESPONSETOOMANY = 37;
    public static final int EVENT_CONNECTIONLAG = 38;
    public static final int EVENT_GLOBALMSG = 39;
    public static final int EVENT_ACCEPTLIST = 40;
    public static final int EVENT_NAMESLIST = 41;
    public static final int EVENT_REORDERCONNECTIONS = 42;
    public static final int EVENT_CHANNELTOPICIS = 43;
    public static final int EVENT_SERVERMAPLIST = 44;
    public static final int EVENT_QUIETLIST = 45;
    public static final int EVENT_BANEXCEPTIONLIST = 46;
    public static final int EVENT_INVITELIST = 47;
    public static final int EVENT_CHANNELQUERY = 48;
    public static final int EVENT_WHOSPECIALRESPONSE = 49;
    public static final int EVENT_MODULESLIST = 50;
    public static final int EVENT_LINKSRESPONSE = 51;
    public static final int EVENT_WHOWAS = 52;
    public static final int EVENT_TRACERESPONSE = 53;
    public static final int EVENT_LOGEXPORTFINISHED = 54;
    public static final int EVENT_DISPLAYNAMECHANGE = 55;
    public static final int EVENT_AVATARCHANGE = 56;
    public static final int EVENT_MESSAGECHANGE = 57;
    public static final int EVENT_WATCHSTATUS = 58;
    public static final int EVENT_TEXTLIST = 59;
    public static final int EVENT_CHANFILTERLIST = 60;

    public static final int EVENT_BACKLOG_START = 100;
    public static final int EVENT_BACKLOG_END = 101;
    public static final int EVENT_BACKLOG_FAILED = 102;
    public static final int EVENT_AUTH_FAILED = 103;
    public static final int EVENT_TEMP_UNAVAILABLE = 104;
    public static final int EVENT_PROGRESS = 105;
    public static final int EVENT_ALERT = 106;
    public static final int EVENT_CACHE_START = 107;
    public static final int EVENT_CACHE_END = 108;
    public static final int EVENT_OOB_START = 109;
    public static final int EVENT_OOB_END = 110;
    public static final int EVENT_OOB_FAILED = 111;
    public static final int EVENT_FONT_DOWNLOADED = 112;

    public static final int EVENT_DEBUG = 999;

    public static String IRCCLOUD_HOST = BuildConfig.HOST;
    public static String IRCCLOUD_PATH = "/";

    public final Object parserLock = new Object();
    private WifiManager.WifiLock wifiLock = null;

    public long clockOffset = 0;

    private float numbuffers = 0;
    private float totalbuffers = 0;
    private int currentcount = 0;

    public boolean ready = false;
    public String globalMsg = null;

    private HashMap<Integer, OOBFetcher> oobTasks = new HashMap<Integer, OOBFetcher>();
    private ArrayList<IRCCloudJSONObject> pendingEdits = new ArrayList<>();

    public synchronized static NetworkConnection getInstance() {
        if (instance == null) {
            instance = new NetworkConnection();
        }
        return instance;
    }

    BroadcastReceiver connectivityListener = new BroadcastReceiver() {

        @Override
        public void onReceive(Context context, Intent intent) {
            try {
                ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
                NetworkInfo ni = cm.getActiveNetworkInfo();

                if (intent.hasExtra("networkInfo") && ((NetworkInfo) intent.getParcelableExtra("networkInfo")).isConnected()) {
                    if (intent.getIntExtra("networkType", 0) == ConnectivityManager.TYPE_VPN) {
                        if (state == STATE_CONNECTED || state == STATE_CONNECTING) {
                            IRCCloudLog.Log(Log.INFO, TAG, "A VPN has connected, reconnecting websocket");
                            cancel_idle_timer();
                            reconnect_timestamp = 0;
                            try {
                                state = STATE_DISCONNECTING;
                                client.disconnect();
                                state = STATE_DISCONNECTED;
                                notifyHandlers(EVENT_CONNECTIVITY, null);
                            } catch (Exception e) {
                            }
                        }
                    }
                }

                IRCCloudLog.Log(Log.INFO, TAG, "Connectivity changed, connected: " + ((ni != null) ? ni.isConnected() : "Unknown") + ", connected or connecting: " + ((ni != null) ? ni.isConnectedOrConnecting() : "Unknown"));

                if (ni != null && ni.isConnected() && (state == STATE_DISCONNECTED || state == STATE_DISCONNECTING) && session != null && handlers.size() > 0 && !notifier) {
                    IRCCloudLog.Log(Log.INFO, TAG, "Network became available, reconnecting");
                    if (idleTimerTask != null)
                        idleTimerTask.cancel();
                    connect();
                } else if (ni == null || !ni.isConnected()) {
                    IRCCloudLog.Log(Log.INFO, TAG, "Network lost, disconnecting");
                    cancel_idle_timer();
                    reconnect_timestamp = 0;
                    try {
                        state = STATE_DISCONNECTING;
                        client.disconnect();
                        state = STATE_DISCONNECTED;
                        notifyHandlers(EVENT_CONNECTIVITY, null);
                    } catch (Exception e) {
                    }
                }
            } catch (Exception e) {
                printStackTraceToCrashlytics(e);
                IRCCloudLog.LogException(e);
            }
        }
    };

    private ConnectivityManager.NetworkCallback connectivityCallback;

    private BroadcastReceiver dataSaverListener = new BroadcastReceiver() {
        @TargetApi(24)
        @Override
        public void onReceive(Context context, Intent intent) {
            ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && cm.isActiveNetworkMetered() && cm.getRestrictBackgroundStatus() == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED) {
                IRCCloudLog.Log(Log.INFO, TAG, "Data Saver was enabled");
                if(!isVisible() && state == STATE_CONNECTED) {
                    notifier = false;
                    disconnect();
                }
            } else {
                IRCCloudLog.Log(Log.INFO, TAG, "Data Saver was disabled");
                if(isVisible() && state != STATE_CONNECTED)
                    connect();
            }
        }
    };

    @SuppressWarnings("deprecation")
    public NetworkConnection() {
        session = IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0).getString("session_key", "");
        String version;
        String network_type = null;
        try {
            version = "/" + IRCCloudApplication.getInstance().getPackageManager().getPackageInfo(IRCCloudApplication.getInstance().getApplicationContext().getPackageName(), 0).versionName;
        } catch (Exception e) {
            version = "";
        }

        try {
            ConnectivityManager cm = (ConnectivityManager) IRCCloudApplication.getInstance().getSystemService(Context.CONNECTIVITY_SERVICE);
            NetworkInfo ni = cm.getActiveNetworkInfo();
            if (ni != null)
                network_type = ni.getTypeName();
        } catch (Exception e) {
        }

         connectivityCallback = new ConnectivityManager.NetworkCallback() {
            @Override
            public void onCapabilitiesChanged(Network network, NetworkCapabilities networkCapabilities) {
                super.onCapabilitiesChanged(network, networkCapabilities);

                /*if(networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN) && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
                    if (state == STATE_CONNECTED || state == STATE_CONNECTING) {
                        IRCCloudLog.Log(Log.INFO, TAG, "A VPN has connected, reconnecting websocket");
                        cancel_idle_timer();
                        reconnect_timestamp = 0;
                        try {
                            state = STATE_DISCONNECTING;
                            client.disconnect();
                            state = STATE_DISCONNECTED;
                            notifyHandlers(EVENT_CONNECTIVITY, null);
                        } catch (Exception e) {
                        }
                    }
                }*/

                IRCCloudLog.Log(Log.INFO, TAG, "Connectivity changed, connected: " + networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET));

                if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && (state == STATE_DISCONNECTED || state == STATE_DISCONNECTING) && session != null && handlers.size() > 0 && !notifier) {
                    IRCCloudLog.Log(Log.INFO, TAG, "Network became available, reconnecting");
                    if (idleTimerTask != null)
                        idleTimerTask.cancel();
                    connect();
                }
            }

            @Override
            public void onLost(Network network) {
                super.onLost(network);
                IRCCloudLog.Log(Log.INFO, TAG, "Network lost, disconnecting");
                cancel_idle_timer();
                reconnect_timestamp = 0;
                try {
                    state = STATE_DISCONNECTING;
                    client.disconnect();
                    state = STATE_DISCONNECTED;
                    notifyHandlers(EVENT_CONNECTIVITY, null);
                } catch (Exception e) {
                }
            }
        };

        try {
            config = new JSONObject(PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext()).getString("config", "{}"));
            if(config.has("file_uri_template"))
                file_uri_template = config.getString("file_uri_template");
            else
                file_uri_template = null;

            if(config.has("pastebin_uri_template"))
                pastebin_uri_template = config.getString("pastebin_uri_template");
            else
                pastebin_uri_template = null;

            if(config.has("avatar_uri_template"))
                avatar_uri_template = config.getString("avatar_uri_template");
            else
                avatar_uri_template = null;

            if(config.has("avatar_redirect_uri_template"))
                avatar_redirect_uri_template = config.getString("avatar_redirect_uri_template");
            else
                avatar_redirect_uri_template = null;
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            config = new JSONObject();
        }

        String userinfojson = IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0).getString("userinfo", null);
        if(userinfojson != null)
            userInfo = new UserInfo(new IRCCloudJSONObject(userinfojson));
        else if(BuildConfig.MOCK_DATA)
            userInfo = new UserInfo();

        useragent = "IRCCloud" + version + " (" + android.os.Build.MODEL + "; " + Locale.getDefault().getCountry().toLowerCase() + "; "
                + "Android " + android.os.Build.VERSION.RELEASE;

        WindowManager wm = (WindowManager) IRCCloudApplication.getInstance().getSystemService(Context.WINDOW_SERVICE);
        useragent += "; " + wm.getDefaultDisplay().getWidth() + "x" + wm.getDefaultDisplay().getHeight();

        if (network_type != null)
            useragent += "; " + network_type;

        useragent += ")";

        IRCCloudLog.Log(Log.INFO, "IRCCloud", useragent);

        WifiManager wfm = (WifiManager) IRCCloudApplication.getInstance().getApplicationContext().getSystemService(Context.WIFI_SERVICE);
        wifiLock = wfm.createWifiLock(TAG);
    }

    public int getState() {
        return state;
    }

    public void disconnect() {
        if (client != null) {
            state = STATE_DISCONNECTING;
            client.disconnect();
        } else {
            state = STATE_DISCONNECTED;
        }
        if (idleTimerTask != null)
            idleTimerTask.cancel();
        IRCCloudApplication.getInstance().cancelNotifierTimer();
        try {
            if (wifiLock.isHeld())
                wifiLock.release();
        } catch (RuntimeException e) {

        }
        reconnect_timestamp = 0;
        synchronized (oobTasks) {
            for (Integer bid : oobTasks.keySet()) {
                try {
                    oobTasks.get(bid).cancel();
                } catch (Exception e) {
                }
            }
            oobTasks.clear();
        }
        for (Buffer b : mBuffers.getBuffers()) {
            if (!b.getScrolledUp())
                mEvents.pruneEvents(b.getBid());
        }
    }

    public JSONObject login(String email, String password) {
        try {
            String tokenResponse = fetch(new URL("https://" + IRCCLOUD_HOST + "/chat/auth-formtoken"), "", null, null, null);
            JSONObject token = new JSONObject(tokenResponse);
            if (token.has("token")) {
                String postdata = "email=" + URLEncoder.encode(email, "UTF-8") + "&password=" + URLEncoder.encode(password, "UTF-8") + "&token=" + token.getString("token");
                String response = fetch(new URL("https://" + IRCCLOUD_HOST + "/chat/login"), postdata, null, token.getString("token"), null);
                if (response.length() < 1) {
                    JSONObject o = new JSONObject();
                    o.put("message", "empty_response");
                    return o;
                } else if (response.charAt(0) != '{') {
                    JSONObject o = new JSONObject();
                    o.put("message", "invalid_response");
                    return o;
                }
                return new JSONObject(response);
            } else {
                return null;
            }
        } catch (UnknownHostException e) {
            printStackTraceToCrashlytics(e);
            return null;
        } catch (IOException e) {
            printStackTraceToCrashlytics(e);
            return null;
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            JSONObject o = new JSONObject();
            try {
                o.put("message", "json_error");
            } catch (JSONException e1) {
            }
            return o;
        } catch (Exception e) {
            printStackTraceToCrashlytics(e);
        }
        return null;
    }

    public JSONObject signup(String realname, String email, String password) {
        try {
            String tokenResponse = fetch(new URL("https://" + IRCCLOUD_HOST + "/chat/auth-formtoken"), "", null, null, null);
            JSONObject token = new JSONObject(tokenResponse);
            if (token.has("token")) {
                String postdata = "realname=" + URLEncoder.encode(realname, "UTF-8") + "&email=" + URLEncoder.encode(email, "UTF-8") + "&password=" + URLEncoder.encode(password, "UTF-8") + "&token=" + token.getString("token");
                String response = fetch(new URL("https://" + IRCCLOUD_HOST + "/chat/signup"), postdata, null, token.getString("token"), null);
                if (response.length() < 1) {
                    JSONObject o = new JSONObject();
                    o.put("message", "empty_response");
                    return o;
                } else if (response.charAt(0) != '{') {
                    JSONObject o = new JSONObject();
                    o.put("message", "invalid_response");
                    return o;
                }
                return new JSONObject(response);
            } else {
                return null;
            }
        } catch (UnknownHostException e) {
            printStackTraceToCrashlytics(e);
            return null;
        } catch (IOException e) {
            printStackTraceToCrashlytics(e);
            return null;
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            JSONObject o = new JSONObject();
            try {
                o.put("message", "json_error");
            } catch (JSONException e1) {
            }
            return o;
        } catch (Exception e) {
            printStackTraceToCrashlytics(e);
        }
        return null;
    }

    public JSONObject request_password(String email) {
        try {
            String tokenResponse = fetch(new URL("https://" + IRCCLOUD_HOST + "/chat/auth-formtoken"), "", null, null, null);
            JSONObject token = new JSONObject(tokenResponse);
            if (token.has("token")) {
                String postdata = "email=" + URLEncoder.encode(email, "UTF-8") + "&token=" + token.getString("token") + "&mobile=1";
                String response = fetch(new URL("https://" + IRCCLOUD_HOST + "/chat/request-access-link"), postdata, null, token.getString("token"), null);
                if (response.length() < 1) {
                    JSONObject o = new JSONObject();
                    o.put("message", "empty_response");
                    return o;
                } else if (response.charAt(0) != '{') {
                    JSONObject o = new JSONObject();
                    o.put("message", "invalid_response");
                    return o;
                }
                return new JSONObject(response);
            } else {
                return null;
            }
        } catch (UnknownHostException e) {
            printStackTraceToCrashlytics(e);
            return null;
        } catch (IOException e) {
            printStackTraceToCrashlytics(e);
            return null;
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            JSONObject o = new JSONObject();
            try {
                o.put("message", "json_error");
            } catch (JSONException e1) {
            }
            return o;
        } catch (Exception e) {
            printStackTraceToCrashlytics(e);
        }
        return null;
    }

    public JSONObject fetchJSON(String url) throws IOException {
        String response = null;
        try {
            response = fetch(new URL(url), null, session, null, null);
            return new JSONObject(response);
        } catch (Exception e) {
            if(response != null)
                IRCCloudLog.Log("Unable to parse JSON: " + response);
            printStackTraceToCrashlytics(e);
        }
        return null;
    }

    public JSONObject fetchJSON(String url, String postdata) throws IOException {
        String response = null;
        try {
            response = fetch(new URL(url), postdata, null, null, null);
            return new JSONObject(response);
        } catch (Exception e) {
            if(response != null)
                IRCCloudLog.Log("Unable to parse JSON: " + response);
            printStackTraceToCrashlytics(e);
        }
        return null;
    }

    public JSONObject fetchJSON(String url, HashMap<String, String>headers) throws IOException {
        String response = null;
        try {
            response = fetch(new URL(url), null, null, null, headers);
            return new JSONObject(response);
        } catch (Exception e) {
            if(response != null)
                IRCCloudLog.Log("Unable to parse JSON: " + response);
            printStackTraceToCrashlytics(e);
        }
        return null;
    }

    public void fetchConfig(ConfigCallback callback) {
        IRCCloudLog.Log(Log.INFO, TAG, "Requesting configuration");
        try {
            new ConfigFetcher(new ConfigCallback() {
                @Override
                public void onConfig(JSONObject o) {
                    try {
                        if (o != null) {
                            config = o;
                            SharedPreferences.Editor prefs = PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext()).edit();
                            prefs.putString("config", config.toString());
                            prefs.apply();

                            if (config.has("file_uri_template"))
                                file_uri_template = config.getString("file_uri_template");
                            else
                                file_uri_template = null;

                            if (config.has("pastebin_uri_template"))
                                pastebin_uri_template = config.getString("pastebin_uri_template");
                            else
                                pastebin_uri_template = null;

                            if (config.has("avatar_uri_template"))
                                avatar_uri_template = config.getString("avatar_uri_template");
                            else
                                avatar_uri_template = null;

                            if (config.has("avatar_redirect_uri_template"))
                                avatar_redirect_uri_template = config.getString("avatar_redirect_uri_template");
                            else
                                avatar_redirect_uri_template = null;

                            if (BuildConfig.ENTERPRISE && !(config.get("enterprise") instanceof JSONObject)) {
                                globalMsg = "Some features, such as push notifications, may not work as expected.  Please download the standard IRCCloud app from the <a href=\"" + config.getString("android_app") + "\">Play Store</a>";
                                notifyHandlers(EVENT_GLOBALMSG, null);
                            }
                            set_pastebin_cookie();

                            if (config.has("api_host")) {
                                set_api_host(config.getString("api_host"));
                            }
                        }
                        if(callback != null)
                            callback.onConfig(o);
                    } catch (Exception e) {
                        printStackTraceToCrashlytics(e);
                    }
                }
            }).connect();
        } catch (Exception e) {
            printStackTraceToCrashlytics(e);
        }
    }

    public static void set_api_host(String host) {
        if (host.startsWith("http://"))
            host = host.substring(7);
        if (host.startsWith("https://"))
            host = host.substring(8);
        if (host.endsWith("/"))
            host = host.substring(0, host.length() - 1);

        SharedPreferences.Editor editor = IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0).edit();
        editor.putString("host", host);
        editor.apply();
        NetworkConnection.IRCCLOUD_HOST = host;
        IRCCloudLog.Log(Log.INFO, TAG, "API host: " + NetworkConnection.IRCCLOUD_HOST);
    }

    public void set_pastebin_cookie() {
        try {
            if (config != null) {
                CookieSyncManager sm = CookieSyncManager.createInstance(IRCCloudApplication.getInstance().getApplicationContext());
                CookieManager cm = CookieManager.getInstance();
                Uri u = Uri.parse(config.getString("pastebin_uri_template"));
                cm.setCookie(u.getScheme() + "://" + u.getHost() + "/", "session=" + session);
                cm.flush();
                sm.sync();
                cm.setAcceptCookie(true);
            }
        } catch (Exception e) {
        }
    }

    public JSONObject registerGCM(String regId, String sk) throws IOException {
        String postdata = "device_id=" + regId + "&session=" + sk;
        try {
            String response = fetch(new URL("https://" + IRCCLOUD_HOST + "/gcm-register"), postdata, sk, null, null);
            if(response.length() > 0)
                return new JSONObject(response);
        } catch (Exception e) {
            printStackTraceToCrashlytics(e);
        }
        return null;
    }

    public JSONObject unregisterGCM(String regId, String sk) throws IOException {
        String postdata = "device_id=" + regId + "&session=" + sk;
        try {
            String response = fetch(new URL("https://" + IRCCLOUD_HOST + "/gcm-unregister"), postdata, sk, null, null);
            if(response.length() > 0)
                return new JSONObject(response);
        } catch (Exception e) {
            printStackTraceToCrashlytics(e);
        }
        return null;
    }

    public JSONObject files(int page) throws IOException {
        try {
            String response = fetch(new URL("https://" + IRCCLOUD_HOST + "/chat/files?page=" + page), null, session, null, null);
            if(response.length() > 0)
                return new JSONObject(response);
        } catch (Exception e) {
            printStackTraceToCrashlytics(e);
        }
        return null;
    }

    public JsonNode propertiesForFile(String fileID) throws IOException {
        try {
            String response = fetch(new URL("https://" + IRCCLOUD_HOST + "/file/json/" + fileID), null, session, null, null);
            if(response.length() > 0)
                return new ObjectMapper().readValue(response, JsonNode.class);
        } catch (Exception e) {
            printStackTraceToCrashlytics(e);
        }
        return null;
    }

    public JSONObject pastebins(int page) throws IOException {
        try {
            String response = fetch(new URL("https://" + IRCCLOUD_HOST + "/chat/pastebins?page=" + page), null, session, null, null);
            if(response.length() > 0)
                return new JSONObject(response);
        } catch (Exception e) {
            printStackTraceToCrashlytics(e);
        }
        return null;
    }

    public JSONObject logExports() throws IOException {
        try {
            String response = fetch(new URL("https://" + IRCCLOUD_HOST + "/chat/log-exports"), null, session, null, null);
            if(response.length() > 0)
                return new JSONObject(response);
        } catch (Exception e) {
            printStackTraceToCrashlytics(e);
        }
        return null;
    }

    public void logout(final String sk) {
        idleTimer.schedule(new TimerTask() {
            @Override
            public void run() {
                try {
                    Log.i("IRCCloud", "Invalidating session");
                    fetch(new URL("https://" + IRCCLOUD_HOST + "/chat/logout"), "session=" + sk, sk, null, null);
                } catch (Exception e) {
                    printStackTraceToCrashlytics(e);
                }
            }
        }, 50);
    }

    public JSONArray networkPresets() throws IOException {
        try {
            String response = fetch(new URL("https://www.irccloud.com/static/networks.json"), null, null, null, null);
            if(response.length() > 0) {
                JSONObject o = new JSONObject(response);
                return o.getJSONArray("networks");
            }
        } catch (Exception e) {
            printStackTraceToCrashlytics(e);
        }
        return null;
    }

    @TargetApi(24)
    public void registerForConnectivity() {
        try {
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                IntentFilter intentFilter = new IntentFilter();
                intentFilter.addAction(ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED);
                IRCCloudApplication.getInstance().getApplicationContext().registerReceiver(dataSaverListener, intentFilter);

                ConnectivityManager cm = (ConnectivityManager) IRCCloudApplication.getInstance().getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
                cm.registerDefaultNetworkCallback(connectivityCallback);
            } else {
                IntentFilter intentFilter = new IntentFilter();
                intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
                IRCCloudApplication.getInstance().getApplicationContext().registerReceiver(connectivityListener, intentFilter);
            }
        } catch (Exception e) {
            printStackTraceToCrashlytics(e);
        }
    }

    public void unregisterForConnectivity() {
        try {
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                ConnectivityManager cm = (ConnectivityManager) IRCCloudApplication.getInstance().getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
                cm.unregisterNetworkCallback(connectivityCallback);
            } else {
                IRCCloudApplication.getInstance().getApplicationContext().unregisterReceiver(connectivityListener);
            }
        } catch (IllegalArgumentException e) {
            //The broadcast receiver hasn't been registered yet
        }
        try {
            IRCCloudApplication.getInstance().getApplicationContext().unregisterReceiver(dataSaverListener);
        } catch (IllegalArgumentException e) {
            //The broadcast receiver hasn't been registered yet
        }
    }

    public void load() {
        /*notifyHandlers(EVENT_CACHE_START, null);
        try {
            String versionName = IRCCloudApplication.getInstance().getPackageManager().getPackageInfo(IRCCloudApplication.getInstance().getApplicationContext().getPackageName(), 0).versionName;
            SharedPreferences prefs = IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0);
            if(!versionName.equals(prefs.getString("cacheVersion", ""))) {
                Log.w("IRCCloud", "App version changed, clearing cache");
                clearOfflineCache();
            }
        } catch (PackageManager.NameNotFoundException e) {
        }

        if(PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext()).getBoolean("enable_cache", true)) {
            mServers.load();
            mBuffers.load();
            mChannels.load();
            if (mServers.count() > 0) {
                String u = IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0).getString("userinfo", null);
                if (u != null && u.length() > 0)
                    userInfo = new UserInfo(new IRCCloudJSONObject(u));
                highest_eid = IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0).getLong("highest_eid", -1);
                streamId = IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0).getString("streamId", null);
                ready = true;
            }
        }
        notifyHandlers(EVENT_CACHE_END, null);*/
    }

    public void save(int delay) {
        /*if (saveTimerTask != null)
            saveTimerTask.cancel();

        saveTimerTask = new TimerTask() {
            @Override
            public void run() {
                synchronized (saveTimer) {
                    saveTimerTask = null;
                    if (PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext()).getBoolean("enable_cache", true)) {
                        final long start = System.currentTimeMillis();
                        Log.i("IRCCloud", "Saving backlog");
                        final SharedPreferences.Editor editor = IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0).edit();
                        editor.remove("streamId");
                        editor.remove("highest_eid");
                        editor.commit();

                        mUsers.save();
                        mEvents.save();
                        mChannels.save();
                        mBuffers.save();
                        mServers.save();
                        TransactionManager.getInstance().getSaveQueue().setTransactionListener(new TransactionListener<List<Model>>() {
                            @Override
                            public void onResultReceived(List<Model> models) {
                                Log.i("IRCCloud", "Saved " + models.size() + " objects in " + (System.currentTimeMillis() - start) + "ms");
                                if (handlers.size() == 0) {
                                    editor.putString("streamId", streamId);
                                    editor.putLong("highest_eid", highest_eid);
                                    try {
                                        editor.putString("cacheVersion", IRCCloudApplication.getInstance().getPackageManager().getPackageInfo(IRCCloudApplication.getInstance().getApplicationContext().getPackageName(), 0).versionName);
                                    } catch (PackageManager.NameNotFoundException e) {
                                        editor.remove("cacheVersion");
                                    }
                                    editor.commit();
                                }
                                TransactionManager.getInstance().getSaveQueue().setTransactionListener(null);
                            }

                            @Override
                            public boolean onReady(BaseTransaction<List<Model>> baseTransaction) {
                                return true;
                            }

                            @Override
                            public boolean hasResult(BaseTransaction<List<Model>> baseTransaction, List<Model> models) {
                                return true;
                            }
                        });
                        TransactionManager.getInstance().getSaveQueue().purgeQueue();
                    }
                }
            }
        };
        saveTimer.schedule(saveTimerTask, delay);*/
    }

    public void connect() {
        connect(false);
    }

    @TargetApi(24)
    public synchronized void connect(boolean ignoreNetworkState) {
        IRCCloudLog.Log(Log.DEBUG, TAG, "connect()");
        Context ctx = IRCCloudApplication.getInstance().getApplicationContext();
        session = ctx.getSharedPreferences("prefs", 0).getString("session_key", "");
        int limit = 100;

        if (session.length() == 0) {
            IRCCloudLog.Log(Log.INFO, TAG, "Session key not set");
            state = BuildConfig.MOCK_DATA ? STATE_CONNECTED : STATE_DISCONNECTED;
            notifyHandlers(EVENT_CONNECTIVITY, null);
            return;
        }

        ConnectivityManager cm = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE);
        if(cm != null) {
            NetworkInfo ni = cm.getActiveNetworkInfo();

            if (!ignoreNetworkState && ni != null && !ni.isConnectedOrConnecting()) {
                IRCCloudLog.Log(Log.INFO, TAG, "No active network connection");
                cancel_idle_timer();
                state = STATE_DISCONNECTED;
                reconnect_timestamp = 0;
                notifyHandlers(EVENT_CONNECTIVITY, null);
                return;
            }

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && cm.isActiveNetworkMetered() && cm.getRestrictBackgroundStatus() == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED) {
                limit = 50;
            }

            if (ni != null && ni.getType() == ConnectivityManager.TYPE_MOBILE && (ni.getSubtype() == TelephonyManager.NETWORK_TYPE_EDGE || ni.getSubtype() == TelephonyManager.NETWORK_TYPE_GPRS || ni.getSubtype() == TelephonyManager.NETWORK_TYPE_CDMA || ni.getSubtype() == TelephonyManager.NETWORK_TYPE_1xRTT)) {
                limit = 25;
            }
        }

        if (state == STATE_CONNECTING || state == STATE_CONNECTED) {
            IRCCloudLog.Log(Log.INFO, TAG, "Ignoring duplicate connect request");
            return;
        }

        state = STATE_CONNECTING;

        if (saveTimerTask != null)
            saveTimerTask.cancel();
        saveTimerTask = null;

        if (oobTasks.size() > 0) {
            IRCCloudLog.Log(Log.DEBUG, TAG, "Clearing OOB tasks before connecting");
        }
        synchronized (oobTasks) {

            for (Integer bid : oobTasks.keySet()) {
                try {
                    oobTasks.get(bid).cancel();
                } catch (Exception e) {
                    printStackTraceToCrashlytics(e);
                }
            }
            oobTasks.clear();
        }

        reconnect_timestamp = 0;
        idle_interval = 0;
        accrued = 0;
        resultCallbacks.clear();
        notifyHandlers(EVENT_CONNECTIVITY, null);

        fetchConfig(new ConnectCallback(limit));
    }

    public interface ConfigCallback {
        void onConfig(JSONObject config);
    }

    private class ConfigFetcher extends HTTPFetcher {
        ConfigCallback callback;
        JSONObject result = null;

        public ConfigFetcher(ConfigCallback callback) throws MalformedURLException {
            super(new URL("https://" + IRCCLOUD_HOST + "/config"));
            this.callback = callback;
        }

        protected void onFetchComplete() {
            if(!isCancelled && callback != null)
                callback.onConfig(result);
        }

        protected void onFetchFailed() {
            if(!isCancelled && callback != null)
                callback.onConfig(result);
        }

        protected void onStreamConnected(InputStream is) throws Exception {
            if (isCancelled)
                return;

            ByteArrayOutputStream os = new ByteArrayOutputStream();
            byte[] buffer = new byte[8192];
            int len;
            while ((len = is.read(buffer)) != -1) {
                os.write(buffer, 0, len);
            }
            String response = os.toString("UTF-8");
            is.close();

            result = new JSONObject(response);
        }
    }

    private class ConnectCallback implements ConfigCallback {
        int limit;

        public ConnectCallback(int limit) {
            this.limit = limit;
        }

        @Override
        public void onConfig(JSONObject config) {
            try {
                if (config != null) {
                    String host = null;
                    int port = -1;
                    host = System.getProperty("http.proxyHost", null);
                    try {
                        port = Integer.parseInt(System.getProperty("http.proxyPort", "8080"));
                    } catch (NumberFormatException e) {
                        port = -1;
                    }

                    if (!wifiLock.isHeld())
                        wifiLock.acquire();

                    Map<String, String> extraHeaders = new HashMap<>();
                    extraHeaders.put("User-Agent", useragent);

                    String url = "wss://" + config.getString("socket_host") + IRCCLOUD_PATH;
                    if (highest_eid > 0 && streamId != null && streamId.length() > 0) {
                        url += "?exclude_archives=1&since_id=" + highest_eid + "&stream_id=" + streamId;
                        if (notifier)
                            url += "&notifier=1";
                        url += "&limit=" + limit;
                    } else if (notifier) {
                        url += "?exclude_archives=1&notifier=1&limit=" + limit;
                    } else {
                        url += "?exclude_archives=1&limit=" + limit;
                    }

                    if (host != null && host.length() > 0 && !host.equalsIgnoreCase("localhost") && !host.equalsIgnoreCase("127.0.0.1") && port > 0) {
                        IRCCloudLog.Log(Log.DEBUG, TAG, "Connecting via proxy: " + host);
                    }

                    IRCCloudLog.Log(Log.DEBUG, TAG, "Attempt: " + failCount);

                    if (client != null) {
                        client.setListener(null);
                        client.disconnect();
                    }

                    TrustManager[] trustManagers = new TrustManager[1];
                    trustManagers[0] = TrustKit.getInstance().getTrustManager(config.getString("socket_host"));
                    WebSocketClient.setTrustManagers(trustManagers);
                    HttpMetric m = null;

                    try {
                        m = FirebasePerformance.getInstance().newHttpMetric(url.replace("wss://", "https://"), FirebasePerformance.HttpMethod.GET);
                        m.start();
                    } catch (Exception e) {

                    }

                    final HttpMetric metric = m;
                    client = new WebSocketClient(URI.create(url), new WebSocketClient.Listener() {
                        @Override
                        public void onConnect() {
                            if (client != null && client.getListener() == this) {
                                IRCCloudLog.Log(Log.DEBUG, TAG, "WebSocket connected");
                                if (metric != null) {
                                    metric.setHttpResponseCode(200);
                                    metric.stop();
                                }
                                try {
                                    JSONObject o = new JSONObject();
                                    o.put("cookie", session);
                                    send("auth", o, null);
                                } catch (JSONException e) {
                                    printStackTraceToCrashlytics(e);
                                }

                                IRCCloudLog.Log(Log.DEBUG, TAG, "Emptying cache");
                                if (saveTimerTask != null)
                                    saveTimerTask.cancel();
                                saveTimerTask = null;
                                final SharedPreferences.Editor editor = IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0).edit();
                                editor.remove("streamId");
                                editor.remove("highest_eid");
                                editor.apply();
                                //Delete.tables(Server.class, Buffer.class, Channel.class);
                                notifyHandlers(EVENT_CONNECTIVITY, null);
                                if (disconnectSockerTimerTask != null)
                                    disconnectSockerTimerTask.cancel();
                                if (notifier) {
                                    disconnectSockerTimerTask = new TimerTask() {
                                        @Override
                                        public void run() {
                                            disconnectSockerTimerTask = null;
                                            if (notifier) {
                                                Log.d("IRCCloud", "Notifier socket expired");
                                                disconnect();
                                            }
                                        }
                                    };
                                    idleTimer.schedule(disconnectSockerTimerTask, 600000);
                                }
                            } else {
                                IRCCloudLog.Log(Log.WARN, "IRCCloud", "Got websocket onConnect for inactive websocket");
                            }
                        }

                        @Override
                        public void onMessage(String message) {
                            if (client != null && client.getListener() == this && message.length() > 0) {
                                try {
                                    synchronized (parserLock) {
                                        parse_object(new IRCCloudJSONObject(mapper.readValue(message, JsonNode.class)), false);
                                    }
                                } catch (Exception e) {
                                    Log.e(TAG, "Unable to parse: " + message);
                                    IRCCloudLog.LogException(e);
                                    printStackTraceToCrashlytics(e);
                                }
                            }
                        }

                        @Override
                        public void onMessage(byte[] data) {
                            //Log.d(TAG, String.format("Got binary message! %s", toHexString(data));
                        }

                        private void closed() {
                            try {
                                if (wifiLock.isHeld())
                                    wifiLock.release();
                            } catch (RuntimeException e) {

                            }

                            IRCCloudLog.Log(Log.DEBUG, TAG, "Clearing OOB tasks");
                            synchronized (oobTasks) {
                                for (Integer bid : oobTasks.keySet()) {
                                    try {
                                        oobTasks.get(bid).cancel();
                                    } catch (Exception e) {
                                        printStackTraceToCrashlytics(e);
                                    }
                                }
                                oobTasks.clear();
                            }
                            IRCCloudLog.Log(Log.DEBUG, TAG, "Clear");

                            if (highest_eid <= 0)
                                streamId = null;

                            ConnectivityManager cm = (ConnectivityManager) IRCCloudApplication.getInstance().getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
                            NetworkInfo ni = cm.getActiveNetworkInfo();
                            if (state == STATE_DISCONNECTING || ni == null || !ni.isConnected()) {
                                cancel_idle_timer();
                                state = STATE_DISCONNECTED;
                            } else {
                                fail();
                            }
                        }

                        @Override
                        public void onDisconnect(int code, String reason) {
                            if (client != null && client.getListener() == this) {
                                IRCCloudLog.Log(Log.DEBUG, TAG, "WebSocket disconnected: " + code + " " + reason);
                                closed();
                            } else {
                                IRCCloudLog.Log(Log.WARN, "IRCCloud", "Got websocket onDisconnect for inactive websocket");
                            }
                        }

                        @Override
                        public void onError(Exception error) {
                            if (client != null && client.getListener() == this) {
                                IRCCloudLog.Log(Log.ERROR, TAG, "The WebSocket encountered an error: " + error.toString());
                                closed();
                            } else {
                                IRCCloudLog.Log(Log.WARN, "IRCCloud", "Got websocket onError for inactive websocket");
                            }
                        }
                    }, extraHeaders);

                    if (client != null) {
                        client.setDebugListener(new WebSocketClient.DebugListener() {
                            @Override
                            public void onDebugMsg(String msg) {
                                IRCCloudLog.Log(Log.DEBUG, "IRCCloud", msg);
                            }
                        });
                        if (host != null && host.length() > 0 && !host.equalsIgnoreCase("localhost") && !host.equalsIgnoreCase("127.0.0.1") && port > 0)
                            client.setProxy(host, port);
                        else
                            client.setProxy(null, -1);
                        client.connect();
                    }
                } else {
                    IRCCloudLog.Log(Log.ERROR, TAG, "Unable to fetch configuration");
                    fail();
                }
            } catch (Exception e) {
                printStackTraceToCrashlytics(e);
                fail();
            }
        }
    }

    private void fail() {
        failCount++;
        if (failCount < 4)
            idle_interval = failCount * 1000;
        else if (failCount < 10)
            idle_interval = 10000;
        else
            idle_interval = 30000;
        schedule_idle_timer();
        IRCCloudLog.Log(Log.DEBUG, TAG, "Reconnecting in " + idle_interval / 1000 + " seconds");
        state = STATE_DISCONNECTED;
        notifyHandlers(EVENT_CONNECTIVITY, null);
    }

    public void logout() {
        streamId = null;
        disconnect();
        try {
            if(IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0).getString("gcm_id", "").length() > 0) {
                BackgroundTaskWorker.unregisterGCM();
            }
        } catch (Exception e) {
            printStackTraceToCrashlytics(e);
        }
        ready = false;
        accrued = 0;
        highest_eid = -1;
        SharedPreferences.Editor prefs = PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext()).edit();
        prefs.remove("uid");
        prefs.remove("name");
        prefs.remove("email");
        prefs.remove("highlights");
        prefs.remove("theme");
        prefs.remove("monospace");
        prefs.apply();
        SharedPreferences.Editor editor = IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0).edit();
        editor.clear();
        editor.apply();
        mServers.clear();
        mBuffers.clear();
        mChannels.clear();
        mUsers.clear();
        mEvents.clear();
        pendingEdits.clear();
        NotificationsList.getInstance().clear();
        NotificationsList.getInstance().pruneNotificationChannels();
        userInfo = null;
        session = null;
        ImageList.getInstance().purge();
        AvatarsList.getInstance().clear();
        mRecentConversations.clear();
        LogExportsList.getInstance().clear();
        FirebaseAnalytics.getInstance(IRCCloudApplication.getInstance().getApplicationContext()).resetAnalyticsData();
        IRCCloudLog.clear();
        if(!BuildConfig.ENTERPRISE)
            IRCCLOUD_HOST = BuildConfig.HOST;
        save(100);
    }

    public void clearOfflineCache() {
        SharedPreferences.Editor editor = IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0).edit();
        editor.remove("userinfo");
        editor.remove("highest_eid");
        editor.remove("streamId");
        editor.apply();
        highest_eid = -1;
        streamId = null;
        disconnect();
        mServers.clear();
        mBuffers.clear();
        mChannels.clear();
        mUsers.clear();
        mEvents.clear();
        pendingEdits.clear();
        connect();
    }

    private int send(String method, JSONObject params, IRCResultCallback callback) {
        if (client == null || (state != STATE_CONNECTED && !method.equals("auth")) || BuildConfig.MOCK_DATA)
            return -1;
        synchronized (resultCallbacks) {
            try {
                params.put("_reqid", ++last_reqid);
                if (callback != null)
                    resultCallbacks.put(last_reqid, callback);
                params.put("_method", method);
                //Log.d(TAG, "Reqid: " + last_reqid + " Method: " + method + " Params: " + params.toString());
                client.send(params.toString());
                return last_reqid;
            } catch (Exception e) {
                printStackTraceToCrashlytics(e);
                return -1;
            }
        }
    }

    public int heartbeat(int cid, int bid, long last_seen_eid, IRCResultCallback callback) {
        return heartbeat(bid, new Integer[]{cid}, new Integer[]{bid}, new Long[]{last_seen_eid}, callback);
    }

    public int heartbeat(int selectedBuffer, Integer cids[], Integer bids[], Long last_seen_eids[], IRCResultCallback callback) {
        try {
            JSONObject heartbeat = new JSONObject();
            for (int i = 0; i < cids.length; i++) {
                JSONObject o;
                if (heartbeat.has(String.valueOf(cids[i]))) {
                    o = heartbeat.getJSONObject(String.valueOf(cids[i]));
                } else {
                    o = new JSONObject();
                    heartbeat.put(String.valueOf(cids[i]), o);
                }
                o.put(String.valueOf(bids[i]), last_seen_eids[i]);
            }

            JSONObject o = new JSONObject();
            o.put("selectedBuffer", selectedBuffer);
            o.put("seenEids", heartbeat.toString());
            return send("heartbeat", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int disconnect(int cid, String message, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("msg", message);
            return send("disconnect", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int reconnect(int cid, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            int reqid = send("reconnect", o, callback);
            if(reqid > 0) {
                Server s = mServers.getServer(cid);
                if(s != null) {
                    s.setStatus("queued");
                    notifyHandlers(EVENT_CONNECTIONLAG, new IRCCloudJSONObject(o));
                }
            }
            return reqid;
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int say(int cid, String to, String message, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            if (to != null)
                o.put("to", to);
            else
                o.put("to", "*");
            o.put("msg", message);
            return send("say", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int reply(int cid, String to, String message, String msgid, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("to", to);
            o.put("reply", message);
            o.put("msgid", msgid);
            return send("reply", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int edit_message(int cid, String to, String message, String msgid, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("to", to);
            o.put("edit", message);
            o.put("msgid", msgid);
            return send("edit-message", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int delete_message(int cid, String to, String msgid, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("to", to);
            o.put("msgid", msgid);
            return send("delete-message", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public JSONObject postSay(int cid, String to, String message, String sk) throws IOException {
        if(to == null)
            to = "*";
        String postdata = "cid=" + cid + "&to=" + URLEncoder.encode(to, "UTF-8") + "&msg=" + URLEncoder.encode(message, "UTF-8") + "&session=" + sk;
        try {
            String response = fetch(new URL("https://" + IRCCLOUD_HOST + "/chat/say"), postdata, sk, null, null);
            return new JSONObject(response);
        } catch (Exception e) {
            printStackTraceToCrashlytics(e);
        }
        return null;
    }

    public JSONObject postHeartbeat(int cid, int bid, long last_seen_eid, String sk) {
        return postHeartbeat(bid, new Integer[]{cid}, new Integer[]{bid}, new Long[]{last_seen_eid}, sk);
    }

    public JSONObject postHeartbeat(int selectedBuffer, Integer cids[], Integer bids[], Long last_seen_eids[], String sk) {
        try {
            JSONObject heartbeat = new JSONObject();
            for (int i = 0; i < cids.length; i++) {
                JSONObject o;
                if (heartbeat.has(String.valueOf(cids[i]))) {
                    o = heartbeat.getJSONObject(String.valueOf(cids[i]));
                } else {
                    o = new JSONObject();
                    heartbeat.put(String.valueOf(cids[i]), o);
                }
                o.put(String.valueOf(bids[i]), last_seen_eids[i]);
            }

            String postdata = "selectedBuffer=" + selectedBuffer + "&seenEids=" + URLEncoder.encode(heartbeat.toString(), "UTF-8") + "&session=" + sk;
            String response = fetch(new URL("https://" + IRCCLOUD_HOST + "/chat/heartbeat"), postdata, sk, null, null);
            return new JSONObject(response);
        } catch (Exception e) {
            printStackTraceToCrashlytics(e);
        }
        return null;
    }


    public int join(int cid, String channel, String key, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("channel", channel);
            o.put("key", key);
            return send("join", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int part(int cid, String channel, String message, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("channel", channel);
            o.put("msg", message);
            return send("part", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int kick(int cid, String channel, String nick, String message, IRCResultCallback callback) {
        return say(cid, channel, "/kick " + nick + " " + message, callback);
    }

    public int mode(int cid, String channel, String mode, IRCResultCallback callback) {
        return say(cid, channel, "/mode " + channel + " " + mode, callback);
    }

    public int invite(int cid, String channel, String nick, IRCResultCallback callback) {
        return say(cid, channel, "/invite " + nick + " " + channel, callback);
    }

    public int archiveBuffer(int cid, long bid, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("id", bid);
            return send("archive-buffer", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int unarchiveBuffer(int cid, long bid, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("id", bid);
            return send("unarchive-buffer", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int renameChannel(String name, int cid, long bid, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("id", bid);
            o.put("name", name);
            return send("rename-channel", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int renameConversation(String name, int cid, long bid, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("id", bid);
            o.put("name", name);
            return send("rename-conversation", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int deleteBuffer(int cid, long bid, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("id", bid);
            return send("delete-buffer", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int deleteServer(int cid, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            return send("delete-connection", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int deleteFile(String id, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("file", id);
            return send("delete-file", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int restoreFile(String id, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("file", id);
            return send("restore-file", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int addServer(String hostname, int port, int ssl, String netname, String nickname, String realname, String server_pass, String nickserv_pass, String joincommands, String channels, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("hostname", hostname);
            o.put("port", port);
            o.put("ssl", String.valueOf(ssl));
            if (netname != null)
                o.put("netname", netname);
            o.put("nickname", nickname);
            o.put("realname", realname);
            o.put("server_pass", server_pass);
            o.put("nspass", nickserv_pass);
            o.put("joincommands", joincommands);
            o.put("channels", channels);
            return send("add-server", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int editServer(int cid, String hostname, int port, int ssl, String netname, String nickname, String realname, String server_pass, String nickserv_pass, String joincommands, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("hostname", hostname);
            o.put("port", port);
            o.put("ssl", String.valueOf(ssl));
            if (netname != null)
                o.put("netname", netname);
            o.put("nickname", nickname);
            o.put("realname", realname);
            o.put("server_pass", server_pass);
            o.put("nspass", nickserv_pass);
            o.put("joincommands", joincommands);
            o.put("cid", cid);
            return send("edit-server", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int ignore(int cid, String mask, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("mask", mask);
            return send("ignore", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int unignore(int cid, String mask, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("mask", mask);
            return send("unignore", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int set_prefs(String prefs, IRCResultCallback callback) {
        try {
            Log.i("IRCCloud", "Setting prefs: " + prefs);
            JSONObject o = new JSONObject();
            o.put("prefs", prefs);
            if(BuildConfig.MOCK_DATA) {
                if(userInfo == null)
                    userInfo = new UserInfo();
                userInfo.prefs = new JSONObject(prefs);
                notifyHandlers(EVENT_USERINFO, userInfo);
            }
            return send("set-prefs", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int set_user_settings(String realname, String hwords, boolean autoaway, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("realname", realname);
            o.put("hwords", hwords);
            o.put("autoaway", autoaway ? "1" : "0");
            return send("user-settings", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int change_password(String email, String oldPassword, String newPassword, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            if(email != null)
                o.put("email", email);
            o.put("password", oldPassword);
            if(newPassword != null)
                o.put("newpassword", newPassword);
            return send("change-password", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int delete_account(String password, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("password", password);
            return send("delete-account", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int ns_help_register(int cid, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            return send("ns-help-register", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int set_nspass(int cid, String nspass, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("nspass", nspass);
            return send("set-nspass", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int whois(int cid, String nick, String server, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("nick", nick);
            if (server != null)
                o.put("server", server);
            return send("whois", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int topic(int cid, String channel, String topic, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("channel", channel);
            o.put("topic", topic);
            return send("topic", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int back(int cid, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            return send("back", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int reorder_connections(String cids, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cids", cids);
            return send("reorder-connections", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int resend_verify_email(IRCResultCallback callback) {
        JSONObject o = new JSONObject();
        return send("resend-verify-email", o, callback);
    }

    public int finalize_upload(String id, String filename, String original_filename, boolean avatar, int cid, int orgId, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("id", id);
            o.put("filename", filename);
            o.put("original_filename", original_filename);
            if(avatar) {
                o.put("type", "avatar");
                if(orgId == -1) {
                    o.put("primary", "1");
                } else if(orgId == 0) {
                    o.put("cid", cid);
                } else {
                    o.put("org", orgId);
                }
            }
            return send("upload-finalise", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int paste(String name, String extension, String contents, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            if(name != null && name.length() > 0)
                o.put("name", name);
            o.put("contents", contents);
            o.put("extension", extension);
            return send("paste", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int edit_paste(String id, String name, String extension, String contents, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("id", id);
            if(name != null && name.length() > 0)
                o.put("name", name);
            o.put("body", contents);
            o.put("extension", extension);
            return send("edit-pastebin", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int delete_paste(String id, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("id", id);
            return send("delete-pastebin", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int export_log(int cid, int bid, String timezone, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            if(cid > 0)
                o.put("cid", cid);
            if(bid > 0)
                o.put("bid", bid);
            o.put("timezone", timezone);
            return send("export-log", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int set_avatar(int cid, int orgId, String avatar_id, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            if(avatar_id != null)
                o.put("id", avatar_id);
            else
                o.put("clear", "1");

            if(orgId == 0)
                o.put("cid", cid);
            else if(orgId != -1)
                o.put("org", orgId);
            else
                o.put("primary", "1");
            return send("set-avatar", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int set_netname(int cid, String name, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("netname", name);
            return send("set-netname", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public void request_backlog(int cid, int bid, long beforeId) {
        try {
            synchronized (oobTasks) {
                if (oobTasks.containsKey(bid)) {
                    IRCCloudLog.Log(Log.WARN, TAG, "Ignoring duplicate backlog request for BID: " + bid);
                    return;
                }
            }
            if (session == null || session.length() == 0) {
                IRCCloudLog.Log(Log.WARN, TAG, "Not fetching backlog before session is set");
                return;
            }
            if (Looper.myLooper() == null)
                Looper.prepare();

            OOBFetcher task;
            if (beforeId > 0)
                task = new OOBFetcher(new URL("https://" + IRCCLOUD_HOST + "/chat/backlog?cid=" + cid + "&bid=" + bid + "&beforeid=" + beforeId), bid);
            else
                task = new OOBFetcher(new URL("https://" + IRCCLOUD_HOST + "/chat/backlog?cid=" + cid + "&bid=" + bid), bid);
            synchronized (oobTasks) {
                oobTasks.put(bid, task);
                if(oobTasks.size() == 1)
                    task.connect();
            }
        } catch (MalformedURLException e) {
            printStackTraceToCrashlytics(e);
        }
    }

    public void request_archives(int cid) {
        try {
            synchronized (oobTasks) {
                if (oobTasks.containsKey(cid)) {
                    IRCCloudLog.Log(Log.WARN, TAG, "Ignoring duplicate archives request for CID: " + cid);
                    return;
                }
            }
            if (session == null || session.length() == 0) {
                IRCCloudLog.Log(Log.WARN, TAG, "Not fetching archives before session is set");
                return;
            }

            OOBFetcher task = new OOBFetcher(new URL("https://" + IRCCLOUD_HOST + "/chat/archives?cid=" + cid), cid);
            synchronized (oobTasks) {
                oobTasks.put(cid, task);
                if(oobTasks.size() == 1)
                    task.connect();
            }
        } catch (MalformedURLException e) {
            printStackTraceToCrashlytics(e);
        }
    }

    public void request_mock_data() {
        try {
            OOBFetcher task = new OOBFetcher(new URL("https://www.irccloud.com/test/bufferview.json"), -1);
            synchronized (oobTasks) {
                oobTasks.put(-1, task);
                if(oobTasks.size() == 1)
                    task.connect();
            }
        } catch (MalformedURLException e) {
            printStackTraceToCrashlytics(e);
        }
    }

    public void upgrade() {
        if (disconnectSockerTimerTask != null)
            disconnectSockerTimerTask.cancel();
        notifier = false;
        send("upgrade_notifier", new JSONObject(), null);
    }

    public void cancel_idle_timer() {
        if (idleTimerTask != null)
            idleTimerTask.cancel();
        reconnect_timestamp = 0;
    }

    public void schedule_idle_timer() {
        if (idleTimerTask != null)
            idleTimerTask.cancel();
        if (idle_interval <= 0 || BuildConfig.MOCK_DATA)
            return;

        try {
            idleTimerTask = new TimerTask() {
                public void run() {
                    if (handlers.size() > 0) {
                        IRCCloudLog.Log(Log.INFO, TAG, "Websocket idle time exceeded, reconnecting...");
                        state = STATE_DISCONNECTING;
                        notifyHandlers(EVENT_CONNECTIVITY, null);
                        if (client != null)
                            client.disconnect();
                        connect();
                    }
                    reconnect_timestamp = 0;
                }
            };
            idleTimer.schedule(idleTimerTask, idle_interval);
            reconnect_timestamp = System.currentTimeMillis() + idle_interval;
        } catch (IllegalStateException e) {
            //It's possible for timer to get canceled by another thread before before it gets scheduled
            //so catch the exception
        }
    }

    public long getReconnectTimestamp() {
        return reconnect_timestamp;
    }

    public interface Parser {
        void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException;
    }

    private class BroadcastParser implements Parser {
        int type;

        BroadcastParser(int t) {
            type = t;
        }

        public void parse(IRCCloudJSONObject object, boolean backlog) {
            if (!backlog)
                notifyHandlers(type, object);
        }
    }

    public HashMap<String, Parser> parserMap = new HashMap<String, Parser>() {{
        //Ignored events
        put("idle", null);
        put("end_of_backlog", null);
        put("user_account", null);
        put("twitch_hosttarget_start", null);
        put("twitch_hosttarget_stop", null);
        put("twitch_usernotice", null);

        put("header", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                if (!(object.has("resumed") && object.getBoolean("resumed"))) {
                    Log.d("IRCCloud", "Socket was not resumed");
                    NotificationsList.getInstance().clearLastSeenEIDs();
                    mEvents.clear();
                    pendingEdits.clear();
                    if(streamId != null) {
                        IRCCloudLog.Log(Log.WARN, "IRCCloud", "Unable to resume socket, requesting full OOB load");
                        highest_eid = 0;
                        streamId = null;
                        failCount = 0;
                        ready = false;
                        if (client != null)
                            client.disconnect();
                        return;
                    }
                }
                idle_interval = object.getLong("idle_interval") + 15000;
                clockOffset = object.getLong("time") - (System.currentTimeMillis() / 1000);
                currentcount = 0;
                currentBid = -1;
                firstEid = -1;
                streamId = object.getString("streamid");
                if (object.has("accrued"))
                    accrued = object.getInt("accrued");
                state = STATE_CONNECTED;
                ImageList.getInstance().clear();
                ImageList.getInstance().clearFailures();
                notifyHandlers(EVENT_CONNECTIVITY, null);
            }
        });

        put("global_system_message", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                String msgType = object.getString("system_message_type");
                if (msgType == null || (!msgType.equalsIgnoreCase("eval") && !msgType.equalsIgnoreCase("refresh"))) {
                    globalMsg = object.getString("msg");
                    notifyHandlers(EVENT_GLOBALMSG, object);
                }
            }
        });

        put("num_invites", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                if (userInfo != null)
                    userInfo.num_invites = object.getInt("num_invites");
            }
        });

        put("stat_user", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                userInfo = new UserInfo(object);
                if(IRCCloudLog.CrashlyticsEnabled)
                    FirebaseCrashlytics.getInstance().setUserId("uid" + userInfo.id);
                SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext());
                SharedPreferences.Editor editor = prefs.edit();
                editor.putString("uid", "uid" + userInfo.id);
                editor.putString("name", userInfo.name);
                editor.putString("email", userInfo.email);
                editor.putString("highlights", userInfo.highlights);
                editor.putBoolean("autoaway", userInfo.auto_away);
                if (userInfo.prefs != null && !BuildConfig.MOCK_DATA) {
                    editor.putBoolean("time-24hr", userInfo.prefs.has("time-24hr") && userInfo.prefs.get("time-24hr") instanceof Boolean && userInfo.prefs.getBoolean("time-24hr"));
                    editor.putBoolean("time-seconds", userInfo.prefs.has("time-seconds") && userInfo.prefs.get("time-seconds") instanceof Boolean && userInfo.prefs.getBoolean("time-seconds"));
                    editor.putBoolean("mode-showsymbol", userInfo.prefs.has("mode-showsymbol") && userInfo.prefs.get("mode-showsymbol") instanceof Boolean && userInfo.prefs.getBoolean("mode-showsymbol"));
                    editor.putBoolean("nick-colors", userInfo.prefs.has("nick-colors") && userInfo.prefs.get("nick-colors") instanceof Boolean && userInfo.prefs.getBoolean("nick-colors"));
                    editor.putBoolean("mention-colors", userInfo.prefs.has("mention-colors") && userInfo.prefs.get("mention-colors") instanceof Boolean && userInfo.prefs.getBoolean("mention-colors"));
                    editor.putBoolean("emoji-disableconvert", !(userInfo.prefs.has("emoji-disableconvert") && userInfo.prefs.get("emoji-disableconvert") instanceof Boolean && userInfo.prefs.getBoolean("emoji-disableconvert")));
                    editor.putBoolean("pastebin-disableprompt", !(userInfo.prefs.has("pastebin-disableprompt") && userInfo.prefs.get("pastebin-disableprompt") instanceof Boolean && userInfo.prefs.getBoolean("pastebin-disableprompt")));
                    editor.putBoolean("hideJoinPart", !(userInfo.prefs.has("hideJoinPart") && userInfo.prefs.get("hideJoinPart") instanceof Boolean && userInfo.prefs.getBoolean("hideJoinPart")));
                    editor.putBoolean("expandJoinPart", !(userInfo.prefs.has("expandJoinPart") && userInfo.prefs.get("expandJoinPart") instanceof Boolean && userInfo.prefs.getBoolean("expandJoinPart")));
                    editor.putBoolean("notifications_all", (userInfo.prefs.has("notifications-all") && userInfo.prefs.get("notifications-all") instanceof Boolean && userInfo.prefs.getBoolean("notifications-all")));
                    editor.putBoolean("notifications_mute", (userInfo.prefs.has("notifications-mute") && userInfo.prefs.get("notifications-mute") instanceof Boolean && userInfo.prefs.getBoolean("notifications-mute")));
                    editor.putBoolean("disableTrackUnread", !(userInfo.prefs.has("disableTrackUnread") && userInfo.prefs.get("disableTrackUnread") instanceof Boolean && userInfo.prefs.getBoolean("disableTrackUnread")));
                    editor.putBoolean("enableReadOnSelect", (userInfo.prefs.has("enableReadOnSelect") && userInfo.prefs.get("enableReadOnSelect") instanceof Boolean && userInfo.prefs.getBoolean("enableReadOnSelect")));
                    editor.putBoolean("ascii-compact", (userInfo.prefs.has("ascii-compact") && userInfo.prefs.get("ascii-compact") instanceof Boolean && userInfo.prefs.getBoolean("ascii-compact")));
                    editor.putBoolean("emoji-nobig", !(userInfo.prefs.has("emoji-nobig") && userInfo.prefs.get("emoji-nobig") instanceof Boolean && userInfo.prefs.getBoolean("emoji-nobig")));
                    editor.putBoolean("files-disableinline", !(userInfo.prefs.has("files-disableinline") && userInfo.prefs.get("files-disableinline") instanceof Boolean && userInfo.prefs.getBoolean("files-disableinline")));
                    editor.putBoolean("chat-nocodespan", !(userInfo.prefs.has("chat-nocodespan") && userInfo.prefs.get("chat-nocodespan") instanceof Boolean && userInfo.prefs.getBoolean("chat-nocodespan")));
                    editor.putBoolean("chat-nocodeblock", !(userInfo.prefs.has("chat-nocodeblock") && userInfo.prefs.get("chat-nocodeblock") instanceof Boolean && userInfo.prefs.getBoolean("chat-nocodeblock")));
                    editor.putBoolean("chat-noquote", !(userInfo.prefs.has("chat-noquote") && userInfo.prefs.get("chat-noquote") instanceof Boolean && userInfo.prefs.getBoolean("chat-noquote")));
                    editor.putBoolean("chat-nocolor", !(userInfo.prefs.has("chat-nocolor") && userInfo.prefs.get("chat-nocolor") instanceof Boolean && userInfo.prefs.getBoolean("chat-nocolor")));
                    editor.putBoolean("inlineimages", (userInfo.prefs.has("inlineimages") && userInfo.prefs.get("inlineimages") instanceof Boolean && userInfo.prefs.getBoolean("inlineimages")));
                    if(userInfo.prefs.has("theme") && !prefs.contains("theme"))
                        editor.putString("theme", userInfo.prefs.getString("theme"));
                    if(userInfo.prefs.has("font") && !prefs.contains("monospace"))
                        editor.putBoolean("monospace", userInfo.prefs.getString("font").equals("mono"));
                    if(userInfo.prefs.has("time-left") && !prefs.contains("time-left")) {
                        editor.putBoolean("time-left", !(userInfo.prefs.has("time-left") && userInfo.prefs.get("time-left") instanceof Boolean && userInfo.prefs.getBoolean("time-left")));
                    }
                    if(userInfo.prefs.has("avatars-off") && !prefs.contains("avatars-off")) {
                        editor.putBoolean("avatars-off", !(userInfo.prefs.has("avatars-off") && userInfo.prefs.get("avatars-off") instanceof Boolean && userInfo.prefs.getBoolean("avatars-off")));
                    }
                    if(userInfo.prefs.has("chat-oneline") && !prefs.contains("chat-oneline")) {
                        editor.putBoolean("chat-oneline", !(userInfo.prefs.has("chat-oneline") && userInfo.prefs.get("chat-oneline") instanceof Boolean && userInfo.prefs.getBoolean("chat-oneline")));
                    }
                    if(userInfo.prefs.has("chat-norealname") && !prefs.contains("chat-norealname")) {
                        editor.putBoolean("chat-norealname", !(userInfo.prefs.has("chat-norealname") && userInfo.prefs.get("chat-norealname") instanceof Boolean && userInfo.prefs.getBoolean("chat-norealname")));
                    }
                    if(userInfo.prefs.has("labs") && userInfo.prefs.getJSONObject("labs").has("avatars") && !prefs.contains("avatar-images")) {
                        editor.putBoolean("avatar-images", userInfo.prefs.getJSONObject("labs").getBoolean("avatars"));
                    }
                    if(userInfo.prefs.has("hiddenMembers") && !prefs.contains("hiddenMembers")) {
                        editor.putBoolean("hiddenMembers", !(userInfo.prefs.has("hiddenMembers") && userInfo.prefs.get("hiddenMembers") instanceof Boolean && userInfo.prefs.getBoolean("hiddenMembers")));
                    }
                } else {
                    editor.putBoolean("time-24hr", false);
                    editor.putBoolean("time-seconds", false);
                    editor.putBoolean("time-left", true);
                    editor.putBoolean("avatars-off", true);
                    editor.putBoolean("chat-oneline", true);
                    editor.putBoolean("chat-norealname", true);
                    editor.putBoolean("mode-showsymbol", false);
                    editor.putBoolean("nick-colors", false);
                    editor.putBoolean("mention-colors", false);
                    editor.putBoolean("emoji-disableconvert", true);
                    editor.putBoolean("pastebin-disableprompt", true);
                    editor.putBoolean("ascii-compact", false);
                    editor.putBoolean("emoji-nobig", true);
                    editor.putBoolean("files-disableinline", true);
                    editor.putBoolean("chat-nocodespan", true);
                    editor.putBoolean("chat-nocodeblock", true);
                    editor.putBoolean("chat-noquote", true);
                    editor.putBoolean("chat-nocolor", true);
                    editor.putBoolean("inlineimages", false);
                    editor.putBoolean("avatar-images", false);
                }
                editor.apply();
                mEvents.clearCaches();
                notifyHandlers(EVENT_USERINFO, userInfo);
            }
        });

        put("set_ignores", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                if(mServers.getServer(object.cid()) != null)
                    mServers.getServer(object.cid()).updateIgnores(object.getJsonNode("masks"));
                if (!backlog)
                    notifyHandlers(EVENT_SETIGNORES, object);
            }
        });
        put("ignore_list", get("set_ignores"));

        put("heartbeat_echo", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                Iterator<Entry<String, JsonNode>> iterator = object.getJsonNode("seenEids").fields();
                while (iterator.hasNext()) {
                    Map.Entry<String, JsonNode> entry = iterator.next();
                    JsonNode eids = entry.getValue();
                    Iterator<Map.Entry<String, JsonNode>> j = eids.fields();
                    while (j.hasNext()) {
                        Map.Entry<String, JsonNode> eidentry = j.next();
                        int bid = Integer.valueOf(eidentry.getKey());
                        long eid = eidentry.getValue().asLong();
                        NotificationsList.getInstance().updateLastSeenEid(bid, eid);
                        Buffer b = mBuffers.getBuffer(bid);
                        if (b != null) {
                            b.setLast_seen_eid(eid);
                            if (mEvents.lastEidForBuffer(bid) <= eid) {
                                b.setUnread(0);
                                b.setHighlights(0);
                            }
                        }
                    }
                }
                NotificationsList.getInstance().deleteOldNotifications();
                if (!backlog) {
                    notifyHandlers(EVENT_HEARTBEATECHO, object);
                }
            }
        });

        //Backlog
        put("oob_include", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                try {
                    ready = false;
                    mBuffers.invalidate();
                    mChannels.invalidate();
                    OOBFetcher t = new OOBFetcher(new URL(object.getString("api_host") + object.getString("url")), -1);
                    synchronized (oobTasks) {
                        oobTasks.put(-1, t);
                    }
                    t.connect();
                } catch (MalformedURLException e) {
                    // TODO Auto-generated catch block
                    printStackTraceToCrashlytics(e);
                }
            }
        });

        put("oob_timeout", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                IRCCloudLog.Log(Log.WARN, "IRCCloud", "OOB timed out");
                highest_eid = 0;
                streamId = null;
                ready = false;
                if (client != null)
                    client.disconnect();
            }
        });

        put("backlog_starts", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                numbuffers = object.getInt("numbuffers");
                totalbuffers = 0;
                currentBid = -1;
                notifyHandlers(EVENT_BACKLOG_START, null);
            }
        });

        put("oob_skipped", new Parser() {
                    @Override
                    public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                        Log.d("IRCCloud", "OOB was skipped");
                        ready = true;
                    }
                });

        put("backlog_cache_init", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                mEvents.deleteEventsBeforeEid(object.bid(), object.eid());
            }
        });

        put("backlog_complete", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                accrued = 0;
                try {
                    Log.d("IRCCloud", "Cleaning up invalid BIDs");
                    mBuffers.dirty = true;
                    mBuffers.purgeInvalidBIDs();
                    mChannels.purgeInvalidChannels();
                    NotificationsList.getInstance().deleteOldNotifications();
                    NotificationsList.getInstance().pruneNotificationChannels();
                    mRecentConversations.prune();
                    mRecentConversations.publishShortcuts();
                    process_pending_edits(false);
                } catch (Exception e) {
                    printStackTraceToCrashlytics(e);
                }
                if (userInfo != null && userInfo.connections > 0 && (mServers.count() == 0 || mBuffers.count() == 0)) {
                    Log.e("IRCCloud", "Failed to load buffers list, reconnecting");
                    notifyHandlers(EVENT_BACKLOG_FAILED, null);
                    streamId = null;
                    if (client != null)
                        client.disconnect();
                } else {
                    failCount = 0;
                    ready = true;
                    notifyHandlers(EVENT_BACKLOG_END, null);
                    if(notifier)
                        save(1000);
                }
            }
        });

        //Misc. popup alerts
        put("bad_channel_key", new BroadcastParser(EVENT_BADCHANNELKEY));
        final Parser alert = new BroadcastParser(EVENT_ALERT);
        String[] alerts = {"too_many_channels", "no_such_channel", "bad_channel_name",
                "no_such_nick", "invalid_nick_change", "chan_privs_needed",
                "accept_exists", "banned_from_channel", "oper_only",
                "no_nick_change", "no_messages_from_non_registered", "not_registered",
                "already_registered", "too_many_targets", "no_such_server",
                "unknown_command", "help_not_found", "accept_full",
                "accept_not", "nick_collision", "nick_too_fast", "need_registered_nick",
                "save_nick", "unknown_mode", "user_not_in_channel",
                "need_more_params", "users_dont_match", "users_disabled",
                "invalid_operator_password", "flood_warning", "privs_needed",
                "operator_fail", "not_on_channel", "ban_on_chan",
                "cannot_send_to_chan", "user_on_channel", "no_nick_given",
                "no_text_to_send", "no_origin", "only_servers_can_change_mode",
                "silence", "no_channel_topic", "invite_only_chan", "channel_full", "channel_key_set",
                "blocked_channel","unknown_error","channame_in_use","pong",
                "monitor_full","mlock_restricted","cannot_do_cmd","secure_only_chan",
                "cannot_change_chan_mode","knock_delivered","too_many_knocks",
                "chan_open","knock_on_chan","knock_disabled","cannotknock","ownmode",
                "nossl","redirect_error","invalid_flood","join_flood","metadata_limit",
                "metadata_targetinvalid","metadata_nomatchingkey","metadata_keyinvalid",
                "metadata_keynotset","metadata_keynopermission","metadata_toomanysubs", "invalid_nick"};
        for (String event : alerts) {
            put(event, alert);
        }

        //Server events
        put("makeserver", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                String away = object.getString("away");
                if (getUserInfo() != null && getUserInfo().auto_away && away != null && away.equals("Auto-away"))
                    away = "";

                Server server = mServers.createServer(object.cid(), object.getString("name"), object.getString("hostname"),
                        object.getInt("port"), object.getString("nick"), object.getString("status"), object.getBoolean("ssl") ? 1 : 0,
                        object.getString("realname"), object.getString("server_pass"), object.getString("nickserv_pass"), object.getString("join_commands"),
                        object.getJsonObject("fail_info"), away, object.getJsonNode("ignores"), (object.has("order") && !object.getString("order").equals("undefined")) ? object.getInt("order") : 0, object.getString("server_realname"),
                        object.getString("ircserver"), (object.has("orgid") && !object.getString("orgid").equals("undefined")) ? object.getInt("orgid") : 0,
                        (object.has("avatars_supported") && !object.getString("avatars_supported").equals("undefined")) ? object.getInt("avatars_supported") : 0,
                        (object.has("slack") && !object.getString("slack").equals("undefined")) ? object.getInt("slack") : 0);

                if(object.has("usermask") && object.getString("usermask").length() > 0)
                    server.setUsermask(object.getString("usermask"));

                if(object.has("deferred_archives"))
                    server.deferred_archives = object.getInt("deferred_archives");

                if(object.has("avatar"))
                    server.setAvatar(object.getString("avatar"));

                if(object.has("avatar_url"))
                    server.setAvatarURL(object.getString("avatar_url"));

                String avatar_url = null;
                if(PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext()).getBoolean("avatar-images", false)) {
                    Event e = new Event();
                    e.cid = server.getCid();
                    e.from = server.getNick();
                    e.type = "buffer_msg";
                    e.hostmask = server.getUsermask();
                    e.avatar = server.getAvatar();
                    e.avatar_url = server.getAvatarURL();

                    avatar_url = e.getAvatarURL(AvatarsList.SHORTCUT_ICON_SIZE());
                }

                NotificationsList.getInstance().updateServerNick(object.cid(), object.getString("nick"), avatar_url, server.isSlack());
                NotificationsList.getInstance().addNotificationGroup(server.getCid(), server.getName() != null && server.getName().length() > 0 ? server.getName() : server.getHostname());

                mBuffers.dirty = true;

                if (!backlog) {
                    notifyHandlers(EVENT_MAKESERVER, server);
                }
            }
        });
        put("server_details_changed", get("makeserver"));
        put("connection_deleted", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                mServers.deleteAllDataForServer(object.cid());
                NotificationsList.getInstance().pruneNotificationChannels();
                mRecentConversations.prune();
                if (!backlog)
                    notifyHandlers(EVENT_CONNECTIONDELETED, object.cid());
            }
        });
        put("status_changed", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                Server s = mServers.getServer(object.cid());
                if(s != null) {
                    s.setStatus(object.getString("new_status"));
                    s.setFail_info(object.getJsonObject("fail_info"));
                }
                mBuffers.dirty = true;
                if (!backlog) {
                    if (object.getString("new_status").equals("disconnected")) {
                        ArrayList<Buffer> buffers = mBuffers.getBuffersForServer(object.cid());
                        for (Buffer b : buffers) {
                            mChannels.deleteChannel(b.getBid());
                        }
                    }
                    notifyHandlers(EVENT_STATUSCHANGED, object);
                }
            }
        });
        put("isupport_params", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                Server s = mServers.getServer(object.cid());
                if(s != null) {
                    s.updateIsupport(object.getJsonObject("params"));
                    s.updateUserModes(object.getString("usermodes"));
                }
            }
        });
        put("reorder_connections", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                JsonNode order = object.getJsonNode("order");
                for (int i = 0; i < order.size(); i++) {
                    mServers.getServer(order.get(i).asInt()).setOrder(i + 1);
                }
                notifyHandlers(EVENT_REORDERCONNECTIONS, object);
            }
        });

        //Buffer events
        put("open_buffer", new BroadcastParser(EVENT_OPENBUFFER));
        put("makebuffer", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                Buffer buffer = mBuffers.createBuffer(object.bid(), object.cid(),
                        (object.has("min_eid") && !object.getString("min_eid").equalsIgnoreCase("undefined")) ? object.getLong("min_eid") : 0,
                        (object.has("last_seen_eid") && !object.getString("last_seen_eid").equalsIgnoreCase("undefined")) ? object.getLong("last_seen_eid") : -1, object.getString("name"), object.getString("buffer_type"),
                        (object.has("archived") && object.getBoolean("archived")) ? 1 : 0, (object.has("deferred") && object.getBoolean("deferred")) ? 1 : 0, (object.has("timeout") && object.getBoolean("timeout")) ? 1 : 0, object.getLong("created"));
                if(buffer.getTimeout() == 1 || buffer.getDeferred() == 1)
                    mEvents.deleteEventsForBuffer(buffer.getBid());
                NotificationsList.getInstance().updateLastSeenEid(buffer.getBid(), buffer.getLast_seen_eid());
                if(buffer.getTimeout() == 1 || buffer.getDeferred() == 1)
                    mEvents.deleteEventsForBuffer(buffer.getBid());
                if(buffer.getServer() != null && buffer.getArchived() == 1)
                    buffer.getServer().deferred_archives--;
                if (!backlog) {
                    notifyHandlers(EVENT_MAKEBUFFER, buffer);
                }
                totalbuffers++;
            }
        });
        put("delete_buffer", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                mBuffers.deleteAllDataForBuffer(object.bid());
                NotificationsList.getInstance().deleteNotificationsForBid(object.bid());
                NotificationsList.getInstance().pruneNotificationChannels();
                mRecentConversations.prune();
                if (!backlog)
                    notifyHandlers(EVENT_DELETEBUFFER, object.bid());
            }
        });
        put("buffer_archived", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                Buffer b = mBuffers.getBuffer(object.bid());
                if(b != null)
                    b.setArchived(1);
                if (!backlog) {
                    mRecentConversations.prune();
                    notifyHandlers(EVENT_BUFFERARCHIVED, object.bid());
                }
            }
        });
        put("buffer_unarchived", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                Buffer b = mBuffers.getBuffer(object.bid());
                if(b != null)
                    b.setArchived(0);
                Server s = mServers.getServer(object.cid());
                if(s != null && s.deferred_archives > 0)
                    s.deferred_archives--;
                if (!backlog) {
                    notifyHandlers(EVENT_BUFFERUNARCHIVED, object.bid());
                }
            }
        });
        put("rename_conversation", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                Buffer b = mBuffers.getBuffer(object.bid());
                if(b != null)
                    b.setName(object.getString("new_name"));
                if (!backlog) {
                    notifyHandlers(EVENT_RENAMECONVERSATION, object.bid());
                }
            }
        });
        put("rename_channel", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                Buffer b = mBuffers.getBuffer(object.bid());
                if(b != null)
                    b.setName(object.getString("new_name"));
                Channel c = mChannels.getChannelForBuffer(object.bid());
                if(c != null)
                    c.name = object.getString("new_name");
                if (!backlog) {
                    notifyHandlers(EVENT_RENAMECONVERSATION, object.bid());
                }
            }
        });
        Parser msg = new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                boolean newEvent = (mEvents.getEvent(object.eid(), object.bid()) == null);
                Event event = mEvents.addEvent(object);
                Buffer b = mBuffers.getBuffer(object.bid());

                if(event.eid == -1) {
                    alert.parse(object, backlog);
                } else {
                    if (b != null && event.isImportant(b.getType())) {
                        if(event.from != null) {
                            User u = mUsers.getUser(event.bid, event.from);
                            if (u != null) {
                                if(u.last_message < event.eid)
                                    u.last_message = event.eid;
                                if(event.highlight && u.last_mention < event.eid)
                                    u.last_mention = event.eid;
                            }
                        }

                        if (event.eid > b.getLast_seen_eid()) {
                            if ((event.highlight || b.isConversation())) {
                                if (newEvent) {
                                    b.setHighlights(b.getHighlights() + 1);
                                    b.setUnread(1);
                                }
                                if (!backlog) {
                                    JSONObject bufferDisabledMap = null;
                                    boolean show = true;
                                    String pref_type = b.isChannel() ? "channel" : "buffer";
                                    if (userInfo != null && userInfo.prefs != null && userInfo.prefs.has(pref_type + "-disableTrackUnread")) {
                                        bufferDisabledMap = userInfo.prefs.getJSONObject(pref_type + "-disableTrackUnread");
                                        if (bufferDisabledMap != null && bufferDisabledMap.has(String.valueOf(b.getBid())) && bufferDisabledMap.getBoolean(String.valueOf(b.getBid())))
                                            show = false;
                                    }
                                    if (userInfo != null && userInfo.prefs != null && userInfo.prefs.has(pref_type + "-notifications-mute")) {
                                        JSONObject bufferMutedMap = userInfo.prefs.getJSONObject(pref_type + "-notifications-mute");
                                        if (bufferMutedMap != null && bufferMutedMap.has(String.valueOf(b.getBid())) && bufferMutedMap.getBoolean(String.valueOf(b.getBid())))
                                            show = false;
                                    }
                                    try {
                                        if (IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0).getString("gcm_token", "").length() > 0)
                                            show = false;
                                    } catch (Exception e) {
                                        show = true;
                                    }
                                    if (show && NotificationsList.getInstance().getNotification(event.eid) == null) {
                                        String message = ColorFormatter.strip(event.msg).toString();
                                        String server_name = b.getServer().getName();
                                        if (server_name == null || server_name.length() == 0)
                                            server_name = b.getServer().getHostname();
                                        String from = event.nick;
                                        if (from == null)
                                            from = (event.from != null) ? event.from : event.server;

                                        NotificationsList.getInstance().addNotification(event.cid, event.bid, event.eid, (event.nick != null) ? event.nick : event.from, message, b.getName(), b.getType(), event.type, server_name, event.getAvatarURL(512));
                                        switch (b.getType()) {
                                            case "conversation":
                                                if (event.type.equals("buffer_me_msg"))
                                                    NotificationsList.getInstance().showNotifications("— " + b.getName() + " " + message);
                                                else
                                                    NotificationsList.getInstance().showNotifications(b.getName() + ": " + message);
                                                break;
                                            case "console":
                                                if (event.from == null || event.from.length() == 0) {
                                                    Server s = mServers.getServer(event.cid);
                                                    if (s.getName() != null && s.getName().length() > 0)
                                                        NotificationsList.getInstance().showNotifications(s.getName() + ": " + message);
                                                    else
                                                        NotificationsList.getInstance().showNotifications(s.getHostname() + ": " + message);
                                                } else {
                                                    NotificationsList.getInstance().showNotifications(event.from + ": " + message);
                                                }
                                                break;
                                            default:
                                                if (event.type.equals("buffer_me_msg"))
                                                    NotificationsList.getInstance().showNotifications(b.getName() + ": — " + event.nick + " " + message);
                                                else
                                                    NotificationsList.getInstance().showNotifications(b.getName() + ": <" + event.from + "> " + message);
                                                break;
                                        }
                                    }
                                }
                            } else {
                                b.setUnread(1);
                            }
                        }
                    }
                }

                if (handlers.size() == 0 && b != null && !b.getScrolledUp() && mEvents.getSizeOfBuffer(b.getBid()) > 200)
                    mEvents.pruneEvents(b.getBid());

                if (event.reqid >= 0) {
                    Event pending = mEvents.findPendingEventForReqid(event.bid, event.reqid);
                    if(pending != null) {
                        try {
                            if (pending.expiration_timer != null)
                                pending.expiration_timer.cancel();
                        } catch (Exception e1) {
                            //Timer already cancelled
                        }
                        if(pending.eid != event.eid)
                            mEvents.deleteEvent(pending.eid, pending.bid);
                    }
                } else if(event.self && b != null && b.isConversation()) {
                    mEvents.clearPendingEvents(event.bid);
                }

                if(event.isMessage()) {
                    String avatar = event.getAvatarURL(AvatarsList.SHORTCUT_ICON_SIZE());
                    AvatarsList.setAvatarURL(event.cid, event.type.equals("buffer_me_msg")?event.nick:event.from_nick, event.eid, avatar);
                    if (b != null && b.isConversation() && b.getName().equalsIgnoreCase(event.from)) {
                        try {
                            if (avatar != null && PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext()).getBoolean("avatar-images", true)) {
                                URL url = new URL(event.getAvatarURL(AvatarsList.SHORTCUT_ICON_SIZE()));
                                if (!ImageList.getInstance().cacheFile(url).exists()) {
                                    ImageList.getInstance().fetchImage(url, null);
                                }
                            }
                        } catch (Exception e) {
                        }
                    }
                }

                if(event.self) {
                    if(b != null)
                        mRecentConversations.updateConversation(event.cid, event.bid, b.getName(), b.getType(), event.eid / 1000);
                    if(!backlog)
                        mRecentConversations.publishShortcuts();
                }

                if (!backlog) {
                    notifyHandlers(EVENT_BUFFERMSG, event);
                }
            }
        };

        String[] msgs = { "buffer_msg", "buffer_me_msg", "wait", "banned", "kill", "connecting_cancelled",
                "target_callerid", "notice", "server_motdstart", "server_welcome", "server_motd", "server_endofmotd",
                "server_nomotd", "server_luserclient", "server_luserop", "server_luserconns", "server_luserme", "server_n_local",
                "server_luserchannels", "server_n_global", "server_yourhost","server_created", "server_luserunknown",
                "services_down", "your_unique_id", "callerid", "target_notified", "myinfo", "hidden_host_set", "unhandled_line",
                "unparsed_line", "connecting_failed", "nickname_in_use", "channel_invite", "motd_response", "socket_closed",
                "channel_mode_list_change", "msg_services", "stats", "statslinkinfo", "statscommands", "statscline",
                "statsnline", "statsiline", "statskline", "statsqline", "statsyline", "statsbline", "statsgline", "statstline",
                "statseline", "statsvline", "statslline", "statsuptime", "statsoline", "statshline", "statssline", "statsuline",
                "statsdebug", "spamfilter", "endofstats", "inviting_to_channel", "error", "too_fast", "no_bots",
                "wallops", "logged_in_as", "sasl_fail", "sasl_too_long", "sasl_aborted", "sasl_already",
                "you_are_operator", "btn_metadata_set", "sasl_success", "version", "channel_name_change",
                "cap_ls", "cap_list", "cap_new", "cap_del", "cap_req","cap_ack","cap_nak","cap_raw","cap_invalid", "help",
                "newsflash", "invited", "server_snomask", "codepage", "logged_out", "nick_locked", "info_response", "generic_server_info",
                "unknown_umode", "bad_ping", "cap_raw", "rehashed_config", "knock", "bad_channel_mask", "kill_deny",
                "chan_own_priv_needed", "not_for_halfops", "chan_forbidden", "starircd_welcome", "zurna_motd",
                "ambiguous_error_message", "list_usage", "list_syntax", "who_syntax", "text", "admin_info",
                "sqline_nick", "user_chghost", "loaded_module", "unloaded_module", "invite_notify" };
        for (String event : msgs) {
            put(event, msg);
        }

        //Channel events
        put("link_channel", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                mEvents.addEvent(object);
                if (!backlog) {
                    notifyHandlers(EVENT_LINKCHANNEL, object);
                }
            }
        });
        put("channel_init", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                JsonNode topic = object.getJsonObject("topic");
                String set_by = "";
                if(topic != null && topic.has("nick") && !topic.get("nick").isNull())
                    set_by = topic.get("nick").asText();
                else if(topic != null && topic.has("server") && !topic.get("server").isNull())
                    set_by = topic.get("server").asText();
                Channel channel = mChannels.createChannel(object.cid(), object.bid(), object.getString("chan"),
                        (topic == null || topic.get("text").isNull()) ? "" : topic.get("text").asText(),
                        (topic == null || !topic.has("time")) ? 0 : topic.get("time").asLong(),
                        set_by, object.getString("channel_type"),
                        object.getLong("timestamp"));
                mChannels.updateMode(object.bid(), object.getString("mode"), object.getJsonObject("ops"), true);
                mChannels.updateURL(object.bid(), object.getString("url"));
                mUsers.deleteUsersForBuffer(object.bid());
                JsonNode users = object.getJsonNode("members");
                if(users != null) {
                    Iterator<JsonNode> iterator = users.elements();
                    while (iterator.hasNext()) {
                        JsonNode user = iterator.next();
                        mUsers.createUser(object.cid(), object.bid(), user.get("nick").asText(), user.get("usermask").asText(), user.has("mode") ? user.get("mode").asText() : "", user.has("ircserver") ? user.get("ircserver").asText() : "", (user.has("away") && user.get("away").asBoolean()) ? 1 : 0, user.hasNonNull("display_name") ? user.get("display_name").asText() : null, false);
                    }
                }
                mBuffers.dirty = true;
                if (!backlog)
                    notifyHandlers(EVENT_CHANNELINIT, channel);
            }
        });
        put("channel_topic", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                mEvents.addEvent(object);
                if (!backlog) {
                    mChannels.updateTopic(object.bid(), object.getString("topic"), object.getLong("eid") / 1000000, object.has("author") ? object.getString("author") : object.getString("server"));
                    notifyHandlers(EVENT_CHANNELTOPIC, object);
                }
            }
        });
        put("channel_url", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                mChannels.updateURL(object.bid(), object.getString("url"));
            }
        });
        put("channel_mode", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                mEvents.addEvent(object);
                if (!backlog) {
                    mChannels.updateMode(object.bid(), object.getString("newmode"), object.getJsonObject("ops"), false);
                    notifyHandlers(EVENT_CHANNELMODE, object);
                }
            }
        });
        put("channel_mode_is", get("channel_mode"));
        put("channel_timestamp", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                if (!backlog) {
                    mChannels.updateTimestamp(object.bid(), object.getLong("timestamp"));
                    notifyHandlers(EVENT_CHANNELTIMESTAMP, object);
                }
            }
        });
        put("joined_channel", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                mEvents.addEvent(object);
                if(object.type().startsWith("you_"))
                    mBuffers.dirty = true;
                if (!backlog) {
                    mUsers.createUser(object.cid(), object.bid(), object.getString("nick"), object.getString("hostmask"), "", object.getString("ircserver"), 0, object.getString("display_name"));
                    notifyHandlers(EVENT_JOIN, object);
                }
            }
        });
        put("you_joined_channel", get("joined_channel"));
        put("parted_channel", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                mEvents.addEvent(object);
                if(object.type().startsWith("you_"))
                    mBuffers.dirty = true;
                if (!backlog) {
                    mUsers.deleteUser(object.bid(), object.getString("nick"));
                    if (object.type().equals("you_parted_channel")) {
                        mChannels.deleteChannel(object.bid());
                        mUsers.deleteUsersForBuffer(object.bid());
                    }
                    notifyHandlers(EVENT_PART, object);
                }
            }
        });
        put("you_parted_channel", get("parted_channel"));
        put("quit", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                mEvents.addEvent(object);
                if (!backlog) {
                    if(object.type().startsWith("you_"))
                        mBuffers.dirty = true;
                    mUsers.deleteUser(object.bid(), object.getString("nick"));
                    notifyHandlers(EVENT_QUIT, object);
                }
            }
        });
        put("quit_server", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                mEvents.addEvent(object);
                if (!backlog) {
                    mBuffers.dirty = true;
                    notifyHandlers(EVENT_QUIT, object);
                }
            }
        });
        put("kicked_channel", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                mEvents.addEvent(object);
                if (!backlog) {
                    mUsers.deleteUser(object.bid(), object.getString("nick"));
                    if (object.type().equals("you_kicked_channel")) {
                        mChannels.deleteChannel(object.bid());
                        mUsers.deleteUsersForBuffer(object.bid());
                        mBuffers.dirty = true;
                    }
                    notifyHandlers(EVENT_KICK, object);
                }
            }
        });
        put("you_kicked_channel", get("kicked_channel"));

        //Member list events
        put("nickchange", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                mEvents.addEvent(object);
                if (!backlog) {
                    mUsers.updateNick(object.bid(), object.getString("oldnick"), object.getString("newnick"));
                    if (object.type().equals("you_nickchange")) {
                        Server s = mServers.getServer(object.cid());
                        if(s != null)
                            s.setNick(object.getString("newnick"));
                        NotificationsList.getInstance().updateServerNick(object.cid(), object.getString("newnick"), s != null ? s.getAvatarURL() : null, s != null && s.isSlack());
                    }
                    notifyHandlers(EVENT_NICKCHANGE, object);
                }
            }
        });
        put("you_nickchange", get("nickchange"));
        put("display_name_change", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                if (!backlog) {
                    mUsers.updateDisplayName(object.cid(), object.getString("nick"), object.getString("display_name"));
                    notifyHandlers(EVENT_DISPLAYNAMECHANGE, object);
                }
            }
        });
        put("user_channel_mode", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                mEvents.addEvent(object);
                if (!backlog) {
                    mUsers.updateMode(object.bid(), object.getString("nick"), object.getString("newmode"));
                    notifyHandlers(EVENT_USERCHANNELMODE, object);
                }
            }
        });
        put("member_updates", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                JsonNode updates = object.getJsonObject("updates");
                Iterator<Map.Entry<String, JsonNode>> i = updates.fields();
                while (i.hasNext()) {
                    Map.Entry<String, JsonNode> e = i.next();
                    JsonNode user = e.getValue();
                    mUsers.updateAway(object.cid(), user.get("nick").asText(), user.get("away").asBoolean() ? 1 : 0);
                    mUsers.updateHostmask(object.bid(), user.get("nick").asText(), user.get("usermask").asText());
                }
                if (!backlog)
                    notifyHandlers(EVENT_MEMBERUPDATES, null);
            }
        });
        put("user_away", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                Buffer b = mBuffers.getBuffer(object.bid());
                if(b != null && b.isConsole()) {
                    mUsers.updateAwayMsg(object.cid(), object.getString("nick"), 1, object.getString("msg"));
                    b = mBuffers.getBufferByName(object.cid(), object.getString("nick"));
                    if (b != null)
                        b.setAway_msg(object.getString("msg"));
                    if (!backlog) {
                        notifyHandlers(EVENT_AWAY, object);
                    }
                }
            }
        });
        put("away", get("user_away"));
        put("user_back", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                Buffer b = mBuffers.getBuffer(object.bid());
                if(b != null && b.isConsole()) {
                    mUsers.updateAwayMsg(object.cid(), object.getString("nick"), 0, "");
                    b = mBuffers.getBufferByName(object.cid(), object.getString("nick"));
                    if (b != null)
                        b.setAway_msg("");
                    if (!backlog) {
                        notifyHandlers(EVENT_AWAY, object);
                    }
                }
            }
        });
        put("self_away", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                if (!backlog) {
                    mUsers.updateAwayMsg(object.cid(), object.getString("nick"), 1, object.getString("away_msg"));
                    if(mServers.getServer(object.cid()) != null)
                        mServers.getServer(object.cid()).setAway(object.getString("away_msg"));
                    notifyHandlers(EVENT_AWAY, object);
                }
            }
        });
        put("self_back", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                mUsers.updateAwayMsg(object.cid(), object.getString("nick"), 0, "");
                if(mServers.getServer(object.cid()) != null)
                    mServers.getServer(object.cid()).setAway("");
                if (!backlog)
                    notifyHandlers(EVENT_SELFBACK, object);
            }
        });
        put("self_details", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                Server s = mServers.getServer(object.cid());
                if(s != null) {
                    s.setUsermask(object.getString("usermask"));
                    if(object.has("server_realname"))
                        s.setServerRealname(object.getString("server_realname"));
                }
                Event e = mEvents.addEvent(object);
                if (!backlog) {
                    notifyHandlers(EVENT_SELFDETAILS, e);
                    e = mEvents.getEvent(e.eid + 1, e.bid);
                    if(e != null)
                        notifyHandlers(EVENT_SELFDETAILS, e);
                }
            }
        });
        put("user_mode", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                mEvents.addEvent(object);
                if (!backlog) {
                    Server s = mServers.getServer(object.cid());
                    if(s != null)
                        s.setMode(object.getString("newmode"));
                    notifyHandlers(EVENT_USERMODE, object);
                }
            }
        });

        //Message updates
        put("empty_msg", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                pendingEdits.add(object);
                process_pending_edits(backlog);
            }
        });

        //Various lists
        put("ban_list", new BroadcastParser(EVENT_BANLIST));
        put("accept_list", new BroadcastParser(EVENT_ACCEPTLIST));
        put("names_reply", new BroadcastParser(EVENT_NAMESLIST));
        put("whois_response", new BroadcastParser(EVENT_WHOIS));
        put("list_response_fetching", new BroadcastParser(EVENT_LISTRESPONSEFETCHING));
        put("list_response_toomany", new BroadcastParser(EVENT_LISTRESPONSETOOMANY));
        put("list_response", new BroadcastParser(EVENT_LISTRESPONSE));
        put("map_list", new BroadcastParser(EVENT_SERVERMAPLIST));
        put("quiet_list", new BroadcastParser(EVENT_QUIETLIST));
        put("ban_exception_list", new BroadcastParser(EVENT_BANEXCEPTIONLIST));
        put("invite_list", new BroadcastParser(EVENT_INVITELIST));
        put("chanfilter_list", new BroadcastParser(EVENT_CHANFILTERLIST));
        put("channel_query", new BroadcastParser(EVENT_CHANNELQUERY));
        put("text", new BroadcastParser(EVENT_TEXTLIST));
        put("who_special_response", new BroadcastParser(EVENT_WHOSPECIALRESPONSE));
        put("modules_list", new BroadcastParser(EVENT_MODULESLIST));
        put("links_response", new BroadcastParser(EVENT_LINKSRESPONSE));
        put("whowas_response", new BroadcastParser(EVENT_WHOWAS));
        put("trace_response", new BroadcastParser(EVENT_TRACERESPONSE));
        put("export_finished", new BroadcastParser(EVENT_LOGEXPORTFINISHED));
        put("avatar_change", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                User u = mUsers.getUser(object.bid(), object.getString("nick"));
                if(u != null) {
                    Event e = new Event();
                    e.cid = object.cid();
                    e.from = object.getString("nick");
                    e.type = "buffer_msg";
                    e.hostmask = u.hostmask;
                    e.avatar = object.getString("avatar");
                    e.avatar_url = object.getString("avatar_url");

                    String avatar_url = e.getAvatarURL(AvatarsList.SHORTCUT_ICON_SIZE());
                    AvatarsList.setAvatarURL(object.cid(), object.getString("nick"), System.currentTimeMillis() * 1000L, avatar_url);
                    RecentConversationsList.getInstance().publishShortcuts();
                    NotificationsList.getInstance().showNotificationsNow();
                }
                if(object.getBoolean("self")) {
                    Server s = mServers.getServer(object.cid());
                    if (s != null) {
                        s.setAvatar(object.getString("avatar"));
                        s.setAvatarURL(object.getString("avatar_url"));

                        Event e = new Event();
                        e.cid = s.getCid();
                        e.from = s.getNick();
                        e.type = "buffer_msg";
                        e.hostmask = s.getUsermask();
                        e.avatar = s.getAvatar();
                        e.avatar_url = s.getAvatarURL();

                        String avatar_url = e.getAvatarURL(AvatarsList.SHORTCUT_ICON_SIZE());

                        NotificationsList.getInstance().updateServerNick(object.cid(), object.getString("nick"), avatar_url, s.isSlack());
                    }
                }
                notifyHandlers(EVENT_AVATARCHANGE, object);
            }
        });
        put("who_response", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                if (!backlog) {
                    Buffer b = mBuffers.getBufferByName(object.cid(), object.getString("subject"));
                    if (b != null) {
                        JsonNode users = object.getJsonNode("users");
                        for (int i = 0; i < users.size(); i++) {
                            JsonNode user = users.get(i);
                            mUsers.updateHostmask(b.getBid(), user.get("nick").asText(), user.get("usermask").asText());
                            mUsers.updateAway(b.getCid(), user.get("nick").asText(), user.get("away").asBoolean() ? 1 : 0);
                        }
                    }
                    notifyHandlers(EVENT_WHOLIST, object);
                }
            }
        });
        put("time", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                Event e = mEvents.addEvent(object);
                if (!backlog) {
                    notifyHandlers(EVENT_ALERT, object);
                    notifyHandlers(EVENT_BUFFERMSG, e);
                }
            }
        });
        put("channel_topic_is", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                Buffer b = mBuffers.getBufferByName(object.cid(), object.getString("chan"));
                if (b != null) {
                    Channel c = mChannels.getChannelForBuffer(b.getBid());
                    if (c != null) {
                        c.topic_author = object.has("author") ? object.getString("author") : object.getString("server");
                        c.topic_time = object.getLong("time");
                        c.topic_text = object.getString("text");
                    }
                }
                if (!backlog)
                    notifyHandlers(EVENT_CHANNELTOPICIS, object);
            }
        });
        put("watch_status", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                object.setEid(System.currentTimeMillis() * 1000L);
                Event e = mEvents.addEvent(object);
                if (!backlog) {
                    notifyHandlers(EVENT_WATCHSTATUS, object);
                    notifyHandlers(EVENT_BUFFERMSG, e);
                }
            }
        });
    }};

    //https://stackoverflow.com/a/11459962
    private static JsonNode mergeJsonNode(JsonNode mainNode, JsonNode updateNode) {
        Iterator<String> fieldNames = updateNode.fieldNames();
        while (fieldNames.hasNext()) {
            String fieldName = fieldNames.next();
            JsonNode jsonNode = mainNode.get(fieldName);
            // if field exists and is an embedded object
            if (jsonNode != null && jsonNode.isObject()) {
                mergeJsonNode(jsonNode, updateNode.get(fieldName));
            }
            else {
                if (mainNode instanceof ObjectNode) {
                    // Overwrite field
                    JsonNode value = updateNode.get(fieldName);
                    ((ObjectNode) mainNode).put(fieldName, value);
                }
            }
        }

        return mainNode;
    }

    private synchronized void process_pending_edits(boolean backlog) {
        ArrayList<IRCCloudJSONObject> edits = pendingEdits;
        pendingEdits = new ArrayList<>();

        for(IRCCloudJSONObject o : edits) {
            JsonNode entities = o.getJsonNode("entities");
            if(entities != null) {
                if (entities.has("delete")) {
                    Event e = mEvents.getEvent(o.bid(), entities.get("delete").asText());
                    if (e != null) {
                        mEvents.deleteEvent(e.eid, e.bid);
                        if(!backlog)
                            notifyHandlers(EVENT_MESSAGECHANGE, o);
                    } else {
                        pendingEdits.add(o);
                    }
                } else if(entities.has("edit")) {
                    Event e = mEvents.getEvent(o.bid(), entities.get("edit").asText());
                    if(e != null) {
                        if (o.eid() >= e.lastEditEID) {
                            if (entities.has("edit_text")) {
                                e.msg = TextUtils.htmlEncode(Normalizer.normalize(entities.get("edit_text").asText(), Normalizer.Form.NFC)).replace("  ", "&nbsp; ");
                                if (e.msg.startsWith(" "))
                                    e.msg = "&nbsp;" + e.msg.substring(1);
                                e.edited = true;
                                if(e.entities instanceof ObjectNode) {
                                    if(e.entities.has("mentions"))
                                        ((ObjectNode)e.entities).remove("mentions");
                                    if(e.entities.has("mention_data"))
                                        ((ObjectNode)e.entities).remove("mention_data");
                                }
                            }
                            if(e.entities != null) {
                                mergeJsonNode(e.entities, entities);
                            } else {
                                e.entities = entities;
                            }
                            e.lastEditEID = o.eid();
                            e.formatted = null;
                            e.html = null;
                            e.ready_for_display = false;
                            e.linkified = false;
                        }
                        if(!backlog)
                            notifyHandlers(EVENT_MESSAGECHANGE, o);
                    } else {
                        pendingEdits.add(o);
                    }
                }
            }
        }

        if(pendingEdits.size() > 0)
            IRCCloudLog.Log(Log.INFO, TAG, "Queued pending edits: " + pendingEdits.size());
    }

    public synchronized void parse_object(IRCCloudJSONObject object, boolean backlog) throws JSONException {
        cancel_idle_timer();
        //Log.d(TAG, "Backlog: " + backlog + " count: " + currentcount + " New event: " + object);
        if (!object.has("type")) {
            //Log.d(TAG, "Response: " + object);
            if (object.has("success") && !object.getBoolean("success") && object.has("message")) {
                IRCCloudLog.Log(Log.ERROR, TAG, "Error: " + object);
                if(object.getString("message").equals("auth")) {
                    logout();
                    notifyHandlers(EVENT_AUTH_FAILED, object);
                } else if(object.getString("message").equals("set_shard")) {
                    disconnect();
                    ready = false;
                    SharedPreferences.Editor editor = IRCCloudApplication.getInstance().getSharedPreferences("prefs", 0).edit();
                    editor.putString("session_key", object.getString("cookie"));
                    if (object.has("websocket_path")) {
                        IRCCLOUD_PATH = object.getString("websocket_path");
                    }
                    editor.putString("path", NetworkConnection.IRCCLOUD_PATH);
                    editor.apply();
                    if (object.has("api_host")) {
                        set_api_host(object.getString("api_host"));
                    }
                    connect();
                } else if(object.getString("message").equals("temp_unavailable")) {
                    notifyHandlers(EVENT_TEMP_UNAVAILABLE, object);
                } else if(object.getString("message").equals("invalid_nick")) {
                    notifyHandlers(EVENT_ALERT, object);
                } else if(backlog) {
                    disconnect();
                    fail();
                }
            }
            if (object.has("_reqid") && resultCallbacks.containsKey(object.getInt("_reqid"))) {
                resultCallbacks.get(object.getInt("_reqid")).onIRCResult(object);
                resultCallbacks.remove(object.getInt("_reqid"));
            }
            return;
        }
        String type = object.type();
        if (type != null && type.length() > 0) {
            //notifyHandlers(EVENT_DEBUG, "Type: " + type + " BID: " + object.bid() + " EID: " + object.eid());
            //IRCCloudLog.Log("New event: " + type);
            //Log.d(TAG, "New event: " + type);
            if ((backlog || accrued > 0) && object.bid() > -1 && object.bid() != currentBid && object.eid() > 0) {
                if(!backlog) {
                    if(firstEid == -1) {
                        firstEid = object.eid();
                        if (firstEid > highest_eid) {
                            Log.w("IRCCloud", "Backlog gap detected, purging cache and reconnecting");
                            highest_eid = 0;
                            streamId = null;
                            failCount = 0;
                            if (client != null)
                                client.disconnect();
                            return;
                        }
                    }
                }
                currentBid = object.bid();
                currentcount = 0;
            }
            Parser p = parserMap.get(type);
            if (p != null) {
                p.parse(object, backlog);
            } else if (!parserMap.containsKey(type)) {
                IRCCloudLog.Log(Log.WARN, TAG, "Unhandled type: " + object.type());
                //Log.w(TAG, "Unhandled type: " + object);
            }

            if (backlog || type.equals("backlog_complete") || accrued > 0) {
                if ((object.bid() > -1 || type.equals("backlog_complete")) && !type.equals("makebuffer") && !type.equals("channel_init")) {
                    currentcount++;
                    if (object.bid() != currentBid) {
                        currentBid = object.bid();
                        currentcount = 0;
                    }
                }
            }
            if (backlog || type.equals("backlog_complete")) {
                if (numbuffers > 0 && currentcount < 100) {
                    notifyHandlers(EVENT_PROGRESS, ((totalbuffers + ((float) currentcount / (float) 100)) / numbuffers) * 1000.0f);
                }
            } else if (accrued > 0) {
                notifyHandlers(EVENT_PROGRESS, ((float) currentcount++ / (float) accrued) * 1000.0f);
            }
            if((!backlog || numbuffers > 0) && object.eid() > highest_eid) {
                highest_eid = object.eid();
                /*if(!backlog) {
                    SharedPreferences.Editor editor = IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0).edit();
                    editor.putLong("highest_eid", highest_eid);
                    editor.apply();
                }*/
            }
        }

        if (!backlog && idle_interval > 0 && accrued < 1)
            schedule_idle_timer();
    }

    public boolean isWifi() {
        ConnectivityManager cm = (ConnectivityManager) IRCCloudApplication.getInstance().getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo ni = cm.getActiveNetworkInfo();
        return ni != null && ni.getType() == ConnectivityManager.TYPE_WIFI;
    }

    public String fetch(URL url, String postdata, String sk, String token, HashMap<String, String>headers) throws Exception {
        if(BuildConfig.MOCK_DATA)
            return null;

        HttpURLConnection conn = null;

        Proxy proxy = null;
        int port = -1;

        String host = System.getProperty("http.proxyHost", null);
        try {
            port = Integer.parseInt(System.getProperty("http.proxyPort", "8080"));
        } catch (NumberFormatException e) {
            port = -1;
        }

        if (host != null && host.length() > 0 && !host.equalsIgnoreCase("localhost") && !host.equalsIgnoreCase("127.0.0.1") && port > 0) {
            InetSocketAddress proxyAddr = new InetSocketAddress(host, port);
            proxy = new Proxy(Proxy.Type.HTTP, proxyAddr);
        }

        if (host != null && host.length() > 0 && !host.equalsIgnoreCase("localhost") && !host.equalsIgnoreCase("127.0.0.1") && port > 0) {
            IRCCloudLog.Log(Log.DEBUG, TAG, "Requesting via proxy: " + host);
        }

        HttpMetric metric = null;
        try {
            metric = FirebasePerformance.getInstance().newHttpMetric(url, postdata != null ? FirebasePerformance.HttpMethod.POST : FirebasePerformance.HttpMethod.GET);
        } catch (Exception e) {

        }
        if (url.getProtocol().toLowerCase().equals("https")) {
            HttpsURLConnection https = (HttpsURLConnection) ((proxy != null) ? url.openConnection(proxy) : url.openConnection(Proxy.NO_PROXY));
            https.setSSLSocketFactory(TrustKit.getInstance().getSSLSocketFactory(url.getHost()));
            conn = https;
        } else {
            conn = (HttpURLConnection) ((proxy != null) ? url.openConnection(proxy) : url.openConnection(Proxy.NO_PROXY));
        }

        conn.setReadTimeout(30000);
        conn.setConnectTimeout(30000);
        conn.setUseCaches(false);
        conn.setRequestProperty("User-Agent", useragent);
        conn.setRequestProperty("Accept", "application/json");
        if(headers != null) {
            for (String key : headers.keySet()) {
                conn.setRequestProperty(key, headers.get(key));
            }
        }
        if (sk != null && url.getProtocol().equals("https") && url.getUserInfo() == null && (url.getHost().equals(IRCCLOUD_HOST) || (!BuildConfig.ENTERPRISE && url.getHost().endsWith(".irccloud.com"))))
            conn.setRequestProperty("Cookie", "session=" + sk);
        if (token != null)
            conn.setRequestProperty("x-auth-formtoken", token);
        if (postdata != null) {
            conn.setRequestMethod("POST");
            conn.setDoOutput(true);
            conn.setRequestProperty("Content-type", "application/x-www-form-urlencoded");
            OutputStream ostr = null;
            try {
                ostr = conn.getOutputStream();
                ostr.write(postdata.getBytes());
            } catch (Exception e) {
                printStackTraceToCrashlytics(e);
            } finally {
                if (ostr != null)
                    ostr.close();
            }
            if(metric != null)
                metric.setRequestPayloadSize(postdata.length());
        }
        InputStream is = null;
        String response = "";

        try {
            ConnectivityManager cm = (ConnectivityManager) IRCCloudApplication.getInstance().getSystemService(Context.CONNECTIVITY_SERVICE);
            NetworkInfo ni = cm.getActiveNetworkInfo();
            if (ni != null && ni.getType() == ConnectivityManager.TYPE_WIFI) {
                IRCCloudLog.Log(Log.DEBUG, TAG, "Loading via WiFi");
            } else {
                IRCCloudLog.Log(Log.DEBUG, TAG, "Loading via mobile");
            }
        } catch (Exception e) {
        }

        if(metric != null)
            metric.start();

        try {
            if (conn.getInputStream() != null) {
                is = conn.getInputStream();
            }
        } catch (IOException e) {
            if (conn.getErrorStream() != null) {
                is = conn.getErrorStream();
            } else {
                printStackTraceToCrashlytics(e);
            }
        }

        if (is != null) {
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            byte[] buffer = new byte[8192];
            int len;
            while ((len = is.read(buffer)) != -1) {
                os.write(buffer, 0, len);
            }
            response = os.toString("UTF-8");
        }
        if(metric != null) {
            metric.setResponsePayloadSize(response.length());
            metric.setHttpResponseCode(conn.getResponseCode());
            metric.setResponseContentType(conn.getContentType());
            metric.stop();
        }
        conn.disconnect();
        return response;
    }

    public void addHandler(IRCEventHandler handler) {
        synchronized (handlers) {
            if (!handlers.contains(handler))
                handlers.add(handler);
            if (saveTimerTask != null)
                saveTimerTask.cancel();
            saveTimerTask = null;
        }
    }

    @TargetApi(24)
    public void removeHandler(IRCEventHandler handler) {
        synchronized (handlers) {
            handlers.remove(handler);
        }
    }

    public boolean uploadsAvailable() {
        return userInfo != null && !userInfo.uploads_disabled;
    }

    public boolean isVisible() {
        return (handlers != null && handlers.size() > 0);
    }

    public void notifyHandlers(int message, Object object) {
        notifyHandlers(message, object, null);
    }

    public void notifyHandlers(int message, final Object object, IRCEventHandler exclude) {
        synchronized (handlers) {
            int bid;

            switch(message) {
                case EVENT_OOB_START:
                    numbuffers = 0;
                    totalbuffers = 0;
                    currentBid = -1;
                    break;
                case EVENT_OOB_END:
                    bid = ((OOBFetcher)object).getBid();
                    ArrayList<Buffer> buffers = mBuffers.getBuffers();
                    for (Buffer b : buffers) {
                        if (b.getTimeout() > 0) {
                            IRCCloudLog.Log(Log.DEBUG, TAG, "Requesting backlog for timed-out buffer: bid" + b.getBid());
                            request_backlog(b.getCid(), b.getBid(), 0);
                        }

                        if(oobTasks.size() > 10)
                            break;
                    }
                    NotificationsList.getInstance().deleteOldNotifications();
                    NotificationsList.getInstance().pruneNotificationChannels();
                    if (bid != -1) {
                        Buffer b = mBuffers.getBuffer(bid);
                        if(b != null) {
                            b.setTimeout(0);
                            b.setDeferred(0);
                        }
                    }
                    oobTasks.remove(bid);
                    if(oobTasks.size() > 0)
                        oobTasks.values().toArray(new OOBFetcher[oobTasks.values().size()])[0].connect();
                    break;
                case EVENT_OOB_FAILED:
                    bid = ((OOBFetcher)object).getBid();
                    if (bid == -1) {
                        IRCCloudLog.Log(Log.ERROR, TAG, "Failed to fetch the initial backlog, reconnecting!");
                        /*try {
                            IRCCloudDatabase.getInstance().endTransaction();
                        } catch (IllegalStateException e) {

                        }*/
                        streamId = null;
                        highest_eid = 0;
                        if (client != null)
                            client.disconnect();
                        return;
                    } else {
                        Buffer b = mBuffers.getBuffer(bid);
                        if (b != null && b.getTimeout() == 1) {
                            //TODO: move this
                            int retryDelay = 1000;
                            IRCCloudLog.Log(Log.WARN, TAG, "Failed to fetch backlog for timed-out buffer, retrying in " + retryDelay + "ms");
                            idleTimer.schedule(new TimerTask() {
                                public void run() {
                                    ((OOBFetcher)object).connect();
                                }
                            }, retryDelay);
                            retryDelay *= 2;
                        } else {
                            IRCCloudLog.Log(Log.ERROR, TAG, "Failed to fetch backlog");
                            synchronized (oobTasks) {
                                oobTasks.remove(bid);
                                if(oobTasks.size() > 0)
                                    oobTasks.values().toArray(new OOBFetcher[oobTasks.values().size()])[0].connect();
                            }
                        }
                    }
                    break;
                default:
                    break;
            }

            if (message == EVENT_PROGRESS || accrued == 0) {
                for (int i = 0; i < handlers.size(); i++) {
                    IRCEventHandler handler = handlers.get(i);
                    if (handler != exclude) {
                        handler.onIRCEvent(message, object);
                    }
                }
            }
        }
    }

    public UserInfo getUserInfo() {
        return userInfo;
    }

    public class UserInfo {
        public int id;
        public String name;
        public String email;
        public boolean verified;
        public int last_selected_bid;
        public long connections;
        public long active_connections;
        public long join_date;
        public boolean auto_away;
        public String limits_name;
        public ObjectNode limits;
        public int num_invites;
        public JSONObject prefs;
        public String highlights;
        public boolean uploads_disabled;
        public String avatar;

        public UserInfo() {
        }

        public UserInfo(IRCCloudJSONObject object) {
            SharedPreferences.Editor editor = IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0).edit();
            editor.putString("userinfo", object.toString());
            editor.apply();

            id = object.getInt("id");
            IRCCloudLog.Log(Log.INFO, "IRCCloud", "Setting UserInfo for uid" + id);

            name = object.getString("name");
            email = object.getString("email");
            verified = object.getBoolean("verified");
            last_selected_bid = object.getInt("last_selected_bid");
            connections = object.getLong("num_connections");
            active_connections = object.getLong("num_active_connections");
            join_date = object.getLong("join_date");
            auto_away = object.getBoolean("autoaway");
            uploads_disabled = object.has("uploads_disabled") && object.getBoolean("uploads_disabled");
            if(object.has("avatar") && object.getString("avatar").length() > 0 && !object.getString("avatar").equals("null"))
                avatar = object.getString("avatar");
            else
                avatar = null;

            if (object.has("prefs") && object.getString("prefs").length() > 0 && !object.getString("prefs").equals("null")) {
                try {
                    IRCCloudLog.Log(Log.INFO, "IRCCloud", "Prefs: " + object.getString("prefs"));
                    prefs = new JSONObject(object.getString("prefs"));
                } catch (JSONException e) {
                    IRCCloudLog.Log(Log.ERROR, "IRCCloud", "Unable to parse prefs: " + object.getString("prefs"));
                    IRCCloudLog.LogException(e);
                    prefs = null;
                }
            } else {
                IRCCloudLog.Log(Log.INFO, "IRCCloud", "User prefs not set");
                prefs = null;
            }

            limits_name = object.getString("limits_name");
            limits = object.getJsonObject("limits");

            if (object.has("highlights")) {
                JsonNode h = object.getJsonNode("highlights");
                highlights = "";
                for (int i = 0; i < h.size(); i++) {
                    if (highlights.length() > 0)
                        highlights += ", ";
                    highlights += h.get(i).asText();
                }
            }
        }
    }

    public static void printStackTraceToCrashlytics(Exception e) {
        StringWriter sw = new StringWriter();
        e.printStackTrace(new PrintWriter(sw));
        String stack = sw.toString();
        for(String s : stack.split("\n")) {
            IRCCloudLog.Log(Log.WARN, TAG, s);
        }
    }

    public static void sendFeedbackReport(Activity ctx) {
        try {
            String bugReport = "Briefly describe the issue below:\n\n\n\n\n" +
                    "===========\n" +
                    "UID: " + PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext()).getString("uid", "") + "\n" +
                    "App version: " + ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), 0).versionName + " (" + ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), 0).versionCode + ")\n" +
                    "Device: " + Build.MODEL + "\n" +
                    "Android version: " + Build.VERSION.RELEASE + "\n" +
                    "Firmware fingerprint: " + Build.FINGERPRINT + "\n";

            File f = new File(ctx.getFilesDir(), "logs");
            f.mkdirs();
            File output = new File(f, "log.txt");

            FileOutputStream out = new FileOutputStream(output);
            out.write(IRCCloudLog.lines().getBytes());
            out.close();

            Intent email = new Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:team@irccloud.com"));
            email.putExtra(Intent.EXTRA_SUBJECT, "IRCCloud for Android");

            List<ResolveInfo> resolveInfos = ctx.getPackageManager().queryIntentActivities(email, 0);
            List<LabeledIntent> intents = new ArrayList<>();
            for (ResolveInfo info : resolveInfos) {
                Intent intent = new Intent(Intent.ACTION_SEND);
                intent.setComponent(new ComponentName(info.activityInfo.packageName, info.activityInfo.name));
                intent.setType("message/rfc822");
                intent.putExtra(Intent.EXTRA_EMAIL, new String[]{"IRCCloud Team <team@irccloud.com>"});
                intent.putExtra(Intent.EXTRA_TEXT, bugReport);
                intent.putExtra(Intent.EXTRA_SUBJECT, "IRCCloud for Android");
                intent.putExtra(Intent.EXTRA_STREAM, FileProvider.getUriForFile(ctx, ctx.getPackageName() + ".fileprovider", output));
                intents.add(new LabeledIntent(intent, info.activityInfo.packageName, info.loadLabel(ctx.getPackageManager()), info.icon));
            }
            Intent chooser = Intent.createChooser(intents.remove(intents.size() - 1), "Send Feedback:");
            chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, intents.toArray(new LabeledIntent[intents.size()]));
            ctx.startActivity(chooser);
        } catch (Exception e) {
            Toast.makeText(ctx, "Unable to generate email report: " + e.getMessage(), Toast.LENGTH_SHORT).show();
            IRCCloudLog.LogException(e);
            NetworkConnection.printStackTraceToCrashlytics(e);
        }
    }
} public class SeparatedListAdapter extends BaseAdapter {

    private final Map<String,Adapter> sections = new LinkedHashMap<String,Adapter>();
    private final ArrayAdapter<String> headers;
    private final static int TYPE_SECTION_HEADER = 0;

    public SeparatedListAdapter(Context context) {
        headers = new ArrayAdapter<String>(context, R.layout.list_header);
    }

    public void addSection(String section, Adapter adapter) {
        this.headers.add(section);
        this.sections.put(section, adapter);
    }

    public Object getItem(int position) {
        for(String section : this.sections.keySet()) {
            Adapter adapter = sections.get(section);
            int size = adapter.getCount() + 1;

            // check if position inside this section
            if(position == 0) return section;
            if(position < size) return adapter.getItem(position - 1);

            // otherwise jump into next section
            position -= size;
        }
        return null;
    }

    public int getCount() {
        // total together all sections, plus one for each section header
        int total = 0;
        for(Adapter adapter : this.sections.values())
            total += adapter.getCount() + 1;
        return total;
    }

    public int getViewTypeCount() {
        // assume that headers count as one, then total all sections
        int total = 1;
        for(Adapter adapter : this.sections.values())
            total += adapter.getViewTypeCount();
        return total;
    }

    public int getItemViewType(int position) {
        int type = 1;
        for(String section : this.sections.keySet()) {
            Adapter adapter = sections.get(section);
            int size = adapter.getCount() + 1;

            // check if position inside this section
            if(position == 0) return TYPE_SECTION_HEADER;
            if(position < size) return type + adapter.getItemViewType(position - 1);

            // otherwise jump into next section
            position -= size;
            type += adapter.getViewTypeCount();
        }
        return -1;
    }

    public boolean areAllItemsSelectable() {
        return false;
    }

    public boolean isEnabled(int position) {
        return (getItemViewType(position) != TYPE_SECTION_HEADER);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        int sectionnum = 0;
        for(String section : this.sections.keySet()) {
            Adapter adapter = sections.get(section);
            int size = adapter.getCount() + 1;

            // check if position inside this section
            if(position == 0) return headers.getView(sectionnum, convertView, parent);
            if(position < size) return adapter.getView(position - 1, convertView, parent);

            // otherwise jump into next section
            position -= size;
            sectionnum++;
        }
        return null;
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

} @Database(entities = {RecentConversation.class, BackgroundTask.class, LogExport.class, Notification.class, Notification_LastSeenEID.class, Notification_ServerNick.class, Avatar.class}, version = IRCCloudDatabase.VERSION, exportSchema = false)
public abstract class IRCCloudDatabase extends RoomDatabase {
    public static final String NAME = "irccloud";
    public static final int VERSION = 13;

    public abstract RecentConversationsList.RecentConversationsDao RecentConversationsDao();
    public abstract BackgroundTaskWorker.BackgroundTasksDao BackgroundTasksDao();
    public abstract LogExportsList.LogExportsDao LogExportsDao();
    public abstract NotificationsList.NotificationsDao NotificationsDao();
    public abstract AvatarsList.AvatarsDao AvatarsDao();

    private static IRCCloudDatabase sInstance;
    public static IRCCloudDatabase getInstance() {
        if (sInstance == null) {
            synchronized (IRCCloudDatabase.class) {
                if (sInstance == null) {
                    sInstance = Room.databaseBuilder(IRCCloudApplication.getInstance().getApplicationContext(), IRCCloudDatabase.class, NAME)
                            .fallbackToDestructiveMigrationFrom(12)
                            .fallbackToDestructiveMigration()
                            .fallbackToDestructiveMigrationOnDowngrade()
                            .allowMainThreadQueries()
                            .build();
                }
            }
        }
        return sInstance;
    }
} public class BuffersList {
    public class comparator implements Comparator<Buffer> {
        public int compare(Buffer b1, Buffer b2) {
            if (b1.getCid() < b2.getCid())
                return -1;
            if (b1.getCid() > b2.getCid())
                return 1;
            if (b1.isConsole())
                return -1;
            if (b2.isConsole())
                return 1;
            if (b1.getBid() == b2.getBid())
                return 0;
            int joined1 = 1, joined2 = 1;
            Channel c = ChannelsList.getInstance().getChannelForBuffer(b1.getBid());
            if (c == null)
                joined1 = 0;
            c = ChannelsList.getInstance().getChannelForBuffer(b2.getBid());
            if (c == null)
                joined2 = 0;
            if ((b1.isConversation() || b1.isMPDM()) && (b2.isChannel() && !b2.isMPDM())) {
                return 1;
            } else if ((b1.isChannel() && !b1.isMPDM()) && (b2.isConversation() || b2.isMPDM())) {
                return -1;
            } else if (joined1 != joined2) {
                return joined2 - joined1;
            } else {
                if (comparator.compare(b1.normalizedName(), b2.normalizedName()) == 0)
                    return (b1.getBid() < b2.getBid()) ? -1 : 1;
                return comparator.compare(b1.normalizedName(), b2.normalizedName());
            }
        }
    }

    private ArrayList<Buffer> buffers;
    private SparseArray<Buffer> buffers_indexed;
    private AlphanumComparator comparator;

    private static BuffersList instance = null;
    public boolean dirty = true;

    public synchronized static BuffersList getInstance() {
        if (instance == null)
            instance = new BuffersList();
        return instance;
    }

    public BuffersList() {
        buffers = new ArrayList<>();
        buffers_indexed = new SparseArray<>();
        Collator collator = Collator.getInstance();
        collator.setStrength(Collator.SECONDARY);
        comparator = new AlphanumComparator(collator);
    }

    public void clear() {
        buffers.clear();
        buffers_indexed.clear();
        //Delete.table(Buffer.class);
    }

    public void load() {
        /*Cursor c = new Select().all().from(Buffer.class).query();
        try {
            long start = System.currentTimeMillis();
            ModelAdapter<Buffer> modelAdapter = FlowManager.getModelAdapter(Buffer.class);
            if (c != null && c.moveToFirst()) {
                buffers = new ArrayList<>(c.getCount());
                buffers_indexed = new SparseArray<>(c.getCount());
                do {
                    Buffer b = modelAdapter.loadFromCursor(c);
                    buffers.add(b);
                    buffers_indexed.put(b.getBid(), b);
                } while(c.moveToNext());
                long time = System.currentTimeMillis() - start;
                android.util.Log.i("IRCCloud", "Loaded " + c.getCount() + " buffers in " + time + "ms");
            }
        } catch (SQLiteException e) {
            buffers.clear();
            buffers_indexed.clear();
        } finally {
            if(c != null)
                c.close();
        }*/
    }

    public void save() {
        //TransactionManager.getInstance().saveOnSaveQueue(buffers);
    }


    public int count() {
        return buffers.size();
    }

    public int firstBid() {
        if (buffers_indexed.size() > 0)
            return buffers_indexed.valueAt(0).getBid();
        else
            return -1;
    }

    public synchronized Buffer createBuffer(int bid, int cid, long min_eid, long last_seen_eid, String name, String type, int archived, int deferred, int timeout, long created) {
        Buffer b = getBuffer(bid);
        if (b == null) {
            b = new Buffer();
            buffers.add(b);
            buffers_indexed.put(bid, b);
        }
        b.setBid(bid);
        b.setCid(cid);
        b.setMin_eid(min_eid);
        b.setLast_seen_eid(last_seen_eid);
        b.setName(name);
        b.setType(type);
        b.setArchived(archived);
        b.setDeferred(deferred);
        b.setTimeout(timeout);
        b.setValid(1);
        b.setCreated(created);
        if(b.getServer() != null)
            b.setServerIsSlack(b.getServer().isSlack());
        if(EventsList.getInstance().lastEidForBuffer(bid) <= last_seen_eid) {
            b.setUnread(0);
            b.setHighlights(0);
        }
        dirty = true;
        return b;
    }

    public synchronized void deleteAllDataForBuffer(int bid) {
        Buffer b = getBuffer(bid);
        if (b != null) {
            if (b.isChannel()) {
                ChannelsList.getInstance().deleteChannel(bid);
                UsersList.getInstance().deleteUsersForBuffer(b.getBid());
            }
            EventsList.getInstance().deleteEventsForBuffer(bid);
            RecentConversationsList.getInstance().deleteConversation(b.getCid(), b.getBid());
            buffers.remove(b);
            //b.delete();
        }
        buffers_indexed.remove(bid);
        dirty = true;
    }

    public synchronized Buffer getBuffer(int bid) {
        return buffers_indexed.get(bid);
    }

    public synchronized Buffer getBufferByName(int cid, String name) {
        for (Buffer b : buffers) {
            if (b.getCid() == cid && b.getName().equalsIgnoreCase(name))
                return b;
        }
        return null;
    }

    public synchronized ArrayList<Buffer> getBuffersForServer(int cid) {
        ArrayList<Buffer> list = new ArrayList<>();
        if (dirty) {
            try {
                Collections.sort(buffers, new comparator());
                dirty = false;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        for (Buffer b : buffers) {
            if (b.getCid() == cid)
                list.add(b);
        }
        return list;
    }

    public synchronized ArrayList<Buffer> getBuffers() {
        ArrayList<Buffer> list = new ArrayList<Buffer>();
        for (Buffer b : buffers) {
            list.add(b);
        }
        return list;
    }

    public synchronized void invalidate() {
        for (Buffer b : buffers) {
            b.setValid(0);
        }
    }

    public synchronized void purgeInvalidBIDs() {
        ArrayList<Buffer> buffersToRemove = new ArrayList<Buffer>();
        Iterator<Buffer> i = buffers.iterator();
        while (i.hasNext()) {
            Buffer b = i.next();
            if (b.getValid() == 0)
                buffersToRemove.add(b);
        }
        i = buffersToRemove.iterator();
        while (i.hasNext()) {
            Buffer b = i.next();
            EventsList.getInstance().deleteEventsForBuffer(b.getBid());
            ChannelsList.getInstance().deleteChannel(b.getBid());
            UsersList.getInstance().deleteUsersForBuffer(b.getBid());
            buffers.remove(b);
            buffers_indexed.remove(b.getBid());
            if (b.isConsole()) {
                ServersList.getInstance().deleteServer(b.getCid());
            }
            //b.delete();
        }
        dirty = true;
    }
} public class LogExportsList {
    @Dao
    public interface LogExportsDao {
        @Query("SELECT * FROM LogExport ORDER BY start_date DESC")
        List<LogExport> getLogExports();

        @Query("SELECT * FROM LogExport WHERE id = :id LIMIT 1")
        LogExport getLogExport(int id);

        @Query("SELECT * FROM LogExport WHERE download_id = :download_id LIMIT 1")
        LogExport getDownload(long download_id);

        @Insert(onConflict = OnConflictStrategy.REPLACE)
        void insert(LogExport logExport);

        @Insert(onConflict = OnConflictStrategy.REPLACE)
        void insertAll(List<LogExport> logExports);

        @Update
        void update(LogExport logExport);

        @Delete
        void delete(LogExport logExport);

        @Query("DELETE FROM LogExport")
        void clear();
    }

    private static LogExportsList instance = null;
    private final Object dbLock = new Object();

    public static LogExportsList getInstance() {
        if (instance == null)
            instance = new LogExportsList();
        return instance;
    }

    public void clear() {
        IRCCloudDatabase.getInstance().LogExportsDao().clear();
    }

    public List<LogExport> getLogExports() {
        return IRCCloudDatabase.getInstance().LogExportsDao().getLogExports();
    }

    public LogExport get(int id) {
        return IRCCloudDatabase.getInstance().LogExportsDao().getLogExport(id);
    }

    public LogExport getDownload(long downloadid) {
        return IRCCloudDatabase.getInstance().LogExportsDao().getDownload(downloadid);
    }

    public void update(JSONObject logs) {
        HashMap<Integer, LogExport> dbCache = new HashMap<>();
        final ArrayList<LogExport> add = new ArrayList<>();

        synchronized (dbLock) {
            for(LogExport e : getLogExports()) {
                dbCache.put(e.getId(), e);
            }
        }

        try {
            add.addAll(createAll(dbCache, logs.getJSONArray("inprogress")));
            add.addAll(createAll(dbCache, logs.getJSONArray("available")));

            for(int i = 0; i < logs.getJSONArray("expired").length(); i++) {
                JSONObject log = logs.getJSONArray("expired").getJSONObject(i);
                if (dbCache.containsKey(log.getInt("id"))) {
                    LogExport e = dbCache.get(log.getInt("id"));
                    if (!e.file().exists())
                        IRCCloudDatabase.getInstance().LogExportsDao().delete(e);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        if(add.size() > 0)
            IRCCloudDatabase.getInstance().runInTransaction(new Runnable() {
                @Override
                public void run() {
                    IRCCloudDatabase.getInstance().LogExportsDao().insertAll(add);
                }
            });
    }

    public LogExport create(JSONObject log) throws JSONException {
        LogExport e = new LogExport();
        e.setId(log.getInt("id"));
        if(log.isNull("cid"))
            e.setCid(-1);
        else
            e.setCid(log.getInt("cid"));
        if(log.isNull("bid"))
            e.setBid(-1);
        else
            e.setBid(log.getInt("bid"));
        e.setFile_name(log.getString("file_name"));
        e.setRedirect_url(log.getString("redirect_url"));
        if(!log.isNull("startdate"))
            e.setStart_date(log.getLong("startdate"));
        if(!log.isNull("finishdate"))
            e.setFinish_date(log.getLong("finishdate"));
        if(!log.isNull("expirydate"))
            e.setExpiry_date(log.getLong("expirydate"));

        return e;
    }

    public LogExport create(JsonNode log) {
        LogExport e = new LogExport();
        e.setId(log.get("id").asInt());
        if(log.get("cid") != null && log.get("cid").isNull())
            e.setCid(-1);
        else
            e.setCid(log.get("cid").asInt());
        if(log.get("bid") != null && log.get("bid").isNull())
            e.setBid(-1);
        else
            e.setBid(log.get("bid").asInt());
        if(log.get("file_name") != null && !log.get("file_name").isNull())
            e.setFile_name(log.get("file_name").asText());
        if(log.get("redirect_url") != null && !log.get("redirect_url").isNull())
            e.setRedirect_url(log.get("redirect_url").asText());
        if(log.get("startdate") != null && !log.get("startdate").isNull())
            e.setStart_date(log.get("startdate").asLong());
        if(log.get("finishdate") != null && !log.get("finishdate").isNull())
            e.setFinish_date(log.get("finishdate").asLong());
        if(log.get("expirydate") != null && !log.get("expirydate").isNull())
            e.setExpiry_date(log.get("expirydate").asLong());

        return e;
    }

    private ArrayList<LogExport> createAll(HashMap<Integer, LogExport> dbCache, JSONArray logs) {
        ArrayList<LogExport> add = new ArrayList<>();

        for(int i = 0; i < logs.length(); i++) {
            try {
                JSONObject log = logs.getJSONObject(i);
                if (!dbCache.containsKey(log.getInt("id"))) {
                    add.add(create(log));
                }
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }

        return add;
    }
} public class Buffer extends BaseObservable {
    public static final String TYPE_CONSOLE = "console";
    public static final String TYPE_CHANNEL = "channel";
    public static final String TYPE_CONVERSATION = "conversation";
    public static final String TYPE_ARCHIVES_HEADER = "archives_header";
    public static final String TYPE_JOIN_CHANNEL = "join_channel";
    public static final String TYPE_SPAM = "spam";
    public static final String TYPE_COLLAPSED = "collapsed";
    public static final String TYPE_PINNED = "pinned";
    public static final String TYPE_ADD_NETWORK = "add_network";
    public static final String DEFAULT_CHANTYPES = "#&!+";

    private enum Type {
        CONSOLE, CHANNEL, CONVERSATION, ARCHIVES_HEADER, JOIN_CHANNEL, SPAM, COLLAPSED, PINNED, ADD_NETWORK, OTHER
    }

    private Server server = null;
    private ColorScheme colorScheme = ColorScheme.getInstance();

    public Server getServer() {
        if(server == null)
            server = ServersList.getInstance().getServer(this.cid);

        return server;
    }

    private int bid = -1;

    private int cid = -1;

    private long min_eid;

    private long last_seen_eid;

    private long created;

    private String name;

    private String type;

    private int archived;

    private int deferred;

    private int timeout;

    private String away_msg;

    private CharSequence draft;

    private String chan_types;

    private boolean scrolledUp;

    private int scrollPosition;

    private int scrollPositionOffset;

    private int unread;

    private int highlights;

    private int valid = 1;

    private Type type_int;

    private String displayName;

    private boolean serverIsSlack;
    private int isMPDM = -1;

    private boolean muted;

    private boolean showServerSuffix;

    public String toString() {
        return "{cid:" + getCid() + ", bid:" + getBid() + ", name: " + getName() + ", type: " + getType() + ", archived: " + getArchived() + "}";
    }

    public String normalizedName() {
        if(isMPDM())
            return getDisplayName().toLowerCase();

        if (getChan_types() == null || getChan_types().length() < 2) {
            Server s = ServersList.getInstance().getServer(getCid());
            if (s != null && s.CHANTYPES != null && s.CHANTYPES.length() > 0)
                setChan_types(s.CHANTYPES);
            else
                setChan_types(DEFAULT_CHANTYPES);
        }
        return getName().toLowerCase().replaceAll("^[" + getChan_types() + "]+", "");
    }


    public int getBid() {
        return bid;
    }

    public void setBid(int bid) {
        this.bid = bid;
    }

    public int getCid() {
        return cid;
    }

    public void setCid(int cid) {
        this.cid = cid;
    }

    public long getMin_eid() {
        return min_eid;
    }

    public void setMin_eid(long min_eid) {
        this.min_eid = min_eid;
    }

    public long getLast_seen_eid() {
        return last_seen_eid;
    }

    public void setLast_seen_eid(long last_seen_eid) {
        if(this.last_seen_eid < last_seen_eid)
            this.last_seen_eid = last_seen_eid;
    }

    public long getCreated() {
        return created;
    }

    public void setCreated(long created) {
        this.created = created;
    }

    @Bindable
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
        this.displayName = null;
        this.isMPDM = -1;
        if(this.bid != -1) {
            BuffersList.getInstance().dirty = true;
        }
    }

    public boolean isMPDM() {
        if(isMPDM == -1) {
            isMPDM = (serverIsSlack && name.matches("#mpdm-(.*)-\\d")) ? 1 : 0;
        }
        return isMPDM == 1;
    }

    public void showServerSuffix(boolean show) {
        this.showServerSuffix = show;
    }

    @Bindable
    public String getServerSuffix() {
        if(showServerSuffix && getServer() != null) {
            String name = getServer().getName();
            if(name == null || name.length() == 0)
                name = getServer().getHostname();
            return " (" + name + ")";
        }
        return "";
    }

    @Bindable
    public String getDisplayName() {
        if(isMPDM()) {
            if (displayName == null) {
                String selfNick = getServer().getNick();
                ArrayList<String> names = new ArrayList<String>(Arrays.asList(name.split("-")));
                names.remove(0);
                names.remove(names.size() - 1);
                for(int i = 0; i < names.size(); i++) {
                    String name = names.get(i);
                    if(name == null || name.length() == 0 || name.equalsIgnoreCase(selfNick)) {
                        names.remove(i);
                        i--;
                    }
                }
                Collator collator = Collator.getInstance();
                collator.setStrength(Collator.SECONDARY);
                Collections.sort(names, new AlphanumComparator(collator));
                displayName = TextUtils.join(", ", names);
            }
            return displayName;
        }
        return name;
    }

    public void setServerIsSlack(boolean serverIsSlack) {
        this.serverIsSlack = serverIsSlack;
        if(this.bid != -1) {
            BuffersList.getInstance().dirty = true;
        }
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
        switch(type) {
            case TYPE_CONSOLE:
                this.type_int = Type.CONSOLE;
                break;
            case TYPE_CHANNEL:
                this.type_int = Type.CHANNEL;
                break;
            case TYPE_CONVERSATION:
                this.type_int = Type.CONVERSATION;
                break;
            case TYPE_ARCHIVES_HEADER:
                this.type_int = Type.ARCHIVES_HEADER;
                break;
            case TYPE_JOIN_CHANNEL:
                this.type_int = Type.JOIN_CHANNEL;
                break;
            case TYPE_SPAM:
                this.type_int = Type.SPAM;
                break;
            case TYPE_COLLAPSED:
                this.type_int = Type.COLLAPSED;
                break;
            case TYPE_PINNED:
                this.type_int = Type.PINNED;
                break;
            case TYPE_ADD_NETWORK:
                this.type_int = Type.ADD_NETWORK;
                break;
            default:
                this.type_int = Type.OTHER;
                break;
        }
    }

    public boolean isConsole() {
        return type_int == Type.CONSOLE;
    }

    @Bindable
    public boolean getIsConsole() {
        return isConsole();
    }

    public boolean isChannel() {
        return type_int == Type.CHANNEL;
    }

    @Bindable
    public boolean getIsChannel() {
        return isChannel();
    }

    public boolean isConversation() {
        return type_int == Type.CONVERSATION;
    }

    @Bindable
    public boolean getIsConversation() {
        return isConversation();
    }

    @Bindable
    public boolean getIsGroupHeading() {
        return isConsole() || type_int == Type.OTHER;
    }

    @Bindable
    public boolean getIsArchivesHeader() {
        return type_int == Type.ARCHIVES_HEADER;
    }

    public int getArchived() {
        return archived;
    }

    public void setArchived(int archived) {
        this.archived = archived;
        if(this.bid != -1) {
            BuffersList.getInstance().dirty = true;
        }
    }

    public boolean isSpam() {
        return type_int == Type.SPAM;
    }

    @Bindable
    public boolean getIsSpam() {
        return isSpam();
    }

    public boolean isCollapsed() {
        return type_int == Type.COLLAPSED;
    }

    @Bindable
    public boolean getIsCollapsed() {
        return isCollapsed();
    }

    public int getDeferred() {
        return deferred;
    }

    public void setDeferred(int deferred) {
        this.deferred = deferred;
    }

    @Bindable
    public int getTimeout() {
        return timeout;
    }

    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }

    public String getAway_msg() {
        return away_msg;
    }

    public void setAway_msg(String away_msg) {
        this.away_msg = away_msg;
    }

    public CharSequence getDraft() {
        return draft;
    }

    public void setDraft(CharSequence draft) {
        this.draft = draft;
    }

    public String getChan_types() {
        return chan_types;
    }

    public void setChan_types(String chan_types) {
        this.chan_types = chan_types;
    }

    public boolean getScrolledUp() {
        return scrolledUp;
    }

    public void setScrolledUp(boolean scrolledUp) {
        this.scrolledUp = scrolledUp;
    }

    public int getScrollPosition() {
        return scrollPosition;
    }

    public void setScrollPosition(int scrollPosition) {
        this.scrollPosition = scrollPosition;
    }

    public int getScrollPositionOffset() {
        return scrollPositionOffset;
    }

    public void setScrollPositionOffset(int scrollPositionOffset) {
        this.scrollPositionOffset = scrollPositionOffset;
    }

    @Bindable
    public int getUnread() {
        JSONObject channelDisabledMap = null;
        JSONObject bufferDisabledMap = null;
        JSONObject channelEnabledMap = null;
        JSONObject bufferEnabledMap = null;
        NetworkConnection conn = NetworkConnection.getInstance();

        if (conn.getUserInfo() != null && conn.getUserInfo().prefs != null) {
            try {
                if (conn.getUserInfo().prefs.has("channel-disableTrackUnread"))
                    channelDisabledMap = conn.getUserInfo().prefs.getJSONObject("channel-disableTrackUnread");
                if (conn.getUserInfo().prefs.has("buffer-disableTrackUnread"))
                    bufferDisabledMap = conn.getUserInfo().prefs.getJSONObject("buffer-disableTrackUnread");
                if (conn.getUserInfo().prefs.has("channel-enableTrackUnread"))
                    channelEnabledMap = conn.getUserInfo().prefs.getJSONObject("channel-enableTrackUnread");
                if (conn.getUserInfo().prefs.has("buffer-enableTrackUnread"))
                    bufferEnabledMap = conn.getUserInfo().prefs.getJSONObject("buffer-enableTrackUnread");
            } catch (JSONException e1) {
                NetworkConnection.printStackTraceToCrashlytics(e1);
            }
        }

        try {
            if (type.equals(TYPE_CHANNEL) && channelDisabledMap != null && channelDisabledMap.has(String.valueOf(bid)) && channelDisabledMap.getBoolean(String.valueOf(bid))) {
                return 0;
            } else if(bufferDisabledMap != null && bufferDisabledMap.has(String.valueOf(bid)) && bufferDisabledMap.getBoolean(String.valueOf(bid))) {
                return 0;
            } else if (type.equals(TYPE_CHANNEL) && channelEnabledMap != null && channelEnabledMap.has(String.valueOf(bid)) && channelEnabledMap.getBoolean(String.valueOf(bid))) {
                return unread;
            } else if(bufferEnabledMap != null && bufferEnabledMap.has(String.valueOf(bid)) && bufferEnabledMap.getBoolean(String.valueOf(bid))) {
                return unread;
            } else if(!type.equals(TYPE_CONVERSATION) && conn.getUserInfo() != null && conn.getUserInfo().prefs != null && conn.getUserInfo().prefs.has("disableTrackUnread") && conn.getUserInfo().prefs.get("disableTrackUnread") instanceof Boolean && conn.getUserInfo().prefs.getBoolean("disableTrackUnread")) {
                return 0;
            }
        } catch (JSONException e) {
            NetworkConnection.printStackTraceToCrashlytics(e);
        }
        return unread;
    }

    public void setUnread(int unread) {
        this.unread = unread;
    }

    @Bindable
    public int getHighlights() {
        return muted ? 0 : highlights;
    }

    @Bindable
    public String getHighlightsString() {
        return String.valueOf(getHighlights());
    }

    public void setHighlights(int highlights) {
        this.highlights = highlights;
    }

    public boolean isMuted() {
        return muted;
    }

    public void setMuted(boolean muted) {
        this.muted = muted;
    }

    public int getValid() {
        return valid;
    }

    public void setValid(int valid) {
        this.valid = valid;
    }

    /*@Override
    public void save() {
        if(bid != -1)
            super.save();
    }*/

    public boolean isJoined() {
        return ChannelsList.getInstance().getChannelForBuffer(bid) != null;
    }

    @Bindable public String getContentDescription() {
        String extra = "";
        if(getUnread() > 0)
            extra += ", unread";
        if(getHighlights() > 0)
            extra += ", " + getHighlights() + " highlights";

        if(isConsole())
            return "Network " + getServer().getName() + extra;
        else if(isCollapsed() && archived > 0)
            return "Collapsed network. Tap to expand" + extra;
        else if(isCollapsed())
            return "Expanded network. Tap to collapse" + extra;
        else if(isConversation())
            return "Conversation with " + getDisplayName() + extra;
        else if(isChannel())
            return "Channel " + normalizedName() + extra;
        else
            return getName() + extra;
    }

    @Bindable
    public int getTextColor() {
        if (type_int == Type.ARCHIVES_HEADER || type_int == Type.COLLAPSED) {
            return colorScheme.archivesHeadingTextColor;
        } else if (type_int == Type.JOIN_CHANNEL) {
            return R.color.row_label_join;
        } else if (isConsole()) {
            if(getServer() != null && getServer().isFailed())
                return colorScheme.networkErrorColor;
            else if(getServer() == null || !getServer().isConnected())
                return colorScheme.inactiveBufferTextColor;
            else
                return colorScheme.bufferTextColor;
        } else if (getArchived() == 1) {
            return (type_int == Type.CHANNEL) ? colorScheme.archivedChannelTextColor : colorScheme.archivedBufferTextColor;
        } else if (isChannel() && !isJoined()) {
            return colorScheme.inactiveBufferTextColor;
        } else if (isSpam()) {
            return colorScheme.networkErrorColor;
        } else {
            return colorScheme.bufferTextColor;
        }
    }

    @Bindable
    public String getIcon() {
        if (type_int == Type.COLLAPSED) {
            return (archived > 0) ? FontAwesome.PLUS_SQUARE_O : FontAwesome.MINUS_SQUARE_O;
        } else if(isChannel()) {
            Channel c = ChannelsList.getInstance().getChannelForBuffer(bid);
            if(c != null && (c.hasMode("k") || (serverIsSlack && !isMPDM() && c.hasMode("s"))))
                return FontAwesome.LOCK;
        } else if(isSpam()) {
            return FontAwesome.EXCLAMATION_TRIANGLE;
        } else if(type_int == Type.PINNED) {
            return FontAwesome.THUMB_TACK;
        } else if(type_int == Type.ADD_NETWORK) {
            return FontAwesome.PLUS_CIRCLE;
        }
        return null;
    }

    @Bindable
    public int getBackgroundResource() {
        if(isChannel() || isConversation())
            return colorScheme.bufferBackgroundDrawable;
        else if(isSpam())
            return R.drawable.row_failed_bg;
        else if(isConsole())
            return (getServer() != null && getServer().isFailed()) ? R.drawable.row_failed_bg : colorScheme.serverBackgroundDrawable;
        else if(type_int == Type.JOIN_CHANNEL)
            return colorScheme.bufferBackgroundDrawable;
        else if(type_int == Type.COLLAPSED)
            return colorScheme.bufferBackgroundDrawable;
        else if(type_int == Type.ARCHIVES_HEADER)
            return (getArchived() == 0)?colorScheme.bufferBackgroundDrawable:R.drawable.archived_bg_selected;
        else if(type_int == Type.PINNED)
            return colorScheme.bufferBackgroundDrawable;
        else if(type_int == Type.ADD_NETWORK)
            return colorScheme.bufferBackgroundDrawable;
        else
            return colorScheme.serverBackgroundDrawable;
    }

    @Bindable
    public int getSelectedBackgroundResource() {
        if(isConsole())
            return (getServer() != null && getServer().isFailed()) ? R.drawable.status_fail_bg : colorScheme.selectedBackgroundDrawable;
        else if(type_int == Type.COLLAPSED)
            return colorScheme.bufferBackgroundDrawable;
        else
            return colorScheme.selectedBackgroundDrawable;
    }

    @Bindable
    public int getSelectedBorderResource() {
        if(isConsole())
            return (getServer() != null && getServer().isFailed()) ? R.drawable.status_fail_bg : colorScheme.selectedBorderDrawable;
        else
            return colorScheme.selectedBorderDrawable;
    }

    @Bindable
    public int getBorderResource() {
        if(isConsole())
            return (getServer() != null && getServer().isFailed()) ? R.drawable.networkErrorBorder : colorScheme.serverBorderDrawable;
        else
            return colorScheme.bufferBorderDrawable;
    }

    @Bindable
    public int getSelectedTextColor() {
        if(type_int == Type.ARCHIVES_HEADER)
            return colorScheme.selectedArchivesHeadingColor;
        else
            return colorScheme.selectedBufferTextColor;
    }

    @Bindable
    public int getUnreadColor() { return isCollapsed() ? colorScheme.collapsedBorderDrawable : R.drawable.row_unread_border; }

    @Bindable
    public boolean getShowSpinner() {
        if(type_int == Type.ARCHIVES_HEADER)
            return (getServer() != null && getServer().deferred_archives > 0);
        else
            return (isConsole() && getServer() != null)? getServer().isConnecting() : (this.timeout > 0);
    }

    @Bindable
    public int getSpamIconColor() {
        return 0xffff6e00;
    }
} @Entity(indices = {@Index("id"), @Index("download_id")})
public class LogExport extends BaseObservable {
    @PrimaryKey
    private int id;

    private int cid;

    private int bid;

    private String file_name;

    private String redirect_url;

    private long start_date;

    private long finish_date;

    private long expiry_date;

    private long download_id;

    private String name;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getCid() {
        return cid;
    }

    public void setCid(int cid) {
        this.cid = cid;
    }

    public int getBid() {
        return bid;
    }

    public void setBid(int bid) {
        this.bid = bid;
    }

    public String getFile_name() {
        return file_name;
    }

    public void setFile_name(String file_name) {
        this.file_name = file_name;
    }

    public String getRedirect_url() {
        return redirect_url;
    }

    public void setRedirect_url(String redirect_url) {
        this.redirect_url = redirect_url;
    }

    public long getStart_date() {
        return start_date;
    }

    public void setStart_date(long start_date) {
        this.start_date = start_date;
    }

    public long getFinish_date() {
        return finish_date;
    }

    public void setFinish_date(long finish_date) {
        this.finish_date = finish_date;
    }

    public long getExpiry_date() {
        return expiry_date;
    }

    public void setExpiry_date(long expiry_date) {
        this.expiry_date = expiry_date;
    }

    public long getDownload_id() {
        return download_id;
    }

    public void setDownload_id(long download_id) {
        this.download_id = download_id;
    }

    public void setStartTime(String startTime) {
        this.startTime = startTime;
    }

    public void setExpiryTime(String expiryTime) {
        this.expiryTime = expiryTime;
    }

    public boolean isDownloadComplete() {
        return downloadComplete;
    }

    public void setDownloadComplete(boolean downloadComplete) {
        this.downloadComplete = downloadComplete;
    }

    public String getFilesize() {
        return filesize;
    }

    public void setFilesize(String filesize) {
        this.filesize = filesize;
    }

    public File getFile() {
        return file;
    }

    public void setFile(File file) {
        this.file = file;
    }

    @Bindable
    public String getName() {
        if(name == null) {
            Buffer b = BuffersList.getInstance().getBuffer(bid);
            Server s = ServersList.getInstance().getServer(cid);

            String serverName = (s != null) ? (s.getName() != null ? s.getName() : s.getHostname()) : "Unknown Network (" + cid + ")";
            String bufferName = (b != null) ? b.getName() : "Unknown Log (" + bid + ")";

            if(bid > 0) {
                if(b != null)
                    name = serverName + ": " + bufferName;
                else
                    return serverName + ": " + bufferName;
            } else if(cid > 0) {
                if(s != null)
                    name = serverName;
                else
                    return serverName;
            } else {
                name = "All Networks";
            }
            if(name != null)
                IRCCloudDatabase.getInstance().LogExportsDao().update(this);
        }
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    private String startTime;
    @Bindable
    public String getStartTime() {
        if(startTime == null)
            startTime = (finish_date == 0 ? "Started " : "Exported ") + DateUtils.getRelativeTimeSpanString(start_date * 1000L, System.currentTimeMillis(), DateUtils.SECOND_IN_MILLIS).toString().toLowerCase();
        return startTime;
    }

    private String expiryTime;
    @Bindable
    public String getExpiryTime() {
        if(expiryTime == null)
            expiryTime = "Expires " + DateUtils.getRelativeTimeSpanString(expiry_date * 1000L, System.currentTimeMillis(), DateUtils.SECOND_IN_MILLIS).toString().toLowerCase();
        return expiryTime;
    }

    private boolean downloadComplete;

    @Bindable
    public int getDownloadProgress() {
        if(download_id > 0 && !downloadComplete) {
            DownloadManager d = (DownloadManager) IRCCloudApplication.getInstance().getApplicationContext().getSystemService(Context.DOWNLOAD_SERVICE);

            final Cursor downloadCursor = d.query(new DownloadManager.Query().setFilterById(download_id));
            if(downloadCursor != null)
                downloadCursor.registerContentObserver(new ContentObserver(null) {
                    @Override
                    public void onChange(boolean selfChange) {
                        super.onChange(selfChange);
                        notifyPropertyChanged(BR.downloadProgress);
                        notifyPropertyChanged(BR.isDownloading);
                        downloadCursor.unregisterContentObserver(this);
                    }
                });
            if(downloadCursor != null && downloadCursor.moveToFirst()) {
                int status = downloadCursor.getInt(downloadCursor.getColumnIndex(DownloadManager.COLUMN_STATUS));
                if (status != DownloadManager.STATUS_FAILED && status != DownloadManager.STATUS_SUCCESSFUL) {
                    int downloaded = downloadCursor.getInt(downloadCursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
                    int total = downloadCursor.getInt(downloadCursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
                    return (int)(downloaded * 100.0f / total);
                } else  {
                    downloadComplete = true;
                    if (status == DownloadManager.STATUS_FAILED) {
                        download_id = 0;
                        IRCCloudDatabase.getInstance().LogExportsDao().update(this);
                    }
                }
            } else {
                download_id = 0;
                IRCCloudDatabase.getInstance().LogExportsDao().update(this);
            }
        }
        return -1;
    }

    @Bindable
    public boolean getIsDownloading() {
        return getDownloadProgress() >= 0;
    }

    @Bindable
    public boolean getIsPreparing() {
        return finish_date == 0;
    }

    private String filesize;
    @Bindable
    public String getFileSize() {
        if(filesize == null) {
            if (getExists()) {
                long total = file().length();
                if (total < 1024) {
                    filesize = total + " B";
                } else {
                    int exp = (int) (Math.log(total) / Math.log(1024));
                    filesize = String.format("%.1f ", total / Math.pow(1024, exp)) + ("KMGTPE".charAt(exp - 1)) + "B";
                }
            }
        }
        return filesize;
    }

    public void download() {
        if(redirect_url != null) {
            path().mkdirs();
            DownloadManager d = (DownloadManager) IRCCloudApplication.getInstance().getApplicationContext().getSystemService(Context.DOWNLOAD_SERVICE);
            DownloadManager.Request r = new DownloadManager.Request(Uri.parse(redirect_url));
            r.addRequestHeader("Cookie", "session=" + NetworkConnection.getInstance().session);
            r.setDestinationUri(Uri.fromFile(file()));
            r.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE);
            r.setVisibleInDownloadsUi(true);
            r.setMimeType("application/zip");
            download_id = d.enqueue(r);
            notifyPropertyChanged(BR.downloadProgress);
            notifyPropertyChanged(BR.isDownloading);
            IRCCloudDatabase.getInstance().LogExportsDao().update(this);
        }
    }

    public File path() {
        return new File(IRCCloudApplication.getInstance().getApplicationContext().getExternalFilesDir(null), "export");
    }

    @Ignore
    private File file;
    public File file() {
        if(file == null && file_name != null)
            file = new File(path(), file_name);
        return file;
    }

    @Bindable
    public boolean getExists() {
        return file() != null && file().exists();
    }

    public String toString() {
        return "{id: " + id + ", cid: " + cid + ", bid: " + bid + ", file_name: " + file_name + ", name: " + getName() + "}";
    }

    public void clearCache() {
        expiryTime = null;
        getExpiryTime();
        notifyPropertyChanged(BR.expiryTime);
        startTime = null;
        getStartTime();
        notifyPropertyChanged(BR.startTime);
        filesize = null;
        getFileSize();
        notifyPropertyChanged(BR.fileSize);
    }
} public class Server extends BaseObservable implements Comparable<Server> {
    private int cid = -1;

    private String name;

    private String hostname;

    private int port;

    private String nick;

    private String from;

    private String avatar;

    private String avatar_url;

    private String status;

    private int ssl;

    private String realname;

    private String server_realname;

    private ObjectNode fail_info;

    private String away;

    private String usermask;

    private String mode;

    private String ircserver;

    private int orgId;

    private int avatars_supported;

    private int slack;

    public ObjectNode isupport = new ObjectMapper().createObjectNode();

    public JsonNode raw_ignores;

    //@Column(name = "server_order")
    private int order;

    public String CHANTYPES;

    public ObjectNode PREFIX;

    public String MODE_OPER = "Y";

    public String MODE_OWNER = "q";

    public String MODE_ADMIN = "a";

    public String MODE_OP = "o";

    public String MODE_HALFOP = "h";

    public String MODE_VOICED = "v";

    private String server_pass;
    private String nickserv_pass;
    private String join_commands;
    public Ignore ignores = new Ignore();
    public int deferred_archives;
    private int isSlack = -1;
    public Buffer collapsed;

    public boolean blocksTyping = false;
    public boolean blocksReplies = false;
    public boolean blocksReactions = false;
    public boolean blocksEdits = false;
    public boolean blocksDeletes = false;

    @Override
    public int compareTo(Server another) {
        if (getOrder() != another.getOrder())
            return Integer.valueOf(getOrder()).compareTo(another.getOrder());
        return Integer.valueOf(getCid()).compareTo(another.getCid());
    }

    public int getCid() {
        return cid;
    }

    public void setCid(int cid) {
        this.cid = cid;
    }

    @Bindable
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Bindable
    public String getHostname() {
        return hostname;
    }

    public void setHostname(String hostname) {
        this.hostname = hostname;
        isSlack = -1;
    }

    public int getPort() {
        return port;
    }

    public void setPort(int port) {
        this.port = port;
    }

    public String getNick() {
        return nick;
    }

    public void setNick(String nick) {
        this.nick = nick;
    }

    public String getFrom() {
        return from;
    }

    public void setFrom(String from) {
        this.from = from;
    }

    public String getAvatar() {
        return avatar;
    }

    public void setAvatar(String avatar) {
        this.avatar = avatar;
    }

    public String getAvatarURL() {
        return avatar_url;
    }

    public void setAvatarURL(String avatar_url) {
        this.avatar_url = avatar_url;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public ObjectNode getFail_info() {
        return fail_info;
    }

    public void setFail_info(ObjectNode fail_info) {
        this.fail_info = fail_info;
    }

    public boolean isConnecting() {
        return this.status.equals("waiting_to_retry") || this.status.equals("queued") || this.status.equals("connecting") || this.status.equals("connected_joining");
    }

    @Bindable
    public boolean getIsConnecting() {
        return isConnecting();
    }

    public boolean isConnected() {
        return this.status.equals("connected_ready");
    }

    @Bindable
    public boolean getIsConnected() {
        return isConnected();
    }

    public boolean isFailed() {
        return (this.status.equals("waiting_to_retry") || this.status.equals("pool_unavailable") || (this.status.equals("disconnected") && this.getFail_info() != null && this.getFail_info().has("type")));
    }

    @Bindable
    public boolean getIsFailed() {
        return isFailed();
    }

    public int getSsl() {
        return ssl;
    }

    public void setSsl(int ssl) {
        this.ssl = ssl;
    }

    public String getRealname() {
        return realname;
    }

    public void setRealname(String realname) {
        this.realname = realname;
    }

    public String getServerRealname() {
        return server_realname;
    }

    public void setServerRealname(String server_realname) {
        this.server_realname = server_realname;
    }

    public String getAway() {
        return away;
    }

    public void setAway(String away) {
        this.away = away;
    }

    public String getUsermask() {
        return usermask;
    }

    public void setUsermask(String usermask) {
        this.usermask = usermask;
    }

    public String getMode() {
        return mode;
    }

    public void setMode(String mode) {
        this.mode = mode;
    }

    public int getOrder() {
        return order;
    }

    public void setOrder(int order) {
        this.order = order;
    }

    public String getServer_pass() {
        return server_pass;
    }

    public void setServer_pass(String server_pass) {
        this.server_pass = server_pass;
    }

    public String getNickserv_pass() {
        return nickserv_pass;
    }

    public void setNickserv_pass(String nickserv_pass) {
        this.nickserv_pass = nickserv_pass;
    }

    public String getJoin_commands() {
        return join_commands;
    }

    public void setJoin_commands(String join_commands) {
        this.join_commands = join_commands;
    }

    public String getIRCServer() {
        return ircserver;
    }

    public void setIRCServer(String ircserver) {
        this.ircserver = ircserver;
        isSlack = -1;
    }

    public int getOrgId() {
        return orgId;
    }

    public void setOrgId(int orgId) {
        this.orgId = orgId;
    }

    public int getAvatars_supported() {
        return avatars_supported;
    }

    public void setAvatars_supported(int avatars_supported) {
        this.avatars_supported = avatars_supported;
    }

    public int getSlack() {
        return slack;
    }

    public void setSlack(int slack) {
        this.slack = slack;
    }

    public void updateUserModes(String modes) {
        if (modes != null && modes.length() > 0) {
            if(isupport != null && isupport.has("OWNER") && isupport.get("OWNER").isTextual() && isupport.get("OWNER").asText().equals(modes.substring(0,1))) {
                MODE_OWNER = modes.substring(0, 1);
                if(MODE_OPER.equalsIgnoreCase(MODE_OWNER))
                    MODE_OPER = "";
            }
        }
    }

    public boolean clientTagDeny(String tagname) {
        if(isupport == null || !isupport.has("CLIENTTAGDENY") || !isupport.get("CLIENTTAGDENY").isTextual())
            return false;

        boolean denied = false;

        String[] tags = isupport.get("CLIENTTAGDENY").asText().split(",");
        for(String tag : tags) {
            if(tag.equals("*") || tag.equals(tagname))
                denied = true;
            if(tag.equals("~" + tagname))
                denied = false;
        }

        return denied;
    }

    public void updateIsupport(ObjectNode params) {
        if(isupport == null)
            isupport = new ObjectMapper().createObjectNode();

        if (params != null && !params.isArray())
            isupport.putAll(params);
        else
            isupport = new ObjectMapper().createObjectNode();

        if (isupport.has("PREFIX") && isupport.get("PREFIX").isObject()) {
            PREFIX = (ObjectNode) isupport.get("PREFIX");
        } else {
            PREFIX = new ObjectMapper().createObjectNode();
            PREFIX.put(MODE_OPER, "!");
            PREFIX.put(MODE_OWNER, "~");
            PREFIX.put(MODE_ADMIN, "&");
            PREFIX.put(MODE_OP, "@");
            PREFIX.put(MODE_HALFOP, "%");
            PREFIX.put(MODE_VOICED, "+");
        }
        if (isupport.has("CHANTYPES") && isupport.get("CHANTYPES").isTextual())
            CHANTYPES = isupport.get("CHANTYPES").asText();
        else
            CHANTYPES = Buffer.DEFAULT_CHANTYPES;

        for(Buffer b : BuffersList.getInstance().getBuffersForServer(cid)) {
            b.setChan_types(CHANTYPES);
        }

        blocksTyping = clientTagDeny("typing") && clientTagDeny("draft/typing");
        blocksReplies = clientTagDeny("draft/reply");
        blocksReactions = blocksReplies || clientTagDeny("draft/react");
        blocksEdits = clientTagDeny("draft/edit") || clientTagDeny("draft/edit-text");
        blocksDeletes = clientTagDeny("draft/delete");
    }

    public void updateIgnores(JsonNode ignores) {
        this.raw_ignores = ignores;
        this.ignores.clear();
        if(ignores != null) {
            for (int i = 0; i < ignores.size(); i++) {
                String mask = ignores.get(i).asText().toLowerCase()
                        .replace("\\", "\\\\")
                        .replace("(", "\\(")
                        .replace(")", "\\)")
                        .replace("[", "\\[")
                        .replace("]", "\\]")
                        .replace("{", "\\{")
                        .replace("}", "\\}")
                        .replace("-", "\\-")
                        .replace("^", "\\^")
                        .replace("$", "\\$")
                        .replace("|", "\\|")
                        .replace("+", "\\+")
                        .replace("?", "\\?")
                        .replace(".", "\\.")
                        .replace(",", "\\,")
                        .replace("#", "\\#")
                        .replace("*", ".*")
                        .replace("!~", "!");
                if (!mask.contains("!"))
                    if (mask.contains("@"))
                        mask = ".*!" + mask;
                    else
                        mask += "!.*";
                if (!mask.contains("@"))
                    if (mask.contains("!"))
                        mask = mask.replace("!", "!.*@");
                    else
                        mask += "@.*";
                if (mask.equals(".*!.*@.*"))
                    continue;
                this.ignores.addMask(mask);
            }
        }
    }

    @Bindable
    public String getIcon() {
        if(isSlack())
            return FontAwesome.SLACK;
        else if(ssl > 0)
            return FontAwesome.SHIELD;
        else
            return FontAwesome.GLOBE;
    }

    public boolean isSlack() {
        if(isSlack == -1)
            isSlack = (slack == 1 || (hostname != null && hostname.endsWith(".slack.com")) || (ircserver != null && ircserver.endsWith(".slack.com"))) ? 1 : 0;
        return isSlack == 1;
    }

    public String getSlackBaseURL() {
        String host = hostname;
        if(host == null || !host.endsWith(".slack.com"))
            host = ircserver;
        if(host != null && host.endsWith(".slack.com"))
            return "https://" + host;
        return null;
    }

    /*@Override
    public void save() {
        if(cid != -1)
            super.save();
    }*/
}
import android.app.DownloadManager;
import android.content.BroadcastReceiver;
import android.content.ClipData;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.PorterDuff;
import android.net.Uri;
import android.os.Bundle;
import android.util.TypedValue;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.Spinner;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ShareCompat;
import androidx.core.content.FileProvider;
import com.google.android.material.snackbar.Snackbar;
import com.google.firebase.analytics.FirebaseAnalytics;
import com.irccloud.android.AsyncTaskEx;
import com.irccloud.android.ColorScheme;
import com.irccloud.android.IRCCloudJSONObject;
import com.irccloud.android.NetworkConnection;
import com.irccloud.android.R;
import com.irccloud.android.SeparatedListAdapter;
import com.irccloud.android.data.IRCCloudDatabase;
import com.irccloud.android.data.collection.BuffersList;
import com.irccloud.android.data.collection.LogExportsList;
import com.irccloud.android.data.model.Buffer;
import com.irccloud.android.data.model.LogExport;
import com.irccloud.android.data.model.Server;
import com.irccloud.android.databinding.RowExportBinding;
import org.json.JSONObject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.TimeZone;
import java.util.Timer;
import java.util.TimerTask;
            } catch (IOException e) {
                NetworkConnection.printStackTraceToCrashlytics(e);
                return null;
            }
        }

        @Override
        protected void onPostExecute(JSONObject jsonObject) {
            if (jsonObject != null) {
                LogExportsList.getInstance().update(jsonObject);
                new RefreshTask().execute((Void)null);
            } else {
                findViewById(R.id.progress).setVisibility(View.GONE);
            }
        }
    }

    private class RefreshTask extends AsyncTaskEx<Void, Void, Void> {
        ArrayList<LogExport> inprogress = new ArrayList<>();
        ArrayList<LogExport> downloaded = new ArrayList<>();
        ArrayList<LogExport> available = new ArrayList<>();

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            if(findViewById(android.R.id.list).getVisibility() != View.VISIBLE)
                findViewById(R.id.progress).setVisibility(View.VISIBLE);
        }

        @Override
        protected Void doInBackground(Void... params) {
            List<LogExport> exports = LogExportsList.getInstance().getLogExports();

            Uri uriToDownload = null;

            if(getIntent() != null)
                uriToDownload = getIntent().getData();

            for(LogExport e : exports) {
                e.clearCache();
                if(e.getFinish_date() == 0 && e.getExpiry_date() == 0)
                    inprogress.add(e);
                else if(e.getExists())
                    downloaded.add(e);
                else
                    available.add(e);

                if(uriToDownload != null && uriToDownload.toString().equals(e.getRedirect_url())) {
                    uriToDownload = null;

                    if(e.getDownload_id() == 0)
                        e.download();
                }
            }
            return null;
        }

        @Override
        protected void onPostExecute(Void result) {
            inProgressAdapter.setExports(inprogress);
            downloadedAdapter.setExports(downloaded);
            availableAdapter.setExports(available);

            SeparatedListAdapter adapter = new SeparatedListAdapter(LogExportsActivity.this);

            if(inProgressAdapter.getCount() > 0)
                adapter.addSection("In Progress", inProgressAdapter);

            if(downloadedAdapter.getCount() > 0)
                adapter.addSection("Downloaded", downloadedAdapter);

            if(availableAdapter.getCount() > 0)
                adapter.addSection("Available", availableAdapter);

            if(adapter.getCount() > 0) {
                findViewById(android.R.id.list).setVisibility(View.VISIBLE);
            }
            findViewById(R.id.progress).setVisibility(View.GONE);
            ((ListView)findViewById(android.R.id.list)).setAdapter(adapter);
        }
    }

    BroadcastReceiver downloadComplete = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            new RefreshTask().execute((Void)null);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setTheme(ColorScheme.getDialogWhenLargeTheme(ColorScheme.getUserTheme()));
        onMultiWindowModeChanged(isMultiWindow());

        final int cid = getIntent().getIntExtra("cid", -1);
        final int bid = getIntent().getIntExtra("bid", -1);

        setContentView(R.layout.activity_export);

        Toolbar toolbar = findViewById(R.id.toolbar);
        toolbar.setVisibility(View.VISIBLE);
        setSupportActionBar(toolbar);

        toolbar.setNavigationIcon(androidx.appcompat.R.drawable.abc_ic_ab_back_material);
        toolbar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                finish();
            }
        });

        if(getSupportActionBar() != null) {
            getSupportActionBar().setElevation(0);
        }

        final Spinner exportType = findViewById(R.id.exportType);
        String types[] = {"This Channel", "This Network", "All Networks"};

        if(cid > 0) {
            Buffer b = BuffersList.getInstance().getBuffer(bid);