code:
/*
 * DSBDirect
 * Copyright (C) 2019 Fynn Godau
 *
 * 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 <https://www.gnu.org/licenses/>.
 *
 * This software is not affiliated with heinekingmedia GmbH, the
 * developer of the DSB platform.
 */

package godau.fynn.dsbdirect.activity.settings;

import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.preference.CheckBoxPreference;
import android.preference.Preference;
import androidx.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.EditText;
import godau.fynn.dsbdirect.BuildConfig;
import godau.fynn.dsbdirect.R;
import godau.fynn.dsbdirect.Utility;
import godau.fynn.dsbdirect.activity.AboutLibrariesActivity;
import godau.fynn.dsbdirect.activity.MainActivity;
import godau.fynn.dsbdirect.manager.DownloadManager;
import godau.fynn.dsbdirect.manager.FileManager;
import godau.fynn.dsbdirect.manager.LoginManager;
import godau.fynn.dsbdirect.table.Table;
import humanize.Humanize;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

// Thanks, https://stackoverflow.com/a/12806877

public class MainSettingsActivity extends SettingsActivity {

    private static final int REQUEST_CODE_STYLING = 1;

    public PreferenceFragment getPreferenceFragment() {
        return new PreferenceFragment();
    }

    public static class PreferenceFragment extends SettingsActivity.PreferenceFragment {

        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);

            // Load the preferences from an XML resource
            addPreferencesFromResource(R.xml.preferences);

            // Behave if pictures are involved
            final boolean containsHtml = getActivity().getIntent().getBooleanExtra(EXTRA_CONTAINS_HTML, true);
            final boolean onlyHtml = getActivity().getIntent().getBooleanExtra(EXTRA_HTML_ONLY, true);

            if (!containsHtml) {
                Preference parse = findPreference("parse");
                parse.setEnabled(false);
                ((CheckBoxPreference) parse).setChecked(false);
                parse.setSummary(R.string.settings_view_parse_not_html);

                // Gray out display empty setting as user will definitely see empty notifications
                Preference displayEmpty = findPreference("displayEmpty");
                displayEmpty.setEnabled(false);
                ((CheckBoxPreference) displayEmpty).setChecked(true);
                // Explain why it is not possible
                displayEmpty.setSummary(R.string.settings_view_parse_not_html);
            } else {
                if (sharedPreferences.getBoolean("parse", true)) {
                    // Explain why it makes sense to download automatically
                    findPreference("autoDownload").setSummary(R.string.settings_polling_download_recommended);

                }

                // Gray out merge preference because merging is not possible when images are involved
                if (!onlyHtml) {
                    Preference merge = findPreference("merge");
                    merge.setEnabled(false);
                    ((CheckBoxPreference) merge).setChecked(false);
                    merge.setSummary(R.string.settings_view_parse_not_exclusively_html);
                }
            }

            // Open Style and Colors settings when user taps Style and Colors settings
            findPreference("styling").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
                @Override
                public boolean onPreferenceClick(Preference preference) {
                    Intent intent = new Intent(getActivity(), StyleSettingsActivity.class);
                    intent.putExtra(EXTRA_HTML_ONLY, onlyHtml);
                    getActivity().startActivityForResult(intent, REQUEST_CODE_STYLING);
                    return true;
                }
            });

            // Set temporary filter setting according to Enable filter setting whenever updated
            findPreference("filter").setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
                @Override
                public boolean onPreferenceChange(Preference preference, Object newValue) {
                    MainActivity.filterEnabled = (Boolean) newValue;
                    return true;
                }
            });

            // Display set filters Set Filter button
            final Preference setFilter = findPreference("set_filter");
            final Handler updateSetFilterSummary = new Handler(new Handler.Callback() {
                @Override
                public boolean handleMessage(Message msg) {
                    Set courseSet = sharedPreferences.getStringSet("courses", new HashSet<String>());
                    String[] concat = new String[courseSet.size() + 2];
                    courseSet.toArray(concat);
                    concat[courseSet.size()] = sharedPreferences.getString("number", "") + sharedPreferences.getString("letter", "");
                    concat[courseSet.size() + 1] = sharedPreferences.getString("name", null);
                    setFilter.setSummary(Utility.smartConcatenate(concat, ", "));
                    return false;
                }
            });
            updateSetFilterSummary.sendEmptyMessage(0);

            // Let Set Filter button open the Set Filter popup
            setFilter.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
                @Override
                public boolean onPreferenceClick(Preference preference) {
                    setFilterPopup(getActivity(), updateSetFilterSummary);

                    return false;
                }
            });

            // Let Check for updates now button Check for updates now
            findPreference("updates_check_now").setOnPreferenceClickListener(getOnCheckUpdateNowClickListener());

            // Disable polling if API level is too low
            if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
                Preference poll = findPreference("poll");
                ((CheckBoxPreference) poll).setChecked(false);
                poll.setEnabled(false);
                poll.setSummary(R.string.settings_view_polling_api_too_low);
                findPreference("polling_description").setEnabled(false);
            }

            // Manage logins by tappig Manage logins
            findPreference("login_manage").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
                @Override
                public boolean onPreferenceClick(Preference preference) {
                    startActivity(new Intent(getActivity(), LoginSettingsActivity.class));
                    return true;
                }
            });

            // Gray out Delete all files now when no files are available
            final Preference clearCache = findPreference("clear_cache_now");
            final FileManager fileManager = new FileManager(getActivity());
            final int fileCount = fileManager.countFiles();

            if (fileCount <= 0) {
                clearCache.setEnabled(false);
            }

            // Delete all files when Delete all files now is pressed
            clearCache.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
                @Override
                public boolean onPreferenceClick(Preference preference) {
                    // Prompt for confirmation
                    new AlertDialog.Builder(getActivity())
                            .setTitle(R.string.clear_cache_popup_title)
                            .setMessage(getString(R.string.clear_cache_popup_message,
                                    Humanize.pluralize(
                                            getString(R.string.one_file), "{0} " + getString(R.string.files),
                                            "{0} " + getString(R.string.files), fileCount
                                    ),
                                    Humanize.binaryPrefix(fileManager.occupiedStorage())))
                            .setNegativeButton(R.string.cancel, null)
                            .setPositiveButton(R.string.confirm, new DialogInterface.OnClickListener() {
                                @Override
                                public void onClick(DialogInterface dialog, int which) {
                                    // Delete all files
                                    fileManager.deleteAllFiles();

                                    // There are no more files left (hopefully)
                                    clearCache.setEnabled(false);
                                }
                            })
                            .show();

                    return true;
                }
            });

            // Schedule or cancel notification when Enable notifications is toggled
            findPreference("poll").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
                @Override
                public boolean onPreferenceClick(Preference preference) {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                        // Utility will check whether polling is enabled and act accordingly
                        u.schedulePolling();
                    }

                    return true;
                }
            });

            // Show version number in About description preference summary
            findPreference("settings_about_description").setSummary(
                    getString(R.string.credits) + "\n" +
                            getString(R.string.settings_about_description_summary, BuildConfig.VERSION_NAME)
            );

            // Let Write the developer an email message button enable the user to Write the developer an email message
            findPreference("settings_about_email").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
                @Override
                public boolean onPreferenceClick(Preference preference) {
                    emailTheDev(getActivity());
                    return false;
                }
            });

            // Request a parser to be developed button should allow users to Request a parser to be developed
            findPreference("settings_about_request_parser").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
                @Override
                public boolean onPreferenceClick(Preference preference) {
                    new AlertDialog.Builder(getActivity())
                            .setTitle(R.string.request_parser_popup_title)
                            .setMessage(R.string.request_parser_popup_message)
                            .setNegativeButton(R.string.dont_request, null)
                            .setPositiveButton(R.string.transfer_now, new DialogInterface.OnClickListener() {
                                @Override
                                public void onClick(DialogInterface dialog, int which) {

                                    final ProgressDialog progressDialog = ProgressDialog.show(
                                            getActivity(), null,
                                            getActivity().getString(R.string.request_parser_uploading_message),
                                            true
                                    );

                                    new Thread(new Runnable() {
                                        @Override
                                        public void run() {

                                            try {

                                                DownloadManager downloadManager = new DownloadManager(getActivity());
                                                LoginManager loginManager = new LoginManager(sharedPreferences);

                                                // Download timetable list
                                                Table[] tables = downloadManager.downloadContentInformation(loginManager.getActiveLogin())
                                                        .getTables();

                                                final boolean success = downloadManager.uploadParserRequest(tables[0].getUri());

                                                getActivity().runOnUiThread(new Runnable() {
                                                    @Override
                                                    public void run() {

                                                        progressDialog.dismiss();

                                                        new AlertDialog.Builder(getActivity())
                                                                .setTitle(success ?
                                                                        R.string.request_parser_uploading_successful_title :
                                                                        R.string.request_parser_uploading_failed_title
                                                                )
                                                                .setMessage(success ?
                                                                        R.string.request_parser_uploading_successful_message :
                                                                        R.string.request_parser_uploading_failed_message
                                                                )
                                                                .setPositiveButton(R.string.ok, null)
                                                                .show();
                                                    }
                                                });

                                            } catch (IOException e) {
                                                e.printStackTrace();

                                                getActivity().runOnUiThread(new Runnable() {
                                                    @Override
                                                    public void run() {
                                                        progressDialog.dismiss();

                                                        new AlertDialog.Builder(getActivity())
                                                                .setTitle(R.string.network_generic_error)
                                                                .setMessage(R.string.network_generic_error_request)
                                                                .setPositiveButton(R.string.ok, null)
                                                                .show();
                                                    }
                                                });
                                            }
                                        }
                                    }).start();

                                }
                            })
                            .show();

                    return true;
                }
            });

            // Manage shortcodes when choosing to Manage shortcodes
            findPreference("shortcode_manage").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
                @Override
                public boolean onPreferenceClick(Preference preference) {
                    startActivity(new Intent(getActivity(), ShortcodeSettingsActivtiy.class));
                    return false;
                }
            });

            // Check out repository when Check out repository is selected
            findPreference("settings_about_repository").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
                @Override
                public boolean onPreferenceClick(Preference preference) {
                    Intent checkOutRepo = new Intent(Intent.ACTION_VIEW);
                    checkOutRepo.setData(Uri.parse(getString(R.string.uri_repository)));
                    startActivity(checkOutRepo);
                    return true;
                }
            });

            // Lead user to page where they can File an issue once they click File an issue
            findPreference("settings_about_issue").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
                @Override
                public boolean onPreferenceClick(Preference preference) {
                    Intent fileAnIssue = new Intent(Intent.ACTION_VIEW);
                    fileAnIssue.setData(Uri.parse(getString(R.string.uri_issue_tracker)));
                    startActivity(fileAnIssue);
                    return true;
                }
            });

            // Show information About libraries upon touching About libraries
            findPreference("settings_about_libraries").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
                @Override
                public boolean onPreferenceClick(Preference preference) {

                    startActivity(new Intent(getActivity(), AboutLibrariesActivity.class));

                    return true;
                }
            });
        }

        private Preference.OnPreferenceClickListener getOnCheckUpdateNowClickListener() {
            return new Preference.OnPreferenceClickListener() {
                @Override
                public boolean onPreferenceClick(Preference preference) {
                    new Utility(getActivity()).checkForUpdate(true, new DownloadManager(getActivity()));
                    return true;
                }
            };
        }

        private String makeTime(int hour, int minute) {
            String minuteString = String.valueOf(minute);
            String hourString = String.valueOf(hour);

            if (minuteString.length() < 2) {
                minuteString = "0" + minuteString;
            }

            if (hourString.length() < 2) {
                hourString = "0" + hourString;
            }
            return hourString + ":" + minuteString;
        }

    }

    public static void setFilterPopup(Context context, @Nullable final Handler ok) {

        // spawn layout
        View promptView = LayoutInflater.from(context).inflate(R.layout.action_filter, null);

        // get sharedPreferences
        final SharedPreferences sharedPreferences1 = new Utility(context).getSharedPreferences();

        // find fields
        final EditText inputNumber = promptView.findViewById(R.id.input_number);
        final EditText inputLetter = promptView.findViewById(R.id.input_letter);
        final EditText inputCourses = promptView.findViewById(R.id.input_courses);
        final EditText inputName = promptView.findViewById(R.id.input_name);

        // fill in data from sharedPreferences

        Set<String> courseSet = sharedPreferences1.getStringSet("courses", new HashSet<String>());
        String[] courseArray = courseSet.toArray(new String[courseSet.size()]);
        String courseString = Utility.smartConcatenate(courseArray, " ");

        inputNumber.setText(sharedPreferences1.getString("number", ""));
        inputLetter.setText(sharedPreferences1.getString("letter", ""));
        inputCourses.setText(courseString);
        inputName.setText(sharedPreferences1.getString("name", ""));

        new AlertDialog.Builder(context)
                .setView(promptView)
                .setPositiveButton(R.string.confirm, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {

                        String[] courses = inputCourses.getText().toString().split(" ");

                        String number = inputNumber.getText().toString();
                        String letter = inputLetter.getText().toString();
                        String name = inputName.getText().toString();
                        sharedPreferences1.edit()
                                .putString("number", number)
                                .putString("letter", letter)
                                .putStringSet("courses", new HashSet<String>(Arrays.asList(courses)))
                                .putString("name", name)
                                .apply();

                        if (ok != null) {
                            ok.sendEmptyMessage(0);
                        }

                    }
                })
                .setNegativeButton(R.string.cancel, null)
                .setCancelable(false) // user might accidentally click it away otherwise
                .setTitle(R.string.action_filter_popup_title)
                .setMessage(R.string.action_filter_popup_message)
                .show();

    }

    public static void emailTheDev(Context context) {
        Intent emailTheDev = new Intent(Intent.ACTION_VIEW);
        emailTheDev.setData(Uri.parse(context.getString(R.string.email_uri, context.getString(R.string.email_body))));

        context.startActivity(emailTheDev);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        recreate();
    }
}


