de.spiritcroc.modular_remote.Util.java Source code

Java tutorial

Introduction

Here is the source code for de.spiritcroc.modular_remote.Util.java

Source

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

package de.spiritcroc.modular_remote;

import android.app.Activity;
import android.app.DialogFragment;
import android.app.Fragment;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.PagerTabStrip;
import android.support.v4.view.PagerTitleStrip;
import android.support.v4.view.ViewPager;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.ForegroundColorSpan;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;

import de.spiritcroc.modular_remote.dialogs.SelectContainerDialog;
import de.spiritcroc.modular_remote.modules.CommandLineFragment;
import de.spiritcroc.modular_remote.modules.Container;
import de.spiritcroc.modular_remote.modules.DisplayFragment;
import de.spiritcroc.modular_remote.modules.HorizontalContainerFragment;
import de.spiritcroc.modular_remote.modules.ModuleFragment;
import de.spiritcroc.modular_remote.modules.PageContainerFragment;
import de.spiritcroc.modular_remote.modules.ScrollContainerFragment;
import de.spiritcroc.modular_remote.modules.SpinnerFragment;
import de.spiritcroc.modular_remote.modules.ToggleFragment;
import de.spiritcroc.modular_remote.modules.WebViewFragment;
import de.spiritcroc.modular_remote.modules.WidgetContainerFragment;

public abstract class Util {
    private static final String LOG_TAG = Util.class.getSimpleName();
    private static final boolean DEBUG = false;

    public static final boolean SCREENSHOT = false;

    private static Context applicationContext;

    public static void init(Context context) {
        if (context != null) {
            applicationContext = context.getApplicationContext();
        }
    }

    // Recovery key constants:
    // Increment if recoveryKey scheme changed (for single modules or full recreationKey)
    public static final int RECREATION_KEY_VERSION = 1;
    public static final String RK_ATTRIBUTE_SEPARATOR = "";
    // Alternative to above, used for unknown numbers:
    public static final String RK_ARRAY_ATTRIBUTE_SEPARATOR = "?";
    public static final String RK_CONTAINER_BRACKET = "";
    public static final String RK_CUSTOMIZED_MENU_SEPARATOR = "";
    public static final String RK_CUSTOMIZED_MENU_ATTRIBUTE_SEPARATOR = "";
    public static final String RK_FRAGMENT_POS = "";
    // Replace empty string in recovery keys with following string in order to be able to recreate
    // with String.split()
    public static final String RK_EMPTY_STRING_REPLACEMENT = "";

    // Strings users should not use, as it would damage the recovery keys
    public static final String[] FORBIDDEN_SUBSTRINGS = { RK_ATTRIBUTE_SEPARATOR, RK_ARRAY_ATTRIBUTE_SEPARATOR,
            RK_CONTAINER_BRACKET, RK_CUSTOMIZED_MENU_SEPARATOR, RK_CUSTOMIZED_MENU_ATTRIBUTE_SEPARATOR,
            RK_EMPTY_STRING_REPLACEMENT, RK_FRAGMENT_POS,
            // Reserved for future needs:
            "", "", "", "", "", "", "", "?", "", "?", "?", "", "", "", "",
            "", "", "", "", "", "", "", "?", "", "", "", "", "", "", "",
            "", "", "", "", "", "", "", "", "", "", "", "", "", "", "",
            "", "", "", "", "", "", "", "", "", "", "", "" };

    /**
     * @return
     * User input if input allowed. null if invalid
     */
    public static String getUserInput(TextView textView, boolean emptyForbidden) {
        String input = textView.getText().toString();
        if (input.equals("")) {
            if (emptyForbidden) {
                Context context = textView.getContext();
                textView.setError(context.getString(R.string.error_should_not_be_empty));
                return null;
            } else {
                return input;
            }
        }
        for (String forbidden : FORBIDDEN_SUBSTRINGS) {
            if (input.contains(forbidden)) {
                Context context = textView.getContext();
                String errorMsg = context.getString(R.string.error_should_not_contain_s, forbidden);
                textView.setError(errorMsg);
                return null;
            }
        }
        return input;
    }

