package com.fox2code.mmm; import static com.fox2code.mmm.MainApplication.o; import static com.fox2code.mmm.manager.ModuleInfo.FLAG_MM_REMOTE_MODULE; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Color; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.view.animation.AccelerateInterpolator; import android.view.animation.DecelerateInterpolator; import android.view.inputmethod.EditorInfo; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.appcompat.widget.SearchView; import androidx.cardview.widget.CardView; 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.androidacy.AndroidacyRepoData; 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.repo.RepoModule; import com.fox2code.mmm.settings.SettingsActivity; import com.fox2code.mmm.utils.ExternalHelper; import com.fox2code.mmm.utils.RuntimeUtils; 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.progressindicator.LinearProgressIndicator; import org.matomo.sdk.extra.TrackHelper; import java.sql.Timestamp; import java.util.ArrayList; import java.util.List; 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 static List localModuleInfoList = new ArrayList<>(); public static List onlineModuleInfoList = new ArrayList<>(); 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; private RuntimeUtils runtimeUtils; public static Boolean isShowingWeblateSb = false; // race condition 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 && !o && !BuildConfig.DEBUG) { throw new RuntimeException("This is not an official build of AMM"); } else if (!o && !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); this.runtimeUtils = new RuntimeUtils(); // add background blur if enabled this.updateBlurState(); //hideActionBar(); runtimeUtils.checkShowInitialSetup(this, this); 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(); // hide search view when scrolling if (newState == RecyclerView.SCROLL_STATE_DRAGGING) { MainActivity.this.searchCard.animate().translationY(-MainActivity.this.searchCard.getHeight()).setInterpolator(new AccelerateInterpolator(2)).start(); } } @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); // if the user scrolled up, show the search bar if (dy < 0) { MainActivity.this.searchCard.animate().translationY(0).setInterpolator(new DecelerateInterpolator(2)).start(); } } }); // same for online this.moduleListOnline.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { if (newState != RecyclerView.SCROLL_STATE_IDLE) MainActivity.this.searchView.clearFocus(); // hide search view when scrolling if (newState == RecyclerView.SCROLL_STATE_DRAGGING) { MainActivity.this.searchCard.animate().translationY(-MainActivity.this.searchCard.getHeight()).setInterpolator(new AccelerateInterpolator(2)).start(); } } @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); // if the user scrolled up, show the search bar if (dy < 0) { MainActivity.this.searchCard.animate().translationY(0).setInterpolator(new DecelerateInterpolator(2)).start(); } } }); 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); Objects.requireNonNull(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 Context context = MainActivity.this; if (runtimeUtils.waitInitialSetupFinished(context, MainActivity.this)) { 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 = Objects.requireNonNull(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); moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter); moduleViewListBuilder.applyTo(moduleListOnline, moduleViewAdapterOnline); 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()); }); maybeShowUpgrade(); 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 runtimeUtils.showWeblateSnackbar(this, this, 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); } public 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); if (bottomNavigationView != null) { bottomNavigationView.setBackgroundColor(Color.TRANSPARENT); bottomNavigationView.setAlpha(0.8F); } else { Timber.w("Bottom navigation view not found"); } // 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) { Context context = MainActivity.this; MainActivity mainActivity = MainActivity.this; runtimeUtils.checkShowInitialSetup(context, mainActivity); // 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); Objects.requireNonNull(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"); moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter); 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 = Objects.requireNonNull(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(moduleViewListBuilder::appendInstalledModules); RepoManager.getINSTANCE().runAfterUpdate(moduleViewListBuilderOnline::appendRemoteModules); this.moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter); this.moduleViewListBuilderOnline.applyTo(moduleListOnline, moduleViewAdapterOnline); }, "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(); } public void maybeShowUpgrade() { if (AndroidacyRepoData.getInstance() == null || AndroidacyRepoData.getInstance().memberLevel == null) { // wait for up to 10 seconds for AndroidacyRepoData to be initialized int i = 0; while (AndroidacyRepoData.getInstance() == null && i < 10) { try { //noinspection BusyWait Thread.sleep(1000); } catch (InterruptedException e) { Timber.e(e); } i++; } if (AndroidacyRepoData.getInstance().isEnabled() && AndroidacyRepoData.getInstance().memberLevel == null) { Timber.d("Member level is null, waiting for it to be initialized"); i = 0; while (AndroidacyRepoData.getInstance().memberLevel == null && i < 20) { i++; try { //noinspection BusyWait Thread.sleep(500); } catch (InterruptedException e) { Timber.e(e); } } } // if it's still null, but it's enabled, throw an error if (AndroidacyRepoData.getInstance().isEnabled() && AndroidacyRepoData.getInstance().memberLevel == null) { Timber.e("AndroidacyRepoData is enabled, but member level is null"); } if (AndroidacyRepoData.getInstance() != null && AndroidacyRepoData.getInstance().isEnabled() && Objects.equals(AndroidacyRepoData.getInstance().memberLevel, "Guest")) { runtimeUtils.showUpgradeSnackbar(this, this); } else { if (!AndroidacyRepoData.getInstance().isEnabled()) { Timber.i("AndroidacyRepoData is disabled, not showing upgrade snackbar 1"); } else if (!Objects.equals(AndroidacyRepoData.getInstance().memberLevel, "Guest")) { Timber.i("AndroidacyRepoData is not Guest, not showing upgrade snackbar 1. Level: %s", AndroidacyRepoData.getInstance().memberLevel); } else { Timber.i("Unknown error, not showing upgrade snackbar 1"); } } } else if (AndroidacyRepoData.getInstance().isEnabled() && Objects.equals(AndroidacyRepoData.getInstance().memberLevel, "Guest")) { runtimeUtils.showUpgradeSnackbar(this, this); } else { if (!AndroidacyRepoData.getInstance().isEnabled()) { Timber.i("AndroidacyRepoData is disabled, not showing upgrade snackbar 2"); } else if (!Objects.equals(AndroidacyRepoData.getInstance().memberLevel, "Guest")) { Timber.i("AndroidacyRepoData is not Guest, not showing upgrade snackbar 2. Level: %s", AndroidacyRepoData.getInstance().memberLevel); } else { Timber.i("Unknown error, not showing upgrade snackbar 2"); } } } }