code:
package godau.fynn.dsbdirect.activity.settings;

import android.app.Activity;
import android.content.SharedPreferences;
import android.os.Bundle;
import godau.fynn.dsbdirect.Utility;

public abstract class SettingsActivity extends Activity {

    public static final String EXTRA_CONTAINS_HTML = "htmlTable";
    public static final String EXTRA_HTML_ONLY = "htmlOnlyTable";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        new Utility(SettingsActivity.this).stylize();

        // Display the fragment as the main content
        getFragmentManager().beginTransaction().replace(android.R.id.content,
                getPreferenceFragment()).commit();

    }

    @Override
    public void onBackPressed() {
        // Possibly trigger recreate
        setResult(RESULT_OK);

        super.onBackPressed();
    }

    abstract PreferenceFragment getPreferenceFragment();

    public static abstract class PreferenceFragment extends android.preference.PreferenceFragment {

        protected Utility u;
        protected SharedPreferences sharedPreferences;

        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);

            // Load correct sharedPreferences
            getPreferenceManager().setSharedPreferencesName("default");

            u = new Utility(getActivity());
            sharedPreferences = u.getSharedPreferences();

        }
    }

}

repo_name: tomjschwanke/DSBDirect

license: mit


code:
package godau.fynn.dsbdirect.activity.settings;

import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.PreferenceScreen;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.EditText;
import godau.fynn.dsbdirect.PreferenceCategory;
import godau.fynn.dsbdirect.R;
import godau.fynn.dsbdirect.manager.DownloadManager;
import godau.fynn.dsbdirect.manager.ShortcodeManager;
import godau.fynn.dsbdirect.table.Shortcode;

import java.io.IOException;

public class ShortcodeSettingsActivtiy extends SettingsActivity {

    public PreferenceFragment getPreferenceFragment() {
        return new PreferenceFragment();
    }

    public static class PreferenceFragment extends SettingsActivity.PreferenceFragment {

        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);

            final ShortcodeManager shortcodeManager = new ShortcodeManager(getActivity());

            // Load correct sharedPreferences
            getPreferenceManager().setSharedPreferencesName("default");

            // Thanks, https://stackoverflow.com/a/37745292
            addPreferencesFromResource(R.xml.preferences_shortcodes);
            PreferenceScreen preferenceScreen = this.getPreferenceScreen();

