com.lloydtorres.stately.helpers.SparkleHelper.java Source code

Java tutorial

Introduction

Here is the source code for com.lloydtorres.stately.helpers.SparkleHelper.java

Source

/**
 * Copyright 2016 Lloyd Torres
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.lloydtorres.stately.helpers;

/*
    
                                     __         _____   _
                                    /  \__..--""  ;-.",'/
                                   ( /  \_         `.' / `.
                                   | |    )  `;.  ,'  / \  \
                                   ( '.  /___/_j_    / ) |  )
                                   '\     /   __\``::'/__'  |
                                    |\_  (   / .-| |-.|  `-,|
                                   .| (   \ ( (WW| \W)j     '
             ..-----,             .|'  ',  \_\_`_|  ``-.
          .-` ..::.  `,___        |,   ._:7        \__/
        ,'  .:::'':::.|.`.`-.    |:'.   \    ______.-'
      .'  .::'      '::\`.`. `-._| \ \   `"7  /
     /   ./:'  ,.--''>-'\ `.`-.(`'  `.`.._/  (
    -   :/:'  |     /    \  `.(   `.  `._/    \
    |  :::'  .'    | * \|/`. (     |`-_./      |
   .'  |||  .'     |   /|\ *`.___.-'           |
   |   |||  |      | *                         |
   |   ':|| '.    / \    *   /             \__/
   | .  |||  |.--'   |      /-,_______\       \
   |/|  |||  |     _/      /     |    |\       \
   ` )  '::. '.   /       /     |     | `--,    \
     \   |||  |   |      |      |     |   /      )
      `. |||  | _/|      |      |      | (       |
        `::||  |  |      |      |      |  \      |
           `-._|  |       \     |       \  `.___/
                   \_______)     \_______)
    
    
 */

import android.content.Context;
import android.content.Intent;
import android.graphics.Typeface;
import android.net.Uri;
import android.support.design.widget.Snackbar;
import android.support.v4.app.FragmentManager;
import android.text.Html;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.text.style.URLSpan;
import android.util.Log;
import android.view.View;
import android.widget.TextView;

import com.lloydtorres.stately.R;
import com.lloydtorres.stately.census.TrendsActivity;
import com.lloydtorres.stately.dto.Assembly;
import com.lloydtorres.stately.dto.Resolution;
import com.lloydtorres.stately.dto.Spoiler;
import com.lloydtorres.stately.explore.ExploreActivity;
import com.lloydtorres.stately.helpers.links.SpoilerSpan;
import com.lloydtorres.stately.helpers.links.URLSpanNoUnderline;
import com.lloydtorres.stately.login.LoginActivity;
import com.lloydtorres.stately.region.MessageBoardActivity;
import com.lloydtorres.stately.report.ReportActivity;
import com.lloydtorres.stately.telegrams.TelegramComposeActivity;
import com.lloydtorres.stately.wa.ResolutionActivity;

import org.atteo.evo.inflector.English;
import org.jsoup.Jsoup;
import org.jsoup.safety.Whitelist;
import org.kefirsf.bb.BBProcessorFactory;
import org.kefirsf.bb.TextProcessor;
import org.sufficientlysecure.htmltextview.HtmlTextView;

import java.math.BigDecimal;
import java.text.Normalizer;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Created by Lloyd on 2016-01-16.
 *
 * SparkleHelper is a collection of common functions and constants used across Stately's
 * many different classes. These include things such as formatters and linkers.
 */

public final class SparkleHelper {
    // Tag used to mark system log print calls
    public static final String APP_TAG = "Stately";

    // Whitelisted protocols
    public static final String[] PROTOCOLS = { "http", "https", ExploreActivity.EXPLORE_PROTOCOL,
            MessageBoardActivity.RMB_PROTOCOL, ResolutionActivity.RESOLUTION_PROTOCOL,
            ReportActivity.REPORT_PROTOCOL };
    // Current NationStates API version
    public static final String API_VERSION = "9";
    // NationStates API
    public static final String DOMAIN_URI = "nationstates.net";
    public static final String BASE_URI = "https://www." + DOMAIN_URI + "/";
    public static final String BASE_URI_NOSLASH = "https://www." + DOMAIN_URI;
    public static final String BASE_URI_REGEX = "https:\\/\\/www\\.nationstates\\.net\\/";

    // Initialized to provide human-readable date strings for Date objects
    public static final SimpleDateFormat sdf = new SimpleDateFormat("dd MMM yyyy", Locale.US);
    public static final SimpleDateFormat sdfNoYear = new SimpleDateFormat("dd MMM", Locale.US);

    // Reference time zone for update-related calculations
    public static final TimeZone TIMEZONE_TORONTO = TimeZone.getTimeZone("America/Toronto");

    // Private constructor
    private SparkleHelper() {
    }

    /**
     * VALIDATION
     * These are functions used to validate inputs.
     */

    /**
     * Normalizes a given String to ASCII characters.
     * Source: http://stackoverflow.com/a/15191508
     * @param target
     * @return
     */
    public static String normalizeToAscii(String target) {
        StringBuilder sb = new StringBuilder(target.length());
        target = Normalizer.normalize(target, Normalizer.Form.NFD);
        for (char c : target.toCharArray()) {
            if (c <= '\u007F')
                sb.append(c);
        }
        return sb.toString();
    }

    public static final Pattern VALID_NATION_NAME = Pattern.compile("^[A-Za-z0-9-_ ]+$");

    /**
     * Checks if the passed in name is a valid NationStates name (i.e. A-Z, a-z, 0-9, -, (space)).
     * @param name The name to be checked.
     * @return Bool if valid or not.
     */
    public static boolean isValidName(String name) {
        String normalizedName = normalizeToAscii(name);
        Matcher validator = VALID_NATION_NAME.matcher(normalizedName);
        return validator.matches();
    }

    /**
     * FORMATTING
     * These are functions used to change an input's format to something nicer.
     */

    /**
     * Turns a proper name into a NationStates ID.
     * @param n the name
     * @return the NS ID
     */
    public static String getIdFromName(String n) {
        if (n != null) {
            String normalizedName = normalizeToAscii(n);
            return normalizedName.toLowerCase(Locale.US).replace(" ", "_");
        }
        return null;
    }

