code:
/*
 * Mostly copied from campfire / bandcampDirect, which mostly copied it
 * from https://developer.android.com/training/animation/zoom.html#java
 * Copyright (C) 2020 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/>.
 */

package godau.fynn.dsbdirect;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.graphics.Point;
import android.graphics.Rect;
import android.view.View;
import android.view.animation.DecelerateInterpolator;
import android.widget.ImageView;
import com.ortiz.touchview.TouchImageView;
import com.squareup.picasso.Picasso;
import godau.fynn.dsbdirect.activity.NoticeBoardActivity;

public class ZoomImage {

    private TouchImageView expandedImageView;
    private View thumbView;
    private View container;

    private String url;

    private Animator currentAnimator;

    private static final int SHORT_ANIMATION_DURATION = 150;

    public boolean zoomed = false;

    public ZoomImage(View thumbView, TouchImageView expandedImageView, View container, String imageUrl) {
        this.thumbView = thumbView;
        this.expandedImageView = expandedImageView;
        this.container = container;
        this.url = imageUrl;

        thumbView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                zoomImage();
            }
        });
    }

    private void zoomImage() {

        // If there already is a zoomed image, refuse to zoom this one and zoom
        // out the other one instead.
        ZoomImage lastZoomed = NoticeBoardActivity.getLastZoomedImage();
        if (lastZoomed != null && lastZoomed.zoomed) {
            lastZoomed.zoomOut();
            return;
        }

        // If there's an animation in progress, cancel it
        // immediately and proceed with this one.
        if (currentAnimator != null) {
            currentAnimator.cancel();
        }

        // Load the high-resolution "zoomed-in" image.
        Picasso.get().load(url).into(expandedImageView);

        // Calculate the starting and ending bounds for the zoomed-in image.
        // This step involves lots of math. Yay, math.
        final Rect startBounds = new Rect();
        final Rect finalBounds = new Rect();
        final Point globalOffset = new Point();

        // The start bounds are the global visible rectangle of the thumbnail,
        // and the final bounds are the global visible rectangle of the container
        // view. Also set the container view's offset as the origin for the
        // bounds, since that's the origin for the positioning animation
        // properties (X, Y).
        thumbView.getGlobalVisibleRect(startBounds);
        container
                .getGlobalVisibleRect(finalBounds, globalOffset);
        startBounds.offset(-globalOffset.x, -globalOffset.y);
        finalBounds.offset(-globalOffset.x, -globalOffset.y);

        // Adjust the start bounds to be the same aspect ratio as the final
        // bounds using the "center crop" technique. This prevents undesirable
        // stretching during the animation. Also calculate the start scaling
        // factor (the end scaling factor is always 1.0).
        float startScale;
        if ((float) finalBounds.width() / finalBounds.height()
                > (float) startBounds.width() / startBounds.height()) {
            // Extend start bounds horizontally
            startScale = (float) startBounds.height() / finalBounds.height();
            float startWidth = startScale * finalBounds.width();
            float deltaWidth = (startWidth - startBounds.width()) / 2;
            startBounds.left -= deltaWidth;
            startBounds.right += deltaWidth;
        } else {
            // Extend start bounds vertically
            startScale = (float) startBounds.width() / finalBounds.width();
            float startHeight = startScale * finalBounds.height();
            float deltaHeight = (startHeight - startBounds.height()) / 2;
            startBounds.top -= deltaHeight;
            startBounds.bottom += deltaHeight;
        }

        // Hide the thumbnail and show the zoomed-in view. When the animation
        // begins, it will position the zoomed-in view in the place of the
        // thumbnail.
        thumbView.setAlpha(0f);
        expandedImageView.setVisibility(View.VISIBLE);

        // Set the pivot point for SCALE_X and SCALE_Y transformations
        // to the top-left corner of the zoomed-in view (the default
        // is the center of the view).
        expandedImageView.setPivotX(0f);
        expandedImageView.setPivotY(0f);

        // Construct and run the parallel animation of the four translation and
        // scale properties (X, Y, SCALE_X, and SCALE_Y).
        AnimatorSet set = new AnimatorSet();
        set
                .play(ObjectAnimator.ofFloat(expandedImageView, View.X,
                        startBounds.left, finalBounds.left))
                .with(ObjectAnimator.ofFloat(expandedImageView, View.Y,
                        startBounds.top, finalBounds.top))
                .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X,
                        startScale, 1f))
                .with(ObjectAnimator.ofFloat(expandedImageView,
                        View.SCALE_Y, startScale, 1f));
        set.setDuration(SHORT_ANIMATION_DURATION);
        set.setInterpolator(new DecelerateInterpolator());
        set.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                currentAnimator = null;
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                currentAnimator = null;
            }
        });
        set.start();
        currentAnimator = set;

        zoomed = true;
        NoticeBoardActivity.setLastZoomedImage(this);

        // Upon clicking the zoomed-in image, it should zoom back down
        // to the original bounds and show the thumbnail instead of
        // the expanded image.
        final float startScaleFinal = startScale;
        expandedImageView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {

                // Refuse to zoom out if expandedImageView is zoomed in
                if (expandedImageView.isZoomed()) {
                    return;
                }

                if (currentAnimator != null) {
                    currentAnimator.cancel();
                }

                // Animate the four positioning/sizing properties in parallel,
                // back to their original values.
                AnimatorSet set = new AnimatorSet();
                set.play(ObjectAnimator
                        .ofFloat(expandedImageView, View.X, startBounds.left))
                        .with(ObjectAnimator
                                .ofFloat(expandedImageView,
                                        View.Y,startBounds.top))
                        .with(ObjectAnimator
                                .ofFloat(expandedImageView,
                                        View.SCALE_X, startScaleFinal))
                        .with(ObjectAnimator
                                .ofFloat(expandedImageView,
                                        View.SCALE_Y, startScaleFinal));
                set.setDuration(SHORT_ANIMATION_DURATION);
                set.setInterpolator(new DecelerateInterpolator());
                set.addListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        thumbView.setAlpha(1f);
                        expandedImageView.setVisibility(View.GONE);
                        currentAnimator = null;
                    }

                    @Override
                    public void onAnimationCancel(Animator animation) {
                        thumbView.setAlpha(1f);
                        expandedImageView.setVisibility(View.GONE);
                        currentAnimator = null;
                    }
                });
                set.start();
                currentAnimator = set;

                zoomed = false;
            }
        });

    }

    public void zoomOut() {
        expandedImageView.callOnClick();
    }

}


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;