            // Download shortcodes from eltern-portal.org
            findPreference("settings_shortcodes_elternportal").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
                @Override
                public boolean onPreferenceClick(Preference preference) {
                    // spawn layout
                    View promptView = LayoutInflater.from(getActivity()).inflate(R.layout.action_shortcodes_elternportal, null);

                    // find fields
                    final EditText inputURL = promptView.findViewById(R.id.input_url);
                    final EditText inputEmail = promptView.findViewById(R.id.input_email);
                    final EditText inputPassword = promptView.findViewById(R.id.input_password);
                    new AlertDialog.Builder(getActivity())
                            .setTitle(R.string.settings_shortcodes_elternportal_popup_title)
                            .setMessage(R.string.settings_shortcodes_elternportal_popup_message)
                            .setCancelable(false)
                            .setNegativeButton(R.string.cancel, null)
                            .setView(promptView)
                            .setPositiveButton(R.string.import_, new DialogInterface.OnClickListener() {
                                @Override
                                public void onClick(DialogInterface dialog, int which) {

                                    //show loader
                                    final ProgressDialog progressDialog = ProgressDialog.show(
                                            getActivity(), null,
                                            getActivity().getString(R.string.request_parser_uploading_message),
                                            true
                                    );
                                    new Thread(new Runnable() {
                                        @Override
                                        public void run() {

                                            try {

                                                DownloadManager downloadManager = new DownloadManager(getActivity());
                                                Shortcode[] shortcodes = downloadManager.downloadShortcodesFromElternportal(inputURL.getText().toString(), inputEmail.getText().toString(), inputPassword.getText().toString());
                                                new ShortcodeManager(getActivity()).write(shortcodes);

                                                getActivity().runOnUiThread(new Runnable() {
                                                    @Override
                                                    public void run() {

                                                        progressDialog.dismiss();

                                                        new AlertDialog.Builder(getActivity())
                                                                .setTitle(R.string.settings_shortcodes_elternportal_popup_success_title)
                                                                .setMessage(R.string.settings_shortcodes_elternportal_popup_success_message)
                                                                .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
                                                                    @Override
                                                                    public void onClick(DialogInterface dialog, int which) {
                                                                        getActivity().recreate();
                                                                    }
                                                                })
                                                                .setCancelable(false) // setOnDismissListener is API 17+ only
                                                                .show();
                                                    }
                                                });

                                            } catch (DownloadManager.LoginFailureException e) {
                                                e.printStackTrace();
                                                getActivity().runOnUiThread(new Runnable() {
                                                    @Override
                                                    public void run() {
                                                        progressDialog.dismiss();

                                                        new AlertDialog.Builder(getActivity())
                                                                .setTitle(R.string.credentials_invalid)
                                                                .setMessage(R.string.settings_shortcodes_elternportal_popop_fail_credentials_invalid)
                                                                .setPositiveButton(R.string.ok, null)
                                                                .show();
                                                    }
                                                });
                                            } catch (IllegalArgumentException e) {
                                                e.printStackTrace();
                                                getActivity().runOnUiThread(new Runnable() {
                                                    @Override
                                                    public void run() {
                                                        progressDialog.dismiss();

                                                        new AlertDialog.Builder(getActivity())
                                                                .setTitle(R.string.settings_shortcodes_elternportal_popop_fail_url)
                                                                .setMessage(R.string.settings_shortcodes_elternportal_popop_fail_url_message)
                                                                .setPositiveButton(R.string.ok, null)
                                                                .show();
                                                    }
                                                });
                                            } catch (IOException e) {
                                                e.printStackTrace();

                                                getActivity().runOnUiThread(new Runnable() {
                                                    @Override
                                                    public void run() {
                                                        progressDialog.dismiss();

                                                        new AlertDialog.Builder(getActivity())
                                                                .setTitle(R.string.network_generic_error)
                                                                .setMessage(R.string.settings_shortcodes_elternportal_popup_fail_network)
                                                                .setPositiveButton(R.string.ok, null)
                                                                .show();
                                                    }
                                                });
                                            }
                                        }
                                    }).start();

                                }
                            })
                            .show();

                    return true;
                }
            });

            // Add shortcodes
            PreferenceCategory categoryEdit = (PreferenceCategory) findPreference("shortcodes");

            for (final Shortcode s : shortcodeManager.read()) {
                Preference shortcodePreference = new Preference(getActivity());

                shortcodePreference.setTitle(s.getDisplayName());

                // Prompt for new display name
                shortcodePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
                    @Override
                    public boolean onPreferenceClick(Preference preference) {

                        // Spawn editText layout
                        final View promptView = LayoutInflater.from(getActivity()).inflate(R.layout.action_edit_shortcode, null);
                        final EditText editText = promptView.findViewById(R.id.editText);
                        final EditText editText1 = promptView.findViewById(R.id.editText1);

                        editText.setText(s.getFrom());
                        editText1.setText(s.getTo());

                        new AlertDialog.Builder(getActivity())
                                .setTitle(getActivity().getString(R.string.settings_shortcodes_edit_popup))
                                .setView(promptView)
                                .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
                                    @Override
                                    public void onClick(DialogInterface dialog, int which) {

                                        // Write changed shortcode to shared preferences

                                        s.setFrom(editText.getText().toString());
                                        s.setTo(editText1.getText().toString());

                                        shortcodeManager.write();

                                        // Reload list by recreating
                                        getActivity().recreate();
                                    }
                                })
                                .setNegativeButton(R.string.cancel, null)
                                .setNeutralButton(R.string.settings_shortcodes_edit_popup_remove, new DialogInterface.OnClickListener() {
                                    @Override
                                    public void onClick(DialogInterface dialog, int which) {

                                        shortcodeManager.removeShortcode(s);

                                        // Reload list by recreating
                                        getActivity().recreate();
                                    }
                                })
                                .show();
                        return true;
                    }
                });

                categoryEdit.addPreference(shortcodePreference);
            }

            // Add Add shortcode button

            Preference newLoginPreference = new Preference(getActivity());
            newLoginPreference.setTitle(R.string.settings_shortcodes_add);

            // TODO: This button could have a '+' icon, but depending on the theme, it would need to be black or white.

            newLoginPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
                @Override
                public boolean onPreferenceClick(Preference preference) {

                    // Spawn editText layout
                    final View promptView = LayoutInflater.from(getActivity()).inflate(R.layout.action_edit_shortcode, null);
                    final EditText editText = promptView.findViewById(R.id.editText);
                    final EditText editText1 = promptView.findViewById(R.id.editText1);

                    new AlertDialog.Builder(getActivity())
                            .setTitle(getActivity().getString(R.string.settings_shortcodes_add_popup))
                            .setView(promptView)
                            .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
                                @Override
                                public void onClick(DialogInterface dialog, int which) {

                                    // Write new shortcode to shared preferences
                                    try {
                                        Shortcode s = new Shortcode(editText.getText().toString(), editText1.getText().toString());

                                        if (shortcodeManager.addShortcode(s)) {
                                            // Reload list by recreating
                                            getActivity().recreate();
                                        } else {
                                            new AlertDialog.Builder(getActivity())
                                                    .setTitle(R.string.settings_shortcodes_edit_popup_failure)
                                                    .setMessage(R.string.settings_shortcodes_edit_popup_failure_needs_from)
                                                    .setPositiveButton(R.string.ok, null)
                                                    .show();
                                        }

                                    } catch (IllegalArgumentException e) {
                                        e.printStackTrace();
                                        new AlertDialog.Builder(getActivity())
                                                .setTitle(R.string.settings_shortcodes_edit_popup_failure)
                                                .setMessage(R.string.settings_shortcodes_edit_popup_failure_illegal_character)
                                                .setPositiveButton(R.string.ok, null)
                                                .show();
                                    }

                                }
                            })
                            .setNegativeButton(R.string.cancel, null)
                            .show();
                    return true;
                }
            });

            preferenceScreen.addPreference(newLoginPreference);

        }

        @Override
        public void onActivityResult(int requestCode, int resultCode, Intent data) {
            if (resultCode == RESULT_OK) {
                getActivity().recreate();
            }
        }
    }
}


code:
/*
 * DSBDirect
 * Copyright (C) 2019 Fynn Godau
 *
 * 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 <https://www.gnu.org/licenses/>.
 *
 * This software is not affiliated with heinekingmedia GmbH, the
 * developer of the DSB platform.
 */

package godau.fynn.dsbdirect.activity.settings;

import android.app.AlertDialog;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.Preference;
import androidx.annotation.ColorInt;
import android.view.LayoutInflater;
import android.view.View;
import godau.fynn.dsbdirect.R;
import godau.fynn.dsbdirect.Utility;
import uz.shift.colorpicker.LineColorPicker;
import uz.shift.colorpicker.OnColorChangedListener;

public class StyleSettingsActivity extends SettingsActivity {

    // 500
    // Thanks, https://stackoverflow.com/a/33261150
    private static final int RED_500 = 0xFFF44336;
    private static final int PINK_500 = 0xFFE91E63;
    private static final int PURPLE_500 = 0xFF9C27B0;
    private static final int DEEP_PURPLE_500 = 0xFF673AB7;
    private static final int INDIGO_500 = 0xFF3F51B5;
    private static final int BLUE_500 = 0xFF2196F3;
    private static final int LIGHT_BLUE_500 = 0xFF03A9F4;
    private static final int CYAN_500 = 0xFF00BCD4;
    private static final int TEAL_500 = 0xFF009688;
    private static final int GREEN_500 = 0xFF4CAF50;
    private static final int LIGHT_GREEN_500 = 0xFF8BC34A;
    private static final int LIME_500 = 0xFFCDDC39;
    private static final int YELLOW_500 = 0xFFFFEB3B;
    private static final int AMBER_500 = 0xFFFFC107;
    private static final int ORANGE_500 = 0xFFFF9800;
    private static final int DEEP_ORANGE_500 = 0xFFFF5722;
    private static final int BROWN_500 = 0xFF795548;
    private static final int GREY_500 = 0xFF9E9E9E;
    private static final int BLUE_GREY_500 = 0xFF607D8B;