    /**
     * This turns a NationStates ID like greater_tern to a nicely formatted string.
     * In the example's case, greater_tern -> Greater Tern
     * @param id The ID to format.
     * @return String of the nicely-formatted name.
     */
    public static String getNameFromId(String id) {
        if (id != null) {
            // IDs have no whitespace and are only separated by underscores.
            String[] words = id.split("_");
            // A list of properly-formatted words.
            List<String> properWords = new ArrayList<String>();

            for (String w : words) {
                // Transform word from lower case to proper case.
                properWords.add(toNormalCase(w));
            }

            // Join all the proper words back together with spaces.
            return joinStringList(properWords, " ");
        }
        return null;
    }

    /**
     * Return a human-readable date string from a UTC timestamp.
     * @param c App context
     * @param sec Unix timestamp.
     * @return A human-readable date string (e.g. moments ago, 1 week ago).
     */
    public static String getReadableDateFromUTC(Context c, long sec) {
        long curTime = System.currentTimeMillis();
        long inputTime = sec * 1000L;
        long timeDiff = inputTime - curTime;
        long timeDiffAbs = Math.abs(timeDiff);

        // If the time diff is zero or positive, it's in the future; past otherwise
        String pastIndicator = (timeDiff >= 0) ? c.getString(R.string.time_from_now)
                : c.getString(R.string.time_ago);
        String template = c.getString(R.string.time_generic_template);

        if (timeDiffAbs < 60000L) {
            // less than a minute
            template = String.format(Locale.US, c.getString(R.string.time_moments_template),
                    c.getString(R.string.time_moments), pastIndicator);
        } else if (timeDiffAbs < 3600000L) {
            // less than an hour
            BigDecimal calc = BigDecimal.valueOf(timeDiffAbs / 60000D);
            int minutes = calc.setScale(0, BigDecimal.ROUND_HALF_UP).intValue();
            template = String.format(Locale.US, template, minutes,
                    English.plural(c.getString(R.string.time_minute), minutes), pastIndicator);
        } else if (timeDiffAbs < 86400000L) {
            // less than a day
            BigDecimal calc = BigDecimal.valueOf(timeDiffAbs / 3600000D);
            int hours = calc.setScale(0, BigDecimal.ROUND_HALF_UP).intValue();
            template = String.format(Locale.US, template, hours,
                    English.plural(c.getString(R.string.time_hour), hours), pastIndicator);
        } else if (timeDiffAbs < 604800000L) {
            // less than a week
            BigDecimal calc = BigDecimal.valueOf(timeDiffAbs / 86400000D);
            int days = calc.setScale(0, BigDecimal.ROUND_HALF_UP).intValue();
            template = String.format(Locale.US, template, days,
                    English.plural(c.getString(R.string.time_day), days), pastIndicator);
        } else {
            template = sdf.format(new Date(inputTime));
        }

        return template;
    }

    /**
     * Returns a formatted date (with no year) given a time in UTC seconds.
     * @param sec UTC seconds
     * @return Formatted date with no year
     */
    public static String getDateNoYearFromUTC(long sec) {
        return sdfNoYear.format(new Date(sec * 1000L));
    }

    /**
     * Returns a number formatted like so: ###,###.## (i.e. US formatting).
     * @param i number to format (can be int, double or long)
     * @return The properly-formatted number as a string.
     */
    public static String getPrettifiedNumber(int i) {
        return NumberFormat.getInstance(Locale.US).format(i);
    }

    public static String getPrettifiedNumber(double d) {
        return NumberFormat.getInstance(Locale.US).format(d);
    }

    public static String getPrettifiedNumber(long l) {
        return NumberFormat.getInstance(Locale.US).format(l);
    }

    /**
     * Takes in the population number from the NationStates API and format it to the NS format.
     * The API returns the population numbers in millions (i.e. 1 million = 1).
     * The NS format is ### million or ##.### billion.
     * @param c Context to get resources.
     * @param pop The population number.
     * @return A nicely-formatted population number with suffix.
     */
    public static String getPopulationFormatted(Context c, double pop) {
        // The lowest population suffix is a million.
        String suffix = c.getString(R.string.million);
        double popHolder = pop;

        if (popHolder >= 1000D) {
            suffix = c.getString(R.string.billion);
            popHolder /= 1000D;
        }

        return String.format(Locale.US, c.getString(R.string.val_currency), getPrettifiedNumber(popHolder), suffix);
    }

    /**
     * Similar to getPrettifiedNumber, but adds a suffix as needed.
     * But this is the same code as getMoneyFormatted!, you say.
     * Well this uses doubles and the other one uses longs.
     * Something something unnecessary casting.
     * @param c app context
     * @param d number to format
     * @return Properly-formatted number as a string
     */
    public static String getPrettifiedSuffixedNumber(Context c, double d) {
        if (d < 1000000D) {
            // If the money is less than 1 million, we don't need a suffix.
            return getPrettifiedNumber(d);
        } else {
            // NS drops the least significant digits depending on the suffix needed.
            // e.g. A value like 10,000,000 is simply 10 million.
            String suffix = "";
            if (d >= 1000000D && d < 1000000000D) {
                suffix = c.getString(R.string.million);
                d /= 1000000D;
            } else if (d >= 1000000000D && d < 1000000000000D) {
                suffix = c.getString(R.string.billion);
                d /= 1000000000D;
            } else if (d >= 1000000000000D) {
                suffix = c.getString(R.string.trillion);
                d /= 1000000000000D;
            }

            return String.format(Locale.US, c.getString(R.string.val_currency), getPrettifiedNumber(d), suffix);
        }
    }

    /**
     * Same as above, but starts at 1000 and uses short suffixes.
     * @param c App context
     * @param d Number to format
     * @return Prettified number with short suffix
     */
    public static String getPrettifiedShortSuffixedNumber(Context c, double d) {
        if (d < 1000D) {
            // We only care about cases greater than 1000
            return getPrettifiedNumber(d);
        } else {
            String suffix = "";
            if (d >= 1000D && d < 1000000D) {
                suffix = c.getString(R.string.thousand_short);
                d /= 1000D;
            } else if (d >= 1000000D && d < 1000000000D) {
                suffix = c.getString(R.string.million_short);
                d /= 1000000D;
            } else if (d >= 1000000000D && d < 1000000000000D) {
                suffix = c.getString(R.string.billion_short);
                d /= 1000000000D;
            } else if (d >= 1000000000000D) {
                suffix = c.getString(R.string.trillion_short);
                d /= 1000000000000D;
            }

            return String.format(Locale.US, c.getString(R.string.val_short), getPrettifiedNumber(d), suffix);
        }
    }