import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageButton;
import android.widget.ListView;
import android.widget.TextView;
import godau.fynn.dsbdirect.R;
import godau.fynn.dsbdirect.Utility;

public class AboutLibrariesActivity extends AppCompatActivity {

    private static Library[] libraries;

    public static final int LICENSE_APACHE_LICENSE_20 = 1;
    public static final int LICENSE_MIT = 2;

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

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

        setContentView(R.layout.activity_about_libraries);

        libraries = new Library[]{
                new Library("jsoup", LICENSE_MIT, "Copyright © 2009 - 2019 Jonathan Hedley (jonathan@hedley.net)", "Jonathan Hedley", "https://jsoup.org"),
                new Library("HorizontalPicker", LICENSE_APACHE_LICENSE_20, null, "Blaž Šolar", "http://blaz.solar/HorizontalPicker/"),
                new Library("Humanize", LICENSE_APACHE_LICENSE_20, null, "mfornos", "http://mfornos.github.io/humanize/"),
                new Library("TouchImageView", LICENSE_MIT, null, "Michael Ortiz", "https://github.com/MikeOrtiz/TouchImageView"),
                new Library("HtmlTextView", LICENSE_APACHE_LICENSE_20, null, "Dominik Schürmann", "https://github.com/sufficientlysecure/html-textview"),
                new Library("ShiftColorPicker", LICENSE_MIT, "The MIT License (MIT)\n\nCopyright (c) 2015 Bogdasarov Bogdan", "Bogdasarov Bogdan", "https://github.com/DASAR/ShiftColorPicker"),
                new Library("Picasso", LICENSE_APACHE_LICENSE_20, null, "Square, Inc.", "https://square.github.io/picasso/"),
        };

        ListView list = findViewById(R.id.list);
        list.setAdapter(new Adapter(AboutLibrariesActivity.this, libraries));

    }

    private static class Library {

        String name;
        private int license;
        @Nullable String foreword;
        String author;
        String url;

        public Library(String name, int license, @Nullable String foreword, String author, String url) {
            this.name = name;
            this.license = license;
            this.foreword = foreword;
            this.author = author;
            this.url = url;
        }

        public String getLicenseName() {
            switch (license) {
                case LICENSE_MIT:
                    return "MIT";
                case LICENSE_APACHE_LICENSE_20:
                    return "Apache 2.0";
                default:
                    return "Unknown license";
            }
        }

        public int getLicenseId() {
            return license;
        }

        public String getLicense() {
            switch (license) {
                case LICENSE_MIT:
                    return MIT_LICENSE;
                case LICENSE_APACHE_LICENSE_20:
                    return APACHE_20_LICENSE;
                default:
                    return "Unknown license";
            }
        }

        public String getDisplayMessage(Context context) {
            String explanation = context.getString(R.string.license_explanation, author, name, getLicenseName());

            if (foreword == null) {
                return explanation + "\n\n" + getLicense();
            } else {
                return explanation + "\n\n" + foreword + "\n\n" + getLicense();
            }
        }
    }

    private class Adapter extends ArrayAdapter<Library> {

        private Library[] libraries;
        private Context context;

        public Adapter(@NonNull Context context, @NonNull Library[] libraries) {
            super(context, R.layout.row_library, libraries);
            this.libraries = libraries;
            this.context = context;
        }

        @NonNull
        @Override
        public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
            LayoutInflater inflater = (LayoutInflater) context
                    .getSystemService(Context.LAYOUT_INFLATER_SERVICE);

            View libraryView;

            final Library lib = libraries[position];

            if (convertView != null) {
                libraryView = convertView;
            } else {
                libraryView = inflater.inflate(R.layout.row_library, parent, false);
            }

            TextView nameView = libraryView.findViewById(R.id.name);
            TextView authorView = libraryView.findViewById(R.id.author);
            TextView licenseView = libraryView.findViewById(R.id.license);
            ImageButton browseView = libraryView.findViewById(R.id.browse);

            nameView.setText(lib.name);
            authorView.setText(lib.author);
            licenseView.setText(lib.getLicenseName());

            browseView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(lib.url));
                    startActivity(intent);
                }
            });

            libraryView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    new AlertDialog.Builder(context)
                            .setTitle(R.string.license)
                            .setMessage(lib.getDisplayMessage(context))
                            .show();
                }
            });

            return libraryView;
        }
    }

    public static final String MIT_LICENSE = "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n" +
            "\n" +
            "The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n" +
            "\n" +
            "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.";

    public static final String APACHE_20_LICENSE = "Apache License\n" +
            "\n" +
            "Version 2.0, January 2004\n" +
            "\n" +
            "http://www.apache.org/licenses/\n" +
            "\n" +
            "TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n" +
            "\n" +
            "1. Definitions.\n" +
            "\n" +
            "\"License\" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.\n" +
            "\n" +
            "\"Licensor\" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.\n" +
            "\n" +
            "\"Legal Entity\" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, \"control\" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.\n" +
            "\n" +
            "\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising permissions granted by this License.\n" +
            "\n" +
            "\"Source\" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.\n" +
            "\n" +
            "\"Object\" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.\n" +
            "\n" +
            "\"Work\" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).\n" +
            "\n" +
            "\"Derivative Works\" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.\n" +
            "\n" +
            "\"Contribution\" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, \"submitted\" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as \"Not a Contribution.\"\n" +
            "\n" +
            "\"Contributor\" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.\n" +
            "\n" +
            "2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.\n" +
            "\n" +
            "3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.\n" +
            "\n" +
            "4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:\n" +
            "\n" +
            "    You must give any other recipients of the Work or Derivative Works a copy of this License; and\n" +
            "    You must cause any modified files to carry prominent notices stating that You changed the files; and\n" +
            "    You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and\n" +
            "    If the Work includes a \"NOTICE\" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.\n" +
            "\n" +
            "    You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.\n" +
            "\n" +
            "5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.\n" +
            "\n" +
            "6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.\n" +
            "\n" +
            "7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.\n" +
            "\n" +
            "8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.\n" +
            "\n" +
            "9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.\n" +
            "\n" +
            "END OF TERMS AND CONDITIONS\n";

}

repo_name: tomjschwanke/DSBDirect

license: mit


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;

import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import com.google.android.material.textfield.TextInputEditText;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import android.view.KeyEvent;
import android.view.View;
import android.widget.Button;
import godau.fynn.dsbdirect.Login;
import godau.fynn.dsbdirect.NewsQuery;
import godau.fynn.dsbdirect.R;
import godau.fynn.dsbdirect.Utility;
import godau.fynn.dsbdirect.activity.settings.MainSettingsActivity;
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 godau.fynn.dsbdirect.table.reader.ReaderFactory;