    // Black
    private static final int BLACK = 0xFF000000;

    // 700
    private static final int RED_700 = 0xFFD32F2F;
    private static final int PINK_700 = 0xFFC2185B;
    private static final int PURPLE_700 = 0xFF7B1FA2;
    private static final int DEEP_PURPLE_700 = 0xFF512DA8;
    private static final int INDIGO_700 = 0xFF303F9F;
    private static final int BLUE_700 = 0xFF1976D2;
    private static final int LIGHT_BLUE_700 = 0xFF0288D1;
    private static final int CYAN_700 = 0xFF0097A7;
    private static final int TEAL_700 = 0xFF00796B;
    private static final int GREEN_700 = 0xFF388E3C;
    private static final int LIGHT_GREEN_700 = 0xFF689F38;
    private static final int LIME_700 = 0xFFAFB42B;
    private static final int YELLOW_700 = 0xFFFBC02D;
    private static final int AMBER_700 = 0xFFFFA000;
    private static final int ORANGE_700 = 0xFFF57C00;
    private static final int DEEP_ORANGE_700 = 0xFFE64A19;
    private static final int BROWN_700 = 0xFF5D4037;
    private static final int GREY_700 = 0xFF616161;
    private static final int BLUE_GREY_700 = 0xFF455A64;

    // A200
    private static final int RED_A200 = 0xFFFF5252;
    private static final int PINK_A200 = 0xFFFF4081;
    private static final int PURPLE_A200 = 0xFFE040FB;
    private static final int DEEP_PURPLE_A200 = 0xFF7C4DFF;
    private static final int INDIGO_A200 = 0xFF536DFE;
    private static final int BLUE_A200 = 0xFF448AFF;
    private static final int LIGHT_BLUE_A200 = 0xFF40C4FF;
    private static final int CYAN_A200 = 0xFF18FFFF;
    private static final int TEAL_A200 = 0xFF64FFDA;
    private static final int GREEN_A200 = 0xFF69F0AE;
    private static final int LIGHT_GREEN_A200 = 0xFFB2FF59;
    private static final int LIME_A200 = 0xFFEEFF41;
    private static final int YELLOW_A200 = 0xFFFFFF00;
    private static final int AMBER_A200 = 0xFFFFD740;
    private static final int ORANGE_A200 = 0xFFFFAB40;
    private static final int DEEP_ORANGE_A200 = 0xFFFF6E40;

    private static final int[] COLOR_PALETTE = {
            RED_500, PINK_500, PURPLE_500, DEEP_PURPLE_500, INDIGO_500, BLUE_500, LIGHT_BLUE_500, CYAN_500, TEAL_500,
            GREEN_500, LIGHT_GREEN_500, LIME_500, YELLOW_500, AMBER_500, ORANGE_500, DEEP_ORANGE_500, BROWN_500,
            GREY_500, BLUE_GREY_500, BLACK
    };

    private static final int[] COLOR_PALETTE_DARK = {
            RED_700, PINK_700, PURPLE_700, DEEP_PURPLE_700, INDIGO_700, BLUE_700, LIGHT_BLUE_700, CYAN_700, TEAL_700,
            GREEN_700, LIGHT_GREEN_700, LIME_700, YELLOW_700, AMBER_700, ORANGE_700, DEEP_ORANGE_700, BROWN_700,
            GREY_700, BLUE_GREY_700, BLACK
    };

    private static final int[] COLOR_PALETTE_ACCENT = {
            RED_A200, PINK_A200, PURPLE_A200, DEEP_PURPLE_A200, INDIGO_A200, BLUE_A200, LIGHT_BLUE_A200, CYAN_A200, TEAL_A200,
            GREEN_A200, LIGHT_GREEN_A200, LIME_A200, YELLOW_A200, AMBER_A200, ORANGE_A200, DEEP_ORANGE_A200
    };

    @Override
    PreferenceFragment getPreferenceFragment() {
        return new PreferenceFragment();
    }

    public static class PreferenceFragment extends SettingsActivity.PreferenceFragment {

        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);

            // Load the preferences from an XML resource
            addPreferencesFromResource(R.xml.preferences_styling);

            final boolean html = getActivity().getIntent().getBooleanExtra(EXTRA_HTML_ONLY, true);
            if (html) {
                // There are no images to be inverted
                Preference invertImages = findPreference("invertImages");
                invertImages.setEnabled(false);
                invertImages.setSummary(R.string.settings_view_invert_images_no_images);
            }

                // Reload settings to change Theme when Theme preference is changed
            findPreference("style").setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
                @Override
                public boolean onPreferenceChange(Preference preference, Object newValue) {
                    getActivity().recreate();
                    return true;
                }
            });

            // Let user choose Primary color when tapping Primary color
            findPreference("colorPrimary").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
                @Override
                public boolean onPreferenceClick(Preference preference) {
                    setColorPopup(getActivity(), COLOR_PALETTE, u.getColorPrimary(), "colorPrimary"); // TODO summary

                    return true;
                }
            });

            // Let user choose Primary color dark after clicking Primary color dark
            findPreference("colorPrimaryDark").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
                @Override
                public boolean onPreferenceClick(Preference preference) {
                    setColorPopup(getActivity(), COLOR_PALETTE_DARK, u.getColorPrimaryDark(), "colorPrimaryDark"); // TODO summary

                    return true;
                }
            });

            // Let user choose Accent color on touch of Accent color
            findPreference("colorAccent").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
                @Override
                public boolean onPreferenceClick(Preference preference) {
                    setColorPopup(getActivity(), COLOR_PALETTE_ACCENT, u.getColorAccent(), "colorAccent"); // TODO summary

                    return true;
                }
            });

        }

    }

    public static void setColorPopup(Context context, int[] colorPalette, @ColorInt int selectedColor, final String preferenceName) {

        // spawn layout
        View promptView = LayoutInflater.from(context).inflate(R.layout.action_pick_color, null);

        // get sharedPreferences
        Utility u = new Utility(context);
        final SharedPreferences sharedPreferences1 = u.getSharedPreferences();

        // find fields
        final LineColorPicker colorPicker = promptView.findViewById(R.id.picker);

        colorPicker.setColors(colorPalette);

        // select currently selected color
        colorPicker.setSelectedColor(selectedColor);

        final AlertDialog al = new AlertDialog.Builder(context)
                .setView(promptView)
                .setTitle(R.string.action_pick_color_popup)
                .setMessage(R.string.action_pick_color_popup_message)
                .show();

        // Dismiss on color pick
        colorPicker.setOnColorChangedListener(new OnColorChangedListener() {
            @Override
            public void onColorChanged(int i) {
                sharedPreferences1.edit().putInt(preferenceName, i)
                        .apply();

                al.cancel();
            }
        });
    }
}


code:
/*
 * DSBDirect
 * Copyright (C) 2019 Fynn Godau
 *
 * 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 <https://www.gnu.org/licenses/>.
 *
 * This software is not affiliated with heinekingmedia GmbH, the
 * developer of the DSB platform.
 */

package godau.fynn.dsbdirect.manager;

import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.util.Base64;
import android.util.Log;

import godau.fynn.dsbdirect.table.*;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.jsoup.Connection;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import godau.fynn.dsbdirect.Login;
import godau.fynn.dsbdirect.QueryMetadata;
import godau.fynn.dsbdirect.R;
import godau.fynn.dsbdirect.Utility;
import godau.fynn.dsbdirect.table.reader.Reader;

public class DownloadManager {

    private Context mContext;

    public DownloadManager(Context context) {
        mContext = context;
    }

