You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
MagiskModuleManager/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java

1631 lines
104 KiB
Java

/*
* Copyright (c) 2023 to present Androidacy and contributors. Names, logos, icons, and the Androidacy name are all trademarks of Androidacy and may not be used without license. See LICENSE for more information.
*/
package com.fox2code.mmm.settings;
import static com.fox2code.mmm.settings.SettingsActivity.RepoFragment.applyMaterial3;
import static java.lang.Integer.parseInt;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.Toast;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.FileProvider;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
import androidx.preference.EditTextPreference;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceGroup;
import androidx.preference.PreferenceManager;
import androidx.preference.SwitchPreferenceCompat;
import androidx.preference.TwoStatePreference;
import androidx.security.crypto.EncryptedSharedPreferences;
import androidx.security.crypto.MasterKey;
import com.fox2code.foxcompat.app.FoxActivity;
import com.fox2code.foxcompat.view.FoxDisplay;
import com.fox2code.foxcompat.view.FoxViewCompat;
import com.fox2code.mmm.AppUpdateManager;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.Constants;
import com.fox2code.mmm.MainActivity;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.R;
import com.fox2code.mmm.UpdateActivity;
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.ActionButtonType;
import com.fox2code.mmm.repo.CustomRepoData;
import com.fox2code.mmm.repo.CustomRepoManager;
import com.fox2code.mmm.repo.RepoData;
import com.fox2code.mmm.repo.RepoManager;
import com.fox2code.mmm.utils.ExternalHelper;
import com.fox2code.mmm.utils.IntentHelper;
import com.fox2code.mmm.utils.ProcessHelper;
import com.fox2code.mmm.utils.io.net.Http;
import com.fox2code.mmm.utils.realm.ReposList;
import com.fox2code.mmm.utils.sentry.SentryMain;
import com.fox2code.rosettax.LanguageActivity;
import com.fox2code.rosettax.LanguageSwitcher;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.navigation.NavigationBarView;
import com.google.android.material.snackbar.BaseTransientBottomBar;
import com.google.android.material.snackbar.Snackbar;
import com.google.android.material.textview.MaterialTextView;
import com.mikepenz.aboutlibraries.LibsBuilder;
import com.topjohnwu.superuser.internal.UiThreadHandler;
import org.apache.commons.io.FileUtils;
import org.matomo.sdk.extra.TrackHelper;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
import io.realm.Realm;
import io.realm.RealmConfiguration;
import io.realm.RealmResults;
import timber.log.Timber;
public class SettingsActivity extends FoxActivity implements LanguageActivity {
// Shamelessly adapted from https://github.com/DrKLO/Telegram/blob/2c71f6c92b45386f0c2b25f1442596462404bb39/TMessagesProj/src/main/java/org/telegram/messenger/SharedConfig.java#L1254
public final static int PERFORMANCE_CLASS_LOW = 0;
public final static int PERFORMANCE_CLASS_AVERAGE = 1;
public final static int PERFORMANCE_CLASS_HIGH = 2;
private static final int LANGUAGE_SUPPORT_LEVEL = 1;
private static boolean devModeStepFirstBootIgnore = MainApplication.Companion.isDeveloper();
private static int devModeStep = 0;
@SuppressLint("RestrictedApi")
private final NavigationBarView.OnItemSelectedListener onItemSelectedListener = item -> {
int itemId = item.getItemId();
if (itemId == R.id.back) {
TrackHelper.track().event("view_list", "main_modules").with(Objects.requireNonNull(MainApplication.getINSTANCE()).getTracker());
startActivity(new Intent(this, MainActivity.class));
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
finish();
return true;
} else //noinspection RedundantIfStatement
if (itemId == R.id.settings_menu_item) {
return true;
}
return false;
};
@PerformanceClass
public static int getDevicePerformanceClass() {
int devicePerformanceClass;
int androidVersion = Build.VERSION.SDK_INT;
int cpuCount = Runtime.getRuntime().availableProcessors();
int memoryClass = ((ActivityManager) Objects.requireNonNull(MainApplication.getINSTANCE()).getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass();
int totalCpuFreq = 0;
int freqResolved = 0;
for (int i = 0; i < cpuCount; i++) {
try (RandomAccessFile reader = new RandomAccessFile(String.format(Locale.ENGLISH, "/sys/devices/system/cpu/cpu%d/cpufreq/cpuinfo_max_freq", i), "r")) {
String line = reader.readLine();
if (line != null) {
totalCpuFreq += parseInt(line) / 1000;
freqResolved++;
}
} catch (Exception ignore) {
}
}
int maxCpuFreq = freqResolved == 0 ? -1 : (int) Math.ceil(totalCpuFreq / (float) freqResolved);
if (androidVersion < 21 || cpuCount <= 2 || memoryClass <= 100 || cpuCount <= 4 && maxCpuFreq != -1 && maxCpuFreq <= 1250 || cpuCount <= 4 && maxCpuFreq <= 1600 && memoryClass <= 128 && androidVersion == 21 || cpuCount <= 4 && maxCpuFreq <= 1300 && memoryClass <= 128 && androidVersion <= 24) {
devicePerformanceClass = PERFORMANCE_CLASS_LOW;
} else if (cpuCount < 8 || memoryClass <= 160 || maxCpuFreq != -1 && maxCpuFreq <= 2050 || maxCpuFreq == -1 && cpuCount == 8 && androidVersion <= 23) {
devicePerformanceClass = PERFORMANCE_CLASS_AVERAGE;
} else {
devicePerformanceClass = PERFORMANCE_CLASS_HIGH;
}
Timber.d("getDevicePerformanceClass: androidVersion=" + androidVersion + " cpuCount=" + cpuCount + " memoryClass=" + memoryClass + " maxCpuFreq=" + maxCpuFreq + " devicePerformanceClass=" + devicePerformanceClass);
return devicePerformanceClass;
}
@SuppressLint("RestrictedApi")
@Override
protected void onCreate(Bundle savedInstanceState) {
devModeStep = 0;
super.onCreate(savedInstanceState);
TrackHelper.track().screen(this).with(Objects.requireNonNull(MainApplication.getINSTANCE()).getTracker());
setContentView(R.layout.settings_activity);
setTitle(R.string.app_name);
//hideActionBar();
BottomNavigationView bottomNavigationView = findViewById(R.id.bottom_navigation);
bottomNavigationView.setOnItemSelectedListener(onItemSelectedListener);
if (savedInstanceState == null) {
SettingsFragment settingsFragment = new SettingsFragment();
getSupportFragmentManager().beginTransaction().replace(R.id.settings, settingsFragment).commit();
}
}
@Override
@SuppressLint("InlinedApi")
public void refreshRosettaX() {
ProcessHelper.restartApplicationProcess(this);
}
@Override
protected void onPause() {
BackgroundUpdateChecker.onMainActivityResume(this);
super.onPause();
}
public @interface PerformanceClass {
}
public static class SettingsFragment extends PreferenceFragmentCompat implements FoxActivity.OnBackPressedCallback {
@SuppressLint("UnspecifiedImmutableFlag")
@Override
@SuppressWarnings("ConstantConditions")
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
String name = "mmmx";
Context context = MainApplication.getINSTANCE();
MasterKey masterKey;
PreferenceManager preferenceManager = getPreferenceManager();
SharedPreferenceDataStore dataStore;
SharedPreferences.Editor editor;
try {
masterKey = new MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build();
dataStore = new SharedPreferenceDataStore(EncryptedSharedPreferences.create(context, name, masterKey, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM));
preferenceManager.setPreferenceDataStore(dataStore);
preferenceManager.setSharedPreferencesName("mmm");
editor = dataStore.getSharedPreferences().edit();
} catch (Exception e) {
Timber.e(e, "Failed to create encrypted shared preferences");
throw new RuntimeException(getString(R.string.error_encrypted_shared_preferences));
}
assert preferenceManager != null;
setPreferencesFromResource(R.xml.root_preferences, rootKey);
applyMaterial3(getPreferenceScreen());
// track all non empty values
SharedPreferences sharedPreferences = dataStore.getSharedPreferences();
// disabled until EncryptedSharedPreferences fixes getAll()
// add bottom navigation bar to the settings
BottomNavigationView bottomNavigationView = requireActivity().findViewById(R.id.bottom_navigation);
if (bottomNavigationView != null) {
bottomNavigationView.setVisibility(View.VISIBLE);
bottomNavigationView.getMenu().findItem(R.id.settings_menu_item).setChecked(true);
}
findPreference("pref_manage_repos").setOnPreferenceClickListener(p -> {
devModeStep = 0;
openFragment(new RepoFragment(), R.string.manage_repos_pref);
return true;
});
ListPreference themePreference = findPreference("pref_theme");
// If transparent theme(s) are set, disable monet
if (themePreference.getValue().equals("transparent_light")) {
Timber.d("disabling monet");
findPreference("pref_enable_monet").setEnabled(false);
// Toggle monet off
((TwoStatePreference) findPreference("pref_enable_monet")).setChecked(false);
editor.putBoolean("pref_enable_monet", false).apply();
// Set summary
findPreference("pref_enable_monet").setSummary(R.string.monet_disabled_summary);
// Same for blur
findPreference("pref_enable_blur").setEnabled(false);
((TwoStatePreference) findPreference("pref_enable_blur")).setChecked(false);
editor.putBoolean("pref_enable_blur", false).apply();
findPreference("pref_enable_blur").setSummary(R.string.blur_disabled_summary);
}
themePreference.setSummaryProvider(p -> themePreference.getEntry());
themePreference.setOnPreferenceChangeListener((preference, newValue) -> {
// You need to reboot your device at least once to be able to access dev-mode
if (devModeStepFirstBootIgnore || !MainApplication.isFirstBoot()) devModeStep = 1;
Timber.d("refreshing activity. New value: %s", newValue);
editor.putString("pref_theme", (String) newValue).apply();
// If theme contains "transparent" then disable monet
if (newValue.toString().contains("transparent")) {
Timber.d("disabling monet");
// Show a dialogue warning the user about issues with transparent themes and
// that blur/monet will be disabled
new MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.transparent_theme_dialogue_title).setMessage(R.string.transparent_theme_dialogue_message).setPositiveButton(R.string.ok, (dialog, which) -> {
// Toggle monet off
((TwoStatePreference) findPreference("pref_enable_monet")).setChecked(false);
editor.putBoolean("pref_enable_monet", false).apply();
// Set summary
findPreference("pref_enable_monet").setSummary(R.string.monet_disabled_summary);
// Same for blur
((TwoStatePreference) findPreference("pref_enable_blur")).setChecked(false);
editor.putBoolean("pref_enable_blur", false).apply();
findPreference("pref_enable_blur").setSummary(R.string.blur_disabled_summary);
// Refresh activity
devModeStep = 0;
UiThreadHandler.handler.postDelayed(() -> {
MainApplication.getINSTANCE().updateTheme();
FoxActivity.getFoxActivity(this).setThemeRecreate(MainApplication.getINSTANCE().getManagerThemeResId());
}, 1);
Intent intent = new Intent(requireContext(), SettingsActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
}).setNegativeButton(R.string.cancel, (dialog, which) -> {
// Revert to system theme
((ListPreference) findPreference("pref_theme")).setValue("system");
// Refresh activity
devModeStep = 0;
}).show();
} else {
findPreference("pref_enable_monet").setEnabled(true);
findPreference("pref_enable_monet").setSummary(null);
findPreference("pref_enable_blur").setEnabled(true);
findPreference("pref_enable_blur").setSummary(null);
devModeStep = 0;
}
UiThreadHandler.handler.postDelayed(() -> {
MainApplication.getINSTANCE().updateTheme();
FoxActivity.getFoxActivity(this).setThemeRecreate(MainApplication.getINSTANCE().getManagerThemeResId());
}, 1);
return true;
});
// Crash reporting
TwoStatePreference crashReportingPreference = findPreference("pref_crash_reporting");
if (!SentryMain.IS_SENTRY_INSTALLED) crashReportingPreference.setVisible(false);
crashReportingPreference.setChecked(MainApplication.Companion.isCrashReportingEnabled());
final Object initialValue = MainApplication.Companion.isCrashReportingEnabled();
crashReportingPreference.setOnPreferenceChangeListener((preference, newValue) -> {
devModeStepFirstBootIgnore = true;
devModeStep = 0;
if (initialValue == newValue) return true;
// Show a dialog to restart the app
MaterialAlertDialogBuilder materialAlertDialogBuilder = new MaterialAlertDialogBuilder(requireContext());
materialAlertDialogBuilder.setTitle(R.string.crash_reporting_restart_title);
materialAlertDialogBuilder.setMessage(R.string.crash_reporting_restart_message);
materialAlertDialogBuilder.setPositiveButton(R.string.restart, (dialog, which) -> {
Intent mStartActivity = new Intent(requireContext(), MainActivity.class);
mStartActivity.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
int mPendingIntentId = 123456;
// If < 23, FLAG_IMMUTABLE is not available
PendingIntent mPendingIntent;
mPendingIntent = PendingIntent.getActivity(requireContext(), mPendingIntentId, mStartActivity, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
AlarmManager mgr = (AlarmManager) requireContext().getSystemService(Context.ALARM_SERVICE);
mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, mPendingIntent);
Timber.d("Restarting app to save crash reporting preference: %s", newValue);
System.exit(0); // Exit app process
});
// Do not reverse the change if the user cancels the dialog
materialAlertDialogBuilder.setNegativeButton(R.string.no, (dialog, which) -> {
});
materialAlertDialogBuilder.show();
return true;
});
Preference enableBlur = findPreference("pref_enable_blur");
// Disable blur on low performance devices
if (getDevicePerformanceClass() < PERFORMANCE_CLASS_AVERAGE) {
// Show a warning
enableBlur.setOnPreferenceChangeListener((preference, newValue) -> {
if (newValue.equals(true)) {
new MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.low_performance_device_dialogue_title).setMessage(R.string.low_performance_device_dialogue_message).setPositiveButton(R.string.ok, (dialog, which) -> {
// Toggle blur on
((TwoStatePreference) findPreference("pref_enable_blur")).setChecked(true);
editor.putBoolean("pref_enable_blur", true).apply();
// Set summary
findPreference("pref_enable_blur").setSummary(R.string.blur_disabled_summary);
}).setNegativeButton(R.string.cancel, (dialog, which) -> {
// Revert to blur on
((TwoStatePreference) findPreference("pref_enable_blur")).setChecked(false);
editor.putBoolean("pref_enable_blur", false).apply();
// Set summary
findPreference("pref_enable_blur").setSummary(null);
}).show();
}
return true;
});
}
Preference disableMonet = findPreference("pref_enable_monet");
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
disableMonet.setSummary(R.string.require_android_12);
disableMonet.setEnabled(false);
}
disableMonet.setOnPreferenceClickListener(preference -> {
UiThreadHandler.handler.postDelayed(() -> {
MainApplication.getINSTANCE().updateTheme();
((FoxActivity) this.requireActivity()).setThemeRecreate(MainApplication.getINSTANCE().getManagerThemeResId());
}, 1);
return true;
});
findPreference("pref_dns_over_https").setOnPreferenceChangeListener((p, v) -> {
Http.setDoh((Boolean) v);
return true;
});
// handle restart required for showcase mode
findPreference("pref_showcase_mode").setOnPreferenceChangeListener((p, v) -> {
if (v.equals(true)) {
new MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.restart).setMessage(R.string.showcase_mode_dialogue_message).setPositiveButton(R.string.ok, (dialog, which) -> {
// Toggle showcase mode on
((TwoStatePreference) findPreference("pref_showcase_mode")).setChecked(true);
editor.putBoolean("pref_showcase_mode", true).apply();
// restart app
Intent mStartActivity = new Intent(requireContext(), MainActivity.class);
mStartActivity.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
int mPendingIntentId = 123456;
PendingIntent mPendingIntent;
mPendingIntent = PendingIntent.getActivity(requireContext(), mPendingIntentId, mStartActivity, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
AlarmManager mgr = (AlarmManager) requireContext().getSystemService(Context.ALARM_SERVICE);
mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, mPendingIntent);
Timber.d("Restarting app to save showcase mode preference: %s", v);
System.exit(0); // Exit app process
}).setNegativeButton(R.string.cancel, (dialog, which) -> {
// Revert to showcase mode on
((TwoStatePreference) findPreference("pref_showcase_mode")).setChecked(false);
editor.putBoolean("pref_showcase_mode", false).apply();
// restart app
Intent mStartActivity = new Intent(requireContext(), MainActivity.class);
mStartActivity.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
int mPendingIntentId = 123456;
PendingIntent mPendingIntent;
mPendingIntent = PendingIntent.getActivity(requireContext(), mPendingIntentId, mStartActivity, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
AlarmManager mgr = (AlarmManager) requireContext().getSystemService(Context.ALARM_SERVICE);
mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, mPendingIntent);
Timber.d("Restarting app to save showcase mode preference: %s", v);
System.exit(0); // Exit app process
}).show();
}
return true;
});
Preference languageSelector = findPreference("pref_language_selector");
languageSelector.setOnPreferenceClickListener(preference -> {
LanguageSwitcher ls = new LanguageSwitcher(getActivity());
ls.setSupportedStringLocales(MainApplication.supportedLocales);
ls.showChangeLanguageDialog(getActivity());
return true;
});
// Handle pref_language_selector_cta by taking user to https://translate.nift4.org/engage/foxmmm/
LongClickablePreference languageSelectorCta = findPreference("pref_language_selector_cta");
languageSelectorCta.setOnPreferenceClickListener(preference -> {
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://translate.nift4.org/engage/foxmmm/"));
startActivity(browserIntent);
return true;
});
// Long click to copy url
languageSelectorCta.setOnPreferenceLongClickListener(v -> {
ClipboardManager clipboard = (ClipboardManager) requireContext().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("URL", "https://translate.nift4.org/engage/foxmmm/");
clipboard.setPrimaryClip(clip);
Toast.makeText(requireContext(), R.string.link_copied, Toast.LENGTH_SHORT).show();
return true;
});
int level = this.currentLanguageLevel();
if (level != LANGUAGE_SUPPORT_LEVEL) {
Timber.e("latest is %s", LANGUAGE_SUPPORT_LEVEL);
languageSelector.setSummary(R.string.language_support_outdated);
} else {
String translatedBy = this.getString(R.string.language_translated_by);
// I don't "translate" english
if (!("Translated by Fox2Code (Put your name here)".equals(translatedBy) || "Translated by Fox2Code".equals(translatedBy))) {
languageSelector.setSummary(R.string.language_translated_by);
} else {
languageSelector.setSummary(null);
}
}
if (!MainApplication.isDeveloper()) {
findPreference("pref_disable_low_quality_module_filter").setVisible(false);
// Find pref_clear_data and set it invisible
Objects.requireNonNull((Preference) findPreference("pref_clear_data")).setVisible(false);
}
// hande clear cache
findPreference("pref_clear_cache").setOnPreferenceClickListener(preference -> {
// Clear cache
new MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.clear_cache_dialogue_title).setMessage(R.string.clear_cache_dialogue_message).setPositiveButton(R.string.yes, (dialog, which) -> {
// Clear app cache
try {
// use apache commons IO to delete the cache
FileUtils.deleteDirectory(requireContext().getCacheDir());
// create a new cache dir
FileUtils.forceMkdir(requireContext().getCacheDir());
// create cache dirs for cronet and webview
FileUtils.forceMkdir(new File(requireContext().getCacheDir(), "cronet"));
FileUtils.forceMkdir(new File(MainApplication.getINSTANCE().getDataDir() + "/cache/WebView/Default/HTTP Cache/Code Cache/wasm"));
FileUtils.forceMkdir(new File(MainApplication.getINSTANCE().getDataDir() + "/cache/WebView/Default/HTTP Cache/Code Cache/js"));
Toast.makeText(requireContext(), R.string.cache_cleared, Toast.LENGTH_SHORT).show();
} catch (Exception e) {
Timber.e(e);
Toast.makeText(requireContext(), R.string.cache_clear_failed, Toast.LENGTH_SHORT).show();
}
}).setNegativeButton(R.string.no, (dialog, which) -> {
// Do nothing
}).show();
return true;
});
if (!SentryMain.IS_SENTRY_INSTALLED || !BuildConfig.DEBUG || InstallerInitializer.peekMagiskPath() == null) {
// Hide the pref_crash option if not in debug mode - stop users from purposely crashing the app
Timber.i(InstallerInitializer.peekMagiskPath());
Objects.requireNonNull((Preference) findPreference("pref_test_crash")).setVisible(false);
} else {
if (findPreference("pref_test_crash") != null && findPreference("pref_clear_data") != null) {
findPreference("pref_test_crash").setOnPreferenceClickListener(preference -> {
// Hard crash the app
// we need a stacktrace to see if the crash is from the app or from the system
throw new RuntimeException("This is a test crash with a stupidly long description to show off the crash handler. Are we having fun yet?");
});
findPreference("pref_clear_data").setOnPreferenceClickListener(preference -> {
// Clear app data
new MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.clear_data_dialogue_title).setMessage(R.string.clear_data_dialogue_message).setPositiveButton(R.string.yes, (dialog, which) -> {
// Clear app data
MainApplication.getINSTANCE().resetApp();
}).setNegativeButton(R.string.no, (dialog, which) -> {
}).show();
return true;
});
} else {
Timber.e("Something is null: %s, %s", findPreference("pref_clear_data"), findPreference("pref_test_crash"));
}
}
if (InstallerInitializer.peekMagiskVersion() < Constants.MAGISK_VER_CODE_INSTALL_COMMAND || !MainApplication.isDeveloper()) {
findPreference("pref_use_magisk_install_command").setVisible(false);
}
Preference debugNotification = findPreference("pref_background_update_check_debug");
Preference updateCheckExcludes = findPreference("pref_background_update_check_excludes");
Preference updateCheckVersionExcludes = findPreference("pref_background_update_check_excludes_version");
debugNotification.setEnabled(MainApplication.Companion.isBackgroundUpdateCheckEnabled());
debugNotification.setVisible(MainApplication.isDeveloper() && !MainApplication.isWrapped && MainApplication.Companion.isBackgroundUpdateCheckEnabled());
debugNotification.setOnPreferenceClickListener(preference -> {
// fake updatable modules hashmap
HashMap<String, String> updateableModules = new HashMap<>();
// count of modules to fake must match the count in the random number generator
Random random = new Random();
int count;
do {
count = random.nextInt(4) + 2;
} while (count == 2);
for (int i = 0; i < count; i++) {
int fakeVersion;
do {
fakeVersion = random.nextInt(10);
} while (fakeVersion == 0);
Timber.d("Fake version: %s, count: %s", fakeVersion, i);
updateableModules.put("FakeModule " + i, "1.0." + fakeVersion);
}
BackgroundUpdateChecker.postNotification(this.requireContext(), updateableModules, count, true);
return true;
});
Preference backgroundUpdateCheck = findPreference("pref_background_update_check");
backgroundUpdateCheck.setVisible(!MainApplication.isWrapped);
// Make uncheckable if POST_NOTIFICATIONS permission is not granted
if (!MainApplication.isNotificationPermissionGranted()) {
// Instead of disabling the preference, we make it uncheckable and when the user
// clicks on it, we show a dialog explaining why the permission is needed
backgroundUpdateCheck.setOnPreferenceClickListener(preference -> {
// set the box to unchecked
((SwitchPreferenceCompat) backgroundUpdateCheck).setChecked(false);
// ensure that the preference is false
MainApplication.getSharedPreferences("mmm").edit().putBoolean("pref_background_update_check", false).apply();
new MaterialAlertDialogBuilder(this.requireContext()).setTitle(R.string.permission_notification_title).setMessage(R.string.permission_notification_message).setPositiveButton(R.string.ok, (dialog, which) -> {
// Open the app settings
Intent intent = new Intent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", this.requireContext().getPackageName(), null);
intent.setData(uri);
this.startActivity(intent);
}).setNegativeButton(R.string.cancel, (dialog, which) -> {
}).show();
return true;
});
backgroundUpdateCheck.setSummary(R.string.background_update_check_permission_required);
}
updateCheckExcludes.setVisible(MainApplication.Companion.isBackgroundUpdateCheckEnabled() && !MainApplication.isWrapped);
backgroundUpdateCheck.setOnPreferenceChangeListener((preference, newValue) -> {
boolean enabled = Boolean.parseBoolean(String.valueOf(newValue));
debugNotification.setEnabled(enabled);
debugNotification.setVisible(MainApplication.isDeveloper() && !MainApplication.isWrapped && enabled);
updateCheckExcludes.setEnabled(enabled);
updateCheckExcludes.setVisible(enabled && !MainApplication.isWrapped);
if (!enabled) {
BackgroundUpdateChecker.onMainActivityResume(this.requireContext());
}
return true;
});
// updateCheckExcludes saves to pref_background_update_check_excludes as a stringset. On clicking, it should open a dialog with a list of all installed modules
updateCheckExcludes.setOnPreferenceClickListener(preference -> {
Collection<LocalModuleInfo> localModuleInfos = ModuleManager.getInstance().getModules().values();
// make sure we have modules
boolean[] checkedItems;
if (!localModuleInfos.isEmpty()) {
String[] moduleNames = new String[localModuleInfos.size()];
checkedItems = new boolean[localModuleInfos.size()];
int i = 0;
// get the stringset pref_background_update_check_excludes
Set<String> stringSetTemp = sharedPreferences.getStringSet("pref_background_update_check_excludes", new HashSet<>());
// copy to a new set so we can modify it
Set<String> stringSet = new HashSet<>(stringSetTemp);
for (LocalModuleInfo localModuleInfo : localModuleInfos) {
moduleNames[i] = localModuleInfo.name;
// Stringset uses id, we show name
checkedItems[i] = stringSet.contains(localModuleInfo.id);
Timber.d("name: %s, checked: %s", moduleNames[i], checkedItems[i]);
i++;
}
new MaterialAlertDialogBuilder(this.requireContext()).setTitle(R.string.background_update_check_excludes).setMultiChoiceItems(moduleNames, checkedItems, (dialog, which, isChecked) -> {
// get id from name
String id;
if (localModuleInfos.stream().anyMatch(localModuleInfo -> localModuleInfo.name.equals(moduleNames[which]))) {
id = localModuleInfos.stream().filter(localModuleInfo -> localModuleInfo.name.equals(moduleNames[which])).findFirst().orElse(null).id;
} else {
id = "";
}
if (!id.isEmpty()) {
if (isChecked) {
stringSet.add(id);
} else {
stringSet.remove(id);
}
}
sharedPreferences.edit().putStringSet("pref_background_update_check_excludes", stringSet).apply();
}).setPositiveButton(R.string.ok, (dialog, which) -> {
}).show();
} else {
new MaterialAlertDialogBuilder(this.requireContext()).setTitle(R.string.background_update_check_excludes).setMessage(R.string.background_update_check_excludes_no_modules).setPositiveButton(R.string.ok, (dialog, which) -> {
}).show();
}
return true;
});
// now handle pref_background_update_check_excludes_version
updateCheckVersionExcludes.setVisible(MainApplication.Companion.isBackgroundUpdateCheckEnabled() && !MainApplication.isWrapped);
updateCheckVersionExcludes.setOnPreferenceClickListener(preference -> {
// get the stringset pref_background_update_check_excludes_version
Set<String> stringSet = sharedPreferences.getStringSet("pref_background_update_check_excludes_version", new HashSet<>());
Timber.d("stringSet: %s", stringSet);
// for every module, add it's name and a text field to the dialog. the text field should accept a comma separated list of versions
Collection<LocalModuleInfo> localModuleInfos = ModuleManager.getInstance().getModules().values();
// make sure we have modules
if (localModuleInfos.isEmpty()) {
new MaterialAlertDialogBuilder(this.requireContext()).setTitle(R.string.background_update_check_excludes).setMessage(R.string.background_update_check_excludes_no_modules).setPositiveButton(R.string.ok, (dialog, which) -> {
}).show();
} else {
LinearLayout layout = new LinearLayout(this.requireContext());
layout.setOrientation(LinearLayout.VERTICAL);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.setMargins(48, 0, 48, 0);
// add a summary
MaterialTextView textView = new MaterialTextView(this.requireContext());
textView.setLayoutParams(params);
textView.setText(R.string.background_update_check_excludes_version_summary);
for (LocalModuleInfo localModuleInfo : localModuleInfos) {
// two views: materialtextview for name, edittext for version
MaterialTextView materialTextView = new MaterialTextView(this.requireContext());
materialTextView.setLayoutParams(params);
materialTextView.setPadding(12, 8, 12, 8);
materialTextView.setTextAppearance(com.google.android.material.R.style.TextAppearance_MaterialComponents_Subtitle1);
materialTextView.setText(localModuleInfo.name);
layout.addView(materialTextView);
EditText editText = new EditText(this.requireContext());
editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
editText.setLayoutParams(params);
editText.setHint(R.string.background_update_check_excludes_version_hint);
// stringset uses id:version, we show version for name
// so we need to get id from name, then get version from stringset
String id = localModuleInfos.stream().filter(localModuleInfo1 -> localModuleInfo1.name.equals(localModuleInfo.name)).findFirst().orElse(null).id;
String version = stringSet.stream().filter(s -> s.startsWith(id)).findFirst().orElse("");
if (!version.isEmpty()) {
editText.setText(version.split(":")[1]);
}
layout.addView(editText);
}
ScrollView scrollView = new ScrollView(this.requireContext());
scrollView.addView(layout);
new MaterialAlertDialogBuilder(this.requireContext()).setTitle(R.string.background_update_check_excludes_version).setView(scrollView).setPositiveButton(R.string.ok, (dialog, which) -> {
Timber.d("ok clicked");
// for every module, get the text field and save it to the stringset
Set<String> stringSetTemp = new HashSet<>();
String prevMod = "";
for (int i = 0; i < layout.getChildCount(); i++) {
if (layout.getChildAt(i) instanceof MaterialTextView mv) {
prevMod = mv.getText().toString();
continue;
}
EditText editText = (EditText) layout.getChildAt(i);
String text = editText.getText().toString();
if (!text.isEmpty()) {
// text can only contain numbers and the characters ^ and $
// so we remove all non-numbers and non ^ and $
text = text.replaceAll("[^0-9^$]", "");
// we have to use module id even though we show name
String finalprevMod = prevMod;
stringSetTemp.add(localModuleInfos.stream().filter(localModuleInfo -> localModuleInfo.name.equals(finalprevMod)).findFirst().orElse(null).id + ":" + text);
Timber.d("text is %s for %s", text, editText.getHint().toString());
} else {
Timber.d("text is empty for %s", editText.getHint().toString());
}
}
sharedPreferences.edit().putStringSet("pref_background_update_check_excludes_version", stringSetTemp).apply();
}).setNegativeButton(R.string.cancel, (dialog, which) -> {
}).show();
}
return true;
});
final LibsBuilder libsBuilder = new LibsBuilder().withShowLoadingProgress(false).withLicenseShown(true).withAboutMinimalDesign(false);
ClipboardManager clipboard = (ClipboardManager) requireContext().getSystemService(Context.CLIPBOARD_SERVICE);
LongClickablePreference linkClickable = findPreference("pref_update");
linkClickable.setVisible(BuildConfig.ENABLE_AUTO_UPDATER && (BuildConfig.DEBUG || AppUpdateManager.Companion.getAppUpdateManager().peekHasUpdate()));
linkClickable.setOnPreferenceClickListener(p -> {
devModeStep = 0;
// open UpdateActivity with CHECK action
Intent intent = new Intent(requireContext(), UpdateActivity.class);
intent.setAction(UpdateActivity.ACTIONS.CHECK.name());
startActivity(intent);
return true;
});
linkClickable.setOnPreferenceLongClickListener(p -> {
String toastText = requireContext().getString(R.string.link_copied);
clipboard.setPrimaryClip(ClipData.newPlainText(toastText, "https://github.com/Androidacy/MagiskModuleManager/releases/latest"));
Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show();
return true;
});
// for pref_background_update_check_debug_download, do the same as pref_update except with DOWNLOAD action
Preference debugDownload = findPreference("pref_background_update_check_debug_download");
debugDownload.setVisible(MainApplication.isDeveloper() && MainApplication.Companion.isBackgroundUpdateCheckEnabled() && !MainApplication.isWrapped);
debugDownload.setOnPreferenceClickListener(p -> {
devModeStep = 0;
Intent intent = new Intent(requireContext(), UpdateActivity.class);
intent.setAction(UpdateActivity.ACTIONS.DOWNLOAD.name());
startActivity(intent);
return true;
});
if (BuildConfig.DEBUG || BuildConfig.ENABLE_AUTO_UPDATER) {
linkClickable = findPreference("pref_report_bug");
linkClickable.setOnPreferenceClickListener(p -> {
devModeStep = 0;
devModeStepFirstBootIgnore = true;
IntentHelper.Companion.openUrl(p.getContext(), "https://github.com/Androidacy/MagiskModuleManager/issues");
return true;
});
linkClickable.setOnPreferenceLongClickListener(p -> {
String toastText = requireContext().getString(R.string.link_copied);
clipboard.setPrimaryClip(ClipData.newPlainText(toastText, "https://github.com/Androidacy/MagiskModuleManager/issues"));
Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show();
return true;
});
} else {
findPreference("pref_report_bug").setVisible(false);
}
linkClickable = findPreference("pref_source_code");
// Set summary to the last commit this build was built from @ User/Repo
// Build userRepo by removing all parts of REMOTE_URL that are not the user/repo
String userRepo = BuildConfig.REMOTE_URL;
// remove .git
userRepo = userRepo.replaceAll("\\.git$", "");
Timber.d("userRepo: %s", userRepo);
// finalUserRepo is the user/repo part of REMOTE_URL
// get everything after .com/ or .org/ or .io/ or .me/ or .net/ or .xyz/ or .tk/ or .co/ minus .git
String finalUserRepo = userRepo.replaceAll("^(https?://)?(www\\.)?(github\\.com|gitlab\\.com|bitbucket\\.org|git\\.io|git\\.me|git\\.net|git\\.xyz|git\\.tk|git\\.co)/", "");
linkClickable.setSummary(String.format(getString(R.string.source_code_summary), BuildConfig.COMMIT_HASH, finalUserRepo));
Timber.d("finalUserRepo: %s", finalUserRepo);
String finalUserRepo1 = userRepo;
linkClickable.setOnPreferenceClickListener(p -> {
if (devModeStep == 2) {
devModeStep = 0;
if (MainApplication.isDeveloper() && !BuildConfig.DEBUG) {
MainApplication.getSharedPreferences("mmm").edit().putBoolean("developer", false).apply();
Toast.makeText(getContext(), // Tell the user something changed
R.string.dev_mode_disabled, Toast.LENGTH_SHORT).show();
} else {
MainApplication.getSharedPreferences("mmm").edit().putBoolean("developer", true).apply();
Toast.makeText(getContext(), // Tell the user something changed
R.string.dev_mode_enabled, Toast.LENGTH_SHORT).show();
}
ExternalHelper.INSTANCE.refreshHelper(getContext());
return true;
}
// build url from BuildConfig.REMOTE_URL and BuildConfig.COMMIT_HASH. May have to remove the .git at the end
IntentHelper.Companion.openUrl(p.getContext(), finalUserRepo1.replace(".git", "") + "/tree/" + BuildConfig.COMMIT_HASH);
return true;
});
linkClickable.setOnPreferenceLongClickListener(p -> {
String toastText = requireContext().getString(R.string.link_copied);
clipboard.setPrimaryClip(ClipData.newPlainText(toastText, BuildConfig.REMOTE_URL + "/tree/" + BuildConfig.COMMIT_HASH));
Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show();
return true;
});
// Next, the pref_androidacy_thanks should lead to the androidacy website
linkClickable = findPreference("pref_androidacy_thanks");
linkClickable.setOnPreferenceClickListener(p -> {
IntentHelper.Companion.openUrl(p.getContext(), "https://www.androidacy.com?utm_source=FoxMagiskModuleManager&utm_medium=app&utm_campaign=FoxMagiskModuleManager");
return true;
});
linkClickable.setOnPreferenceLongClickListener(p -> {
String toastText = requireContext().getString(R.string.link_copied);
clipboard.setPrimaryClip(ClipData.newPlainText(toastText, "https://www.androidacy.com?utm_source=FoxMagiskModuleManager&utm_medium=app&utm_campaign=FoxMagiskModuleManager"));
Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show();
return true;
});
// pref_fox2code_thanks should lead to https://github.com/Fox2Code
linkClickable = findPreference("pref_fox2code_thanks");
linkClickable.setOnPreferenceClickListener(p -> {
IntentHelper.Companion.openUrl(p.getContext(), "https://github.com/Fox2Code");
return true;
});
linkClickable.setOnPreferenceLongClickListener(p -> {
String toastText = requireContext().getString(R.string.link_copied);
clipboard.setPrimaryClip(ClipData.newPlainText(toastText, "https://github.com/Fox2Code"));
Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show();
return true;
});
// handle pref_save_logs which saves logs to our external storage and shares them
Preference saveLogs = findPreference("pref_save_logs");
saveLogs.setOnPreferenceClickListener(p -> {
// Save logs to external storage
File logsFile = new File(requireContext().getExternalFilesDir(null), "logs.txt");
FileOutputStream fileOutputStream = null;
try {
//noinspection ResultOfMethodCallIgnored
logsFile.createNewFile();
fileOutputStream = new FileOutputStream(logsFile);
// first, write some info about the device
fileOutputStream.write(("FoxMagiskModuleManager version: " + BuildConfig.VERSION_NAME + " (" + BuildConfig.VERSION_CODE + ")\n").getBytes());
fileOutputStream.write(("Android version: " + Build.VERSION.RELEASE + " (" + Build.VERSION.SDK_INT + ")\n").getBytes());
fileOutputStream.write(("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (" + Build.DEVICE + ")\n").getBytes());
fileOutputStream.write(("Magisk version: " + InstallerInitializer.peekMagiskVersion() + "\n").getBytes());
fileOutputStream.write(("Has internet: " + (RepoManager.getINSTANCE().hasConnectivity() ? "Yes" : "No") + "\n").getBytes());
// read our logcat but format the output to be more readable
Process process = Runtime.getRuntime().exec("logcat -d -v tag");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = bufferedReader.readLine()) != null) {
fileOutputStream.write((line + "\n").getBytes());
}
} catch (IOException e) {
e.printStackTrace();
Toast.makeText(requireContext(), R.string.error_saving_logs, Toast.LENGTH_SHORT).show();
return true;
} finally {
if (fileOutputStream != null) {
try {
fileOutputStream.close();
} catch (IOException ignored) {
}
}
}
// Share logs
Intent shareIntent = new Intent();
// create a new intent and grantUriPermission to the file provider
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
shareIntent.putExtra(Intent.EXTRA_STREAM, FileProvider.getUriForFile(requireContext(), BuildConfig.APPLICATION_ID + ".file-provider", logsFile));
shareIntent.setType("text/plain");
startActivity(Intent.createChooser(shareIntent, getString(R.string.share_logs)));
return true;
});
// pref_contributors should lead to the contributors page
linkClickable = findPreference("pref_contributors");
linkClickable.setOnPreferenceClickListener(p -> {
// Remove the .git if it exists and add /graphs/contributors
String url = BuildConfig.REMOTE_URL;
if (url.endsWith(".git")) {
url = url.substring(0, url.length() - 4);
}
url += "/graphs/contributors";
IntentHelper.Companion.openUrl(p.getContext(), url);
return true;
});
linkClickable.setOnPreferenceLongClickListener(p -> {
String toastText = requireContext().getString(R.string.link_copied);
// Remove the .git if it exists and add /graphs/contributors
String url = BuildConfig.REMOTE_URL;
if (url.endsWith(".git")) {
url = url.substring(0, url.length() - 4);
}
url += "/graphs/contributors";
clipboard.setPrimaryClip(ClipData.newPlainText(toastText, url));
Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show();
return true;
});
linkClickable = findPreference("pref_support");
linkClickable.setOnPreferenceClickListener(p -> {
devModeStep = 0;
IntentHelper.Companion.openUrl(p.getContext(), "https://t.me/Fox2Code_Chat");
return true;
});
linkClickable.setOnPreferenceLongClickListener(p -> {
String toastText = requireContext().getString(R.string.link_copied);
clipboard.setPrimaryClip(ClipData.newPlainText(toastText, "https://t.me/Fox2Code_Chat"));
Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show();
return true;
});
// pref_announcements to https://t.me/androidacy
linkClickable = findPreference("pref_announcements");
linkClickable.setOnPreferenceClickListener(p -> {
devModeStep = 0;
IntentHelper.Companion.openUrl(p.getContext(), "https://t.me/androidacy");
return true;
});
linkClickable.setOnPreferenceLongClickListener(p -> {
String toastText = requireContext().getString(R.string.link_copied);
clipboard.setPrimaryClip(ClipData.newPlainText(toastText, "https://t.me/androidacy"));
Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show();
return true;
});
findPreference("pref_show_licenses").setOnPreferenceClickListener(p -> {
devModeStep = devModeStep == 1 ? 2 : 0;
BackgroundUpdateChecker.onMainActivityResume(this.requireContext());
openFragment(libsBuilder.supportFragment(), R.string.licenses);
return true;
});
// Determine if this is an official build based on the signature
String flavor = BuildConfig.FLAVOR;
String type = BuildConfig.BUILD_TYPE;
// Set the summary of pref_pkg_info to something like default-debug v1.0 (123) (Official)
String pkgInfo = getString(R.string.pref_pkg_info_summary, flavor + "-" + type, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE, MainApplication.o ? getString(R.string.official) : getString(R.string.unofficial));
findPreference("pref_pkg_info").setSummary(pkgInfo);
// special easter egg :)
var ref = new Object() {
int versionClicks = 0;
};
findPreference("pref_pkg_info").setOnPreferenceClickListener(p -> {
ref.versionClicks++;
Timber.d("Version clicks: %d", ref.versionClicks);
// if it's been 3 clicks, toast "yer a wizard, harry" or "keep tapping to enter hogwarts"
if (ref.versionClicks == 3) {
// random choice of 1 or 2
Random rand = new Random();
int n = rand.nextInt(2) + 1;
Toast.makeText(p.getContext(), n == 1 ? R.string.yer_a_wizard_harry : R.string.keep_tapping_to_enter_hogwarts, Toast.LENGTH_SHORT).show();
}
if (ref.versionClicks == 7) {
ref.versionClicks = 0;
IntentHelper.Companion.openUrl(p.getContext(), "https://www.youtube.com/watch?v=dQw4w9WgXcQ");
}
return true;
});
LongClickablePreference pref_donate_fox = findPreference("pref_donate_fox");
if (!BuildConfig.FLAVOR.equals("play")) {
pref_donate_fox.setOnPreferenceClickListener(p -> {
// open fox
IntentHelper.Companion.openUrl(getFoxActivity(this), "https://paypal.me/fox2code");
return true;
});
// handle long click on pref_donate_fox
pref_donate_fox.setOnPreferenceLongClickListener(p -> {
// copy to clipboard
String toastText = requireContext().getString(R.string.link_copied);
clipboard.setPrimaryClip(ClipData.newPlainText(toastText, "https://paypal.me/fox2code"));
Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show();
return true;
});
} else {
pref_donate_fox.setVisible(false);
}
// now handle pref_donate_androidacy
LongClickablePreference pref_donate_androidacy = findPreference("pref_donate_androidacy");
if (!BuildConfig.FLAVOR.equals("play")) {
if (AndroidacyRepoData.getInstance().isEnabled() && Objects.equals(AndroidacyRepoData.getInstance().memberLevel, "Guest") || AndroidacyRepoData.getInstance().memberLevel == null) {
pref_donate_androidacy.setOnPreferenceClickListener(p -> {
// copy FOX2CODE promo code to clipboard and toast user that they can use it for half off any subscription
String toastText = requireContext().getString(R.string.promo_code_copied);
clipboard.setPrimaryClip(ClipData.newPlainText(toastText, "FOX2CODE"));
Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show();
// open androidacy
IntentHelper.Companion.openUrl(getFoxActivity(this), "https://www.androidacy.com/membership-join/?utm_source=foxmmm&utm_medium=app&utm_campaign=donate");
return true;
});
// handle long click on pref_donate_androidacy
pref_donate_androidacy.setOnPreferenceLongClickListener(p -> {
// copy to clipboard
String toastText = requireContext().getString(R.string.link_copied);
clipboard.setPrimaryClip(ClipData.newPlainText(toastText, "https://www.androidacy.com/membership-join/?utm_source=foxmmm&utm_medium=app&utm_campaign=donate"));
Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show();
return true;
});
} else {
// set text to "Thank you for your support!"
pref_donate_androidacy.setSummary(R.string.androidacy_thanks_up);
pref_donate_androidacy.setTitle(R.string.androidacy_thanks_up_title);
}
} else {
pref_donate_androidacy.setVisible(false);
}
}
private void openFragment(Fragment fragment, @StringRes int title) {
FoxActivity compatActivity = getFoxActivity(this);
compatActivity.setOnBackPressedCallback(this);
compatActivity.setTitle(title);
compatActivity.getSupportFragmentManager().beginTransaction().replace(R.id.settings, fragment).setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE).commit();
}
@Override
public boolean onBackPressed(FoxActivity compatActivity) {
compatActivity.setTitle(R.string.app_name);
compatActivity.getSupportFragmentManager().beginTransaction().replace(R.id.settings, this).setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE).commit();
return true;
}
private int currentLanguageLevel() {
int declaredLanguageLevel = this.getResources().getInteger(R.integer.language_support_level);
if (declaredLanguageLevel != LANGUAGE_SUPPORT_LEVEL) return declaredLanguageLevel;
if (!this.getResources().getConfiguration().getLocales().get(0).getLanguage().equals("en") && this.getResources().getString(R.string.notification_update_pref).equals("Background modules update check") && this.getResources().getString(R.string.notification_update_desc).equals("May increase battery usage")) {
return 0;
}
return LANGUAGE_SUPPORT_LEVEL;
}
}
@SuppressWarnings("ConstantConditions")
public static class RepoFragment extends PreferenceFragmentCompat {
/**
* <i>says proudly</i>: I stole it
* <p>
* namely, from <a href="https://github.com/NeoApplications/Neo-Wellbeing/blob/9fca4136263780c022f9ec6433c0b43d159166db/app/src/main/java/org/eu/droid_ng/wellbeing/prefs/SettingsActivity.java#L101">neo wellbeing</a>
*/
public static void applyMaterial3(Preference p) {
if (p instanceof PreferenceGroup pg) {
for (int i = 0; i < pg.getPreferenceCount(); i++) {
applyMaterial3(pg.getPreference(i));
}
}
if (p instanceof SwitchPreferenceCompat) {
p.setWidgetLayoutResource(R.layout.preference_material_switch);
}
}
@SuppressLint({"RestrictedApi", "UnspecifiedImmutableFlag"})
public void onCreatePreferencesAndroidacy() {
// Bind the pref_show_captcha_webview to captchaWebview('https://production-api.androidacy.com/')
// Also require dev mode
// CaptchaWebview.setVisible(false);
Preference androidacyTestMode = Objects.requireNonNull(findPreference("pref_androidacy_test_mode"));
if (!MainApplication.isDeveloper()) {
androidacyTestMode.setVisible(false);
} else {
// Show a warning if user tries to enable test mode
androidacyTestMode.setOnPreferenceChangeListener((preference, newValue) -> {
if (Boolean.parseBoolean(String.valueOf(newValue))) {
// Use MaterialAlertDialogBuilder
new MaterialAlertDialogBuilder(this.requireContext()).setTitle(R.string.warning).setCancelable(false).setMessage(R.string.androidacy_test_mode_warning).setPositiveButton(android.R.string.ok, (dialog, which) -> {
// User clicked OK button
MainApplication.getSharedPreferences("mmm").edit().putBoolean("androidacy_test_mode", true).apply();
// Check the switch
Intent mStartActivity = new Intent(requireContext(), MainActivity.class);
mStartActivity.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
int mPendingIntentId = 123456;
// If < 23, FLAG_IMMUTABLE is not available
PendingIntent mPendingIntent;
mPendingIntent = PendingIntent.getActivity(requireContext(), mPendingIntentId, mStartActivity, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
AlarmManager mgr = (AlarmManager) requireContext().getSystemService(Context.ALARM_SERVICE);
mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, mPendingIntent);
Timber.d("Restarting app to save staging endpoint preference: %s", newValue);
System.exit(0); // Exit app process
}).setNegativeButton(android.R.string.cancel, (dialog, which) -> {
// User cancelled the dialog
// Uncheck the switch
SwitchPreferenceCompat switchPreferenceCompat = (SwitchPreferenceCompat) androidacyTestMode;
switchPreferenceCompat.setChecked(false);
// There's probably a better way to do this than duplicate code but I'm too lazy to figure it out
MainApplication.getSharedPreferences("mmm").edit().putBoolean("androidacy_test_mode", false).apply();
}).show();
} else {
MainApplication.getSharedPreferences("mmm").edit().putBoolean("androidacy_test_mode", false).apply();
// Show dialog to restart app with ok button
new MaterialAlertDialogBuilder(this.requireContext()).setTitle(R.string.warning).setCancelable(false).setMessage(R.string.androidacy_test_mode_disable_warning).setNeutralButton(android.R.string.ok, (dialog, which) -> {
// User clicked OK button
Intent mStartActivity = new Intent(requireContext(), MainActivity.class);
mStartActivity.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
int mPendingIntentId = 123456;
// If < 23, FLAG_IMMUTABLE is not available
PendingIntent mPendingIntent;
mPendingIntent = PendingIntent.getActivity(requireContext(), mPendingIntentId, mStartActivity, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
AlarmManager mgr = (AlarmManager) requireContext().getSystemService(Context.ALARM_SERVICE);
mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, mPendingIntent);
Timber.d("Restarting app to save staging endpoint preference: %s", newValue);
System.exit(0); // Exit app process
}).show();
}
return true;
});
}
// Get magisk_alt_repo enabled state from realm db
RealmConfiguration realmConfig = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build();
Realm realm1 = Realm.getInstance(realmConfig);
ReposList reposList = realm1.where(ReposList.class).equalTo("id", "magisk_alt_repo").findFirst();
if (reposList != null) {
// Set the switch to the current state
SwitchPreferenceCompat magiskAltRepoEnabled = Objects.requireNonNull(findPreference("pref_magisk_alt_repo_enabled"));
magiskAltRepoEnabled.setChecked(reposList.isEnabled());
}
// add listener to magisk_alt_repo_enabled switch to update realm db
Preference magiskAltRepoEnabled = Objects.requireNonNull(findPreference("pref_magisk_alt_repo_enabled"));
magiskAltRepoEnabled.setOnPreferenceChangeListener((preference, newValue) -> {
// Update realm db
Realm realm = Realm.getInstance(realmConfig);
realm.executeTransaction(realm2 -> {
ReposList reposList1 = realm2.where(ReposList.class).equalTo("id", "magisk_alt_repo").findFirst();
if (reposList1 != null) {
reposList1.setEnabled(Boolean.parseBoolean(String.valueOf(newValue)));
} else {
Timber.e("Alt Repo not found in realm db");
}
});
return true;
});
// Disable toggling the pref_androidacy_repo_enabled on builds without an
// ANDROIDACY_CLIENT_ID or where the ANDROIDACY_CLIENT_ID is empty
SwitchPreferenceCompat androidacyRepoEnabled = Objects.requireNonNull(findPreference("pref_androidacy_repo_enabled"));
if (Objects.equals(BuildConfig.ANDROIDACY_CLIENT_ID, "")) {
androidacyRepoEnabled.setOnPreferenceClickListener(preference -> {
new MaterialAlertDialogBuilder(this.requireContext()).setTitle(R.string.androidacy_repo_disabled).setCancelable(false).setMessage(R.string.androidacy_repo_disabled_message).setPositiveButton(R.string.download_full_app, (dialog, which) -> {
// User clicked OK button. Open GitHub releases page
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://www.androidacy.com/downloads/?view=FoxMMM&utm_source=FoxMMM&utm_medium=app&utm_campaign=FoxMMM"));
startActivity(browserIntent);
}).show();
// Revert the switch to off
androidacyRepoEnabled.setChecked(false);
// Disable in realm db
RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build();
Realm realm = Realm.getInstance(realmConfiguration);
realm.executeTransaction(realm2 -> {
ReposList repoRealmResults = realm2.where(ReposList.class).equalTo("id", "androidacy_repo").findFirst();
assert repoRealmResults != null;
repoRealmResults.setEnabled(false);
realm2.insertOrUpdate(repoRealmResults);
realm2.close();
});
return false;
});
} else {
// get if androidacy repo is enabled from realm db
RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build();
Realm realm = Realm.getInstance(realmConfiguration);
ReposList repoRealmResults = realm.where(ReposList.class).equalTo("id", "androidacy_repo").findFirst();
if (repoRealmResults == null) {
throw new IllegalStateException("Androidacy repo not found in realm db");
}
boolean androidacyRepoEnabledPref = repoRealmResults.isEnabled();
// set the switch to the current state
androidacyRepoEnabled.setChecked(androidacyRepoEnabledPref);
// add a click listener to the switch
androidacyRepoEnabled.setOnPreferenceClickListener(preference -> {
boolean enabled = androidacyRepoEnabled.isChecked();
// save the new state
realm.executeTransaction(realm2 -> {
ReposList repoRealmResults1 = realm2.where(ReposList.class).equalTo("id", "androidacy_repo").findFirst();
repoRealmResults1.setEnabled(enabled);
});
return true;
});
if (androidacyRepoEnabledPref) {
// get user role from AndroidacyRepoData.userInfo
String[][] userInfo = AndroidacyRepoData.getInstance().userInfo;
if (userInfo != null) {
String userRole = userInfo[0][1];
if (Objects.nonNull(userRole) && !Objects.equals(userRole, "Guest")) {
// Disable the pref_androidacy_repo_api_donate preference
LongClickablePreference prefAndroidacyRepoApiD = Objects.requireNonNull(findPreference("pref_androidacy_repo_donate"));
prefAndroidacyRepoApiD.setEnabled(false);
prefAndroidacyRepoApiD.setSummary(R.string.upgraded_summary);
prefAndroidacyRepoApiD.setTitle(R.string.upgraded);
prefAndroidacyRepoApiD.setIcon(R.drawable.baseline_check_24);
} else if (BuildConfig.FLAVOR.equals("play")) {
// Disable the pref_androidacy_repo_api_token preference and hide the donate button
LongClickablePreference prefAndroidacyRepoApiD = Objects.requireNonNull(findPreference("pref_androidacy_repo_donate"));
prefAndroidacyRepoApiD.setEnabled(false);
prefAndroidacyRepoApiD.setVisible(false);
}
}
String[] originalApiKeyRef = new String[]{MainApplication.getSharedPreferences("androidacy").getString("pref_androidacy_api_token", "")};
// Get the dummy pref_androidacy_repo_api_token preference with id pref_androidacy_repo_api_token
// we have to use the id because the key is different
EditTextPreference prefAndroidacyRepoApiKey = Objects.requireNonNull(findPreference("pref_androidacy_repo_api_token"));
// add validation to the EditTextPreference
// string must be 64 characters long, and only allows alphanumeric characters
prefAndroidacyRepoApiKey.setTitle(R.string.api_key);
prefAndroidacyRepoApiKey.setSummary(R.string.api_key_summary);
prefAndroidacyRepoApiKey.setDialogTitle(R.string.api_key);
prefAndroidacyRepoApiKey.setDefaultValue(originalApiKeyRef[0]);
// Set the value to the current value
prefAndroidacyRepoApiKey.setText(originalApiKeyRef[0]);
prefAndroidacyRepoApiKey.setVisible(true);
prefAndroidacyRepoApiKey.setOnBindEditTextListener(editText -> {
editText.setSingleLine();
// Make the single line wrap
editText.setHorizontallyScrolling(false);
// Set the height to the maximum required to fit the text
editText.setMaxLines(Integer.MAX_VALUE);
// Make ok button say "Save"
editText.setImeOptions(EditorInfo.IME_ACTION_DONE);
});
prefAndroidacyRepoApiKey.setPositiveButtonText(R.string.save_api_key);
prefAndroidacyRepoApiKey.setOnPreferenceChangeListener((preference, newValue) -> {
// validate the api key client side first. should be 64 characters long, and only allow alphanumeric characters
if (!newValue.toString().matches("[a-zA-Z0-9]{64}")) {
// Show snack bar with error
Snackbar.make(requireView(), R.string.api_key_mismatch, BaseTransientBottomBar.LENGTH_LONG).show();
// Restore the original api key
prefAndroidacyRepoApiKey.setText(originalApiKeyRef[0]);
prefAndroidacyRepoApiKey.performClick();
return false;
}
// Make sure originalApiKeyRef is not null
if (originalApiKeyRef[0].equals(newValue)) return true;
// get original api key
String apiKey = String.valueOf(newValue);
// Show snack bar with indeterminate progress
Snackbar.make(requireView(), R.string.checking_api_key, BaseTransientBottomBar.LENGTH_INDEFINITE).setAction(R.string.cancel, v -> {
// Restore the original api key
prefAndroidacyRepoApiKey.setText(originalApiKeyRef[0]);
}).show();
// Check the API key on a background thread
new Thread(() -> {
// If key is empty, just remove it and change the text of the snack bar
if (apiKey.isEmpty()) {
MainApplication.getSharedPreferences("androidacy").edit().remove("pref_androidacy_api_token").apply();
new Handler(Looper.getMainLooper()).post(() -> {
Snackbar.make(requireView(), R.string.api_key_removed, BaseTransientBottomBar.LENGTH_SHORT).show();
// Show dialog to restart app with ok button
new MaterialAlertDialogBuilder(this.requireContext()).setTitle(R.string.restart).setCancelable(false).setMessage(R.string.api_key_restart).setNeutralButton(android.R.string.ok, (dialog, which) -> {
// User clicked OK button
Intent mStartActivity = new Intent(requireContext(), MainActivity.class);
mStartActivity.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
int mPendingIntentId = 123456;
// If < 23, FLAG_IMMUTABLE is not available
PendingIntent mPendingIntent;
mPendingIntent = PendingIntent.getActivity(requireContext(), mPendingIntentId, mStartActivity, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
AlarmManager mgr = (AlarmManager) requireContext().getSystemService(Context.ALARM_SERVICE);
mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, mPendingIntent);
Timber.d("Restarting app to save token preference: %s", newValue);
System.exit(0); // Exit app process
}).show();
});
} else {
// If key < 64 chars, it's not valid
if (apiKey.length() < 64) {
new Handler(Looper.getMainLooper()).post(() -> {
Snackbar.make(requireView(), R.string.api_key_invalid, BaseTransientBottomBar.LENGTH_SHORT).show();
// Save the original key
MainApplication.getSharedPreferences("androidacy").edit().putString("pref_androidacy_api_token", originalApiKeyRef[0]).apply();
// Re-show the dialog with an error
prefAndroidacyRepoApiKey.performClick();
// Show error
prefAndroidacyRepoApiKey.setDialogMessage(getString(R.string.api_key_invalid));
});
} else {
// If the key is the same as the original, just show a snack bar
if (apiKey.equals(originalApiKeyRef[0])) {
new Handler(Looper.getMainLooper()).post(() -> Snackbar.make(requireView(), R.string.api_key_unchanged, BaseTransientBottomBar.LENGTH_SHORT).show());
return;
}
boolean valid = false;
try {
valid = AndroidacyRepoData.getInstance().isValidToken(apiKey);
} catch (IOException ignored) {
}
// If the key is valid, save it
if (valid) {
originalApiKeyRef[0] = apiKey;
RepoManager.getINSTANCE().getAndroidacyRepoData().setToken(apiKey);
MainApplication.getSharedPreferences("androidacy").edit().putString("pref_androidacy_api_token", apiKey).apply();
// Snackbar with success and restart button
new Handler(Looper.getMainLooper()).post(() -> {
Snackbar.make(requireView(), R.string.api_key_valid, BaseTransientBottomBar.LENGTH_SHORT).show();
// Show dialog to restart app with ok button
new MaterialAlertDialogBuilder(this.requireContext()).setTitle(R.string.restart).setCancelable(false).setMessage(R.string.api_key_restart).setNeutralButton(android.R.string.ok, (dialog, which) -> {
// User clicked OK button
Intent mStartActivity = new Intent(requireContext(), MainActivity.class);
mStartActivity.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
int mPendingIntentId = 123456;
// If < 23, FLAG_IMMUTABLE is not available
PendingIntent mPendingIntent;
mPendingIntent = PendingIntent.getActivity(requireContext(), mPendingIntentId, mStartActivity, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
AlarmManager mgr = (AlarmManager) requireContext().getSystemService(Context.ALARM_SERVICE);
mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, mPendingIntent);
Timber.d("Restarting app to save token preference: %s", newValue);
System.exit(0); // Exit app process
}).show();
});
} else {
new Handler(Looper.getMainLooper()).post(() -> {
Snackbar.make(requireView(), R.string.api_key_invalid, BaseTransientBottomBar.LENGTH_SHORT).show();
// Save the original key
MainApplication.getINSTANCE().getSharedPreferences("androidacy", 0).edit().putString("pref_androidacy_api_token", originalApiKeyRef[0]).apply();
// Re-show the dialog with an error
prefAndroidacyRepoApiKey.performClick();
// Show error
prefAndroidacyRepoApiKey.setDialogMessage(getString(R.string.api_key_invalid));
});
}
}
}
}).start();
return true;
});
}
}
}
@SuppressLint("RestrictedApi")
public void updateCustomRepoList(boolean initial) {
RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build();
Realm realm = Realm.getInstance(realmConfiguration);
// get all repos that are not built-in
int CUSTOM_REPO_ENTRIES = 0;
// array of custom repos
ArrayList<String> customRepos = new ArrayList<>();
RealmResults<ReposList> customRepoDataDB = realm.where(ReposList.class).findAll();
for (ReposList repo : customRepoDataDB) {
if (!repo.getId().equals("androidacy_repo") && !repo.getId().equals("magisk_alt_repo")) {
CUSTOM_REPO_ENTRIES++;
customRepos.add(repo.getUrl());
}
}
Timber.d("%d repos: %s", CUSTOM_REPO_ENTRIES, customRepos);
final CustomRepoManager customRepoManager = RepoManager.getINSTANCE().getCustomRepoManager();
for (int i = 0; i < CUSTOM_REPO_ENTRIES; i++) {
// get the id of the repo at current index in customRepos
CustomRepoData repoData = customRepoManager.getRepo(customRepos.get(i));
assert repoData != null;
// convert repoData to a json string for logging
Timber.d("RepoData for %d is %s", i, repoData.toJSON());
setRepoData(repoData, "pref_custom_repo_" + i);
if (initial) {
Preference preference = findPreference("pref_custom_repo_" + i + "_delete");
if (preference == null) continue;
final int index = i;
preference.setOnPreferenceClickListener(preference1 -> {
if (realm.isInTransaction()) {
realm.commitTransaction();
}
realm.beginTransaction();
Objects.requireNonNull(realm.where(ReposList.class).equalTo("id", repoData.preferenceId).findFirst()).deleteFromRealm();
realm.commitTransaction();
customRepoManager.removeRepo(index);
updateCustomRepoList(false);
preference1.setVisible(false);
return true;
});
}
}
// any custom repo prefs larger than the number of custom repos need to be hidden. max is 5
// loop up until 5, and for each that's greater than the number of custom repos, hide it. we start at 0
// if custom repos is zero, just hide them all
if (CUSTOM_REPO_ENTRIES == 0) {
for (int i = 0; i < 5; i++) {
Preference preference = findPreference("pref_custom_repo_" + i);
if (preference == null) continue;
preference.setVisible(false);
}
} else {
for (int i = 0; i < 5; i++) {
Preference preference = findPreference("pref_custom_repo_" + i);
if (preference == null) continue;
if (i >= CUSTOM_REPO_ENTRIES) {
preference.setVisible(false);
}
}
}
Preference preference = findPreference("pref_custom_add_repo");
if (preference == null) return;
preference.setVisible(customRepoManager.canAddRepo() && customRepoManager.getRepoCount() < 5);
if (initial) { // Custom repo add button part.
preference = findPreference("pref_custom_add_repo_button");
if (preference == null) return;
preference.setOnPreferenceClickListener(preference1 -> {
final Context context = this.requireContext();
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context);
final EditText input = new EditText(context);
input.setHint(R.string.custom_url);
input.setHorizontallyScrolling(true);
input.setMaxLines(1);
builder.setIcon(R.drawable.ic_baseline_add_box_24);
builder.setTitle(R.string.add_repo);
// make link in message clickable
builder.setMessage(R.string.add_repo_message);
builder.setView(input);
builder.setPositiveButton("OK", (dialog, which) -> {
String text = String.valueOf(input.getText());
text = text.trim();
// string should not be empty, start with https://, and not contain any spaces. http links are not allowed.
if (text.matches("^https://.*") && !text.contains(" ") && !text.isEmpty()) {
if (customRepoManager.canAddRepo(text)) {
final CustomRepoData customRepoData = customRepoManager.addRepo(text);
new Thread("Add Custom Repo Thread") {
@Override
public void run() {
try {
customRepoData.quickPrePopulate();
UiThreadHandler.handler.post(() -> updateCustomRepoList(false));
} catch (Exception e) {
Timber.e(e);
// show new dialog
new Handler(Looper.getMainLooper()).post(() -> new MaterialAlertDialogBuilder(context).setTitle(R.string.error_adding).setMessage(e.getMessage()).setPositiveButton(android.R.string.ok, (dialog1, which1) -> {
}).show());
}
}
}.start();
} else {
Snackbar.make(requireView(), R.string.invalid_repo_url, BaseTransientBottomBar.LENGTH_LONG).show();
}
} else {
Snackbar.make(requireView(), R.string.invalid_repo_url, BaseTransientBottomBar.LENGTH_LONG).show();
}
});
builder.setNegativeButton("Cancel", (dialog, which) -> dialog.cancel());
builder.setNeutralButton("Docs", (dialog, which) -> {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/Androidacy/MagiskModuleManager/blob/master/docs/DEVELOPERS.md#custom-repo-format"));
startActivity(intent);
});
AlertDialog alertDialog = builder.show();
final Button positiveButton = alertDialog.getButton(DialogInterface.BUTTON_POSITIVE);
// validate as they type
input.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence charSequence, int start, int before, int count) {
Timber.i("checking repo url validity");
// show error if string is empty, does not start with https://, or contains spaces
if (charSequence.toString().isEmpty()) {
input.setError(getString(R.string.empty_field));
Timber.d("No input for repo");
positiveButton.setEnabled(false);
} else if (!charSequence.toString().matches("^https://.*")) {
input.setError(getString(R.string.invalid_repo_url));
Timber.d("Non https link for repo");
positiveButton.setEnabled(false);
} else if (charSequence.toString().contains(" ")) {
input.setError(getString(R.string.invalid_repo_url));
Timber.d("Repo url has space");
positiveButton.setEnabled(false);
} else if (!customRepoManager.canAddRepo(charSequence.toString())) {
input.setError(getString(R.string.repo_already_added));
Timber.d("Could not add repo for misc reason");
positiveButton.setEnabled(false);
} else {
// enable ok button
Timber.d("Repo URL is ok");
positiveButton.setEnabled(true);
}
}
@Override
public void afterTextChanged(Editable s) {
}
});
positiveButton.setEnabled(false);
int dp10 = FoxDisplay.dpToPixel(10), dp20 = FoxDisplay.dpToPixel(20);
FoxViewCompat.setMargin(input, dp20, dp10, dp20, dp10);
return true;
});
}
}
private void setRepoData(String url) {
final RepoData repoData = RepoManager.getINSTANCE().get(url);
setRepoData(repoData, "pref_" + (repoData == null ? RepoManager.internalIdOfUrl(url) : repoData.preferenceId));
}
private void setRepoData(final RepoData repoData, String preferenceName) {
if (repoData == null) return;
Timber.d("Setting preference " + preferenceName + " to " + repoData);
ClipboardManager clipboard = (ClipboardManager) requireContext().getSystemService(Context.CLIPBOARD_SERVICE);
Preference preference = findPreference(preferenceName);
if (preference == null) return;
if (!preferenceName.contains("androidacy") && !preferenceName.contains("magisk_alt_repo")) {
if (repoData != null) {
RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build();
Realm realm = Realm.getInstance(realmConfiguration);
RealmResults<ReposList> repoDataRealmResults = realm.where(ReposList.class).equalTo("id", repoData.preferenceId).findAll();
Timber.d("Setting preference " + preferenceName + " because it is not the Androidacy repo or the Magisk Alt Repo");
if (repoData.isForceHide() || repoDataRealmResults.isEmpty()) {
Timber.d("Hiding preference " + preferenceName + " because it is null or force hidden");
hideRepoData(preferenceName);
return;
} else {
Timber.d("Showing preference %s because the forceHide status is %s and the RealmResults is %s", preferenceName, repoData.isForceHide(), repoDataRealmResults.toString());
preference.setTitle(repoData.getName());
preference.setVisible(true);
// set website, support, and submitmodule as well as donate
if (repoData.getWebsite() != null) {
findPreference(preferenceName + "_website").setOnPreferenceClickListener((preference1 -> {
IntentHelper.Companion.openUrl(getFoxActivity(this), repoData.getWebsite());
return true;
}));
} else {
findPreference(preferenceName + "_website").setVisible(false);
}
if (repoData.getSupport() != null) {
findPreference(preferenceName + "_support").setOnPreferenceClickListener((preference1 -> {
IntentHelper.Companion.openUrl(getFoxActivity(this), repoData.getSupport());
return true;
}));
} else {
findPreference(preferenceName + "_support").setVisible(false);
}
if (repoData.getSubmitModule() != null) {
findPreference(preferenceName + "_submit").setOnPreferenceClickListener((preference1 -> {
IntentHelper.Companion.openUrl(getFoxActivity(this), repoData.getSubmitModule());
return true;
}));
} else {
findPreference(preferenceName + "_submit").setVisible(false);
}
if (repoData.getDonate() != null) {
findPreference(preferenceName + "_donate").setOnPreferenceClickListener((preference1 -> {
IntentHelper.Companion.openUrl(getFoxActivity(this), repoData.getDonate());
return true;
}));
} else {
findPreference(preferenceName + "_donate").setVisible(false);
}
}
realm.close();
} else {
Timber.d("Hiding preference " + preferenceName + " because it's data is null");
hideRepoData(preferenceName);
return;
}
}
preference = findPreference(preferenceName + "_enabled");
if (preference != null) {
// Handle custom repo separately
if (repoData instanceof CustomRepoData) {
preference.setTitle(R.string.custom_repo_always_on);
// Disable the preference
preference.setEnabled(false);
return;
} else {
((TwoStatePreference) preference).setChecked(repoData.isEnabled());
preference.setTitle(repoData.isEnabled() ? R.string.repo_enabled : R.string.repo_disabled);
preference.setOnPreferenceChangeListener((p, newValue) -> {
p.setTitle(((Boolean) newValue) ? R.string.repo_enabled : R.string.repo_disabled);
// Show snackbar telling the user to refresh the modules list or restart the app
Snackbar.make(requireView(), R.string.repo_enabled_changed, BaseTransientBottomBar.LENGTH_LONG).show();
return true;
});
}
}
preference = findPreference(preferenceName + "_website");
String homepage = repoData.getWebsite();
if (preference != null) {
if (!homepage.isEmpty()) {
preference.setVisible(true);
preference.setOnPreferenceClickListener(p -> {
IntentHelper.Companion.openUrl(getFoxActivity(this), homepage);
return true;
});
((LongClickablePreference) preference).setOnPreferenceLongClickListener(p -> {
String toastText = requireContext().getString(R.string.link_copied);
clipboard.setPrimaryClip(ClipData.newPlainText(toastText, homepage));
Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show();
return true;
});
} else {
preference.setVisible(false);
}
}
preference = findPreference(preferenceName + "_support");
String supportUrl = repoData.getSupport();
if (preference != null) {
if (supportUrl != null && !supportUrl.isEmpty()) {
preference.setVisible(true);
preference.setIcon(ActionButtonType.supportIconForUrl(supportUrl));
preference.setOnPreferenceClickListener(p -> {
IntentHelper.Companion.openUrl(getFoxActivity(this), supportUrl);
return true;
});
((LongClickablePreference) preference).setOnPreferenceLongClickListener(p -> {
String toastText = requireContext().getString(R.string.link_copied);
clipboard.setPrimaryClip(ClipData.newPlainText(toastText, supportUrl));
Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show();
return true;
});
} else {
preference.setVisible(false);
}
}
preference = findPreference(preferenceName + "_donate");
String donateUrl = repoData.getDonate();
if (preference != null) {
if (donateUrl != null) {
preference.setVisible(true);
preference.setIcon(ActionButtonType.donateIconForUrl(donateUrl));
preference.setOnPreferenceClickListener(p -> {
IntentHelper.Companion.openUrl(getFoxActivity(this), donateUrl);
return true;
});
((LongClickablePreference) preference).setOnPreferenceLongClickListener(p -> {
String toastText = requireContext().getString(R.string.link_copied);
clipboard.setPrimaryClip(ClipData.newPlainText(toastText, donateUrl));
Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show();
return true;
});
} else {
preference.setVisible(false);
}
}
preference = findPreference(preferenceName + "_submit");
String submissionUrl = repoData.getSubmitModule();
if (preference != null) {
if (submissionUrl != null && !submissionUrl.isEmpty()) {
preference.setVisible(true);
preference.setOnPreferenceClickListener(p -> {
IntentHelper.Companion.openUrl(getFoxActivity(this), submissionUrl);
return true;
});
((LongClickablePreference) preference).setOnPreferenceLongClickListener(p -> {
String toastText = requireContext().getString(R.string.link_copied);
clipboard.setPrimaryClip(ClipData.newPlainText(toastText, submissionUrl));
Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show();
return true;
});
} else {
preference.setVisible(false);
}
}
}
private void hideRepoData(String preferenceName) {
Preference preference = findPreference(preferenceName);
if (preference == null) return;
preference.setVisible(false);
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
getPreferenceManager().setSharedPreferencesName("mmm");
setPreferencesFromResource(R.xml.repo_preferences, rootKey);
applyMaterial3(getPreferenceScreen());
setRepoData(RepoManager.MAGISK_ALT_REPO);
setRepoData(RepoManager.ANDROIDACY_MAGISK_REPO_ENDPOINT);
updateCustomRepoList(true);
onCreatePreferencesAndroidacy();
}
}
}