import java.io.IOException;

public class LoginActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);

        final TextInputEditText idEditText = findViewById(R.id.id);
        final TextInputEditText passEditText = findViewById(R.id.pass);
        final Button login = findViewById(R.id.login);

        final Utility u = new Utility(LoginActivity.this);

        login.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String id = idEditText.getText().toString();
                String pass = passEditText.getText().toString();
                final Login login = new Login(id, pass);

                // Indicate that something is happening
                indicateProgress(true);

                final DownloadManager downloadManager = new DownloadManager(LoginActivity.this);

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

                            Table[] tables = downloadManager.downloadContentInformation(login).getTables();

                            // If no exception occurred, the credentials are correct
                            LoginManager loginManager = new LoginManager(LoginActivity.this);

                            // Only prompt user for their preferences when they log in with their first login
                            boolean promptPreferences = loginManager.getLoginCount() <= 0;

                            loginManager.addLogin(login);

                            setResult(RESULT_OK);

                            if (promptPreferences) {
                                potentiallyPromptForFilter(downloadManager, u, login.getId(), tables);
                            } else {
                                // Skip questions
                                finish();
                            }

                        } catch (DownloadManager.LoginFailureException e) {

                            runOnUiThread(new Runnable() {
                                @Override
                                public void run() {
                                    // Credentials invalid
                                    new AlertDialog.Builder(LoginActivity.this)
                                            .setTitle(R.string.credentials_popup_title)
                                            .setMessage(R.string.credentials_popup_message)
                                            .setPositiveButton(R.string.ok, null)
                                            .setNeutralButton(R.string.news_fix, new DialogInterface.OnClickListener() {
                                                @Override
                                                public void onClick(DialogInterface dialog, int which) {
                                                    new Thread(new NewsQuery(LoginActivity.this, downloadManager))
                                                            .start();
                                                }
                                            })
                                            .setNegativeButton(R.string.credentials_popup_open_preferences, new DialogInterface.OnClickListener() {
                                                @Override
                                                public void onClick(DialogInterface dialog, int which) {
                                                    startActivity(new Intent(LoginActivity.this, MainSettingsActivity.class));
                                                }
                                            })
                                            .show();

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

                            runOnUiThread(new Runnable() {
                                @Override
                                public void run() {
                                    // Error
                                    new AlertDialog.Builder(LoginActivity.this)
                                            .setTitle(R.string.network_generic_error)
                                            .setMessage(R.string.network_generic_error_credentials)
                                            .setPositiveButton(R.string.ok, null)
                                            .show();
                                    indicateProgress(false);
                                }
                            });

                            e.printStackTrace();
                        }
                    }
                }).start();

            }
        });

        // Handel enter presses
        passEditText.setOnKeyListener(new View.OnKeyListener() {
            @Override
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_DOWN) {
                    login.callOnClick();

                    return true;
                }

                return false;
            }
        });
    }

    private void potentiallyPromptForFilter(final DownloadManager downloadManager, final Utility u, final String id, Table[] tables) {

        try {

            if (tables[0].isHtml()) {
                FileManager fileManager = new FileManager(LoginActivity.this);

                // Download the first table
                String html = fileManager.getHtmlTable(tables[0], downloadManager);

                // If a reader can be gotten, parsing is probably possible
                if (u.getReader(html, id) != null) {
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            indicateProgress(false);
                            MainSettingsActivity.setFilterPopup(LoginActivity.this, new Handler(new Handler.Callback() {
                                @Override
                                public boolean handleMessage(Message msg) {
                                    // Ensure parse and filter are on
                                    u.getSharedPreferences().edit()
                                            .putBoolean("parse", true)
                                            .putBoolean("filter", true)
                                            .apply();

                                    promptForNotificationsOnUiThreadAndFinish();

                                    return false;
                                }
                            }));
                        }
                    });
                } else {
                    // Parsing seems to be impossible
                    u.getSharedPreferences().edit()
                            .putBoolean("parse", false)
                            .apply();
                    promptForNotificationsOnUiThreadAndFinish();
                }

            } else {
                promptForNotificationsOnUiThreadAndFinish();
            }

        } catch (DownloadManager.NoContentException e) {

            promptForNotificationsOnUiThreadAndFinish();

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

    private void promptForNotificationsOnUiThreadAndFinish() {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                indicateProgress(false);
                new AlertDialog.Builder(LoginActivity.this)
                        .setTitle(R.string.notifications_popup_title)
                        .setMessage(R.string.notifications_popup_message)
                        .setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                new Utility(LoginActivity.this).getSharedPreferences()
                                        .edit()
                                        .putBoolean("poll", true)
                                        .apply();
                                finish();
                            }
                        })
                        .setNegativeButton(R.string.no, new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                new Utility(LoginActivity.this).getSharedPreferences()
                                        .edit()
                                        .putBoolean("poll", false)
                                        .apply();
                                finish();
                            }
                        })
                        .show();
            }
        });

    }

    private void indicateProgress(boolean yes) {
        if (yes) {
            findViewById(R.id.login).setEnabled(false);
            findViewById(R.id.progressBar).setVisibility(View.VISIBLE);
        } else {
            findViewById(R.id.login).setEnabled(true);
            findViewById(R.id.progressBar).setVisibility(View.GONE);
        }
    }

    @Override
    public void finish() {
        new Utility(LoginActivity.this)
                .getSharedPreferences()
                .edit()
                .putBoolean("login", true)
                .apply();

        super.finish();
    }
}


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;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.drawable.BitmapDrawable;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import androidx.annotation.StringRes;
import com.google.android.material.snackbar.Snackbar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.appcompat.widget.Toolbar;
import android.util.Log;
import android.view.*;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.widget.*;
import com.ortiz.touchview.TouchImageView;
import com.wefika.horizontalpicker.HorizontalPicker;
import godau.fynn.dsbdirect.*;
import godau.fynn.dsbdirect.activity.settings.MainSettingsActivity;
import godau.fynn.dsbdirect.activity.settings.SettingsActivity;
import godau.fynn.dsbdirect.manager.DownloadManager;
import godau.fynn.dsbdirect.manager.FileManager;
import godau.fynn.dsbdirect.manager.LoginManager;
import godau.fynn.dsbdirect.table.Adapter;
import godau.fynn.dsbdirect.table.Entry;
import godau.fynn.dsbdirect.table.Table;
import godau.fynn.dsbdirect.table.reader.ReaderRunnable;
import humanize.Humanize;
import humanize.time.TimeMillis;