    /**
     * Make a GET request (synchronously)
     *
     * @param url           URL to be requested
     * @param body          Request body (JSON String)
     * @param requestMethod Usually either GET or POST
     * @return Response
     * @throws IOException If networking error or other IO exception
     */
    private @NonNull
    InputStream request(String url, @Nullable String body, String requestMethod) throws IOException {

        if (!isNetworkAvailable()) throw new IOException();

        // Encode the url correctly, as the file name part can contain Umlaute or other weird things
        String[] urlParts =
                url.split("/(?!.*/)"); // Matches only the last '/' character (using a negative lookahead)

        url = urlParts[0] + "/" + URLEncoder.encode(
                urlParts[1] // This is the part that has to be encoded correctly
                        .replaceAll(
                                "%20", " " /* Spaces are already encoded as "%20". Let's decode them quickly so we won't have
                                 * %20 encoded as something like "%2520"
                                 */
                        ), "ISO-8859-1" // UTF-8 won't work here
        )
                .replaceAll(
                        "\\+", "%20" /* Spaces are encoded again, but they are now '+' chars. That's unfortunately not
                         * correct.  Let's replace them with "%20"s.
                         */
                );

        URL connectwat = new URL(url);
        HttpURLConnection urlConnection = (HttpURLConnection) connectwat.openConnection();

        urlConnection.setRequestMethod(requestMethod);

        // Add DNT header, as if it does anything
        urlConnection.addRequestProperty("DNT", "1");

        if (body != null) {

            // Get headers from sharedPreferences so they can be set through news
            HashSet queryHeaders = (HashSet) new Utility(mContext).getSharedPreferences()
                    .getStringSet("queryHeaders", new HashSet<>(Arrays.asList(
                            "Referer: https://www.dsbmobile.de/",
                            "Content-Type: application/json;charset=utf-8"
                    )));

            // Add each header to query
            for (Object header : queryHeaders) {
                String queryHeader = (String) header;

                // Split header into two parts
                String[] queryHeaderParts = queryHeader.split(": ");

                // Check whether header really is two parts
                if (queryHeaderParts.length != 2) {
                    Log.e("DOWNLOADHEADER", "invalid header: " + queryHeader);
                    continue;
                }

                // Add header to request
                urlConnection.setRequestProperty(queryHeaderParts[0], queryHeaderParts[1]);

                Log.d("DOWNLOADHEADER", queryHeader);
            }

            OutputStream outputStream = urlConnection.getOutputStream();
            OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream);
            outputStreamWriter.write(body);
            outputStreamWriter.flush();
            outputStreamWriter.close();
            outputStream.close();
        }

        urlConnection.connect();