    /**
     * @return
     * Size if valid, else -1
     */
    public static double getSizeInput(TextView textView) {
        String input = textView.getText().toString();
        if (input.equals("")) {
            Context context = textView.getContext();
            textView.setError(context.getString(R.string.error_should_not_be_empty));
            return -1;
        }
        try {
            double result = Double.parseDouble(input);
            if (result > 0) {
                return result;
            } else {
                Context context = textView.getContext();
                textView.setError(context.getString(R.string.error_should_be_higher_than_zero));
                return -1;
            }
        } catch (Exception e) {
            Context context = textView.getContext();
            textView.setError(context.getString(R.string.error_invalid));
            return -1;
        }
    }

    /**
     * Loads the current x value
     * @return
     * Whether the x value has changed since last checking it
     */
    public static boolean newX(View view) {
        int previous = bufferedX;
        return getX(view, true) != previous;
    }

    private static int bufferedX = -1;

    private static int getX(View view, boolean force) {
        if (!force && bufferedX != -1) {
            return bufferedX;
        }
        int newX = view.getMeasuredWidth();
        if (newX > 0) {
            bufferedX = newX;
        }
        if (DEBUG)
            Log.v(LOG_TAG, "getX" + bufferedX);
        return bufferedX;
    }

    public static int getX(View view) {
        return getX(view, false);
    }

    /**
     * Loads the current y value
     * @return
     * Whether the y value has changed since last checking it
     */
    public static boolean newY(View view) {
        int previous = bufferedY;
        return getY(view, true) != previous;
    }

    private static int bufferedY = -1;

    public static int getY(View view, boolean force) {
        if (!force && bufferedY != -1) {
            return bufferedY;
        }
        int newY = view.getMeasuredHeight();
        if (view instanceof ViewPager) {
            ViewPager viewPager = (ViewPager) view;
            for (int i = 0; i < viewPager.getChildCount(); i++) {
                View child = viewPager.getChildAt(i);
                if (child.getVisibility() != View.GONE
                        && (child instanceof PagerTabStrip || child instanceof PagerTitleStrip)) {
                    // Some views use space within the viewPager, but are not part of the page
                    newY -= child.getMeasuredHeight();
                }
            }
        }
        if (newY > 0) {
            bufferedY = newY;
        }
        if (DEBUG)
            Log.v(LOG_TAG, "getY " + bufferedY);
        return bufferedY;
    }

    public static int getY(View view) {
        return getY(view, false);
    }

    public static void resizeLayoutWidth(View containerView, LinearLayout layout, double size) {
        resizeLayoutWidth(containerView, layout, size, LinearLayout.LayoutParams.MATCH_PARENT);
    }

    public static void resizeLayoutWidth(View containerView, LinearLayout layout, double size,
            int valueForNegativeSize) {
        if (size > 0) {
            layout.getLayoutParams().width = getWidthFromBlockUnits(containerView, size, false);
        } else {
            layout.getLayoutParams().width = valueForNegativeSize;
        }
        layout.requestLayout();
    }

    public static void resizeLayoutHeight(View containerView, LinearLayout layout, double size) {
        resizeLayoutHeight(containerView, layout, size, LinearLayout.LayoutParams.MATCH_PARENT);
    }

    public static void resizeLayoutHeight(View containerView, LinearLayout layout, double size,
            int valueForNegativeSize) {
        if (size > 0) {
            layout.getLayoutParams().height = getHeightFromBlockUnits(containerView, size, false);
        } else {
            layout.getLayoutParams().height = valueForNegativeSize;
        }
        layout.requestLayout();
    }

    public static int getWidthFromBlockUnits(View containerView, double size, boolean pos) {
        int screenWidth = getX(containerView);
        if (size <= 0) {
            return 0;
        }

        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(containerView.getContext());
        int blockUnits = getBlockUnits(preferences, false, containerView);
        if (blockUnits <= 0) {
            blockUnits = 4;
        }

        if (pos) {
            return (int) Math.ceil(size * screenWidth / blockUnits);
        } else {
            return (int) Math.floor(size * screenWidth / blockUnits);
        }
    }

    public static int getHeightFromBlockUnits(View containerView, double size, boolean pos) {
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(containerView.getContext());
        int screenHeight = getY(containerView);
        if (size <= 0) {
            return 0;
        }

        int blockUnits = getBlockUnits(preferences, true, containerView);
        if (blockUnits <= 0) {
            blockUnits = 6;
        }

        if (pos) {
            return (int) Math.ceil(size * screenHeight / blockUnits);
        } else {
            return (int) Math.floor(size * screenHeight / blockUnits);
        }
    }