import java.io.File;
import java.io.IOException;
import java.util.*;

public class MainActivity extends AppCompatActivity {

    private static final int REQUEST_LOGIN = 1;

    private Table mCurrentTable;

    private Login login;

    private DownloadManager.ContentInformation contentInformation;

    private Table[] mTables;
    private Date mTimetabledate;

    private boolean mTimetabledateDisplayed = false;

    private Utility u;
    private FileManager mFileManager;
    private DownloadManager mDownloadManager;
    private LoginManager mLoginManager;

    private Queue<Runnable> mReaderTasks = new LinkedList<>();

    private TextView mTextView;

    private WebView mWebView;

    private Adapter mAdapter;

    private boolean mParse = true;
    private boolean mMerge = true;

    private Menu mMenu;
    public static boolean filterEnabled;
    public static boolean initialized = false; //only set filter_enabled once
    private static int offlineFilterPage = 0;

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

        u = new Utility(MainActivity.this);
        u.stylize();

        long drawTimeStart = System.currentTimeMillis();

        setContentView(R.layout.activity_main);

        // Style horizontal picker
        ((HorizontalPicker) findViewById(R.id.page)).setBackgroundColor(u.getColorPrimary());

        long drawTimeEnd = System.currentTimeMillis();
        Log.d("DRAW", "rendering layout took " + (drawTimeEnd - drawTimeStart) + " milliseconds");

        final SharedPreferences sharedPreferences = u.getSharedPreferences();

        mParse = sharedPreferences.getBoolean("parse", true);
        mMerge = sharedPreferences.getBoolean("merge", true);

        Toolbar toolbar = findViewById(R.id.toolbar);
        toolbar.setBackgroundColor(u.getColorPrimary());
        setSupportActionBar(toolbar);

        setOverflowButtonColor(this, getResources().getColor(R.color.white));

        mTextView = findViewById(R.id.text);

        // Get managers
        mFileManager = new FileManager(MainActivity.this);
        mDownloadManager = new DownloadManager(this);
        mLoginManager = new LoginManager(MainActivity.this);

        int previousVersion = sharedPreferences.getInt("version", BuildConfig.VERSION_CODE);

        // Migrate 1.8.1 (version code 13) users to avoid login screen
        if (previousVersion <= 13) {
            sharedPreferences.edit().putBoolean("login", true).apply();
        }

        // Delete useless auth token from versions up to 2.4.1 (version code 22)
        if (previousVersion <= 22) {
            sharedPreferences.edit().remove("token").apply();
        }

        // Migrate login from up to version 2.5.5 (version code 29)
        if (previousVersion <= 29) {
            mLoginManager.addLogin(new Login(
                    sharedPreferences.getString("id", ""),
                    sharedPreferences.getString("pass", "")
            ));
            sharedPreferences.edit().remove("pass").apply();
        }

        // Migrate shortcodes stored in a StringSet preference from up to version 2.6 (version code 32)
        if (previousVersion <= 32) {
            Set<String> shortcodeSet = sharedPreferences.getStringSet("shortcodes", new LinkedHashSet<String>());
            String[] shortcodes = shortcodeSet.toArray(new String[shortcodeSet.size()]);
            Arrays.sort(shortcodes);
            StringBuilder shortcodesRaw = new StringBuilder();

            Arrays.sort(shortcodes); //sort alphabetical
            for (int i = 0; i < shortcodes.length; i++) {
                shortcodesRaw.append(shortcodes[i].replace("\n", ""));
                if(i != shortcodes.length - 1)
                    shortcodesRaw.append("\n");
            }
            SharedPreferences.Editor editor = sharedPreferences.edit();
            editor.remove("shortcodes");
            editor.putString("shortcodes", shortcodesRaw.toString());
            editor.apply();
        }