        return new BufferedInputStream(urlConnection.getInputStream());
    }

    /**
     * Downloads a list of all contents currently available from DSB (synchronously)
     *
     * @param login Login to log in with
     * @return A ContentInformation object with all contents in DSB
     * @throws LoginFailureException       If the credentials or the request in general are incorrect
     * @throws UnexpectedResponseException If response is invalid JSON
     * @throws IOException                 If request fails in general (networking error?)
     */
    public ContentInformation downloadContentInformation(Login login) throws IOException {

        JSONArray contentArray = downloadContentJSONArray(login);

        Table[] tables = null;
        List<Notice> notices = null;
        List<NewsItem> news = null;

        try {
            for (int i = 0; i < contentArray.length(); i++) {
                JSONObject contentObject = contentArray.getJSONObject(i);

                String contentName = contentObject.getString("Title");

                JSONArray childs = contentObject.getJSONObject("Root").getJSONArray("Childs");

                // It has been observed that News are before Pläne if they exist
                switch (contentName) {
                    case "Pläne":
                        tables = Reader.readTableList(childs);
                        break;
                    case "News":
                        news = Reader.readNewsList(childs);
                        break;
                    case "Aushänge":
                        notices = Reader.readNoticeList(childs);
                }
            }
        } catch (JSONException e) {
            // Response is invalid, throw further
            throw new UnexpectedResponseException(e.getCause());
        }

        return new ContentInformation(tables, notices, news);
    }

    /**
     * Downloads and returns the Childs JSON array of Inhalte from DSB (synchronously)
     *
     * @param login Login to log in with
     * @return JSON array containing the Childs of Inhalte of server's response
     * @throws LoginFailureException       If the credentials or the request in general are incorrect
     * @throws UnexpectedResponseException If response is invalid JSON
     * @throws IOException                 If request fails in general (networking error?)
     */
    private JSONArray downloadContentJSONArray(Login login) throws IOException {
        Log.d("DOWNLOAD", "downloading data");

        if (!login.isNonZeroLength()) {
            // Empty credentials are not valid
            throw new LoginFailureException();
        }

        // Make request body
        JSONObject bodyObject;
        SharedPreferences sharedPreferences = new Utility(mContext).getSharedPreferences();
        try {
            // Query body base json might be overwritten by news, otherwise use hardcoded value
            String queryBodyBaseJson = sharedPreferences
                    .getString("queryBodyBaseJson", mContext.getString(R.string.query_body_base_json));
            Log.d("DOWNLOADBASEJSON", queryBodyBaseJson);

            bodyObject = new JSONObject(queryBodyBaseJson);
            login.put(bodyObject);

            // Add things configurable through news
            boolean sendAppId = sharedPreferences.getBoolean("querySendAppId", true);
            boolean sendAndroidVersion = sharedPreferences.getBoolean("querySendAndroidVersion", true);
            boolean sendDeviceModel = sharedPreferences.getBoolean("querySendDeviceModel", true);
            boolean sendLanguage = sharedPreferences.getBoolean("querySendLanguage", true);
            boolean sendDate = sharedPreferences.getBoolean("querySendDate", false);
            boolean sendLastDate = sharedPreferences.getBoolean("querySendLastDate", false);

            // Generate AppId
            if (sendAppId) {
                bodyObject.put("AppId", QueryMetadata.getAppId());
            }

            // Attach random android version
            if (sendAndroidVersion) {
                bodyObject.put("OsVersion", QueryMetadata.getAndroidVersion());
            }

            // Attach random device name
            if (sendDeviceModel) {
                bodyObject.put("Device", QueryMetadata.getDeviceModel());
            }

            // Attach some language
            if (sendLanguage) {
                bodyObject.put("Language", QueryMetadata.getLanguage());
            }

            // Send date
            if (sendDate) {
                // Date should look like this: 2019-10-04T14:21:3728600
                SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss:SSSS000");
                String date = dateFormat.format(new Date());
                Log.d("DOWNLOADDATE", date);
                bodyObject.put("Date", date);

                // Send last date
                if (sendLastDate) {
                    bodyObject.put("LastUpdate", sharedPreferences.getString("queryLastDate", ""));
                    sharedPreferences.edit()
                            .putString("queryLastDate", date)
                            .apply();
                }
            }

        } catch (JSONException e) {
            e.printStackTrace();
            // Shouldn't happen! Throw further as IOException since we don't really know what happened anyway
            throw new IOException(e.getCause());
        }

        // Request url
        String url = getEndpoint(sharedPreferences.getInt("queryEndpoint", 0));
        Log.d("DOWNLOADENDPOINT", url);

        // Request
        String response = string(
                request(url, obfuscateQuery(bodyObject), "POST"),
                "UTF-8"
        );

        // If request is very invalid, there won't be any json in the response, only some plain text…
        if (response.equals("Unzulässige Anforderung")) {
            Log.d("DOWNLOAD", "request failed: " + bodyObject.toString() + " obfuscated to " + obfuscateQuery(bodyObject));
            throw new UnexpectedResponseException();
        }

        try {
            JSONObject responseBody = deobfuscateResponse(response);

            // Check result code
            int resultcode = responseBody.getInt("Resultcode");
            switch (resultcode) {
                case 0:
                    // All is good, continue
                    break;
                case 1:
                    // Invalid credentials ("ResultStatusInfo": "Login fehlgeschlagen")
                    throw new LoginFailureException();
                default:
                    Log.d("DOWNLOAD", "unexpected Resultcode " + resultcode + ": " + response);
                    throw new UnexpectedResponseException();
            }

            JSONArray resultMenuItems = responseBody.getJSONArray("ResultMenuItems");
            resultMenu:
            for (int i = 0; i < resultMenuItems.length(); i++) {
                JSONObject resultMenuItem = resultMenuItems.getJSONObject(i);
                String resultMenuItemTitle = resultMenuItem.getString("Title");

                // Just to be sure that we select Inhalte, in practice it has only been observed to be the first one
                if (resultMenuItemTitle.equals("Inhalte")) {

                    return resultMenuItem.getJSONArray("Childs");
                }
            }

            // No Inhalte…?
            throw new UnexpectedResponseException("No Inhalte found");

        } catch (JSONException | EOFException e) {
            e.printStackTrace();
            // Response is invalid, throw further
            throw new UnexpectedResponseException(e.getCause());
        }
    }

    /**
     * Return the corresponding url for the endpoint id.
     *
     * @param id 0 (mobile) / 1 (web) / 2 (ihkmobile) / 3 (appihkbb)
     */
    private String getEndpoint(int id) throws IOException {
        switch (id) {
            case 0:
            default:
                return "https://app.dsbcontrol.de/JsonHandler.ashx/GetData";
            case 1:
                String webConfiguration = string(
                        request("https://www.dsbmobile.de/scripts/configuration.js", null, "GET"),
                        "UTF-8"
                );
                return "https://www.dsbmobile.de/" + webConfiguration.split("'")[3];

            case 2:
                return "https://ihkmobile.dsbcontrol.de/new/JsonHandlerWeb.ashx/GetData";
            case 3:
                return "https://appihkbb.dsbcontrol.de/new/JsonHandlerWeb.ashx/GetData";
        }
    }

    /**
     * Downloads an html table file (synchronously)
     *
     * @param table       The table which is to be downloaded
     * @param fileManager A file manager to be used to save the file
     * @return The downloaded html String
     * @throws IOException In case of networking error
     */
    public String downloadHtmlTable(final Table table, final FileManager fileManager) throws IOException { // TODO
        Log.d("DOWNLOAD", "downloading html file at " + table.getUri());

        // Request
        String html = string(request(table.getUri(), null, "GET"), "ISO-8859-1");

        // If these characters appear, the wrong encoding has been used
        if (html.contains("ï»¿")) {
            try {
                html = new String(html.getBytes("ISO-8859-1"), "UTF-8");
            } catch (UnsupportedEncodingException e) {
                // So be it!
                e.printStackTrace();
            }

        }

        // Save file
        fileManager.saveFile(table, html);

        return html;
    }

    /**
     * Downloads a bitmap table file (synchronously)
     *
     * @param table       The table which is to be downloaded
     * @param fileManager A file manager to be used to save the file
     * @return The bitmap
     * @throws IOException In case of networking error
     */
    public Bitmap downloadImageTable(final Table table, final FileManager fileManager) throws IOException { // TODO
        // We're doing bitmaps.
        Log.d("DOWNLOAD", "downloading image file at " + table.getUri());

        // Request bitmap
        InputStream inputStream = request(table.getUri(), null, "GET");

        // Create bitmap
        Bitmap bitmap = BitmapFactory.decodeStream(inputStream);

        // Save bitmap
        fileManager.saveFile(table, bitmap);

        // Return bitmap
        return bitmap;
    }

    /**
     * Obfuscate query. The DSB server requires this.
     *

     * Queries are "compressed" using gzip (saving less than half a kilobyte) and then encoded using base64.
     * The result of that is then again hidden inside some more JSON.
     *
     * @param query The JSON query you want to make
     * @return The body you will have to send to the server to execute the query
     */
    private String obfuscateQuery(JSONObject query) throws IOException {
        String queryString = query.toString();

        // Thanks, https://stackoverflow.com/a/6718707
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream(queryString.length());
        GZIPOutputStream gzip = new GZIPOutputStream(outputStream);
        gzip.write(queryString.getBytes());
        gzip.close();
        byte[] gzipped = outputStream.toByteArray();
        outputStream.close();

        String encoded = Base64.encodeToString(gzipped, Base64.NO_WRAP); // Line wraps are useless here!

        return mContext.getString(R.string.query_body_outer_json, encoded);
    }

    /**
     * Deobfuscate response. The DSB server gives obfuscated responses.
     *

     * Just the reverse of {@link #obfuscateQuery(JSONObject)}, except that "some more JSON" is different for responses
     * compared to queries.
     *
     * @param response The response the server gave you
     * @return The JSON hidden inside the response
     */
    private JSONObject deobfuscateResponse(String response) throws JSONException, IOException {

        JSONObject responseObject = new JSONObject(response);
        String encoded = responseObject.getString("d");

        byte[] gzipped = Base64.decode(encoded, Base64.DEFAULT);

        // Who knows how this works… thanks again, https://stackoverflow.com/a/6718707
        final int BUFFER_SIZE = 32;
        ByteArrayInputStream is = new ByteArrayInputStream(gzipped);
        GZIPInputStream gis = new GZIPInputStream(is, BUFFER_SIZE);
        StringBuilder stringBuilder = new StringBuilder();
        byte[] data = new byte[BUFFER_SIZE];
        int bytesRead;
        while ((bytesRead = gis.read(data)) != -1) {
            stringBuilder.append(new String(data, 0, bytesRead));
        }
        gis.close();
        is.close();

        return new JSONObject(stringBuilder.toString());
    }

    public JSONObject downloadUpdateCheck() throws IOException {
        Log.d("DOWNLOAD", "downloading update check");

        try {
            return new JSONObject(string(request(mContext.getString(R.string.uri_versioncheck), null, "GET"), "UTF-8"));
        } catch (JSONException e) {
            throw new UnexpectedResponseException(e.getCause());
        }
    }

    public JSONObject downloadNews() throws IOException {
        Log.d("DOWNLOAD", "downloading news");

        try {
            return new JSONObject(string(request(mContext.getString(R.string.uri_news), null, "GET"), "UTF-8"));
        } catch (JSONException e) {
            throw new UnexpectedResponseException(e.getCause());
        }
    }

    // Thanks, https://stackoverflow.com/a/35446009
    private String string(InputStream in, String charsetName) throws IOException {
        ByteArrayOutputStream result = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int length;
        while ((length = in.read(buffer)) != -1) {
            result.write(buffer, 0, length);
        }

        return result.toString(charsetName);

    }

    /**
     * Uploads a url to the server at https://dsb.bixilon.de to ask the developer to develop a parser for it.
     *

The server code is available at fynngodau/dsbdirect-filedump.
     *
     * @param url The url to upload. Must be at https://app.dsbcontrol.de
     * @return whether the server returned 200 Success
     * @throws IOException in case of a network error
     */
    public boolean uploadParserRequest(String url) throws IOException {
        // request(…) returns an InputStream, not the response code as it is pretty much always 200, so we can't use it here

        if (!isNetworkAvailable()) {
            throw new IOException();
        }

        URL connectwat = new URL(mContext.getString(R.string.uri_requestparser));
        HttpURLConnection urlConnection = (HttpURLConnection) connectwat.openConnection();

        urlConnection.setRequestMethod("POST");

        OutputStream outputStream = urlConnection.getOutputStream();
        OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream);
        outputStreamWriter.write("url=" + url);
        outputStreamWriter.flush();
        outputStreamWriter.close();
        outputStream.close();

        urlConnection.connect();

        if (urlConnection.getResponseCode() == 200) return true;
        else return false;
    }

    private boolean isNetworkAvailable() {
        ConnectivityManager connectivityManager
                = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo();
        return activeNetworkInfo != null && activeNetworkInfo.isConnected();
    }

    /**
     * Download all shortcodes from eltern-portal.org (synchronously)
     *
     * @param url      URL to be requested
     * @param email    Login Username (email)
     * @param password Password for login
     * @return Shortcode    null on error, array on success
     * @throws IOException              If networking error or other IO exception
     * @throws IllegalArgumentException If the provided url is incorrect
     * @throws LoginFailureException    If the credentials were not correct
     */
    public Shortcode[] downloadShortcodesFromElternportal(String url, String email, String password)
            throws IOException, IllegalArgumentException, LoginFailureException {
        //is network available?
        if (!isNetworkAvailable()) throw new IOException();
        //attach an / if needed
        if (!url.endsWith("/"))
            url = url + "/";
        //check url
        if (!url.endsWith(".eltern-portal.org/")) {
            //url invalid
            throw new IllegalArgumentException();
        }
        if (url.startsWith("http://")) {
            url = url.replace("http://", "https://"); //force https
        }
        if (!url.toLowerCase().matches("^\\w+://.*")) {
            url = "https://" + url;
        }
        //check if url is parsable
        try {
            URL u = new URL(url);
            u.toURI();
        } catch (URISyntaxException e) {
            //nope. not valid
            e.printStackTrace();
            throw new IllegalArgumentException(e);
        }

        //obtain session key
        Connection.Response res = null;
        res = Jsoup.connect(url)
                .execute();

        //login with obtained session key
        res = Jsoup.connect(url + "includes/project/auth/login.php")
                .data("username", email, "password", password)
                .method(Connection.Method.POST)
                .cookies(res.cookies())
                .followRedirects(false)
                .execute();

        if (res.body().contains("Fehler bei der Anmeldung") || res.header("Location").contains(".eltern-portal.org/login?errno=1&username=")) {
            //fail: wrong session key or username/password wrong
            throw new LoginFailureException();
        }

        //get html of school information
        Document doc = Jsoup.connect(url + "service/schulinformationen")
                .cookies(res.cookies())
                .get();
        Elements e = doc.getElementById("asam_content").select("div[class=row m_bot]");
        boolean getthem = false; //there are not only shotcodes also school infos. Wait until shortcodes appear
        List<Shortcode> shortcodes = new ArrayList<Shortcode>();
        for (Element s : e) {
            String raw = s.select("div[class=col-md-4],div[class=col-md-6]").text();
            //empty line. skipping
            if (raw.equals(""))
                continue;
            if (raw.contains("Homepage")) { //Homepage is the last school info. Follows by shortcodes
                getthem = true;
                continue;
            }
            if (!getthem)
                continue;

            String split[] = raw.split(" ", 3); //short short n a m e
            shortcodes.add(new Shortcode(split[0], split[2]));
        }
        Shortcode array[] = new Shortcode[shortcodes.size()];
        array = shortcodes.toArray(array);
        return array;
    }

    /**
     * Holds information about all the content that is currently offered at the DSB. Notices
     * and news are treated as the same.
     */
    public static class ContentInformation implements Serializable {
        private Table[] tables;
        private ArrayList<NoticeBoardItem> notices;

        public ContentInformation(Table[] tables, List<Notice> notices, List<NewsItem> news) {
            this.tables = tables == null ? new Table[0] : tables;
            this.notices = new ArrayList<>();
            if (notices != null) this.notices.addAll(notices);
            if (news != null) this.notices.addAll(news);
        }

        public Table[] getTables() {
            return tables;
        }

        public ArrayList<NoticeBoardItem> getNotices() {
            return notices;
        }

        /**
         * @return Whether at least one notice board item is available.
         */
        public boolean hasNotices() {
            return notices.size() > 0;
        }
    }

    static public class UnexpectedResponseException extends IOException {
        public UnexpectedResponseException() {
            super();
        }

        public UnexpectedResponseException(Throwable cause) {
            super(cause);
        }

        public UnexpectedResponseException(String message) {
            super(message);
        }
    }

    /**
     * Possible causes: credentials incorrect, server rejected login due to an invalid request
     */
    static public class LoginFailureException extends UnexpectedResponseException {
    }

    static public class NoContentException extends UnexpectedResponseException {
    }
}