    public static int blockRound(View containerView, double d, boolean yDir) {
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(containerView.getContext());
        int blockUnits, screenSize;
        blockUnits = getBlockUnits(preferences, yDir, containerView);
        if (yDir) {
            screenSize = getY(containerView);
        } else {
            screenSize = getX(containerView);
        }
        return (int) Math.round(d / screenSize * blockUnits);
    }

    private static int getBlockUnits(SharedPreferences preferences, boolean y, View v) {
        boolean landscape = v.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
        return getBlockUnits(preferences, y, landscape);
    }

    private static int getBlockUnits(SharedPreferences preferences, boolean y, boolean landscape) {
        if (Preferences.ORIENTATION_SHARE_LAYOUT
                .equals(preferences.getString(Preferences.ORIENTATION, Preferences.ORIENTATION_SHARE_LAYOUT))) {
            landscape = false;
        }
        if (y) {
            if (landscape) {
                return getPreferenceInt(preferences, Preferences.BLOCK_SIZE_HEIGHT_LANDSCAPE, 4);
            } else {
                return getPreferenceInt(preferences, Preferences.BLOCK_SIZE_HEIGHT, 6);
            }
        } else {
            if (landscape) {
                return getPreferenceInt(preferences, Preferences.BLOCK_SIZE_LANDSCAPE, 6);
            } else {
                return getPreferenceInt(preferences, Preferences.BLOCK_SIZE, 4);
            }
        }
    }

    private static final double suggestedBlockSizeInInch = 0.18;
    private static final int suggestMultipleOf = 2;

    public static int suggestBlockSize(Activity activity, boolean yDir) {
        DisplayMetrics dm = new DisplayMetrics();
        activity.getWindowManager().getDefaultDisplay().getMetrics(dm);
        double size;
        if (yDir) {
            size = (double) dm.heightPixels / (double) dm.densityDpi;
        } else {
            size = (double) dm.widthPixels / (double) dm.densityDpi;
        }
        return ((int) (size / suggestedBlockSizeInInch / suggestMultipleOf)) * suggestMultipleOf;
    }

    /**
     * @return
     * The root container in the hierarchy of the fragment
     */
    public static ModuleFragment getPrimeContainer(ModuleFragment fragment) {
        ModuleFragment primeContainer = fragment;
        while (primeContainer.getParent() != null && primeContainer.getParent() instanceof ModuleFragment) {
            primeContainer = (ModuleFragment) primeContainer.getParent();
        }
        return primeContainer;
    }

    public static PageContainerFragment getPage(ModuleFragment fragment) {
        if (fragment instanceof PageContainerFragment) {
            return (PageContainerFragment) fragment;
        }
        ModuleFragment parent = fragment;
        while (parent.getParent() != null && parent.getParent() instanceof ModuleFragment) {
            parent = (ModuleFragment) parent.getParent();
            if (parent instanceof PageContainerFragment) {
                return (PageContainerFragment) parent;
            }
        }
        return null;
    }

