package com.fox2code.mmm;

import static com.fox2code.mmm.MainApplication.Iof;
import static com.fox2code.mmm.manager.ModuleInfo.FLAG_MM_REMOTE_MODULE;

import android.Manifest;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Color;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.widget.CheckBox;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.appcompat.widget.SearchView;
import androidx.cardview.widget.CardView;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;

import com.fox2code.foxcompat.app.FoxActivity;
import com.fox2code.foxcompat.view.FoxDisplay;
import com.fox2code.mmm.background.BackgroundUpdateChecker;
import com.fox2code.mmm.installer.InstallerInitializer;
import com.fox2code.mmm.manager.LocalModuleInfo;
import com.fox2code.mmm.manager.ModuleManager;
import com.fox2code.mmm.module.ModuleViewAdapter;
import com.fox2code.mmm.module.ModuleViewListBuilder;
import com.fox2code.mmm.repo.RepoManager;
import com.fox2code.mmm.settings.SettingsActivity;
import com.fox2code.mmm.utils.ExternalHelper;
import com.fox2code.mmm.utils.io.net.Http;
import com.fox2code.mmm.utils.realm.ReposList;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.progressindicator.LinearProgressIndicator;
import com.google.android.material.snackbar.Snackbar;

import org.matomo.sdk.extra.TrackHelper;

import java.sql.Timestamp;
import java.util.Objects;

import io.realm.Realm;
import io.realm.RealmConfiguration;
import timber.log.Timber;