    /**
     * Helper function that capitalizes the first letter of a word.
     * @param w
     * @return
     */
    public static String toNormalCase(String w) {
        String prop = "";
        if (w.length() == 0) {
            prop = w;
        } else if (w.length() == 1) {
            prop = w.substring(0, 1).toUpperCase(Locale.US);
        } else {
            prop = w.substring(0, 1).toUpperCase(Locale.US) + w.substring(1);
        }
        return prop;
    }

    public static final Pattern CURRENCY_PLURALIZE = Pattern.compile("^(.+?)( +of .+)?$");

    /**
     * Takes in a currency name from the NationStates API and formats it to the
     * plural form using NS format.
     * @param currency The currency unit.
     * @return A nicely-formatted pluralized currency string in NS format.
     */
    public static String getCurrencyPlural(String currency) {
        Matcher m = CURRENCY_PLURALIZE.matcher(currency);
        m.matches();
        String pluralize = m.group(1);
        String suffix = m.group(2);
        pluralize = English.plural(pluralize);

        if (suffix != null) {
            return pluralize + suffix;
        } else {
            return pluralize;
        }
    }

    /**
     * Takes in a money value and currency name from the NationStates API and formats it to the
     * NS format.
     * The NationStates API returns money value as a long, but in-game money is represented like
     * so: #,### [suffix].
     * @param c Context to get string.
     * @param money The amount of money as a long.
     * @param currency The currency unit.
     * @return A nicely-formatted string in NS format.
     */
    public static String getMoneyFormatted(Context c, long money, String currency) {
        if (money < 1000000L) {
            // If the money is less than 1 million, we don't need a suffix.
            return String.format(Locale.US, c.getString(R.string.val_currency), getPrettifiedNumber(money),
                    getCurrencyPlural(currency));
        } else {
            // NS drops the least significant digits depending on the suffix needed.
            // e.g. A value like 10,000,000 is simply 10 million.
            String suffix = "";
            if (money >= 1000000L && money < 1000000000L) {
                suffix = c.getString(R.string.million);
                money /= 1000000L;
            } else if (money >= 1000000000L && money < 1000000000000L) {
                suffix = c.getString(R.string.billion);
                money /= 1000000000L;
            } else if (money >= 1000000000000L) {
                suffix = c.getString(R.string.trillion);
                money /= 1000000000000L;
            }

            return String.format(Locale.US, c.getString(R.string.val_suffix_currency), getPrettifiedNumber(money),
                    suffix, getCurrencyPlural(currency));
        }
    }

    /**
     * UTILITY
     * These are convenient tools to call from any class.
     */

    /**
     * Takes in a list of strings and a delimiter and returns a string that combines
     * the elements of the list, separated by the delimiter.
     * @param list List of strings to join.
     * @param delimiter Delimiter to separate each string.
     * @return Merged string.
     */
    public static String joinStringList(Collection<String> list, String delimiter) {
        if (list == null || list.size() < 0) {
            return "";
        }

        StringBuilder mergedString = new StringBuilder();
        int i = 0;
        for (String s : list) {
            if (s != null) {
                mergedString.append(s);

                if (i < list.size() - 1) {
                    mergedString.append(delimiter);
                }
                i++;
            }
        }
        return mergedString.toString();
    }

    /**
     * Makes sure that the specified ID is within range, then returns the properly-split
     * data from the raw array. The format returned is [scale, unit, background image]
     * @param rawCensusData Array of raw census units from arrays.xml
     * @param id Census ID to use
     * @return Formatted census data
     */
    public static String[] getCensusScale(String[] rawCensusData, int id) {
        int censusId = id;
        // if census ID is out of bounds, set it as unknown
        if (censusId >= rawCensusData.length - 1) {
            censusId = rawCensusData.length - 1;
        }
        String[] censusType = rawCensusData[censusId].split("##");
        return censusType;
    }

    // The number of hours a resolution is on the WA chamber floor
    public static final int WA_RESOLUTION_DURATION = 96;

    /**
     * Calculates the remaining time for a WA resolution in human-readable form.
     * @param c App context
     * @param hoursElapsed Number of hours passed since voting started
     * @return Time remaining in human-readable form
     */
    public static String calculateResolutionEnd(Context c, int hoursElapsed) {
        Calendar cal = new GregorianCalendar();
        cal.setTimeZone(TIMEZONE_TORONTO);

        // Round up to nearest hour
        if (cal.get(Calendar.MINUTE) >= 1) {
            cal.add(Calendar.HOUR, 1);
        }
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 0);

        cal.add(Calendar.HOUR, WA_RESOLUTION_DURATION - hoursElapsed);