    public static void restoreContentFromRecreationKey(Container container, String key, boolean menuEnabled) {
        String[] args = split(key, getSeparator(container), 0);
        for (int i = 1; i < args.length; i++) {
            String[] subArgs = split(args[i], RK_ATTRIBUTE_SEPARATOR, 1);
            ModuleFragment fragment = null;
            if (ModuleFragment.WEB_VIEW_FRAGMENT.equals(subArgs[0])) {
                fragment = WebViewFragment.recoverFromRecreationKey(args[i]);
            } else if (ModuleFragment.SCROLL_CONTAINER_FRAGMENT.equals(subArgs[0])) {
                fragment = ScrollContainerFragment.recoverFromRecreationKey(args[i]);
            } else if (ModuleFragment.WIDGET_CONTAINER_FRAGMENT.equals(subArgs[0])) {
                fragment = WidgetContainerFragment.recoverFromRecreationKey(args[i]);
            } else if (ModuleFragment.HORIZONTAL_CONTAINER.equals(subArgs[0])) {
                fragment = HorizontalContainerFragment.recoverFromRecreationKey(args[i]);
            } else if (ModuleFragment.BUTTON_FRAGMENT.equals(subArgs[0])) {
                fragment = DisplayFragment.recoverFromRecreationKey(args[i], true);
            } else if (ModuleFragment.DISPLAY_FRAGMENT.equals(subArgs[0])) {
                fragment = DisplayFragment.recoverFromRecreationKey(args[i], false);
            } else if (ModuleFragment.SPINNER_FRAGMENT.equals(subArgs[0])) {
                fragment = SpinnerFragment.recoverFromRecreationKey(args[i]);
            } else if (ModuleFragment.TOGGLE_FRAGMENT.equals(subArgs[0])) {
                fragment = ToggleFragment.recoverFromRecreationKey(args[i]);
            } else if (ModuleFragment.COMMAND_LINE_FRAGMENT.equals(subArgs[0])) {
                fragment = CommandLineFragment.recoverFromRecreationKey(args[i]);
            } else {
                Log.w(LOG_TAG, "restoreContentFromRecreationKey: Could not find fragment with " + "className "
                        + subArgs[0]);
            }
            if (fragment != null) {
                fragment.setMenuEnabled(menuEnabled);
                container.addFragment(fragment, false);
            }
        }
    }

    public static String getSeparator(Container container) {
        return getSeparator(container.getDepth());
    }

    private static String getSeparator(int depth) {
        return RK_CONTAINER_BRACKET + depth + RK_CONTAINER_BRACKET;
    }

    public static String updateRecreationKey(String recreationKey, int oldDepth, int newDepth) {
        if (oldDepth < newDepth) {
            int highestDepth = oldDepth;
            while (recreationKey.contains(getSeparator(highestDepth))) {
                highestDepth++;
            }
            highestDepth--;
            // Increase found separators
            int diff = newDepth - oldDepth;
            for (int i = highestDepth; i >= oldDepth; i--) {
                recreationKey = recreationKey.replaceAll(getSeparator(i), getSeparator(i + diff));
            }
        } else if (oldDepth > newDepth) {
            // Decrease found separators
            int diff = newDepth - oldDepth;
            for (int i = oldDepth; recreationKey.contains(getSeparator(i)); i++) {
                recreationKey = recreationKey.replaceAll(getSeparator(i), getSeparator(i + diff));
            }
        }
        return recreationKey;
    }