        // Inform users about name change since version 2.6 or 2.6.1 (which was not tagged in master – version code 33)
        if (previousVersion <= 33) {
            new AlertDialog.Builder(MainActivity.this)
                    .setTitle(R.string.migration_dsbdirect_rename)
                    .setMessage(R.string.migration_dsbdirect_rename_message)
                    .setPositiveButton(R.string.ok, null)
                    .setNeutralButton(R.string.email_the_dev, new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            MainSettingsActivity.emailTheDev(MainActivity.this);
                        }
                    })
                    .show();
        }

        // Migrate users to most recent version by wiping news after update
        if (previousVersion < BuildConfig.VERSION_CODE) {
            Log.d("MIGRATION", "wiping news");
            NewsQuery.wipeNews(MainActivity.this);
        }

        // Start loading or show login screen
        if (mLoginManager.canLogin()) {
            login = mLoginManager.getActiveLogin();

            if (!initialized) {
                filterEnabled = u.getSharedPreferences().getBoolean("filter", false);
                initialized = true;
            }

            // Start loading
            new Thread(new Runnable() {
                @Override
                public void run() {
                    getTimetables();
                }
            }).start();
        } else {
            // Login required
            Intent loginIntent = new Intent(MainActivity.this, LoginActivity.class);
            startActivityForResult(loginIntent, REQUEST_LOGIN);
        }

        // Update checker
        if (u.shouldCheckForUpdates()) {
            u.checkForUpdate(false, mDownloadManager);
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            u.schedulePolling();
        }

        // Save version code for potential What's new stuff or migrations
        sharedPreferences.edit().putInt("version", BuildConfig.VERSION_CODE).apply();
    }

    private void openNoticeBoard() {
        Intent intent = new Intent(MainActivity.this, NoticeBoardActivity.class);
        if (contentInformation != null)
            intent.putExtra(NoticeBoardActivity.EXTRA_CONTENT_INFORMATION, contentInformation);
        startActivity(intent);
    }

    private void networkErrorToUi(Exception e) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                Snackbar
                        .make(findViewById(R.id.root),
                                getString(R.string.you_are_offline),
                                Snackbar.LENGTH_SHORT)
                        .show();
                // Enter offline mode
                offlineMode();

            }
        });

        e.printStackTrace();
    }

    private void getTimetables() {

        try {
            contentInformation = mDownloadManager.downloadContentInformation(login);
            mTables = contentInformation.getTables();

            // No content
            if (mTables.length == 0) {
                throw new DownloadManager.UnexpectedResponseException();
            }

            // Find out whether all tables might be parsable
            boolean findAllHtml = true;
            for (Table table : mTables) {
                if (!table.isHtml()) {
                    findAllHtml = false;
                    break;
                }
            }

            final boolean allHtml = findAllHtml;

            // It's not possible to merge if not all pages are html
            if (!allHtml) mMerge = false;

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

                    mTextView.setText(getString(R.string.timetable_uri_acquired));

                    // Find date
                    mTimetabledate = mTables[0].getPublishedDate();

                    // Display notices button
                    if (mMenu != null && contentInformation.hasNotices()) {
                        mMenu.add(R.string.action_notices)
                                .setIcon(R.drawable.ic_newspaper)
                                .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
                                    @Override
                                    public boolean onMenuItemClick(MenuItem item) {
                                        openNoticeBoard();
                                        return true;
                                    }
                                }).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
                    }

                    if (mMerge && mParse && allHtml) {
                        // All tables can be displayed at once
                        displayMultipleHtmlTimetables(new ArrayList<>(Arrays.asList(mTables)));

                        // Ensure pagePicker is gone
                        findViewById(R.id.page).setVisibility(View.GONE);
                    } else {
                        // Every table has to be displayed separately
                        // Display the first table
                        mCurrentTable = mTables[0];
                        displayTimetable(mTables[0]);

                        // Let user view other tables if necessary
                        if (mTables.length > 1) {
                            HorizontalPicker pagePicker = findViewById(R.id.page);
                            pagePicker.setVisibility(View.VISIBLE);

                            // Display table titles as values in horizontal picker
                            String[] titles = new String[mTables.length];
                            for (int i = 0; i < mTables.length; i++) {
                                titles[i] = mTables[i].getTitle();
                            }
                            pagePicker.setValues(titles);

                            pagePicker.setOnItemSelectedListener(new HorizontalPicker.OnItemSelected() {
                                @Override
                                public void onItemSelected(int index) {
                                    displayTimetable(mTables[index]);
                                }
                            });

                            pagePicker.setOnItemClickedListener(new HorizontalPicker.OnItemClicked() {
                                @Override
                                public void onItemClicked(int index) {
                                    displayTimetabledate(R.string.timetable_published, mTables[index].getPublishedDate());
                                }
                            });
                        }
                    }
                }
            });
        } catch (final DownloadManager.UnexpectedResponseException e) {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    if (e instanceof DownloadManager.LoginFailureException) {
                        mTextView.setText(R.string.network_login_denied);
                    } else {
                        mTextView.setText(R.string.network_invalid_response);
                    }
                    final Button fix = findViewById(R.id.fix);
                    fix.setVisibility(View.VISIBLE);
                    fix.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            fix.setVisibility(View.GONE);
                            new Thread(new NewsQuery(MainActivity.this, mDownloadManager)).start();
                        }
                    });
                }
            });
            e.printStackTrace();
        } catch (IOException e) {
            networkErrorToUi(e);
        }
    }

    /**
     * Displays timetable, no matter whether it has already been downloaded or not.
     * @param table The table to be displayed
     */
    private void displayTimetable(final Table table) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {

                    if (table.isHtml()) {

                        // Possibly init WebView early
                        final SharedPreferences sharedPreferences = u.getSharedPreferences();
                        if (sharedPreferences.getBoolean("renderWebViewEarly", false)) {
                            // Do init async to this thread: use new thread (async to this thread) to then run on ui thread (synchronous to new thread)
                            new Thread(new Runnable() {
                                @Override
                                public void run() {
                                    runOnUiThread(new Runnable() {
                                        @Override
                                        public void run() {
                                            Log.d("DRAW", "(possibly) initializing WebView while getting file");
                                            initWebView();

                                            // We don't know whether it should be rendered early next time again
                                            sharedPreferences
                                                    .edit()
                                                    .putBoolean("renderWebViewEarly", false)
                                                    .apply();
                                        }
                                    });
                                }
                            }).start();
                        }

                        final String html = mFileManager.getHtmlTable(table, mDownloadManager);
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                displayHtml(html);
                            }
                        });
                    } else {
                        final Bitmap image = mFileManager.getImageTable(table, mDownloadManager);
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                displayImage(image);
                            }
                        });
                    }

                } catch (final IOException e) {
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            networkErrorToUi(e);
                        }
                    });
                    e.printStackTrace();
                }

            }
        }).start();
    }

    /**
     * Displays file available offline already
     *
     * @param file   File to be displayed
     * @param isHtml Whether the file is html
     */
    private void displayFile(File file, boolean isHtml) {
        if (isHtml) {
            String html = mFileManager.readHtmlFile(file);
            displayHtml(html);
        } else {
            Bitmap bitmap = mFileManager.readBitmapFile(file);
            displayImage(bitmap);
        }
    }

    private void displayHtml(final String response) {
        // In case an image had been shown previously, hide it
        ImageView image = findViewById(R.id.tableimage);
        image.setVisibility(View.GONE);

        if (mParse) {
            // Parse table

            // Create runnable
            final ReaderRunnable readerRunnable = new ReaderRunnable(MainActivity.this, response,
                    login.getId(), mReaderTasks
            );
            readerRunnable.addHandlers(
                    new Handler(new Handler.Callback() {
                        @Override
                        public boolean handleMessage(Message msg) {
                            // Display parsed entries
                            displayEntries(readerRunnable.getResult());

                            String schoolName = readerRunnable.getSchoolName();
                            if (schoolName != null) {
                                // Display school name as window title if configured via preference, is debug build or multiple logins are configured
                                if (u.getSharedPreferences().getBoolean(Utility.SUPER_SECRET_SETTING_FORCE_SCHOOL_NAME_AS_WINDOW_TITLE, getResources().getBoolean(R.bool.school_name_as_window_title_default))
                                        || mLoginManager.getLoginCount() > 1) {
                                    setTitle(schoolName);
                                }

                                // Set login display name to school name if doesn't already have one
                                if (!mLoginManager.getActiveLogin().hasDisplayName()) {
                                    mLoginManager.getActiveLogin().setDisplayName(schoolName);
                                    mLoginManager.write();
                                }
                            }

                            return false;
                        }
                    }),
                    new Handler(new Handler.Callback() {
                        @Override
                        public boolean handleMessage(Message msg) {
                            // Display error message
                            displayErrorEntry();
                            return false;
                        }
                    })
            );

            // Start and add thread
            mReaderTasks.add(readerRunnable);

            // If the task we just added is the only task queued
            if (mReaderTasks.size() == 1) {
                // Start this task in a new thread
                new Thread(readerRunnable).start();
            }

        } else {
            ((TextView) findViewById(R.id.date)).setText("");

            // Render HTML code in WebView

            initWebView();
            mWebView.setVisibility(View.VISIBLE);

            // Save that the WebView should be rendered early next time
            u.getSharedPreferences()
                    .edit()
                    .putBoolean("renderWebViewEarly", true)
                    .apply();

            try {
                mWebView.loadData(response, "text/html", null);
            } catch (NullPointerException e) {
                e.printStackTrace();
                mWebView.setVisibility(View.GONE);
                mTextView.setText(getString(R.string.error_displaying));
                mTextView.setVisibility(View.VISIBLE);
            }
        }

        potentiallyDisplayTimetabledate();
    }

    private void initWebView() {
        if (mWebView == null) {
            long webViewInitStart = System.currentTimeMillis();
            mWebView = new WebView(this);
            mWebView.setLayoutParams(new ViewGroup.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
            );
            ((ViewGroup) findViewById(R.id.content)).addView(mWebView);

            WebSettings webSettings = mWebView.getSettings();
            webSettings.setBlockNetworkLoads(true);
            webSettings.setJavaScriptEnabled(false);

            mWebView.setVisibility(View.GONE);

            Log.d("DRAW", "initialized WebView within " + (System.currentTimeMillis() - webViewInitStart)
                    + " milliseconds");
        }
    }

    private void displayErrorEntry() {
        ArrayList<Entry> error = new ArrayList<>();
        error.add(new Entry(null, null, null, getString(R.string.error_parsing_partial), null, null));
        displayEntries(error);
    }

    private void displayEntries(final ArrayList<Entry> entries) {

        ListView listView = findViewById(R.id.table);

        if (mAdapter == null || !mMerge) {
            mAdapter = new Adapter(MainActivity.this, entries);
            ((TextView) findViewById(R.id.date)).setText("");

            listView.setAdapter(mAdapter);
        } else {
            mAdapter.addAll(entries);
        }

        listView.setVisibility(View.VISIBLE);

        if (mAdapter.size() > 0) {
            mTextView.setVisibility(View.GONE);

            final TextView dateView = findViewById(R.id.date);
            dateView.setVisibility(View.VISIBLE);

            listView.setOnScrollListener(new AbsListView.OnScrollListener() {
                @Override
                public void onScrollStateChanged(AbsListView view, int scrollState) {

                }

                @Override
                public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
                    try {
                        dateView.setText(u.formatDate(mAdapter.get(firstVisibleItem).getDate()));
                    } catch (IndexOutOfBoundsException | NullPointerException e) {
                        // does not matter because this happens when list is empty
                    }

                }
            });

            // Enable long pressing to share unless super secret setting is enabled
            if (u.getSharedPreferences().getBoolean(Utility.SUPER_SECRET_SETTING_HOLD_TO_SHARE, true)) {
                listView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
                    @Override
                    public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
                        Intent shareIntent = new Intent(Intent.ACTION_SEND);
                        shareIntent.putExtra(Intent.EXTRA_TEXT, mAdapter.get(position).getShareText(u));
                        shareIntent.setType("text/plain");
                        startActivity(Intent.createChooser(shareIntent, getString(R.string.share_title)));

                        return true;
                    }
                });
            }

        } else {
            mTextView.setText(R.string.empty);
            mTextView.setVisibility(View.VISIBLE);
            listView.setVisibility(View.GONE);
        }
    }

    /**
     * Display multiple html tables at once (concatenate them)
     *
     * @param tables Tables to be potentially downloaded and displayed
     */
    private void displayMultipleHtmlTimetables(final ArrayList<Table> tables) {

        new Thread(new Runnable() {
            @Override
            public void run() {
                final Table table = tables.get(0);

                tables.remove(table);
                try {
                    final String html = mFileManager.getHtmlTable(table, mDownloadManager);

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

                            displayHtml(html);

                            // Display remaining tables
                            // This has to be done after the previous entries have been parsed to not mix up the order
                            if (tables.size() > 0) {
                                // To understand recursion, you first have to understand recursion
                                displayMultipleHtmlTimetables(tables);
                            }
                        }
                    });
                } catch (IOException e) {
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            displayErrorEntry(); // Missing just a part!

                        }
                    });
                    e.printStackTrace();
                }

            }
        }).start();
    }

    private void displayImage(Bitmap bitmap) {
        // Hide text
        mTextView.setVisibility(View.GONE);

        // Hide list, date text and webView in case they had previously been displayed
        ListView listView = findViewById(R.id.table);
        TextView dateText = findViewById(R.id.date);
        dateText.setText("");
        dateText.setVisibility(View.GONE);
        listView.setVisibility(View.GONE);

        if (mWebView != null) {
            mWebView.setVisibility(View.GONE);
        }

        // Zoom out
        TouchImageView image = findViewById(R.id.tableimage);
        image.resetZoom();

        BitmapDrawable drawable = new BitmapDrawable(bitmap);

        // Invert colors
        if (u.getSharedPreferences().getBoolean("invertImages", false)) {
            // Thanks, https://stackoverflow.com/a/17871384
            final float[] NEGATIVE = {
                    -1.0f, 0, 0, 0, 255, // red
                    0, -1.0f, 0, 0, 255, // green
                    0, 0, -1.0f, 0, 255, // blue
                    0, 0, 0, 1.0f, 0  // alpha
            };
            drawable.setColorFilter(new ColorMatrixColorFilter(NEGATIVE));
        }

        // Show image
        image.setImageDrawable(drawable);
        image.setVisibility(View.VISIBLE);

        potentiallyDisplayTimetabledate();

    }

    private void offlineMode() {

        // Change title to notify user offline mode is enabled
        getSupportActionBar().setTitle(R.string.app_name_offline);

        // Check checkbox
        if (mMenu != null) {
            mMenu.findItem(R.id.action_view_history).setChecked(true);
        }

        setTempFilterIcon(mMenu);

        // Don't merge
        mMerge = false;

        // Find every file, sorted descending by last modified
        final File[] files = mFileManager.getFilesSorted();

        if (files.length == 0) {
            // We're done
            return;
        }

        // Read every file to a Table
        mTables = new Table[files.length];

        for (int i = 0; i < files.length; i++) {
            File file = files[i];

            String url = file.getName().replaceAll("\\d+-", "");

            Date date;
            // Who knows what files might be in our file system
            try {
                String time = file.getName().split("-")[0];
                date = new Date(Long.parseLong(time));
            } catch (NumberFormatException e) {
                e.printStackTrace();
                date = new Date();
            }

            boolean isHtml;
            try {
                String[] fileNameParts = file.getName().split("\\.");
                String suffix = fileNameParts[fileNameParts.length - 1];

                isHtml = suffix.contains("htm");
            } catch (ArrayIndexOutOfBoundsException e) {
                // In this case, there was no dot in the filename

                isHtml = false;
            }

            mTables[i] = new Table(url, date, isHtml, Humanize.naturalTime(date));
        }

        // Don't do it. It's dumb
        //mTimetabledate = mTables[0].getPublishedDate();

        // Display the first table
        HorizontalPicker pagePicker = findViewById(R.id.page);
        pagePicker.setSelectedItem(0);
        mCurrentTable = mTables[0];
        if(offlineFilterPage != 0) {
            displayFile(files[offlineFilterPage], mTables[offlineFilterPage].isHtml());
            displayTimetabledate(R.string.timetable_published, mTables[offlineFilterPage].getPublishedDate());
            pagePicker.setSelectedItem(offlineFilterPage);
        }else
            displayFile(files[0], mTables[0].isHtml());

        // Let user view other files
        if (mTables.length > 1) {
            pagePicker.setVisibility(View.VISIBLE);

            // Initialize CharSequence[] for setting them as values
            CharSequence[] values = new CharSequence[mTables.length];
            for (int i = 0; i < mTables.length; i++) {
                values[i] = Humanize.naturalTime(mTables[i].getPublishedDate());
            }

            pagePicker.setValues(values);
            pagePicker.setOnItemSelectedListener(new HorizontalPicker.OnItemSelected() {
                @Override
                public void onItemSelected(int index) {
                    displayFile(files[index], mTables[index].isHtml());
                    offlineFilterPage = index;
                }
            });

            pagePicker.setOnItemClickedListener(new HorizontalPicker.OnItemClicked() {
                @Override
                public void onItemClicked(int index) {
                    displayTimetabledate(R.string.timetable_published, mTables[index].getPublishedDate());
                }
            });
        }
    }

    private void potentiallyDisplayTimetabledate() {
        // Only do this once
        if (!mTimetabledateDisplayed && mTimetabledate != null) {
            displayTimetabledate(R.string.timetable_last_changed, mTimetabledate);
            mTimetabledateDisplayed = true;
        }
    }

    private void displayTimetabledate(@StringRes int message, Date mTimetabledate) {
        String ago = Humanize.naturalTime(mTimetabledate, TimeMillis.HOUR);
        if (ago.length() <= 0) {
            ago = Humanize.naturalTime(mTimetabledate, TimeMillis.SECOND);
        }
        Log.d("TIMETABLEDATE", ago);
        Snackbar
                .make(findViewById(R.id.contentCoordinator),
                        getString(message, ago),
                        Snackbar.LENGTH_LONG)
                .show();
    }

    public static void setOverflowButtonColor(final Activity activity, final int color) {
        // from https://stackoverflow.com/a/36278375 by https://stackoverflow.com/users/5064289/barun-kumar
        final String overflowDescription = activity.getString(R.string.abc_action_menu_overflow_description);
        final ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
        final ViewTreeObserver viewTreeObserver = decorView.getViewTreeObserver();
        viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                final ArrayList<View> outViews = new ArrayList<View>();
                decorView.findViewsWithText(outViews, overflowDescription, View.FIND_VIEWS_WITH_CONTENT_DESCRIPTION);
                if (outViews.isEmpty()) {
                    return;
                }
                AppCompatImageView overflow = (AppCompatImageView) outViews.get(0);
                overflow.setColorFilter(color);
            }
        });
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu, menu);

        if (getResources().getBoolean(R.bool.news_check_button)) {
            menu.add(Menu.NONE, 4, Menu.NONE, R.string.action_check_news);
        }

        // Add all inactive logins as a submenu if there is one
        if (mLoginManager.getLoginCount() > 1) {
            SubMenu loginMenu = menu.addSubMenu(R.string.action_switch_login);
            for (Login l :
                    mLoginManager.getInactiveLogins()) {
                loginMenu.add(Menu.NONE, Integer.parseInt(l.getId()), Menu.NONE, l.getDisplayName());
            }
            loginMenu.add(Menu.NONE, 5, Menu.NONE, R.string.action_add_login).setIcon(R.drawable.ic_add_black_24dp);
        }
        setTempFilterIcon(menu);

        // Pass it around!
        mMenu = menu;

        return super.onCreateOptionsMenu(menu);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.action_reload:
                recreate();

                break;
            case R.id.action_temp_filters:
                //enable/disable filters for current session
                filterEnabled = !filterEnabled;
                if(mMenu.findItem(R.id.action_view_history).isChecked())
                    offlineMode();
                else
                    recreate();
                break;
            case R.id.action_view_history:
                if (item.isChecked()) {
                    offlineFilterPage = 0;
                    recreate();
                } else {
                    item.setChecked(true);
                    offlineMode();
                }
                break;
            case R.id.action_settings:
                Intent settingsIntent = new Intent(MainActivity.this, MainSettingsActivity.class);

                // Pass along whether plan contains html files and whether plan only consists of html files
                if (mTables != null) {
                    // At least one file must be html
                    boolean containsHtml = false;
                    boolean allHtml = true;
                    for (Table table : mTables) {
                        if (table.isHtml()) {
                            // This table is html, therefore the plan contains html
                            containsHtml = true;
                        } else {
                            // This table is not html, therefore it can't be that all pages are html
                            allHtml = false;
                        }
                    }

                    settingsIntent.putExtra(SettingsActivity.EXTRA_CONTAINS_HTML, containsHtml);
                    settingsIntent.putExtra(SettingsActivity.EXTRA_HTML_ONLY, allHtml);
                }

                startActivityForResult(settingsIntent,
                        MainSettingsActivity.class.getName().length() // I needed a constant number, so I chose something simple & easy to remember
                );
                break;
            case 5:
                startActivityForResult(new Intent(MainActivity.this, LoginActivity.class), REQUEST_LOGIN);
                break;
            case 4:
                new Thread(new NewsQuery(MainActivity.this, mDownloadManager)).start();
                break;
            default:
                // Find out whether this id fits any login id
                for (Login l :
                        mLoginManager.getLogins()) {
                    if (item.getItemId() == Integer.parseInt(l.getId())) {
                        Log.d("LOGINSWITCH", "to login " + l.getDisplayName());
                        mLoginManager.setActiveLogin(l);
                        recreate();
                        return true;
                    }
                }

                // If we got here, the user's action was not recognized.
                // Invoke the superclass to handle it.
                return super.onOptionsItemSelected(item);

        }
        return true;
    }

    private void showAuthPrompt() {
        new AlertDialog.Builder(MainActivity.this)
                .setTitle(R.string.credentials_popup_title)
                .setMessage(R.string.credentials_popup_message)
                .setNegativeButton(R.string.dismiss, null)
                .setPositiveButton(R.string.login_login, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        startActivityForResult(new Intent(MainActivity.this, LoginActivity.class), REQUEST_LOGIN);
                    }
                })
                .show();
    }

    @Override
    public void recreate() {
        super.recreate();

        // Reset pagePicker position
        ((HorizontalPicker) findViewById(R.id.page)).setSelectedItem(0);

    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        //int settingsActivityRequestCode = SettingsActivity.class.getName().length(); // A way to get a cool number
        // Whatever request this is happened, we want to reload if it was successful
        if (resultCode == RESULT_OK) {
            recreate(); // Because we can't really refresh very well
        } else if (resultCode == RESULT_CANCELED && requestCode == REQUEST_LOGIN) {
            // Login was cancelled, quit if not debug and there are no logins
            if (getResources().getBoolean(R.bool.authenticate_quit_on_cancel) && mLoginManager.getLoginCount() < 1)
                finish();
        }

        super.onActivityResult(requestCode, resultCode, data);
    }
    private void setTempFilterIcon(Menu menu){
        if (menu == null)
            return;
        if(u.getSharedPreferences().getBoolean("filter", false)&&u.getSharedPreferences().getBoolean("parse", false)){
            if(filterEnabled){
                menu.findItem(R.id.action_temp_filters).setIcon(R.drawable.ic_visibility_24px);
                menu.findItem(R.id.action_temp_filters).setTitle(R.string.action_temp_filters_disable);
            } else{
                menu.findItem(R.id.action_temp_filters).setIcon(R.drawable.ic_visibility_off_24px);
                menu.findItem(R.id.action_temp_filters).setTitle(R.string.action_temp_filters_enable);
            }
            menu.findItem(R.id.action_temp_filters).setVisible(true);
        }
    }

}