public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRefreshListener, SearchView.OnQueryTextListener, SearchView.OnCloseListener, OverScrollManager.OverScrollHelper {
    private static final int PRECISION = 10000;
    public static boolean doSetupNowRunning = true;
    public static boolean doSetupRestarting = false;
    public final ModuleViewListBuilder moduleViewListBuilder;
    public final ModuleViewListBuilder moduleViewListBuilderOnline;
    public LinearProgressIndicator progressIndicator;
    private ModuleViewAdapter moduleViewAdapter;
    private ModuleViewAdapter moduleViewAdapterOnline;
    private SwipeRefreshLayout swipeRefreshLayout;
    private int swipeRefreshLayoutOrigStartOffset;
    private int swipeRefreshLayoutOrigEndOffset;
    private long swipeRefreshBlocker = 0;
    private int overScrollInsetTop;
    private int overScrollInsetBottom;
    private RecyclerView moduleList;
    private RecyclerView moduleListOnline;
    private CardView searchCard;
    private SearchView searchView;
    private boolean initMode;

    public MainActivity() {
        this.moduleViewListBuilder = new ModuleViewListBuilder(this);
        this.moduleViewListBuilderOnline = new ModuleViewListBuilder(this);
        this.moduleViewListBuilder.addNotification(NotificationType.INSTALL_FROM_STORAGE);
    }

    @Override
    protected void onResume() {
        BackgroundUpdateChecker.onMainActivityResume(this);
        super.onResume();
    }

    @SuppressLint("RestrictedApi")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        this.initMode = true;
        if (doSetupRestarting) {
            doSetupRestarting = false;
        }
        BackgroundUpdateChecker.onMainActivityCreate(this);
        super.onCreate(savedInstanceState);
        TrackHelper.track().screen(this).with(MainApplication.getINSTANCE().getTracker());
        // track enabled repos
        RealmConfiguration realmConfig = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).build();
        Realm realm = Realm.getInstance(realmConfig);
        StringBuilder enabledRepos = new StringBuilder();
        realm.executeTransaction(r -> {
            for (ReposList r2 : r.where(ReposList.class).equalTo("enabled", true).findAll()) {
                enabledRepos.append(r2.getUrl()).append(":").append(r2.getName()).append(",");
            }
        });
        if (enabledRepos.length() > 0) {
            enabledRepos.setLength(enabledRepos.length() - 1);
        }
        TrackHelper.track().event("enabled_repos", enabledRepos.toString()).with(MainApplication.getINSTANCE().getTracker());
        realm.close();
        // hide this behind a buildconfig flag for now, but crash the app if it's not an official build and not debug
        if (BuildConfig.ENABLE_PROTECTION && !Iof && !BuildConfig.DEBUG) {
            throw new RuntimeException("This is not an official build of FoxMMM");
        } else if (!Iof && !BuildConfig.DEBUG) {
            Timber.w("You may be running an untrusted build.");
            // Show a toast to warn the user
            Toast.makeText(this, R.string.not_official_build, Toast.LENGTH_LONG).show();
        }
        Timestamp ts = new Timestamp(System.currentTimeMillis() - (30L * 24 * 60 * 60 * 1000));
        // check if this build has expired
        Timestamp buildTime = new Timestamp(BuildConfig.BUILD_TIME);
        // if the build time is more than 30 days ago, throw an exception
        if (ts.getTime() >= buildTime.getTime()) {
            throw new IllegalStateException("This build has expired. Please download a stable build or update to the latest version.");
        }
        setContentView(R.layout.activity_main);
        this.setTitle(R.string.app_name);
        // set window flags to ignore status bar
        this.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            WindowManager.LayoutParams layoutParams = this.getWindow().getAttributes();
            layoutParams.layoutInDisplayCutoutMode = // Support cutout in Android 9
                    WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
            this.getWindow().setAttributes(layoutParams);
        }
        this.progressIndicator = findViewById(R.id.progress_bar);
        this.swipeRefreshLayout = findViewById(R.id.swipe_refresh);
        this.swipeRefreshLayoutOrigStartOffset = this.swipeRefreshLayout.getProgressViewStartOffset();
        this.swipeRefreshLayoutOrigEndOffset = this.swipeRefreshLayout.getProgressViewEndOffset();
        this.swipeRefreshBlocker = Long.MAX_VALUE;
        this.moduleList = findViewById(R.id.module_list);
        this.moduleListOnline = findViewById(R.id.module_list_online);
        this.searchCard = findViewById(R.id.search_card);
        this.searchView = findViewById(R.id.search_bar);
        this.searchView.setIconified(true);
        this.moduleViewAdapter = new ModuleViewAdapter();
        this.moduleViewAdapterOnline = new ModuleViewAdapter();
        this.moduleList.setAdapter(this.moduleViewAdapter);
        this.moduleListOnline.setAdapter(this.moduleViewAdapterOnline);
        this.moduleList.setLayoutManager(new LinearLayoutManager(this));
        this.moduleListOnline.setLayoutManager(new LinearLayoutManager(this));
        this.moduleList.setItemViewCacheSize(4); // Default is 2
        this.swipeRefreshLayout.setOnRefreshListener(this);
        // add background blur if enabled
        this.updateBlurState();
        //hideActionBar();
        checkShowInitialSetup();
        this.moduleList.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
                if (newState != RecyclerView.SCROLL_STATE_IDLE)
                    MainActivity.this.searchView.clearFocus();
            }
        });
        this.searchCard.setRadius(this.searchCard.getHeight() / 2F);
        this.searchView.setMinimumHeight(FoxDisplay.dpToPixel(16));
        this.searchView.setImeOptions(EditorInfo.IME_ACTION_SEARCH | EditorInfo.IME_FLAG_NO_FULLSCREEN);
        this.searchView.setOnQueryTextListener(this);
        this.searchView.setOnCloseListener(this);
        this.searchView.setOnQueryTextFocusChangeListener((v, h) -> {
            if (!h) {
                String query = this.searchView.getQuery().toString();
                if (query.isEmpty()) {
                    this.searchView.setIconified(true);
                }
            }
            this.cardIconifyUpdate();
        });
        this.searchView.setEnabled(false); // Enabled later
        this.cardIconifyUpdate();
        this.updateScreenInsets(this.getResources().getConfiguration());

        // on the bottom nav, there's a settings item. open the settings activity when it's clicked.
        BottomNavigationView bottomNavigationView = findViewById(R.id.bottom_navigation);
        bottomNavigationView.setOnItemSelectedListener(item -> {
            if (item.getItemId() == R.id.settings_menu_item) {
                TrackHelper.track().event("view_list", "settings").with(MainApplication.getINSTANCE().getTracker());
                startActivity(new Intent(MainActivity.this, SettingsActivity.class));
                overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
                finish();
            } else if (item.getItemId() == R.id.online_menu_item) {
                TrackHelper.track().event("view_list", "online_modules").with(MainApplication.getINSTANCE().getTracker());
                // set module_list_online as visible and module_list as gone. fade in/out
                this.moduleListOnline.setAlpha(0F);
                this.moduleListOnline.setVisibility(View.VISIBLE);
                this.moduleListOnline.animate().alpha(1F).setDuration(300).setListener(null);
                this.moduleList.animate().alpha(0F).setDuration(300).setListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        MainActivity.this.moduleList.setVisibility(View.GONE);
                    }
                });
                // clear search view
                this.searchView.setQuery("", false);
                this.searchView.clearFocus();
            } else if (item.getItemId() == R.id.installed_menu_item) {
                TrackHelper.track().event("view_list", "installed_modules").with(MainApplication.getINSTANCE().getTracker());
                // set module_list_online as gone and module_list as visible. fade in/out
                this.moduleList.setAlpha(0F);
                this.moduleList.setVisibility(View.VISIBLE);
                this.moduleList.animate().alpha(1F).setDuration(300).setListener(null);
                this.moduleListOnline.animate().alpha(0F).setDuration(300).setListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        MainActivity.this.moduleListOnline.setVisibility(View.GONE);
                    }
                });
                // set search view to cleared
                this.searchView.setQuery("", false);
                this.searchView.clearFocus();
            }
            return true;
        });
        // update the padding of blur_frame to match the new bottom nav height
        View blurFrame = findViewById(R.id.blur_frame);
        blurFrame.post(() -> blurFrame.setPadding(blurFrame.getPaddingLeft(), blurFrame.getPaddingTop(), blurFrame.getPaddingRight(), bottomNavigationView.getHeight()));
        // for some reason, root_container has a margin at the top. remove it.
        View rootContainer = findViewById(R.id.root_container);
        rootContainer.post(() -> {
            ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) rootContainer.getLayoutParams();
            params.topMargin = 0;
            rootContainer.setLayoutParams(params);
            rootContainer.setY(0F);
        });
        // reset update module and update module count in main application
        MainApplication.getINSTANCE().resetUpdateModule();
        InstallerInitializer.tryGetMagiskPathAsync(new InstallerInitializer.Callback() {
            @Override
            public void onPathReceived(String path) {
                Timber.i("Got magisk path: %s", path);
                if (InstallerInitializer.peekMagiskVersion() < Constants.MAGISK_VER_CODE_INSTALL_COMMAND)
                    moduleViewListBuilder.addNotification(NotificationType.MAGISK_OUTDATED);
                if (!MainApplication.isShowcaseMode())
                    moduleViewListBuilder.addNotification(NotificationType.INSTALL_FROM_STORAGE);
                ModuleManager.getINSTANCE().scan();
                ModuleManager.getINSTANCE().runAfterScan(moduleViewListBuilder::appendInstalledModules);
                ModuleManager.getINSTANCE().runAfterScan(moduleViewListBuilderOnline::appendRemoteModules);
                this.commonNext();
            }

            @Override
            public void onFailure(int error) {
                Timber.e("Failed to get magisk path!");
                moduleViewListBuilder.addNotification(InstallerInitializer.getErrorNotification());
                moduleViewListBuilderOnline.addNotification(InstallerInitializer.getErrorNotification());
                this.commonNext();
            }

            public void commonNext() {
                if (BuildConfig.DEBUG) {
                    Timber.d("Common next");
                    moduleViewListBuilder.addNotification(NotificationType.DEBUG);
                }
                NotificationType.NO_INTERNET.autoAdd(moduleViewListBuilderOnline);
                // hide progress bar is repo-manager says we have no internet
                if (!RepoManager.getINSTANCE().hasConnectivity()) {
                    Timber.i("No connection, hiding progress");
                    runOnUiThread(() -> {
                        progressIndicator.setVisibility(View.GONE);
                        progressIndicator.setIndeterminate(false);
                        progressIndicator.setMax(PRECISION);
                    });
                }
                updateScreenInsets(); // Fix an edge case
                if (waitInitialSetupFinished()) {
                    Timber.d("waiting...");
                    return;
                }
                swipeRefreshBlocker = System.currentTimeMillis() + 5_000L;
                if (MainApplication.isShowcaseMode())
                    moduleViewListBuilder.addNotification(NotificationType.SHOWCASE_MODE);
                if (!Http.hasWebView()) {
                    // Check Http for WebView availability
                    moduleViewListBuilder.addNotification(NotificationType.NO_WEB_VIEW);
                    // disable online tab
                    runOnUiThread(() -> {
                        bottomNavigationView.getMenu().getItem(1).setEnabled(false);
                        bottomNavigationView.setSelectedItemId(R.id.installed_menu_item);
                    });
                }
                moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter);

                Timber.i("Scanning for modules!");
                if (BuildConfig.DEBUG) Timber.i("Initialize Update");
                final int max = ModuleManager.getINSTANCE().getUpdatableModuleCount();
                if (RepoManager.getINSTANCE().getCustomRepoManager() != null && RepoManager.getINSTANCE().getCustomRepoManager().needUpdate()) {
                    Timber.w("Need update on create");
                } else if (RepoManager.getINSTANCE().getCustomRepoManager() == null) {
                    Timber.w("CustomRepoManager is null");
                }
                // update compat metadata
                if (BuildConfig.DEBUG) Timber.i("Check Update Compat");
                AppUpdateManager.getAppUpdateManager().checkUpdateCompat();
                if (BuildConfig.DEBUG) Timber.i("Check Update");
                // update repos
                if (Http.hasWebView()) {
                    RepoManager.getINSTANCE().update(value -> runOnUiThread(max == 0 ? () -> progressIndicator.setProgressCompat((int) (value * PRECISION), true) : () -> progressIndicator.setProgressCompat((int) (value * PRECISION * 0.75F), true)));
                }
                // various notifications
                NotificationType.NEED_CAPTCHA_ANDROIDACY.autoAdd(moduleViewListBuilder);
                NotificationType.NEED_CAPTCHA_ANDROIDACY.autoAdd(moduleViewListBuilderOnline);
                NotificationType.DEBUG.autoAdd(moduleViewListBuilder);
                NotificationType.DEBUG.autoAdd(moduleViewListBuilderOnline);
                if (Http.hasWebView() && !NotificationType.REPO_UPDATE_FAILED.shouldRemove()) {
                    moduleViewListBuilder.addNotification(NotificationType.REPO_UPDATE_FAILED);
                } else {
                    if (!Http.hasWebView()) {
                        runOnUiThread(() -> {
                            progressIndicator.setProgressCompat(PRECISION, true);
                            progressIndicator.setVisibility(View.GONE);
                            searchView.setEnabled(false);
                            updateScreenInsets(getResources().getConfiguration());
                        });
                        return;
                    }
                    // Compatibility data still needs to be updated
                    AppUpdateManager appUpdateManager = AppUpdateManager.getAppUpdateManager();
                    if (BuildConfig.DEBUG) Timber.i("Check App Update");
                    if (BuildConfig.ENABLE_AUTO_UPDATER && appUpdateManager.checkUpdate(true))
                        moduleViewListBuilder.addNotification(NotificationType.UPDATE_AVAILABLE);
                    if (BuildConfig.DEBUG) Timber.i("Check Json Update");
                    if (max != 0) {
                        int current = 0;
                        for (LocalModuleInfo localModuleInfo : ModuleManager.getINSTANCE().getModules().values()) {
                            // if it has updateJson and FLAG_MM_REMOTE_MODULE is not set on flags, check for json update
                            // this is a dirty hack until we better store if it's a remote module
                            // the reasoning is that remote repos are considered "validated" while local modules are not
                            // for instance, a potential attacker could hijack a perfectly legitimate module and inject an updateJson with a malicious update - thereby bypassing any checks repos may have, without anyone noticing until it's too late
                            if (localModuleInfo.updateJson != null && (localModuleInfo.flags & FLAG_MM_REMOTE_MODULE) == 0) {
                                if (BuildConfig.DEBUG) Timber.i(localModuleInfo.id);
                                try {
                                    localModuleInfo.checkModuleUpdate();
                                } catch (Exception e) {
                                    Timber.e(e);
                                }
                                current++;
                                final int currentTmp = current;
                                runOnUiThread(() -> progressIndicator.setProgressCompat((int) ((1F * currentTmp / max) * PRECISION * 0.25F + (PRECISION * 0.75F)), true));
                            }
                        }
                    }
                }
                if (BuildConfig.DEBUG) Timber.i("Apply");
                RepoManager.getINSTANCE().runAfterUpdate(moduleViewListBuilderOnline::appendRemoteModules);
                moduleViewListBuilderOnline.applyTo(moduleListOnline, moduleViewAdapterOnline);
                // if moduleViewListBuilderOnline has the upgradeable notification, show a badge on the online repo nav item
                if (MainApplication.getINSTANCE().modulesHaveUpdates) {
                    Timber.i("Applying badge");
                    new Handler(Looper.getMainLooper()).post(() -> {
                        final var badge = bottomNavigationView.getOrCreateBadge(R.id.online_menu_item);
                        badge.setVisible(true);
                        badge.setNumber(MainApplication.getINSTANCE().updateModuleCount);
                        badge.applyTheme(MainApplication.getInitialApplication().getTheme());
                        Timber.i("Badge applied");
                    });
                }
                runOnUiThread(() -> {
                    progressIndicator.setProgressCompat(PRECISION, true);
                    progressIndicator.setVisibility(View.GONE);
                    searchView.setEnabled(true);
                    updateScreenInsets(getResources().getConfiguration());
                });
                Timber.i("Finished app opening state!");
            }
        }, true);
        // if system lang is not in MainApplication.supportedLocales, show a snackbar to ask user to help translate
        if (!MainApplication.supportedLocales.contains(this.getResources().getConfiguration().getLocales().get(0).getLanguage())) {
            // call showWeblateSnackbar() with language code and language name
            showWeblateSnackbar(this.getResources().getConfiguration().getLocales().get(0).getLanguage(), this.getResources().getConfiguration().getLocales().get(0).getDisplayLanguage());
        }
        ExternalHelper.INSTANCE.refreshHelper(this);
        this.initMode = false;
        // add preference listener to set isMatomoAllowed
        SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, key) -> {
            if (key.equals("pref_analytics_enabled")) {
                MainApplication.getINSTANCE().isMatomoAllowed = sharedPreferences.getBoolean(key, false);
                MainApplication.getINSTANCE().getTracker().setOptOut(MainApplication.getINSTANCE().isMatomoAllowed);
                Timber.d("Matomo is allowed change: %s", MainApplication.getINSTANCE().isMatomoAllowed);
            }
            if (MainApplication.getINSTANCE().isMatomoAllowed) {
                String value = sharedPreferences.getString(key, null);
                // then log
                if (value != null) {
                    TrackHelper.track().event("pref_changed", key + "=" + value).with(MainApplication.getINSTANCE().getTracker());
                }
            }
            Timber.d("Preference changed: %s", key);
        };
        MainApplication.getSharedPreferences("mmm").registerOnSharedPreferenceChangeListener(listener);
    }

    private void cardIconifyUpdate() {
        boolean iconified = this.searchView.isIconified();
        int backgroundAttr = iconified ? MainApplication.isMonetEnabled() ? com.google.android.material.R.attr.colorSecondaryContainer : // Monet is special...
                com.google.android.material.R.attr.colorSecondary : com.google.android.material.R.attr.colorPrimarySurface;
        Resources.Theme theme = this.searchCard.getContext().getTheme();
        TypedValue value = new TypedValue();
        theme.resolveAttribute(backgroundAttr, value, true);
        this.searchCard.setCardBackgroundColor(value.data);
        this.searchCard.setAlpha(iconified ? 0.80F : 1F);
    }

    private void updateScreenInsets() {
        this.runOnUiThread(() -> this.updateScreenInsets(this.getResources().getConfiguration()));
    }

    private void updateScreenInsets(Configuration configuration) {
        boolean landscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE;
        int bottomInset = (landscape ? 0 : this.getNavigationBarHeight());
        int statusBarHeight = getStatusBarHeight() + FoxDisplay.dpToPixel(2);
        this.swipeRefreshLayout.setProgressViewOffset(false, swipeRefreshLayoutOrigStartOffset + statusBarHeight, swipeRefreshLayoutOrigEndOffset + statusBarHeight);
        this.moduleViewListBuilder.setHeaderPx(statusBarHeight);
        this.moduleViewListBuilderOnline.setHeaderPx(statusBarHeight);
        this.moduleViewListBuilder.setFooterPx(FoxDisplay.dpToPixel(4) + bottomInset + this.searchCard.getHeight());
        this.moduleViewListBuilderOnline.setFooterPx(FoxDisplay.dpToPixel(4) + bottomInset + this.searchCard.getHeight());
        this.searchCard.setRadius(this.searchCard.getHeight() / 2F);
        this.moduleViewListBuilder.updateInsets();
        //this.actionBarBlur.invalidate();
        this.overScrollInsetTop = statusBarHeight;
        this.overScrollInsetBottom = bottomInset;
        // set root_container to have zero padding
        findViewById(R.id.root_container).setPadding(0, statusBarHeight, 0, 0);
    }

    private void updateBlurState() {
        if (MainApplication.isBlurEnabled()) {
            // set bottom navigation bar color to transparent blur
            BottomNavigationView bottomNavigationView = findViewById(R.id.bottom_navigation);
            bottomNavigationView.setBackgroundColor(Color.TRANSPARENT);
            bottomNavigationView.setAlpha(0.8F);
            // set dialogs to have transparent blur
            getWindow().addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND);
        }
    }

    @Override
    public void refreshUI() {
        super.refreshUI();
        if (this.initMode) return;
        this.initMode = true;
        Timber.i("Item Before");
        this.searchView.setQuery("", false);
        this.searchView.clearFocus();
        this.searchView.setIconified(true);
        this.cardIconifyUpdate();
        this.updateScreenInsets();
        this.updateBlurState();
        this.moduleViewListBuilder.setQuery(null);
        Timber.i("Item After");
        this.moduleViewListBuilder.refreshNotificationsUI(this.moduleViewAdapter);
        InstallerInitializer.tryGetMagiskPathAsync(new InstallerInitializer.Callback() {
            @Override
            public void onPathReceived(String path) {
                checkShowInitialSetup();
                // Wait for doSetupNow to finish
                while (doSetupNowRunning) {
                    try {
                        //noinspection BusyWait
                        Thread.sleep(100);
                    } catch (InterruptedException ignored) {
                        Thread.currentThread().interrupt();
                    }
                }
                if (InstallerInitializer.peekMagiskVersion() < Constants.MAGISK_VER_CODE_INSTALL_COMMAND)
                    moduleViewListBuilder.addNotification(NotificationType.MAGISK_OUTDATED);
                if (!MainApplication.isShowcaseMode())
                    moduleViewListBuilder.addNotification(NotificationType.INSTALL_FROM_STORAGE);
                ModuleManager.getINSTANCE().scan();
                ModuleManager.getINSTANCE().runAfterScan(moduleViewListBuilder::appendInstalledModules);
                this.commonNext();
            }

            @Override
            public void onFailure(int error) {
                Timber.e("Error: %s", error);
                moduleViewListBuilder.addNotification(InstallerInitializer.getErrorNotification());
                moduleViewListBuilderOnline.addNotification(InstallerInitializer.getErrorNotification());
                this.commonNext();
            }

            public void commonNext() {
                Timber.i("Common Before");
                if (MainApplication.isShowcaseMode())
                    moduleViewListBuilder.addNotification(NotificationType.SHOWCASE_MODE);
                NotificationType.NEED_CAPTCHA_ANDROIDACY.autoAdd(moduleViewListBuilderOnline);
                NotificationType.NO_INTERNET.autoAdd(moduleViewListBuilderOnline);
                if (AppUpdateManager.getAppUpdateManager().checkUpdate(false))
                    moduleViewListBuilder.addNotification(NotificationType.UPDATE_AVAILABLE);
                RepoManager.getINSTANCE().updateEnabledStates();
                if (RepoManager.getINSTANCE().getCustomRepoManager().needUpdate()) {
                    runOnUiThread(() -> {
                        progressIndicator.setIndeterminate(false);
                        progressIndicator.setMax(PRECISION);
                    });
                    if (BuildConfig.DEBUG) Timber.i("Check Update");
                    RepoManager.getINSTANCE().update(value -> runOnUiThread(() -> progressIndicator.setProgressCompat((int) (value * PRECISION), true)));
                    runOnUiThread(() -> {
                        progressIndicator.setProgressCompat(PRECISION, true);
                        progressIndicator.setVisibility(View.GONE);
                    });
                }
                if (BuildConfig.DEBUG) Timber.i("Apply");
                RepoManager.getINSTANCE().runAfterUpdate(moduleViewListBuilderOnline::appendRemoteModules);
                Timber.i("Common Before applyTo");
                moduleViewListBuilderOnline.applyTo(moduleListOnline, moduleViewAdapterOnline);
                Timber.i("Common After");
            }
        });
        this.initMode = false;
    }

    @Override
    protected void onWindowUpdated() {
        this.updateScreenInsets();
    }

    @Override
    public void onRefresh() {
        if (this.swipeRefreshBlocker > System.currentTimeMillis() || this.initMode || this.progressIndicator == null || this.progressIndicator.getVisibility() == View.VISIBLE || doSetupNowRunning) {
            this.swipeRefreshLayout.setRefreshing(false);
            return; // Do not double scan
        }
        if (BuildConfig.DEBUG) Timber.i("Refresh");
        this.progressIndicator.setVisibility(View.VISIBLE);
        this.progressIndicator.setProgressCompat(0, false);
        this.swipeRefreshBlocker = System.currentTimeMillis() + 5_000L;
        // this.swipeRefreshLayout.setRefreshing(true); ??
        new Thread(() -> {
            Http.cleanDnsCache(); // Allow DNS reload from network
            final int max = ModuleManager.getINSTANCE().getUpdatableModuleCount();
            RepoManager.getINSTANCE().update(value -> runOnUiThread(max == 0 ? () -> progressIndicator.setProgressCompat((int) (value * PRECISION), true) : () -> progressIndicator.setProgressCompat((int) (value * PRECISION * 0.75F), true)));
            NotificationType.NEED_CAPTCHA_ANDROIDACY.autoAdd(moduleViewListBuilder);
            if (!NotificationType.NO_INTERNET.shouldRemove()) {
                moduleViewListBuilderOnline.addNotification(NotificationType.NO_INTERNET);
            } else if (!NotificationType.REPO_UPDATE_FAILED.shouldRemove()) {
                moduleViewListBuilder.addNotification(NotificationType.REPO_UPDATE_FAILED);
            } else {
                // Compatibility data still needs to be updated
                AppUpdateManager appUpdateManager = AppUpdateManager.getAppUpdateManager();
                if (BuildConfig.DEBUG) Timber.i("Check App Update");
                if (BuildConfig.ENABLE_AUTO_UPDATER && appUpdateManager.checkUpdate(true))
                    moduleViewListBuilder.addNotification(NotificationType.UPDATE_AVAILABLE);
                if (BuildConfig.DEBUG) Timber.i("Check Json Update");
                if (max != 0) {
                    int current = 0;
                    for (LocalModuleInfo localModuleInfo : ModuleManager.getINSTANCE().getModules().values()) {
                        if (localModuleInfo.updateJson != null && (localModuleInfo.flags & FLAG_MM_REMOTE_MODULE) == 0) {
                            if (BuildConfig.DEBUG) Timber.i(localModuleInfo.id);
                            try {
                                localModuleInfo.checkModuleUpdate();
                            } catch (Exception e) {
                                Timber.e(e);
                            }
                            current++;
                            final int currentTmp = current;
                            runOnUiThread(() -> progressIndicator.setProgressCompat((int) ((1F * currentTmp / max) * PRECISION * 0.25F + (PRECISION * 0.75F)), true));
                        }
                    }
                }
            }
            if (BuildConfig.DEBUG) Timber.i("Apply");
            runOnUiThread(() -> {
                this.progressIndicator.setVisibility(View.GONE);
                this.swipeRefreshLayout.setRefreshing(false);
            });
            NotificationType.NEED_CAPTCHA_ANDROIDACY.autoAdd(moduleViewListBuilder);
            RepoManager.getINSTANCE().updateEnabledStates();
            RepoManager.getINSTANCE().runAfterUpdate(moduleViewListBuilderOnline::appendRemoteModules);
            this.moduleViewListBuilderOnline.applyTo(moduleListOnline, moduleViewAdapterOnline);
            this.moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter);
        }, "Repo update thread").start();
    }

    @Override
    public boolean onQueryTextSubmit(final String query) {
        this.searchView.clearFocus();
        if (this.initMode) return false;
        TrackHelper.track().search(query).with(MainApplication.getINSTANCE().getTracker());
        if (this.moduleViewListBuilder.setQueryChange(query)) {
            Timber.i("Query submit: %s on offline list", query);
            new Thread(() -> this.moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter), "Query update thread").start();
        }
        // same for online list
        if (this.moduleViewListBuilderOnline.setQueryChange(query)) {
            Timber.i("Query submit: %s on online list", query);
            new Thread(() -> this.moduleViewListBuilderOnline.applyTo(moduleListOnline, moduleViewAdapterOnline), "Query update thread").start();
        }
        return true;
    }

    @Override
    public boolean onQueryTextChange(String query) {
        if (this.initMode) return false;
        TrackHelper.track().search(query).with(MainApplication.getINSTANCE().getTracker());
        if (this.moduleViewListBuilder.setQueryChange(query)) {
            Timber.i("Query submit: %s on offline list", query);
            new Thread(() -> this.moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter), "Query update thread").start();
        }
        // same for online list
        if (this.moduleViewListBuilderOnline.setQueryChange(query)) {
            Timber.i("Query submit: %s on online list", query);
            new Thread(() -> this.moduleViewListBuilderOnline.applyTo(moduleListOnline, moduleViewAdapterOnline), "Query update thread").start();
        }
        return false;
    }

    @Override
    public boolean onClose() {
        if (this.initMode) return false;
        if (this.moduleViewListBuilder.setQueryChange(null)) {
            new Thread(() -> this.moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter), "Query update thread").start();
        }
        // same for online list
        if (this.moduleViewListBuilderOnline.setQueryChange(null)) {
            new Thread(() -> this.moduleViewListBuilderOnline.applyTo(moduleListOnline, moduleViewAdapterOnline), "Query update thread").start();
        }
        return false;
    }

    @Override
    public int getOverScrollInsetTop() {
        return this.overScrollInsetTop;
    }

    @Override
    public int getOverScrollInsetBottom() {
        return this.overScrollInsetBottom;
    }

    @Override
    public void onConfigurationChanged(@NonNull Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        this.updateScreenInsets();
    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        this.updateScreenInsets();
    }

    @SuppressLint("RestrictedApi")
    private void ensurePermissions() {
        if (BuildConfig.DEBUG) Timber.i("Ensure Permissions");
        // First, check if user has said don't ask again by checking if pref_dont_ask_again_notification_permission is true
        if (!PreferenceManager.getDefaultSharedPreferences(this).getBoolean("pref_dont_ask_again_notification_permission", false)) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
                if (BuildConfig.DEBUG) Timber.i("Request Notification Permission");
                if (FoxActivity.getFoxActivity(this).shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)) {
                    // Show a dialog explaining why we need this permission, which is to show
                    // notifications for updates
                    runOnUiThread(() -> {
                        if (BuildConfig.DEBUG) Timber.i("Show Notification Permission Dialog");
                        MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
                        builder.setTitle(R.string.permission_notification_title);
                        builder.setMessage(R.string.permission_notification_message);
                        // Don't ask again checkbox
                        View view = getLayoutInflater().inflate(R.layout.dialog_checkbox, null);
                        CheckBox checkBox = view.findViewById(R.id.checkbox);
                        checkBox.setText(R.string.dont_ask_again);
                        checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> PreferenceManager.getDefaultSharedPreferences(this).edit().putBoolean("pref_dont_ask_again_notification_permission", isChecked).apply());
                        builder.setView(view);
                        builder.setPositiveButton(R.string.permission_notification_grant, (dialog, which) -> {
                            // Request the permission
                            this.requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 0);
                            doSetupNowRunning = false;
                        });
                        builder.setNegativeButton(R.string.cancel, (dialog, which) -> {
                            // Set pref_background_update_check to false and dismiss dialog
                            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
                            prefs.edit().putBoolean("pref_background_update_check", false).apply();
                            dialog.dismiss();
                            doSetupNowRunning = false;
                        });
                        builder.show();
                        if (BuildConfig.DEBUG) Timber.i("Show Notification Permission Dialog Done");
                    });
                } else {
                    // Request the permission
                    if (BuildConfig.DEBUG) Timber.i("Request Notification Permission");
                    this.requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 0);
                    if (BuildConfig.DEBUG) {
                        // Log if granted via onRequestPermissionsResult
                        boolean granted = ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED;
                        Timber.i("Request Notification Permission Done. Result: %s", granted);
                    }
                    doSetupNowRunning = false;
                }
                // Next branch is for < android 13 and user has blocked notifications
            } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU && !NotificationManagerCompat.from(this).areNotificationsEnabled()) {
                runOnUiThread(() -> {
                    MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
                    builder.setTitle(R.string.permission_notification_title);
                    builder.setMessage(R.string.permission_notification_message);
                    // Don't ask again checkbox
                    View view = getLayoutInflater().inflate(R.layout.dialog_checkbox, null);
                    CheckBox checkBox = view.findViewById(R.id.checkbox);
                    checkBox.setText(R.string.dont_ask_again);
                    checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> PreferenceManager.getDefaultSharedPreferences(this).edit().putBoolean("pref_dont_ask_again_notification_permission", isChecked).apply());
                    builder.setView(view);
                    builder.setPositiveButton(R.string.permission_notification_grant, (dialog, which) -> {
                        // Open notification settings
                        Intent intent = new Intent();
                        intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
                        Uri uri = Uri.fromParts("package", getPackageName(), null);
                        intent.setData(uri);
                        startActivity(intent);
                        doSetupNowRunning = false;
                    });
                    builder.setNegativeButton(R.string.cancel, (dialog, which) -> {
                        // Set pref_background_update_check to false and dismiss dialog
                        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
                        prefs.edit().putBoolean("pref_background_update_check", false).apply();
                        dialog.dismiss();
                        doSetupNowRunning = false;
                    });
                    builder.show();
                });
            } else {
                doSetupNowRunning = false;
            }
        } else {
            if (BuildConfig.DEBUG)
                Timber.i("Notification Permission Already Granted or Don't Ask Again");
            doSetupNowRunning = false;
        }
    }

    // Method to show a setup box on first launch
    @SuppressLint({"InflateParams", "RestrictedApi", "UnspecifiedImmutableFlag", "ApplySharedPref"})
    private void checkShowInitialSetup() {
        if (BuildConfig.DEBUG) Timber.i("Checking if we need to run setup");
        // Check if this is the first launch using prefs and if doSetupRestarting was passed in the intent
        SharedPreferences prefs = MainApplication.getSharedPreferences("mmm");
        boolean firstLaunch = !Objects.equals(prefs.getString("last_shown_setup", null), "v1");
        // First launch
        // this is intentionally separate from the above if statement, because it needs to be checked even if the first launch check is true due to some weird edge cases
        if (getIntent().getBooleanExtra("doSetupRestarting", false)) {
            // Restarting setup
            firstLaunch = false;
        }
        if (BuildConfig.DEBUG) {
            Timber.i("First launch: %s, pref value: %s", firstLaunch, prefs.getString("last_shown_setup", null));
        }
        if (firstLaunch) {
            doSetupNowRunning = true;
            // Launch setup wizard
            if (BuildConfig.DEBUG) Timber.i("Launching setup wizard");
            // Show setup activity
            Intent intent = new Intent(this, SetupActivity.class);
            finish();
            startActivity(intent);
        } else {
            ensurePermissions();
        }
    }

    /**
     * @return true if the load workflow must be stopped.
     */
    private boolean waitInitialSetupFinished() {
        if (BuildConfig.DEBUG) Timber.i("waitInitialSetupFinished");
        if (doSetupNowRunning) updateScreenInsets(); // Fix an edge case
        try {
            // Wait for doSetupNow to finish
            while (doSetupNowRunning) {
                //noinspection BusyWait
                Thread.sleep(50);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return true;
        }
        return doSetupRestarting;
    }

    /**
     * Shows a snackbar offering to take users to Weblate if their language is not available.
     *
     * @param language     The language code.
     * @param languageName The language name.
     */
    @SuppressLint("RestrictedApi")
    private void showWeblateSnackbar(String language, String languageName) {
        Snackbar snackbar = Snackbar.make(findViewById(R.id.root_container), getString(R.string.language_not_available, languageName), Snackbar.LENGTH_LONG);
        snackbar.setAction(R.string.ok, v -> {
            Intent intent = new Intent(Intent.ACTION_VIEW);
            intent.setData(Uri.parse("https://translate.nift4.org/engage/foxmmm/?language=" + language));
            startActivity(intent);
        });
        snackbar.show();
    }
}