    /*
    Following code to obtain unique view id so my container concept works is from
    https://stackoverflow.com/questions/1714297/android-view-setidint-id-programmatically-how-to-avoid-id-conflicts
     */
    private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1);

    public static int generateViewId() {
        for (;;) {
            final int result = sNextGeneratedId.get();
            // aapt-generated IDs have the high byte nonzero; clamp to the range under that.
            int newValue = result + 1;
            if (newValue > 0x00FFFFFF)
                newValue = 1; // Roll over to 1, not 0.
            if (sNextGeneratedId.compareAndSet(result, newValue)) {
                return result;
            }
        }
    }

    public static String createCommandChain(String bossCommand, String command) {
        if (bossCommand.contains("")) {
            command = bossCommand.replaceFirst("", command);
        } else {
            String copy = command;
            command = bossCommand;
            for (int i = 0; i < copy.length(); i++) {
                command = command.replaceFirst("", "" + copy.charAt(i));
            }
        }
        return command;
    }

    public static void suggestPreviousIps(final Fragment fragment, final AutoCompleteTextView textView) {
        Activity activity = fragment.getActivity();
        final TcpConnectionManager tcpConnectionManager = TcpConnectionManager
                .getInstance(activity.getApplicationContext());
        textView.setAdapter(new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1,
                tcpConnectionManager.getConnectionSuggestions()));

        if (fragment instanceof ReceiverIpSelectorUser) {
            textView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
                @Override
                public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                    TcpConnectionManager.TcpConnection connection = tcpConnectionManager
                            .getTcpConnection(textView.getText().toString());
                    ((ReceiverIpSelectorUser) fragment).setReceiverType(connection.getType());
                }
            });
        }
    }

    public static long getPageId(Context context) {
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
        long lastId = sharedPreferences.getLong(Preferences.LAST_PAGE_ID, -1);
        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.putLong(Preferences.LAST_PAGE_ID, ++lastId).apply();
        return lastId;
    }

    public static class StringReference {
        public String value;
    }

    public static View scrollView(View view) {
        ScrollView scrollView = new ScrollView(view.getContext());
        scrollView.addView(view);
        return scrollView;
    }

    /**
     * @return
     * Recreation key that can contain "" values and still be recovered with String.split()
     */
    public static String fixRecreationKey(String key, String separator) {
        return key.replaceAll(separator + separator, separator + RK_EMPTY_STRING_REPLACEMENT + separator);
    }

    /**
     * @param minArgCount
     * Number of expected arguments, if there are fewer: fill with empty strings
     * Use 0 if possible out-of-range exception already handled
     * @return
     * Args that were supposed to be saved with fixRecreationKey
     */
    public static String[] recoverRecreationArgs(String[] args, int minArgCount) {
        String[] result = new String[Math.max(args.length, minArgCount)];
        for (int i = 0; i < args.length; i++) {
            if (RK_EMPTY_STRING_REPLACEMENT.equals(args[i])) {
                result[i] = "";
            } else {
                result[i] = args[i];
            }
        }
        // Fill with empty args
        for (int i = args.length; i < result.length; i++) {
            result[i] = "";
        }
        return result;
    }

    public static String[] split(String s, String separator, int minArgCount) {
        return recoverRecreationArgs(s.split(separator), minArgCount);
    }

    public static void addFragmentToContainer(Activity activity, @NonNull ModuleFragment fragment,
            PageContainerFragment page, @Nullable Container container) {
        if (container == null) {
            new SelectContainerDialog().setValues(page, fragment).show(activity.getFragmentManager(),
                    "SelectContainerDialog");
        } else {
            fragment.prepareDepthChange();
            container.addFragment(fragment, false);
            if (activity instanceof MainActivity) {
                // Scroll to newly added fragment
                Container parent = fragment instanceof Container ? (Container) fragment : fragment.getParent();
                while (parent != null && !(parent instanceof PageContainerFragment)
                        && parent instanceof ModuleFragment) {
                    parent = ((ModuleFragment) parent).getParent();
                }
                if (parent instanceof PageContainerFragment) {
                    ((MainActivity) activity).scrollToPage((PageContainerFragment) parent);
                }
            }
        }
    }

    public static void addWidgetToContainer(Activity activity, PageContainerFragment page,
            @Nullable Container container, @Nullable DialogFragment dismissDialog) {
        if (container == null) {
            new SelectContainerDialog().addWidget(page).show(activity.getFragmentManager(),
                    "SelectContainerDialog");
        } else if (activity instanceof MainActivity) {
            ((MainActivity) activity).setAddWidgetListener(dismissDialog);
            ((MainActivity) activity).addWidget(container);
        } else {
            Log.w(LOG_TAG, "addWidgetToContainer: !(activity instanceof MainActivity)");
        }
    }

    public static Spannable getContainerContentReadableName(Container container, @Nullable String prefix) {
        if (prefix == null) {
            prefix = "";
        }
        if (container.isEmpty()) {
            return new SpannableString(prefix + container.getReadableName());
        } else {
            ModuleFragment[] fragments = container.getFragments();
            String name = prefix + container.getReadableName();
            int spanStartIndex = name.length();
            name += " [" + fragments[0].getReadableName();
            int contentColor = fragments[0].getActivity().getResources().getColor(R.color.text_container_content);
            for (int i = 1; i < fragments.length; i++) {
                name += "; " + fragments[i].getReadableName();
            }
            name += "]";
            Spannable nameSpan = new SpannableString(name);
            nameSpan.setSpan(new ForegroundColorSpan(contentColor), spanStartIndex, name.length(),
                    Spannable.SPAN_INCLUSIVE_INCLUSIVE);
            return nameSpan;
        }
    }

    public static Container[] getAllContainers(Container container) {
        ArrayList<Container> list = new ArrayList<>();
        list.add(container);
        ModuleFragment[] fragments = container.getFragments();
        for (ModuleFragment fragment : fragments) {
            if (fragment instanceof Container) {
                Container[] addition = ((Container) fragment).getAllContainers();
                list.addAll(Arrays.asList(addition));
            }
        }
        return list.toArray(new Container[list.size()]);
    }

    public static boolean hasContainerOverlappingFragments(Container container) {
        ModuleFragment[] fragments = container.getFragments();
        ArrayList<Container> containedContainers = new ArrayList<>();
        for (int i = 0; i < fragments.length; i++) {
            if (fragments[i] instanceof Container) {
                containedContainers.add((Container) fragments[i]);
            }
            View currentView = fragments[i].getView();
            if (currentView != null) {
                Rect currentRect = new Rect(currentView.getLeft(), currentView.getTop(), currentView.getRight(),
                        currentView.getBottom());

                for (int j = i + 1; j < fragments.length; j++) {
                    View checkView = fragments[j].getView();
                    if (checkView != null && currentRect.intersects(checkView.getLeft(), checkView.getTop(),
                            checkView.getRight(), checkView.getBottom())) {
                        Log.i(LOG_TAG, fragments[i].getReadableName() + " overlaps with "
                                + fragments[j].getReadableName());
                        return true;
                    }
                }
            }
        }

        for (int i = 0; i < containedContainers.size(); i++) {
            if (hasContainerOverlappingFragments(containedContainers.get(i))) {
                return true;
            }
        }

        return false;
    }

    public static int getPreferenceInt(SharedPreferences sharedPreferences, String key, int defaultValue) {
        try {
            return Integer.parseInt(sharedPreferences.getString(key, "" + defaultValue));
        } catch (Exception e) {
            return defaultValue;
        }
    }

    /**
     * Use instead of context.getString() if context might be null
     * Requires init() to be called before at least once
     */
    public static String getACString(int resId) {
        if (applicationContext == null) {
            Log.e(LOG_TAG, "getADString: applicationContext == null");
            return "";
        } else {
            return applicationContext.getString(resId);
        }
    }

    /**
     * Use this to test which chars the device is able to display
     */
    public static String getDebugText() {
        final String[][] test = { { "00", " " },
                //{"01", ""},
                //{"01", ""},//alternate
                //{"01", ""},//alternate
                //{"01", ""},//alternate
                //{"01", "x"},//alternate
                //{"01", "&#10536;"},//alternate
                //{"01", "\u2928"},//alternate
                { "01", "x" }, //alternate
                { "02", "" },
                //{"03", ""},
                { "03", "x" }, //alternate
                //{"04", ""},
                //{"04", "\u23F6\u23F7"},//alternate
                { "04", "" }, //alternate
                { "05", "DOL" }, { "06", "BY" }, { "07", "I" }, { "08", "II" }, { "09", "" }, { "0A", "" },
                { "0B", "" }, { "0C", "." }, { "0D", ".0" }, { "0E", ".5" }, { "0F", "" }, { "10", "0" },
                { "11", "1" }, { "12", "2" }, { "13", "3" }, { "14", "4" }, { "15", "5" }, { "16", "6" },
                { "17", "7" }, { "18", "8" }, { "19", "9" }, { "1A", "A" }, { "1B", "B" }, { "1C", "C" },
                { "1D", "F" }, { "1E", "M" }, { "1F", " " }, { "20", " " }, { "21", "!" }, { "22", "\"" },
                { "23", "#" }, { "24", "$" }, { "25", "%" }, { "26", "&" }, { "27", "\'" }, { "28", "(" },
                { "29", ")" }, { "2A", "*" }, { "2B", "+" }, { "2C", "," }, { "2D", "-" }, { "2E", "." },
                { "2F", "/" }, { "30", "0" }, { "31", "1" }, { "32", "2" }, { "33", "3" }, { "34", "4" },
                { "35", "5" }, { "36", "6" }, { "37", "7" }, { "38", "8" }, { "39", "9" }, { "3A", ":" },
                { "3B", ";" }, { "3C", "<" }, { "3D", "=" }, { "3E", ">" }, { "3F", "?" }, { "40", "@" },
                { "41", "A" }, { "42", "B" }, { "43", "C" }, { "44", "D" }, { "45", "E" }, { "46", "F" },
                { "47", "G" }, { "48", "H" }, { "49", "I" }, { "4A", "J" }, { "4B", "K" }, { "4C", "L" },
                { "4D", "M" }, { "4E", "N" }, { "4F", "O" }, { "50", "P" }, { "51", "Q" }, { "52", "R" },
                { "53", "S" }, { "54", "T" }, { "55", "U" }, { "56", "V" }, { "57", "W" }, { "58", "X" },
                { "59", "Y" }, { "5A", "Z" }, { "5B", "[" }, { "5C", "\\" }, { "5D", "]" }, { "5E", "^" },
                { "5F", "_" }, { "60", "||" }, { "61", "a" }, { "62", "b" }, { "63", "c" }, { "64", "d" },
                { "65", "e" }, { "66", "f" }, { "67", "g" }, { "68", "h" }, { "69", "i" }, { "6A", "j" },
                { "6B", "k" }, { "6C", "l" }, { "6D", "m" }, { "6E", "n" }, { "6F", "o" }, { "70", "p" },
                { "71", "q" }, { "72", "r" }, { "73", "s" }, { "74", "t" }, { "75", "u" }, { "76", "v" },
                { "77", "w" }, { "78", "x" }, { "79", "y" }, { "7A", "z" }, { "7B", "{" }, { "7C", "|" },
                { "7D", "}" }, { "7E", "~" }, { "7F", "" }, { "80", "" }, { "81", "" }, { "82", "IJ" },
                { "83", "ij" }, { "84", "" }, { "85", "" }, { "86", " " }, { "87", " " }, { "88", " " },
                { "89", " " }, { "8A", " " }, { "8B", " " }, { "8C", "?" }, { "8D", "" }, { "8E", "" },
                { "8F", "" }, { "90", "+" }, { "91", "" },
                //{"92", "\uD83D\uDDC0"},
                { "92", "\uD83D\uDCC1" }, //alternate
                //{"92", "\uD83D\uDCC2"},//alternate
                { "93", " " }, { "94", " " }, { "95", " " }, { "96", " " }, { "97", " " }, { "98", " " },
                { "99", " " }, { "9A", " " }, { "9B", " " }, { "9C", " " }, { "9D", " " }, { "9E", " " },
                { "9F", " " }, { "A0", " " }, { "A1", "" }, { "A2", "" }, { "A3", "" }, { "A4", "" },
                { "A5", "" }, { "A6", "" }, { "A7", "" }, { "A8", "" }, { "A9", "" }, { "AA", "" },
                { "AB", "" }, { "AC", "" }, { "AD", "-" }, { "AE", "" }, { "AF", "" }, { "B0", "" },
                { "B1", "" }, { "B2", "" }, { "B3", "" }, { "B4", "?" }, { "B5", "" }, { "B6", "" },
                { "B7", "" }, { "B8", "" }, { "B9", "" }, { "BA", "" }, { "BB", "" }, { "BC", "" },
                { "BD", "" }, { "BE", "" }, { "BF", "" }, { "C0", "" }, { "C1", "?" }, { "C2", "" },
                { "C3", "" }, { "C4", "" }, { "C5", "" }, { "C6", "" }, { "C7", "" }, { "C8", "" },
                { "C9", "" }, { "CA", "" }, { "CB", "" }, { "CC", "" }, { "CD", "?" }, { "CE", "" },
                { "CF", "" }, { "D0", "?" }, { "D1", "" }, { "D2", "" }, { "D3", "" }, { "D4", "" },
                { "D5", "" }, { "D6", "" }, { "D7", "" }, { "D8", "" }, { "D9", "" }, { "DA", "" },
                { "DB", "" }, { "DC", "" }, { "DD", "?" }, { "DE", "" }, { "DF", "" }, { "E0", "" },
                { "E1", "" }, { "E2", "" }, { "E3", "" }, { "E4", "" }, { "E5", "" }, { "E6", "" },
                { "E7", "" }, { "E8", "" }, { "E9", "" }, { "EA", "" }, { "EB", "" }, { "EC", "" },
                { "ED", "" }, { "EE", "" }, { "EF", "" }, { "F0", "" }, { "F1", "" }, { "F2", "" },
                { "F3", "" }, { "F4", "" }, { "F5", "" }, { "F6", "" }, { "F7", "" }, { "F8", "" },
                { "F9", "" }, { "FA", "" }, { "FB", "" }, { "FC", "" }, { "FD", "" }, { "FE", "" },
                { "FF", "" } };
        String s = "PIONEER:";
        for (int i = 0; i < test.length; i++) {
            s += "\n" + test[i][0] + ":" + test[i][1] + ":";
        }
        s += "\nOTHER:\n?\n?\n?\n?\n?\n";
        return s;
    }
}