code:
package godau.fynn.dsbdirect.activity

// Created by Jasper Michalke <jasper.michalke@jasmich.de> under license EUPL 1.2.

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import android.view.View
import android.widget.ListView
import android.widget.TextView
import godau.fynn.dsbdirect.Login
import godau.fynn.dsbdirect.R
import godau.fynn.dsbdirect.Utility
import godau.fynn.dsbdirect.ZoomImage
import godau.fynn.dsbdirect.manager.DownloadManager
import godau.fynn.dsbdirect.manager.LoginManager
import godau.fynn.dsbdirect.table.NoticeAdapter

class NoticeBoardActivity : AppCompatActivity() {

    private lateinit var noticeAdapter: NoticeAdapter
    private lateinit var login: Login

    companion object {
        const val EXTRA_CONTENT_INFORMATION = "content"
        @JvmStatic var lastZoomedImage: ZoomImage? = null
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_notice_board)

        val u = Utility(this)
        u.stylize()

        val mLoginManager = LoginManager(this)

        login = mLoginManager.activeLogin
        // Start loading
        Thread(Runnable { loadNotices() }).start()

        val toolbar: Toolbar = findViewById<Toolbar>(R.id.toolbar)
        toolbar.setBackgroundColor(u.colorPrimary)
        setSupportActionBar(toolbar)
        supportActionBar!!.setDisplayHomeAsUpEnabled(true)
    }

    private fun loadNotices() {
        var dManager = DownloadManager(this)

        var content: DownloadManager.ContentInformation
        if (intent.hasExtra(EXTRA_CONTENT_INFORMATION)) {
            content = intent.getSerializableExtra(EXTRA_CONTENT_INFORMATION) as DownloadManager.ContentInformation
        } else {
            content = dManager.downloadContentInformation(login)
        }

        runOnUiThread {
            val listView = findViewById<ListView>(R.id.table)
            if (::noticeAdapter.isInitialized) {
                noticeAdapter.addAll(content.notices)
            } else {
                noticeAdapter = NoticeAdapter(this, content.notices, findViewById(R.id.contentCoordinator))
                listView.adapter = noticeAdapter
            }
            listView.visibility = View.VISIBLE
            findViewById<TextView>(R.id.text).visibility = View.GONE
        }
    }

    override fun onBackPressed() {
        if (lastZoomedImage != null && lastZoomedImage!!.zoomed) {
            lastZoomedImage!!.zoomOut();
        } else
            super.onBackPressed()
    }
}


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