        Date d = cal.getTime();
        return getReadableDateFromUTC(c, d.getTime() / 1000L);
    }

    /**
     * Checks if the given string indicates that the given stat is for a WA member.
     * @param c App context
     * @param stat WA state indicator
     * @return bool if stat indicates its a WA member
     */
    public static boolean isWaMember(Context c, String stat) {
        return stat.equals(c.getString(R.string.nation_wa_member))
                || stat.equals(c.getString(R.string.nation_wa_delegate));
    }

    /**
     * Starts the ExploreActivity for the given ID and mode.
     * @param c App context
     * @param n The nation ID
     * @param mode Mode if nation or region
     */
    public static void startExploring(Context c, String n, int mode) {
        Intent exploreActivityLaunch = new Intent(c, ExploreActivity.class);
        exploreActivityLaunch.putExtra(ExploreActivity.EXPLORE_ID, n);
        exploreActivityLaunch.putExtra(ExploreActivity.EXPLORE_MODE, mode);
        c.startActivity(exploreActivityLaunch);
    }

    /**
     * Starts the TrendsActivity for the given target and census ID.
     * @param c App context
     * @param target Target ID
     * @param mode Mode if nation or region
     * @param id Census ID
     */
    public static void startTrends(Context c, String target, int mode, int id) {
        Intent trendsActivityLaunch = new Intent(c, TrendsActivity.class);
        if (target != null) {
            trendsActivityLaunch.putExtra(TrendsActivity.TREND_DATA_TARGET, target);
        }
        trendsActivityLaunch.putExtra(TrendsActivity.TREND_DATA_MODE, mode);
        trendsActivityLaunch.putExtra(TrendsActivity.TREND_DATA_ID, id);
        c.startActivity(trendsActivityLaunch);
    }

    /**
     * Starts the TelegramComposeActivity and prefills it with data (if provided).
     * @param c App context
     * @param recipients A string of recipients, can be null or empty
     * @param replyId Reply ID, can be filled or TelegramComposeActivity.NO_REPLY_ID
     */
    public static void startTelegramCompose(Context c, String recipients, int replyId) {
        Intent telegramComposeActivityLaunch = new Intent(c, TelegramComposeActivity.class);
        telegramComposeActivityLaunch.putExtra(TelegramComposeActivity.RECIPIENTS_DATA, recipients);
        telegramComposeActivityLaunch.putExtra(TelegramComposeActivity.REPLY_ID_DATA, replyId);
        c.startActivity(telegramComposeActivityLaunch);
    }

    /**
     * Launches a LoginActivity without autologging in.
     * @param c App context
     */
    public static void startAddNation(Context c) {
        Intent loginActivityLaunch = new Intent(c, LoginActivity.class);
        loginActivityLaunch.putExtra(LoginActivity.NOAUTOLOGIN_KEY, true);
        c.startActivity(loginActivityLaunch);
    }

    /**
     * Launches a ReportActivity with the fields filled in.
     * @param c App context
     * @param type Type of report to file
     * @param id Target ID of the report
     * @param user Target user of the report
     */
    public static void startReport(Context c, int type, int id, String user) {
        Intent reportActivityLaunch = new Intent(c, ReportActivity.class);
        reportActivityLaunch.putExtra(ReportActivity.REPORT_TYPE, type);
        reportActivityLaunch.putExtra(ReportActivity.REPORT_ID, id);
        reportActivityLaunch.putExtra(ReportActivity.REPORT_USER, user);
        c.startActivity(reportActivityLaunch);
    }

    /**
     * LINK AND HTML PROCESSING
     * These are functions used to transform raw NationStates BBCode and formatting into clickable
     * links and formatted text. Separate from the other formatting functions due to their unique
     * nature.
     */

    /**
     * Builds a link invoking an explore activity to the specified ID, and puts it into the
     * appropriate TextView.
     * @param c App context
     * @param t Target TextView
     * @param template The original text with the old formatting.
     * @param oTarget The old format that needs to be replaced.
     * @param nTarget The new format (usually a name) to replace the old.
     * @param mode If target is a nation or a region.
     * @return Returns the new text content for further manipulation.
     */
    public static String activityLinkBuilder(Context c, TextView t, String template, String oTarget, String nTarget,
            int mode) {
        final String urlFormat = "<a href=\"%s/%d\">%s</a>";
        String tempHolder = template;
        String targetActivity = ExploreActivity.EXPLORE_TARGET;

        // Name needs to be formatted back to its NationStates ID first for the URL.
        targetActivity = targetActivity + getIdFromName(nTarget);
        targetActivity = String.format(Locale.US, urlFormat, targetActivity, mode, nTarget);

        tempHolder = tempHolder.replace(oTarget, targetActivity);
        setStyledTextView(c, t, tempHolder);

        return tempHolder;
    }

    /**
     * Stylify text view to primary colour and no underline
     * @param c App context
     * @param t TextView
     */
    public static void styleLinkifiedTextView(Context c, TextView t) {
        // Get individual spans and replace them with clickable ones.
        Spannable s = new SpannableString(t.getText());
        URLSpan[] spans = s.getSpans(0, s.length(), URLSpan.class);
        for (URLSpan span : spans) {
            int start = s.getSpanStart(span);
            int end = s.getSpanEnd(span);
            s.removeSpan(span);
            span = new URLSpanNoUnderline(c, span.getURL());
            s.setSpan(span, start, end, 0);
        }

        t.setText(s);
        // Need to set this to allow for clickable TextView links.
        if (!(t instanceof HtmlTextView)) {
            t.setMovementMethod(LinkMovementMethod.getInstance());
        }
    }

    /**
     * Given a regex and some content, get all pairs of (old, new) where old is a string matching
     * the regex in the content, and new is the proper name to replace the old string.
     * @param regex Regex statement
     * @param content Target content
     * @return
     */
    public static Set<Map.Entry<String, String>> getReplacePairFromRegex(Pattern regex, String content,
            boolean isName) {
        String holder = content;
        // (old, new) replacement pairs
        Map<String, String> replacePairs = new HashMap<String, String>();

        Matcher m = regex.matcher(holder);
        while (m.find()) {
            String properFormat;
            if (isName) {
                // Nameify the ID found and put the (old, new) pair into the map
                properFormat = getNameFromId(m.group(1));
            } else {
                properFormat = m.group(1);
            }
            replacePairs.put(m.group(), properFormat);
        }
        return replacePairs.entrySet();
    }

    public static Set<Map.Entry<String, String>> getDoubleReplacePairFromRegex(Pattern regex, String afterFormat,
            String content) {
        String holder = content;
        // (old, new) replacement pairs
        Map<String, String> replacePairs = new HashMap<String, String>();

        Matcher m = regex.matcher(holder);
        while (m.find()) {
            String properFormat = String.format(Locale.US, afterFormat, m.group(1), m.group(2));
            replacePairs.put(m.group(), properFormat);
        }
        return replacePairs.entrySet();
    }

    /**
     * A helper function used to 1) find all strings to be replaced and 2) linkifies them.
     * @param c App context
     * @param t TextView
     * @param content Target content
     * @param regex Regex statement
     * @param mode If nation or region
     * @return
     */
    public static String linkifyHelper(Context c, TextView t, String content, Pattern regex, int mode) {
        String holder = content;
        Set<Map.Entry<String, String>> set = getReplacePairFromRegex(regex, holder, true);

        for (Map.Entry<String, String> n : set) {
            holder = activityLinkBuilder(c, t, holder, n.getKey(), n.getValue(), mode);
        }

        return holder;
    }

    /**
     * Wrapper for Html.fromHtml, which has different calls depending on the API version.
     * @param src
     * @return
     */
    public static Spanned fromHtml(String src) {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
            return Html.fromHtml(src, Html.FROM_HTML_MODE_COMPACT);
        } else {
            return Html.fromHtml(src);
        }
    }

    public static final Pattern NS_HAPPENINGS_NATION = Pattern.compile("@@(.*?)@@");
    public static final Pattern NS_HAPPENINGS_REGION = Pattern.compile("%%(.*?)%%");
    public static final Pattern NS_RMB_POST_LINK = Pattern.compile(
            "<a href=\"\\/region=(.+)\\/page=display_region_rmb\\?postid=(\\d+)#p\\d+\" rel=\"nofollow\">");
    public static final Pattern NS_INTERNAL_LINK = Pattern.compile("<a href=\"(page=.+)\" rel=\"nofollow\">");

    /**
     * A formatter used to linkify @@nation@@ and %%region%% text in NationStates' happenings.
     * @param c App context
     * @param t TextView
     * @param content Target content
     */
    public static void setHappeningsFormatting(Context c, TextView t, String content) {
        String holder = "<base href=\"" + BASE_URI_NOSLASH + "\">" + content;
        holder = Jsoup.clean(holder, Whitelist.basic().preserveRelativeLinks(true).addTags("br").addTags("a"));
        holder = holder.replace("&amp;#39;", "'");
        holder = holder.replace("&amp;", "&");

        // Replace RMB links with targets to the RMB activity
        holder = regexDoubleReplace(holder, NS_RMB_POST_LINK,
                "<a href=\"" + MessageBoardActivity.RMB_TARGET + "%s/%s\">");

        // Replace internal links with valid links
        holder = regexReplace(holder, NS_INTERNAL_LINK, "<a href=\"" + BASE_URI + "%s\">");

        // Linkify nations (@@NATION@@)
        holder = linkifyHelper(c, t, holder, NS_HAPPENINGS_NATION, ExploreActivity.EXPLORE_NATION);
        holder = linkifyHelper(c, t, holder, NS_HAPPENINGS_REGION, ExploreActivity.EXPLORE_REGION);

        if (holder.contains("EO:")) {
            String[] newTargets = holder.split(":");
            String newTarget = newTargets[1].substring(0, newTargets[1].length() - 1);
            String template = String.format(Locale.US, c.getString(R.string.region_eo), holder);
            holder = activityLinkBuilder(c, t, template, "EO:" + newTarget + ".", getNameFromId(newTarget),
                    ExploreActivity.EXPLORE_REGION);
        }

        if (holder.contains("EC:")) {
            String[] newTargets = holder.split(":");
            String newTarget = newTargets[1].substring(0, newTargets[1].length() - 1);
            String template = String.format(Locale.US, c.getString(R.string.region_ec), holder);
            holder = activityLinkBuilder(c, t, template, "EC:" + newTarget + ".", getNameFromId(newTarget),
                    ExploreActivity.EXPLORE_REGION);
        }

        // In case there are no nations or regions to linkify, set and style TextView here too
        t.setText(fromHtml(holder));
        styleLinkifiedTextView(c, t);
    }

    /**
     * Basic HTML formatter that returns a styled version of the string.
     * @param content Target content
     * @return Styled spanned object
     */
    public static Spanned getHtmlFormatting(String content) {
        String holder = Jsoup.clean(content, Whitelist.none().addTags("br"));
        holder = holder.replace("&amp;#39;", "'");
        holder = holder.replace("&amp;", "&");
        return fromHtml(holder);
    }

    /**
     * Regex patterns
     */
    public static final Pattern NS_RAW_NATION_LINK = Pattern
            .compile("(?i)\\b(?:https?:\\/\\/|)(?:www\\.|)nationstates\\.net\\/nation=([\\w-]*)(?:\\/|)$");
    public static final Pattern NS_RAW_REGION_LINK = Pattern
            .compile("(?i)\\b(?:https?:\\/\\/|)(?:www\\.|)nationstates\\.net\\/region=([\\w-]*)(?:\\/|)$");
    public static final Pattern NS_RAW_REGION_LINK_TG = Pattern
            .compile("(?i)\\b(?:https?:\\/\\/|)(?:www\\.|)nationstates\\.net\\/region=([\\w-]*)\\?tgid=[0-9].*");
    public static final Pattern NS_BBCODE_NATION = Pattern.compile("(?i)\\[nation\\](.*?)\\[\\/nation\\]");
    public static final Pattern NS_BBCODE_NATION_2 = Pattern.compile("(?i)\\[nation=.*?\\](.*?)\\[\\/nation\\]");
    public static final Pattern NS_BBCODE_NATION_3 = Pattern.compile("(?i)\\[nation=(.*?)\\]");
    public static final Pattern NS_BBCODE_REGION = Pattern.compile("(?i)\\[region\\](.*?)\\[\\/region\\]");
    public static final Pattern NS_BBCODE_REGION_2 = Pattern.compile("(?i)\\[region=(.*?)\\]");
    public static final String NS_REGEX_URI_SCHEME = "(?:(?:http|https):\\/\\/nationstates\\.net\\/|www\\.nationstates\\.net\\/|(?:http|https):\\/\\/www\\.nationstates\\.net\\/|\\/|)";
    public static final Pattern NS_BBCODE_URL_NATION = Pattern
            .compile("(?i)\\[url=" + NS_REGEX_URI_SCHEME + "nation=([\\w-]*)(?:\\/|)\\]");
    public static final Pattern NS_BBCODE_URL_REGION = Pattern
            .compile("(?i)\\[url=" + NS_REGEX_URI_SCHEME + "region=([\\w-]*)(?:\\/|)\\]");

    public static final Pattern BBCODE_B = Pattern.compile("(?i)(?s)\\[b\\](.*?)\\[\\/b\\]");
    public static final Pattern BBCODE_I = Pattern.compile("(?i)(?s)\\[i\\](.*?)\\[\\/i\\]");
    public static final Pattern BBCODE_U = Pattern.compile("(?i)(?s)\\[u\\](.*?)\\[\\/u\\]");
    public static final Pattern BBCODE_PRE = Pattern.compile("(?i)(?s)\\[pre\\](.*?)\\[\\/pre\\]");
    public static final Pattern BBCODE_PROPOSAL = Pattern
            .compile("(?i)(?s)\\[proposal=(.*?)\\](.*?)\\[\\/proposal\\]");
    public static final Pattern BBCODE_COLOR = Pattern.compile("(?i)(?s)\\[colou?r=(.*?)\\](.*?)\\[\\/colou?r\\]");
    public static final Pattern BBCODE_INTERNAL_URL = Pattern
            .compile("(?i)(?s)\\[url=((?:pages\\/|page=).*?)\\](.*?)\\[\\/url\\]");

    /**
     * Transform NationStates' BBCode-formatted content into HTML
     * @param c App context
     * @param t TextView
     * @param content Target content
     * @param fm FragmentManager to show spoiler dialogs in
     */
    public static void setBbCodeFormatting(Context c, TextView t, String content, FragmentManager fm) {
        if (content == null || content.length() < 0) {
            return;
        }

        String holder = content.trim();
        holder = holder.replace("\n", "<br>");
        holder = holder.replace("&amp;#39;", "'");
        holder = holder.replace("&amp;", "&");
        holder = Jsoup.clean(holder, Whitelist.simpleText().addTags("br"));

        // Replace raw NS nation and region links with Stately versions
        holder = linkifyHelper(c, t, holder, NS_RAW_NATION_LINK, ExploreActivity.EXPLORE_NATION);
        holder = linkifyHelper(c, t, holder, NS_RAW_REGION_LINK, ExploreActivity.EXPLORE_REGION);
        holder = linkifyHelper(c, t, holder, NS_RAW_REGION_LINK_TG, ExploreActivity.EXPLORE_REGION);
        holder = regexReplace(holder, NS_BBCODE_URL_NATION,
                "[url=" + ExploreActivity.EXPLORE_TARGET + "%s/" + ExploreActivity.EXPLORE_NATION + "]");
        holder = regexReplace(holder, NS_BBCODE_URL_REGION,
                "[url=" + ExploreActivity.EXPLORE_TARGET + "%s/" + ExploreActivity.EXPLORE_REGION + "]");

        // Basic BBcode processing
        holder = holder.replaceAll("(?i)\\[hr\\]", "<br>");

        // Process lists first (they're problematic!)
        TextProcessor processor = BBProcessorFactory.getInstance()
                .create(c.getResources().openRawResource(R.raw.bbcode));
        holder = processor.process(holder);
        holder = holder.replace("&lt;", "<");
        holder = holder.replace("&gt;", ">");
        holder = holder.replace("[*]", "<li>");
        holder = Jsoup.clean(holder, Whitelist.relaxed());

        // Q: Why don't you use the BBCode parser instead of doing this manually? :(
        // A: Because it misses some tags for some reason, so it's limited to lists for now.
        holder = regexReplace(holder, BBCODE_B, "<b>%s</b>");
        holder = regexReplace(holder, BBCODE_I, "<i>%s</i>");
        holder = regexReplace(holder, BBCODE_U, "<u>%s</u>");
        holder = regexReplace(holder, BBCODE_PRE, "<code>%s</code>");
        holder = regexDoubleReplace(holder, BBCODE_PROPOSAL, "<a href=\"" + Resolution.PATH_PROPOSAL + "\">%s</a>");
        holder = regexResolutionFormat(holder);
        holder = regexExtract(holder, BBCODE_RESOLUTION_GENERIC);
        holder = regexDoubleReplace(holder, BBCODE_COLOR, "<font color=\"%s\">%s</font>");
        holder = regexDoubleReplace(holder, BBCODE_INTERNAL_URL, "<a href=\"" + BASE_URI_NOSLASH + "/%s\">%s</a>");
        holder = regexGenericUrlFormat(c, holder);
        holder = regexQuoteFormat(c, t, holder);

        // Extract and replace spoilers
        List<Spoiler> spoilers = getSpoilerReplacePairs(c, holder);
        for (int i = 0; i < spoilers.size(); i++) {
            Spoiler s = spoilers.get(i);
            holder = holder.replace(s.raw, s.replacer);
        }

        // Linkify nations and regions
        holder = linkifyHelper(c, t, holder, NS_BBCODE_NATION, ExploreActivity.EXPLORE_NATION);
        holder = linkifyHelper(c, t, holder, NS_BBCODE_NATION_2, ExploreActivity.EXPLORE_NATION);
        holder = linkifyHelper(c, t, holder, NS_BBCODE_NATION_3, ExploreActivity.EXPLORE_NATION);
        holder = linkifyHelper(c, t, holder, NS_BBCODE_REGION, ExploreActivity.EXPLORE_REGION);
        holder = linkifyHelper(c, t, holder, NS_BBCODE_REGION_2, ExploreActivity.EXPLORE_REGION);

        // In case there are no nations or regions to linkify, set and style TextView here too
        setStyledTextView(c, t, holder, spoilers, fm);
    }

    public static final Pattern BBCODE_SPOILER = Pattern.compile("(?i)(?s)\\[spoiler\\](.*?)\\[\\/spoiler\\]");
    public static final Pattern BBCODE_SPOILER_2 = Pattern
            .compile("(?i)(?s)\\[spoiler=(.*?)\\](.*?)\\[\\/spoiler\\]");

    /**
     * Helper function that extracts spoilers from BBCode for later use.
     * @param c App context
     * @param target Target content
     * @return List of spoilers
     */
    public static List<Spoiler> getSpoilerReplacePairs(Context c, String target) {
        String holder = target;
        List<Spoiler> spoilers = new ArrayList<Spoiler>();

        // Handle spoilers without titles first
        Matcher m1 = BBCODE_SPOILER.matcher(holder);
        while (m1.find()) {
            Spoiler s = new Spoiler();
            s.content = m1.group(1);
            s.raw = m1.group();
            s.replacer = c.getString(R.string.spoiler_warn_link);
            spoilers.add(s);
        }

        // Handle spoilers with titles next
        Matcher m2 = BBCODE_SPOILER_2.matcher(holder);
        while (m2.find()) {
            Spoiler s = new Spoiler();
            // Gets rid of HTML in title
            s.title = Jsoup.parse(m2.group(1)).text();
            s.content = m2.group(2);
            s.raw = m2.group();
            s.replacer = String.format(Locale.US, c.getString(R.string.spoiler_warn_title_link), s.title);
            spoilers.add(s);
        }

        return spoilers;
    }

    /**
     * Helper used for setting and styling an HTML string into a TextView.
     * @param c App context
     * @param t Target TextView
     * @param holder Content
     */
    public static void setStyledTextView(Context c, TextView t, String holder) {
        if (t instanceof HtmlTextView) {
            try {
                ((HtmlTextView) t).setHtml(holder);
            } catch (Exception e) {
                logError(e.toString());
                t.setText(c.getString(R.string.bbcode_parse_error));
                t.setTypeface(t.getTypeface(), Typeface.ITALIC);
            }
        } else {
            t.setText(fromHtml(holder));
        }
        styleLinkifiedTextView(c, t);
    }

    /**
     * Overloaded to deal with spoilers.
     */
    public static void setStyledTextView(Context c, TextView t, String holder, List<Spoiler> spoilers,
            FragmentManager fm) {
        if (t instanceof HtmlTextView) {
            try {
                ((HtmlTextView) t).setHtml(holder);
            } catch (Exception e) {
                logError(e.toString());
                t.setText(c.getString(R.string.bbcode_parse_error));
                t.setTypeface(t.getTypeface(), Typeface.ITALIC);
            }
        } else {
            t.setText(fromHtml(holder));
        }

        // Deal with spoilers here
        styleLinkifiedTextView(c, t); // Ensures TextView contains a spannable
        Spannable span = new SpannableString(t.getText());
        String rawSpan = span.toString();
        int startFromIndex = 0;

        for (int i = 0; i < spoilers.size(); i++) {
            Spoiler s = spoilers.get(i);
            int start = rawSpan.indexOf(s.replacer, startFromIndex);
            if (start != -1) {
                int end = start + s.replacer.length();
                startFromIndex = end;
                SpoilerSpan clickyDialog = new SpoilerSpan(c, s, fm);
                span.setSpan(clickyDialog, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            }
        }
        t.setText(span);
    }

    /**
     * Replaces all matches of a given regex with the supplied string template. Only accepts
     * one parameter.
     * @param target Target content
     * @param regexBefore Regex to use
     * @param afterFormat String template
     * @return Returns content with all matched substrings replaced
     */
    public static String regexReplace(String target, Pattern regexBefore, String afterFormat) {
        String holder = target;
        Set<Map.Entry<String, String>> set = getReplacePairFromRegex(regexBefore, holder, false);

        for (Map.Entry<String, String> n : set) {
            // disabling whitelisting since improperly-nested tags are common in NS BBCode :(
            String replacer = n.getValue();
            String properFormat = String.format(Locale.US, afterFormat, replacer); //Jsoup.clean(String.format(afterFormat, n.getValue()), Whitelist.basic().addProtocols("a", "href", PROTOCOLS));
            holder = holder.replace(n.getKey(), properFormat);
        }

        return holder;
    }

    /**
     * Similar to regexReplace, but takes in two characters
     * @param target Target content
     * @param regexBefore Regex to use
     * @param afterFormat String template
     * @return
     */
    public static String regexDoubleReplace(String target, Pattern regexBefore, String afterFormat) {
        String holder = target;
        Set<Map.Entry<String, String>> set = getDoubleReplacePairFromRegex(regexBefore, afterFormat, holder);

        for (Map.Entry<String, String> n : set) {
            // disabling whitelisting since improperly-nested tags are common in NS BBCode :(
            String replacer = n.getValue(); //Jsoup.clean(n.getValue(), Whitelist.basic().addProtocols("a", "href", PROTOCOLS));
            holder = holder.replace(n.getKey(), replacer);
        }

        return holder;
    }

    public static final Pattern BBCODE_RESOLUTION_GA_SC = Pattern
            .compile("(?i)(?s)\\[resolution=(GA|SC)#([0-9]+)\\](.*?)\\[\\/resolution\\]");
    public static final Pattern BBCODE_RESOLUTION_GENERIC = Pattern
            .compile("(?i)(?s)\\[resolution=.+\\](.*?)\\[\\/resolution\\]");
    public static final Pattern BBCODE_URL_RESOLUTION = Pattern.compile("(?i)(?s)\\[url=" + NS_REGEX_URI_SCHEME
            + "page=WA_past_resolutions\\/council=(1|2)\\/start=([0-9]+)\\](.*?)\\[\\/url\\]");
    public static final String BBCODE_RESOLUTION_GA = "GA";

    /**
     * Processes the resolution tag by linkifying it to ResolutionActivity.
     * @param content
     * @return
     */
    public static String regexResolutionFormat(String content) {
        String holder = content;

        Matcher m = BBCODE_RESOLUTION_GA_SC.matcher(holder);
        while (m.find()) {
            int councilId = BBCODE_RESOLUTION_GA.equals(m.group(1)) ? Assembly.GENERAL_ASSEMBLY
                    : Assembly.SECURITY_COUNCIL;
            int resolutionId = Integer.valueOf(m.group(2)) - 1;
            String properFormat = regexResolutionFormatHelper(councilId, resolutionId, m.group(3));
            holder = holder.replace(m.group(), properFormat);
        }

        Matcher m2 = BBCODE_URL_RESOLUTION.matcher(holder);
        while (m2.find()) {
            int councilId = Integer.valueOf(m2.group(1));
            int resolutionId = Integer.valueOf(m2.group(2));
            String properFormat = regexResolutionFormatHelper(councilId, resolutionId, m2.group(3));
            holder = holder.replace(m2.group(), properFormat);
        }

        return holder;
    }

    /**
     * Helper function for building links to ResolutionActivity
     * @param councilId
     * @param resolutionId
     * @param content
     * @return
     */
    public static String regexResolutionFormatHelper(int councilId, int resolutionId, String content) {
        return String.format(Locale.US, "<a href=\"" + ResolutionActivity.RESOLUTION_TARGET + "%d/%d\">%s</a>",
                councilId, resolutionId, content);
    }

    /**
     * Convenience class used by regexQuoteFormat() to format blockquotes with author attrib.
     * @param c App context
     * @param t Target TextView
     * @param regex Regex to use
     * @param content Original string
     * @return Formatted string
     */
    public static String regexQuoteFormatHelper(Context c, TextView t, Pattern regex, String content) {
        String holder = content;
        Map<String, String> replacePairs = new HashMap<String, String>();
        Matcher m = regex.matcher(holder);
        while (m.find()) {
            String properFormat = String.format(Locale.US, "<blockquote><i>@@%s@@:<br />%s</i></blockquote>",
                    getNameFromId(m.group(1)), m.group(2));
            replacePairs.put(m.group(), properFormat);
        }
        Set<Map.Entry<String, String>> set = replacePairs.entrySet();
        for (Map.Entry<String, String> n : set) {
            String replacer = n.getValue();
            holder = holder.replace(n.getKey(), replacer);
        }
        holder = linkifyHelper(c, t, holder, NS_HAPPENINGS_NATION, ExploreActivity.EXPLORE_NATION);
        return holder;
    }

    public static final Pattern BBCODE_URL = Pattern.compile("(?i)(?s)\\[url=(.*?)\\](.*?)\\[\\/url\\]");
    public static final Pattern RAW_HTTP_LINK = Pattern
            .compile("(?i)(?<=^|\\s|<br \\/>|<br>|<b>|<i>|<u>)((?:http|https):\\/\\/[^\\s\\[\\<]+)");
    public static final Pattern RAW_WWW_LINK = Pattern
            .compile("(?i)(?<=^|\\s|<br \\/>|<br>|<b>|<i>|<u>)(www\\.[^\\s\\[\\<]+)");

    /**
     * Finds all raw URL links and URL tags and linkifies them properly in a nice format.
     * @param c App context.
     * @param content Target string.
     * @return Parsed results.
     */
    public static String regexGenericUrlFormat(Context c, String content) {
        String holder = content;

        Map<String, String> replaceBasic = new HashMap<String, String>();
        Matcher m0 = BBCODE_URL.matcher(holder);
        while (m0.find()) {
            String template = "<a href=\"%s\">%s</a>";
            Uri link = Uri.parse(m0.group(1)).normalizeScheme();
            if (link.getScheme() == null) {
                template = "<a href=\"http://%s\">%s</a>";
            }
            String replaceText = String.format(Locale.US, template, link.toString(), m0.group(2));
            replaceBasic.put(m0.group(), replaceText);
        }
        Set<Map.Entry<String, String>> setBasic = replaceBasic.entrySet();
        for (Map.Entry<String, String> e : setBasic) {
            holder = holder.replace(e.getKey(), e.getValue());
        }

        Map<String, String> replaceRaw = new HashMap<String, String>();

        Matcher m1 = RAW_HTTP_LINK.matcher(holder);
        while (m1.find()) {
            Uri link = Uri.parse(m1.group(1)).normalizeScheme();
            String replaceText = String.format(Locale.US, c.getString(R.string.clicky_link_http), link.toString(),
                    link.getHost());
            replaceRaw.put(m1.group(), replaceText);
        }

        Matcher m2 = RAW_WWW_LINK.matcher(holder);
        while (m2.find()) {
            Uri link = Uri.parse("http://" + m2.group(1)).normalizeScheme();
            String replaceText = String.format(Locale.US, c.getString(R.string.clicky_link_http), link.toString(),
                    link.getHost());
            replaceRaw.put(m2.group(), replaceText);
        }

        Set<Map.Entry<String, String>> set = replaceRaw.entrySet();
        for (Map.Entry<String, String> e : set) {
            holder = holder.replaceAll(
                    "(?<=^|\\s|<br \\/>|<br>|<b>|<i>|<u>)\\Q" + e.getKey() + "\\E(?=$|[\\s\\[\\<])", e.getValue());
        }

        return holder;
    }

    public static final Pattern BBCODE_QUOTE = Pattern.compile("(?i)(?s)\\[quote\\](.*?)\\[\\/quote\\]");
    public static final Pattern BBCODE_QUOTE_1 = Pattern
            .compile("(?i)(?s)\\[quote=(.*?);[0-9]+\\](.*?)\\[\\/quote\\]");
    public static final Pattern BBCODE_QUOTE_2 = Pattern.compile("(?i)(?s)\\[quote=(.*?)\\](.*?)\\[\\/quote\\]");

    /**
     * Used for formatting blockquotes
     * @param context App context
     * @param content Original string
     * @return Formatted string
     */
    public static String regexQuoteFormat(Context context, TextView t, String content) {
        String holder = content;

        // handle basic quotes
        holder = regexReplace(holder, BBCODE_QUOTE, "<blockquote><i>%s</i></blockquote>");

        // handle quotes with parameters on them
        // in this case, [quote=name;id]...
        holder = regexQuoteFormatHelper(context, t, BBCODE_QUOTE_1, holder);
        // in this case, just [quote=name]...
        holder = regexQuoteFormatHelper(context, t, BBCODE_QUOTE_2, holder);

        return holder;
    }

    /**
     * Extracts a capture group from a regex
     * @param target Target content
     * @param regex Regex
     * @return
     */
    public static String regexExtract(String target, Pattern regex) {
        String holder = target;
        Set<Map.Entry<String, String>> set = getReplacePairFromRegex(regex, holder, false);

        for (Map.Entry<String, String> n : set) {
            holder = holder.replace(n.getKey(), n.getValue());
        }

        return holder;
    }

    /**
     * Removes all substrings which match the regex
     * @param target Target content
     * @param regex Regex
     * @return
     */
    public static String regexRemove(String target, Pattern regex) {
        String holder = target;
        Set<Map.Entry<String, String>> set = getReplacePairFromRegex(regex, holder, false);

        for (Map.Entry<String, String> n : set) {
            holder = holder.replace(n.getKey(), "");
        }

        return holder;
    }

    /**
     * LOGGING
     * These are function calls used to log events and other things.
     */

    /**
     * Shows a long snackbar in the given view.
     * @param view View
     * @param str Snackbar message
     */
    public static void makeSnackbar(View view, String str) {
        Snackbar.make(view, str, Snackbar.LENGTH_LONG).show();
    }

    /**
     * Logs a system error. Mostly used so that APP_TAG doesn't have to repeat.
     * @param message Message
     */
    public static void logError(String message) {
        Log.e(APP_TAG, message);
    }
}