code:
/*
 * DSBDirect
 * Copyright (C) 2019 Fynn Godau
 *
 * 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 <https://www.gnu.org/licenses/>.
 *
 * This software is not affiliated with heinekingmedia GmbH, the
 * developer of the DSB platform.
 */

package godau.fynn.dsbdirect.manager;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import androidx.annotation.Nullable;
import android.util.Log;
import godau.fynn.dsbdirect.Utility;
import godau.fynn.dsbdirect.table.Table;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;

import static godau.fynn.dsbdirect.table.reader.Reader.CONTENT_IMAGE;

public class FileManager {

    private Context mContext;

    public FileManager(Context context) {
        mContext = context;

        deleteOldFiles();
    }

    /**
     * Write a String to a file
     *
     * @param table    Table whose contents are to be saved
     * @param response Contents (of table) to be saved
     */
    public void saveFile(final Table table, final String response) {

        new Thread(new Runnable() {
            @Override
            public void run() {
                String filename = getFilename(table.getUri(), table.getPublishedDate());

                File file = new File(mContext.getFilesDir(), filename);

                try {
                    FileOutputStream outputStream = new FileOutputStream(file);
                    outputStream.write(response.getBytes());
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();

    }

    /**
     * Write bitmap to a file
     *
     * @param table  The table the file belongs to
     * @param bitmap The bitmap to be written into a file
     */
    public void saveFile(final Table table, final Bitmap bitmap) {

        new Thread(new Runnable() {
            @Override
            public void run() {
                String filename = getFilename(table.getUri(), table.getPublishedDate());

                File file = new File(mContext.getFilesDir(), filename);

                try {
                    FileOutputStream outputStream = new FileOutputStream(file);
                    bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
        }).start();

    }

    public @Nullable
    String readHtmlFile(File file) {
        // Read a String from a File

        try {
            FileInputStream fileInputStream = new FileInputStream(file);
            byte[] bytes = new byte[(int) file.length()];
            fileInputStream.read(bytes);

            return new String(bytes);
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    public @Nullable
    Bitmap readBitmapFile(File file) {
        // Read a Bitmap from a File

        try {
            FileInputStream fileInputStream = new FileInputStream(file);
            return BitmapFactory.decodeStream(fileInputStream);
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * Check whether this specific version of a file has already been downloaded
     *
     * @param filename The filename (use {@link #getFilename(String, Date)})
     * @return Whether the file is already downloaded or not
     * @see #exists(Table)
     */
    private boolean exists(String filename) {
        File file = new File(mContext.getFilesDir(), filename);
        return file.exists();
    }

    private File[] listFiles() {
        return mContext.getFilesDir().listFiles();
    }

    private void deleteOldFiles() {
        // Automatically delete old files

        new Thread(new Runnable() {
            @Override
            public void run() {
                String storeDurationString = new Utility(mContext).getSharedPreferences().getString("cache_duration", "604800000");
                try {
                    int storeDuration = Integer.parseInt(storeDurationString);

                    File[] files = mContext.getFilesDir().listFiles();

                    for (File file : files) {
                        long lastModified = file.lastModified();
                        long now = new Date().getTime();
                        long delta = now - lastModified;
                        if (delta > storeDuration) {
                            boolean deleted = file.delete();
                            Log.d("AUTODELETE", "could delete file " + file.getName() + "? " + deleted);
                        }
                    }
                } catch (NumberFormatException e) {
                    // Normal behavior if files shouldn't be deleted
                    // Also prevents crash when someone plays around with the sharedPreferences file
                    Log.d("AUTODELETE", "deletion duration is not a valid int. files not deleted");
                }

            }
        }).start();
    }

    private String getFilename(String uri, Date date) {
        String path = Uri.parse(uri).getPath().replaceAll("[\\\\/]", "-");
        return date.getTime() + "-" + path;
    }

    // WELCOME TO WRAP(PER) ZONE

    /**
     * @param filename The filename
     * @return File belonging to corresponding filename
     */
    private File getFile(String filename) {
        return new File(mContext.getFilesDir(), filename);
    }

    private @Nullable
    String readHtmlFile(Table table) {
        return readHtmlFile(getFile(getFilename(table.getUri(), table.getPublishedDate())));
    }

    private Bitmap readBitmapFile(Table table) {
        return readBitmapFile(getFile(getFilename(table.getUri(), table.getPublishedDate())));
    }

    /**
     * Check whether this exact table file has already been downloaded, using table name and timestamp
     *
     * @param table Table to be checked
     * @return Whether the table has already been downloaded
     * @see #exists(String)
     */
    public boolean exists(Table table) {
        return exists(getFilename(table.getUri(), table.getPublishedDate()));
    }

    public int countFiles() {
        return listFiles().length;
    }

    public long occupiedStorage() {
        long sum = 0;

        for (File file :
                listFiles()) {
            sum += file.length();
        }

        return sum;
    }

    public void deleteAllFiles() {
        for (File file :
                listFiles()) {
            boolean deleted = file.delete();
            Log.d("MANUALDELETE", "could delete file " + file.getName() + "? " + deleted);
        }
    }

    /**
     * @return all files sorted by last order descending
     */
    public File[] getFilesSorted() {
        File[] files = listFiles();
        Arrays.sort(files, new Comparator<File>() {
            @Override
            public int compare(File o1, File o2) {
                if (o1.lastModified() < o2.lastModified()) {
                    return 1;
                } else if (o1.lastModified() == o2.lastModified()) {
                    return 0;
                } else if (o1.lastModified() > o2.lastModified()) {
                    return -1;
                }
                // We should never get here but whatever
                return 0;
            }
        });
        return files;
    }

    public String getHtmlTable(Table table, DownloadManager downloadManager) throws IOException {
        // Test whether it really is html
        if (!table.isHtml()) {
            throw new IllegalArgumentException();
        }

        if (exists(table)) {
            return readHtmlFile(table);
        } else {
            return downloadManager.downloadHtmlTable(table, this);
        }
    }

    public Bitmap getImageTable(Table table, DownloadManager downloadManager) throws IOException {
        // Test whether it really is an image
        if (table.getContentType() != CONTENT_IMAGE) {
            throw new IllegalArgumentException();
        }

        if (exists(table)) {
            return readBitmapFile(table);
        } else {
            return downloadManager.downloadImageTable(table, this);
        }
    }

    /**
     * Download table without returning it
     *
     * @param table Table to be downloaded
     * @return <b>Nothing!</b>
     */
    public void getTable(Table table, DownloadManager downloadManager) throws IOException {
        if (table.isHtml()) {
            getHtmlTable(table, downloadManager);
        } else {
            getImageTable(table, downloadManager);
        }
    }
}


code:
package godau.fynn.dsbdirect.manager;

import android.content.Context;
import android.content.SharedPreferences;
import godau.fynn.dsbdirect.Login;
import godau.fynn.dsbdirect.Utility;
import godau.fynn.dsbdirect.table.Shortcode;

import java.util.Arrays;
import java.util.Set;
import java.util.TreeSet;

public class LoginManager {

    private SharedPreferences sharedPreferences;

    private Login[] logins;

    public LoginManager(SharedPreferences sharedPreferences) {
        this.sharedPreferences = sharedPreferences;
        logins = read();
    }

    public LoginManager(Utility u) {
        sharedPreferences = u.getSharedPreferences();
        logins = read();
    }

    public LoginManager(Context context) {
        sharedPreferences = new Utility(context).getSharedPreferences();

        logins = read();
    }

    /**
     * @param login The login to store
     * @return Whether the login was added
     */
    public boolean addLogin(Login login) {

        // Don't save logins that are lacking either id or pass
        if (!login.isNonZeroLength()) {
            return false;
        }

        // Detect duplicate logins
        for (Login l :
                logins) {
            if (l.getId().equals(login.getId())) {
                // Cancel saving this login as it is already added
                return false;
            }
        }

        // Create new logins array with a size larger than the old one by 1
        logins = Arrays.copyOf(logins, logins.length + 1);
        // Save new login in new space
        logins[logins.length - 1] = login;

        write();

        // Let most recently added login be the active one
        setActiveLogin(login);

        return true;
    }

    /**
     * @param login The login to remove
     * @return Whether the login was successfully removed
     */
    public boolean removeLogin(Login login) {

        // Don't bother if this login is not present here
        if (!contains(login)) {
            return false;
        }

        // Create new logins array with a size smaller than the old one by 1
        Login[] logins = new Login[this.logins.length - 1];

        int added = 0; // Track the next unused index in new logins array; incremented when accessed

        for (int i = 0; i < getLoginCount(); i++) {
            // Don't add the login to be removed again
            if (!this.logins[i].equals(login)) {
                logins[added++] = this.logins[i];
            }
        }

        // Save new logins array
        this.logins = logins;

        write();

        return true;
    }

    /**
     * @param login The login to be searched for
     * @return Whether {@link #logins} contains {@code login}
     */
    private boolean contains(Login login) {
        for (Login l :
                logins) {
            if (l.equals(login)) return true;
        }
        return false;
    }

    public Login[] read() {

        Set<String> loginSet = sharedPreferences.getStringSet("logins", new TreeSet<String>());
        String[] loginStrings = loginSet.toArray(new String[loginSet.size()]);

        Login[] logins = new Login[loginSet.size()];

        for (int i = 0; i < loginSet.size(); i++) {
            try {
                logins[i] = new Login(loginStrings[i]);
            } catch (IllegalArgumentException e) {
                // Better not be null
                // TODO logins[i] = new Login("", "");
                e.printStackTrace();
            }
        }

        this.logins = logins;

        return logins;
    }

    /**
     * Serialize logins and write them to shared preferences
     */
    public void write() {
        String[] loginStrings = new String[logins.length];

        for (int i = 0; i < loginStrings.length; i++) {
            loginStrings[i] = logins[i].serialize();
        }

        sharedPreferences
                .edit()
                .putStringSet("logins", new TreeSet<>(Arrays.asList(loginStrings)))
                .apply();

    }

    /**
     * Get login that is chosen at the moment
     * @return The login, or null if none is present
     */
    public Login getActiveLogin() {
        String activeLoginId = getActiveLoginId();

        for (Login l :
                logins) {
            if (l.getId().equals(activeLoginId)) {
                return l;
            }
        }

        // No active login found, let the first one in the array be the active one
        setActiveLogin(logins[0]);
        return logins[0];
    }

    private void setActiveLogin(String id) {
        sharedPreferences.edit().putString("id", id).apply();
    }

    /**
     * Read id of active login from shared preferences
     */
    private String getActiveLoginId() {
        return sharedPreferences
                .getString("id", "");
    }

    /**
     * @return Whether a valid login is present that can be used to log in
     */
    public boolean canLogin() {
        return logins.length > 0 && logins[0].isNonZeroLength();
    }

    public int getLoginCount() {
        return logins.length;
    }

    /**
     * @return An array of all currently not active logins
     */
    public Login[] getInactiveLogins() {
        Login[] inactiveLogins = new Login[logins.length - 1];
        String activeLogin = getActiveLoginId();
        int index = 0; // Index of next not filled slot in inactive logins array (incremented while accessing)
        for (Login l :
                logins) {
            if (!l.getId().equals(activeLogin)) {
                inactiveLogins[index++] = l;
            }
        }

        return inactiveLogins;
    }

    public Login[] getLogins() {
        return logins;
    }

    public void setActiveLogin(Login l) {
        setActiveLogin(l.getId());
    }
}


code:
package godau.fynn.dsbdirect.manager;

import android.content.Context;
import godau.fynn.dsbdirect.Utility;
import godau.fynn.dsbdirect.table.Shortcode;

import java.util.Arrays;

public class ShortcodeManager {

    private Context context;
    private Shortcode[] shortcodes;

    public ShortcodeManager(Context context) {
        this.context = context;
    }

    public Shortcode[] read() {

        String[] shortcodeStrings;
        String raw = new Utility(context).getSharedPreferences().getString("shortcodes", "");
        if(raw.isEmpty())
            shortcodeStrings = new String[] {};
        else
            shortcodeStrings = raw.split("\n");

        Shortcode[] shortcodes = new Shortcode[shortcodeStrings.length];

        for (int i = 0; i < shortcodeStrings.length; i++) {
            try {
                shortcodes[i] = new Shortcode(shortcodeStrings[i]);
            } catch (IllegalArgumentException e) {
                // Better not be null
                shortcodes[i] = new Shortcode("", "");
                e.printStackTrace();
            }
        }

        this.shortcodes = shortcodes;

        return shortcodes;
    }

    public void write(Shortcode[] shortcodes) {
        this.shortcodes = shortcodes;
        write();
    }

    public void write() {
        String[] shortcodeStrings = new String[shortcodes.length];
        StringBuilder shortcodesRaw = new StringBuilder();

        for (int i = 0; i < shortcodes.length; i++) {
            shortcodeStrings[i] = shortcodes[i].serialize().replace("\n", "");
        }
        Arrays.sort(shortcodeStrings); //sort alphabetical
        for (int i = 0; i < shortcodes.length; i++) {
            shortcodesRaw.append(shortcodeStrings[i]);
            if(i != shortcodes.length - 1)
                shortcodesRaw.append("\n");
        }

        new Utility(context).getSharedPreferences()
                .edit()
                .putString("shortcodes", shortcodesRaw.toString())
                .apply();

    }

    /**
     * @param shortcode The shortcode to remove
     * @return Whether the shortcode was successfully removed
     */
    public boolean removeShortcode(Shortcode shortcode) {

        // Don't remove if not contained
        boolean contained = false;
        for (Shortcode s :
                shortcodes) {
            if (s.equals(shortcode)) contained = true;
        }
        if (!contained) return false;

        // Create new shortcodes array with a size smaller than the old one by 1
        Shortcode[] shortcodes = new Shortcode[this.shortcodes.length - 1];

        int added = 0; // Track the next unused index in new shortcodes array; incremented when accessed

        for (int i = 0; i < this.shortcodes.length; i++) {
            // Don't add the login to be removed again
            if (!this.shortcodes[i].equals(shortcode)) {
                shortcodes[added++] = this.shortcodes[i];
            }
        }

        // Save new logins array
        write(shortcodes);

        return true;
    }

    /**
     * @param shortcode The shortcode to store
     * @return Whether the login was added
     */
    public boolean addShortcode(Shortcode shortcode) {

        // Don't save logins that are lacking either from or to
        if (shortcode.getFrom().isEmpty() || shortcode.getTo().isEmpty()) {
            return false;
        }

        // Create new shortcodes array with a size larger than the old one by 1
        shortcodes = Arrays.copyOf(shortcodes, shortcodes.length + 1);
        // Save new shortcode in new space
        shortcodes[shortcodes.length - 1] = shortcode;

        write();

        return true;
    }

    public String replace(String s) {
        if(!new Utility(context).getSharedPreferences().getBoolean("shortcodes_enabled", false))
            return s;
        if (shortcodes == null) {
            read();
        }
        for (Shortcode shortcode :
                shortcodes) {
            if (shortcode.containedIn(s)) {
                s = shortcode.replace(s);
                break;
            }
        }
        return s;
    }
}