import android.app.AlertDialog;
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.Login;
import godau.fynn.dsbdirect.R;
import godau.fynn.dsbdirect.activity.LoginActivity;
import godau.fynn.dsbdirect.manager.LoginManager;

public class LoginSettingsActivity extends SettingsActivity {

    private static final int REQUEST_LOGIN = 0;

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

    public static class PreferenceFragment extends SettingsActivity.PreferenceFragment {

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

            final LoginManager loginManager = new LoginManager(sharedPreferences);

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

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

            findPreference("settings_none_description")
                    .setSummary(R.string.settings_login_description);

            // Add logins
            for (final Login l : loginManager.getLogins()) {
                Preference loginPreference = new Preference(getActivity());

                loginPreference.setTitle(l.getDisplayName());
                if (l.hasDisplayName()) {
                    loginPreference.setSummary(l.getId());
                }

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

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

                        if (l.hasDisplayName()) {
                            editText.setText(l.getDisplayName());
                        }
                        new AlertDialog.Builder(getActivity())
                                .setTitle(getActivity().getString(R.string.settings_login_alias_popup, l.getId()))
                                .setMessage(R.string.settings_login_alias_popup_message)
                                .setView(promptView)
                                .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
                                    @Override
                                    public void onClick(DialogInterface dialog, int which) {
                                        if (editText.length() <= 0) {
                                            // To allow display name to be set to school name automatically
                                            l.setDisplayName(null);
                                        } else {
                                            l.setDisplayName(editText.getText().toString());
                                        }
                                        // Write changed login alias to shared preferences
                                        loginManager.write();

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

                                        loginManager.removeLogin(l);

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

                preferenceScreen.addPreference(loginPreference);
            }

            // Add Add login button

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

            // 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) {

                    startActivityForResult(new Intent(getActivity(), LoginActivity.class), REQUEST_LOGIN);

                    // TODO: A drawback of this button is that school name can only be set as display alias when its plan is viewed with parsing enabled, not immediately when added here.

                    return true;
                }
            });

            preferenceScreen.addPreference(newLoginPreference);

        }

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