package com.thebluealliance.androidclient; import com.google.common.base.Predicate; import com.thebluealliance.androidclient.activities.HomeActivity; import com.thebluealliance.androidclient.activities.ViewEventActivity; import com.thebluealliance.androidclient.activities.ViewMatchActivity; import com.thebluealliance.androidclient.activities.ViewTeamActivity; import com.thebluealliance.androidclient.helpers.EventHelper; import com.thebluealliance.androidclient.helpers.MatchHelper; import com.thebluealliance.androidclient.helpers.TeamHelper; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.res.Resources; import android.graphics.Typeface; import android.net.Uri; import android.os.Build; import android.provider.Settings; import android.support.annotation.RawRes; import android.support.v7.app.AlertDialog; import android.text.Html; import android.text.SpannableStringBuilder; import android.text.format.DateFormat; import android.text.style.StyleSpan; import android.util.ArrayMap; import android.util.TypedValue; import android.view.View; import android.view.Window; import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.PrintWriter; import java.io.StringWriter; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.Format; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import me.xuender.unidecode.Unidecode; public final class Utilities { private Utilities() { // not used } public static int getPixelsFromDp(Context c, int dipValue) { Resources r = c.getResources(); return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dipValue, r.getDisplayMetrics()); } public static String exceptionStacktraceToString(Exception e) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); e.printStackTrace(pw); return sw.toString(); } public static int getFirstCompWeek(Date date) { Calendar cal = Calendar.getInstance(); cal.setTime(date); return getFirstCompWeek(cal.get(Calendar.YEAR)); } public static int getFirstCompWeek(int year) { int offset = year - 1992; if (Constants.FIRST_COMP_WEEK.length > offset && year != -1) { return offset >= Constants.FIRST_COMP_WEEK.length || offset < 0 ? Constants.FIRST_COMP_WEEK[Constants.FIRST_COMP_WEEK.length - 1] : Constants.FIRST_COMP_WEEK[offset]; } else { //if no data for this year, return the most recent data TbaLogger.w("No first competition week data available for " + year + ". Using most recent year."); return Constants.FIRST_COMP_WEEK[Constants.FIRST_COMP_WEEK.length - 1]; } } public static int getCmpWeek(int year) { int offset = year - 1992; if (Constants.CMP_WEEK.length > offset) { return Constants.CMP_WEEK[offset]; } else { //if no data for this year, return the most recent data TbaLogger.w("No first championship week data available for " + year + ". Using most recent year."); return Constants.CMP_WEEK[Constants.CMP_WEEK.length - 1]; } } public static String getAsciiApproximationOfUnicode(String input) { return Unidecode.decode(input); } /** * Replaces unicode characters with their ASCII equivalents and appends an asterisk to each * term. * <p> * For example, an input of "Über" would result in the string "Uber*". * * @param query the query from the user to prepare * @return the prepared query */ public static String getPreparedQueryForSearch(String query) { // Prepare text for query. We will split the query by spaces, append an asterisk to the end of // each component, and the put the string back together. query = getAsciiApproximationOfUnicode(query); String[] splitQuery = query.split("\\s+"); for (int i = 0; i < splitQuery.length; i++) { splitQuery[i] = splitQuery[i] + "*"; } String finalQuery = ""; for (String aSplitQuery : splitQuery) { finalQuery += (aSplitQuery + " "); } return finalQuery; } public static Intent getIntentForTBAUrl(Context c, Uri data) { TbaLogger.d("Uri: " + data.toString()); List<String> urlParts = data.getPathSegments(); // Check if this is actually a TBA URL // Simply checks if the host matches (*.)thebluealliance.com String tbaHostPattern = "(.*\\.?)thebluealliance.com"; Pattern pattern = Pattern.compile(tbaHostPattern); if (!pattern.matcher(data.getHost()).matches()) { return null; } Intent intent = null; if (urlParts != null) { if (urlParts.isEmpty()) { //we caught the homepage (so there's no next part of the URL. //open the home screen //TODO once we get "glancables" up, make this link to special, dynamic content return HomeActivity.newInstance(c, R.id.nav_item_events); } System.out.println(urlParts.get(0)); switch (urlParts.get(0)) { //switch on areas of tba that we can view here case "teams": intent = HomeActivity.newInstance(c, R.id.nav_item_teams); break; case "team": if (indexExists(urlParts, 1) && TeamHelper.validateTeamKey("frc" + urlParts.get(1))) { if (indexExists(urlParts, 2) && urlParts.get(2).matches("\\d\\d\\d\\d")) { intent = ViewTeamActivity.newInstance(c, "frc" + urlParts.get(1), Integer.parseInt(urlParts.get(2))); } else { intent = ViewTeamActivity.newInstance(c, "frc" + urlParts.get(1)); } } break; case "": case "events": intent = HomeActivity.newInstance(c, R.id.nav_item_events); break; case "event": if (indexExists(urlParts, 1) && EventHelper.validateEventKey(urlParts.get(1))) { intent = ViewEventActivity.newInstance(c, urlParts.get(1)); } break; case "match": if (indexExists(urlParts, 1) && MatchHelper.validateMatchKey(urlParts.get(1))) { intent = ViewMatchActivity.newInstance(c, urlParts.get(1)); } break; case "gameday": intent = HomeActivity.newInstance(c, R.id.nav_item_gameday); break; default: intent = null; } } return intent; } public static boolean indexExists(List<String> data, int index) { return data != null && !data.isEmpty() && data.size() >= (index + 1) && data.get(index) != null && !data.get(index).isEmpty(); } public static int getCurrentYear() { Calendar cal = Calendar.getInstance(); return cal.get(Calendar.YEAR); } public static int getCurrentCompWeek() { return EventHelper.competitionWeek(new Date()); } public static String getOrdinalFor(int value) { int hundredRemainder = value % 100; int tenRemainder = value % 10; if (hundredRemainder - tenRemainder == 10) { return "th"; } switch (tenRemainder) { case 1: return "st"; case 2: return "nd"; case 3: return "rd"; default: return "th"; } } public static void showHelpDialog(Context c, @RawRes int rawText, String dialogTitle) { String helpText; try { BufferedReader br = new BufferedReader(new InputStreamReader(c.getResources().openRawResource(rawText))); StringBuilder sb = new StringBuilder(); String line = br.readLine(); while (line != null) { sb.append(line); sb.append(System.getProperty("line.separator")); line = br.readLine(); } helpText = sb.toString(); } catch (Exception e) { e.printStackTrace(); helpText = "Error reading help file."; } AlertDialog.Builder builder = new AlertDialog.Builder(c); builder.setTitle(dialogTitle); builder.setMessage(Html.fromHtml(helpText)); builder.setCancelable(true); builder.setNeutralButton(c.getString(R.string.close_stats_help), (dialog, which) -> { dialog.cancel(); } ); builder.create().show(); } public static boolean isInteger(String s) { try { Integer.parseInt(s); } catch (NumberFormatException e) { return false; } // only got here if we didn't return false return true; } public static boolean isDebuggable() { return BuildConfig.DEBUG; } /** * Get the <a href="http://developer.android.com/reference/android/os/Build.html#SERIAL">hardware * serial number</a> I hope this actually works universally, android UUIDs are irritatingly * difficult * * @return UUID */ public static String getUUID() { return Build.SERIAL; } /** * Utility method to create a comma separated list of strings. Useful when you have a list of * things that you want to express in a human-readable list, e.g. teams in a match. * <p> * If the length of the list is 1, this method will return the input string verbatim. * <p> * If the length of the list is 2, the returned string will be formatted like "XXXX and YYYY". * <p> * If the length of the list is 3 or more, the returned string will be formatted like "XXXX, * YYYY, and ZZZZ". * <p> * This uses a localized "and" string. */ public static String stringifyListOfStrings(Context context, ArrayList<String> strings) { String finalString = ""; Resources r = context.getResources(); int size = strings.size(); if (size == 0) { finalString = ""; } else if (size == 1) { finalString = strings.get(0); } else if (size == 2) { finalString = strings.get(0) + " " + r.getString(R.string.and) + " " + strings.get(1); // e.g. "111 and 1114" } else if (size > 2) { finalString += strings.get(0); for (int i = 1; i < size; i++) { if (i < size - 1) { finalString += ", " + strings.get(i); } else { finalString += ", " + r.getString(R.string.and) + " " + strings.get(i); } } // e.g. "111, 1114, and 254 } return finalString; } /** * @return a comma-separated CharSequence of the given names, applying bold style to names that * satisfy the given predicate. */ public static CharSequence boldNameList(Iterable<? extends CharSequence> names, Predicate<String> beBold) { final SpannableStringBuilder result = new SpannableStringBuilder(); boolean first = true; for (CharSequence name : names) { if (first) { first = false; } else { result.append(", "); } if (beBold.apply(name.toString())) { supportAppend(result, name, new StyleSpan(Typeface.BOLD), 0); } else { result.append(name); } } return result; } private static SpannableStringBuilder supportAppend(SpannableStringBuilder builder, CharSequence text, Object what, int flags) { int start = builder.length(); builder.append(text); builder.setSpan(what, start, builder.length(), flags); return builder; } final protected static char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); private static String bytesToHexString(byte[] bytes) { char[] hexChars = new char[bytes.length * 2]; for (int j = 0; j < bytes.length; j++) { int v = bytes[j] & 0xFF; hexChars[j * 2] = HEX_ARRAY[v >>> 4]; hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; } String output = new String(hexChars); return output.toLowerCase(); } public static String sha256(String input) { String hash = null; try { MessageDigest digest = MessageDigest.getInstance("SHA-256"); digest.update(input.getBytes()); hash = bytesToHexString(digest.digest()); } catch (NoSuchAlgorithmException e) { TbaLogger.e("Can't find SHA-256 algorithm."); e.printStackTrace(); } return hash; } public static boolean hasKApis() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; } public static boolean hasLApis() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; } public static boolean hasMApis() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; } public static String getDeviceUUID(Context context) { return Settings.Secure.getString(context.getApplicationContext().getContentResolver(), Settings.Secure.ANDROID_ID); } public static String getBuildTimestamp(Context c) { /* Check the last modified time of classes.dex, * which was when the app was last built */ try { ApplicationInfo ai = c.getPackageManager().getApplicationInfo(c.getPackageName(), 0); ZipFile zf = new ZipFile(ai.sourceDir); ZipEntry ze = zf.getEntry("classes.dex"); long time = ze.getTime(); Format dateFormat = DateFormat.getDateFormat(c); Format timeFormat = DateFormat.getTimeFormat(c); Date date = new java.util.Date(time); String s = dateFormat.format(date) + " " + timeFormat.format(date); zf.close(); return s; } catch (Exception e) { return null; } } public static String getVersionNumber() { /* If this changes, make sure to also change it in SettingsActivity */ if (BuildConfig.VERSION_NAME.contains("/")) { return BuildConfig.VERSION_NAME.split("/")[0]; } else { return BuildConfig.VERSION_NAME; } } /** * {@link ArrayMap} is more memory efficient than {@link HashMap}, so prefer that if possible */ public static <K, V> Map<K, V> getMapForPlatform(Class<K> key, Class<V> value) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { return new ArrayMap<>(); } else { return new HashMap<>(); } } /** * On API 23+, this allows us to set the color of the status bar icons (either light or dark) * to look better with the status bar background. If the background is light, the icons will be * tinted gray/black; otherwise, they will be the default white. * <p> * This is safe to be called from any API level, as this method checks the API level before * trying to use the new feature. * * @param window the window to be modified * @param lightBackground if the background of the status bar is light */ public static void setLightStatusBar(Window window, boolean lightBackground) { if (!hasMApis()) { return; } int vis = window.getDecorView().getSystemUiVisibility(); // Set light if (lightBackground) { vis |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; } else { vis &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; } window.getDecorView().setSystemUiVisibility(vis); } }