package com.fox2code.mmm; import static com.fox2code.mmm.utils.IntentHelper.getActivity; import android.annotation.SuppressLint; import android.app.PendingIntent; import android.content.ComponentName; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Resources; import android.os.Bundle; import android.view.View; import android.view.WindowManager; import android.webkit.CookieManager; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.fragment.app.FragmentActivity; import com.fox2code.foxcompat.app.FoxActivity; import com.fox2code.mmm.databinding.ActivitySetupBinding; import com.fox2code.mmm.repo.RepoManager; import com.fox2code.mmm.utils.realm.ReposList; import com.fox2code.rosettax.LanguageActivity; import com.fox2code.rosettax.LanguageSwitcher; import com.google.android.material.bottomnavigation.BottomNavigationItemView; import com.google.android.material.button.MaterialButton; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.materialswitch.MaterialSwitch; import com.topjohnwu.superuser.internal.UiThreadHandler; import org.apache.commons.io.FileUtils; import java.io.File; import java.io.IOException; import java.util.Objects; import io.realm.Realm; import io.realm.RealmConfiguration; import timber.log.Timber; public class SetupActivity extends FoxActivity implements LanguageActivity { private int cachedTheme; private boolean realmDatabasesCreated; @SuppressLint({"ApplySharedPref", "RestrictedApi"}) @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.setTitle(R.string.setup_title); this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, 0); createFiles(); disableUpdateActivityForFdroidFlavor(); // Set theme SharedPreferences prefs = MainApplication.getSharedPreferences("mmm"); switch (prefs.getString("theme", "system")) { case "light" -> setTheme(R.style.Theme_MagiskModuleManager_Monet_Light); case "dark" -> setTheme(R.style.Theme_MagiskModuleManager_Monet_Dark); case "system" -> setTheme(R.style.Theme_MagiskModuleManager_Monet); case "black" -> setTheme(R.style.Theme_MagiskModuleManager_Monet_Black); case "transparent_light" -> setTheme(R.style.Theme_MagiskModuleManager_Transparent_Light); } ActivitySetupBinding binding = ActivitySetupBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); View view = binding.getRoot(); ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_background_update_check))).setChecked(BuildConfig.ENABLE_AUTO_UPDATER); ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_crash_reporting))).setChecked(BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING); // pref_crash_reporting_pii ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_crash_reporting_pii))).setChecked(BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING_PII); // pref_analytics_enabled ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_app_analytics))).setChecked(BuildConfig.DEFAULT_ENABLE_ANALYTICS); // assert that both switches match the build config on debug builds if (BuildConfig.DEBUG) { assert ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_background_update_check))).isChecked() == BuildConfig.ENABLE_AUTO_UPDATER; assert ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_crash_reporting))).isChecked() == BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING; } // Repos are a little harder, as the enabled_repos build config is an arraylist ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_androidacy_repo))).setChecked(BuildConfig.ENABLED_REPOS.contains("androidacy_repo")); ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_magisk_alt_repo))).setChecked(BuildConfig.ENABLED_REPOS.contains("magisk_alt_repo")); // On debug builds, log when a switch is toggled if (BuildConfig.DEBUG) { ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_background_update_check))).setOnCheckedChangeListener((buttonView, isChecked) -> Timber.i("Automatic update Check: %s", isChecked)); ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_crash_reporting))).setOnCheckedChangeListener((buttonView, isChecked) -> Timber.i("Crash Reporting: %s", isChecked)); ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_crash_reporting_pii))).setOnCheckedChangeListener((buttonView, isChecked) -> Timber.i("Crash Reporting PII: %s", isChecked)); ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_androidacy_repo))).setOnCheckedChangeListener((buttonView, isChecked) -> Timber.i("Androidacy Repo: %s", isChecked)); ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_magisk_alt_repo))).setOnCheckedChangeListener((buttonView, isChecked) -> Timber.i("Magisk Alt Repo: %s", isChecked)); } // Setup popup dialogue for the setup_theme_button MaterialButton themeButton = view.findViewById(R.id.setup_theme_button); themeButton.setOnClickListener(v -> { // Create a new dialog for the theme picker MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); builder.setTitle(R.string.setup_theme_title); // Create a new array of theme names (system, light, dark, black, transparent light) String[] themeNames = new String[]{getString(R.string.theme_system), getString(R.string.theme_light), getString(R.string.theme_dark), getString(R.string.theme_black), getString(R.string.theme_transparent_light)}; // Create a new array of theme values (system, light, dark, black, transparent_light) String[] themeValues = new String[]{"system", "light", "dark", "black", "transparent_light"}; // if pref_theme is set, check the relevant theme_* menu item, otherwise check the default (theme_system) String prefTheme = prefs.getString("pref_theme", "system"); int checkedItem = 0; switch (prefTheme) { case "system": break; case "light": checkedItem = 1; break; case "dark": checkedItem = 2; break; case "black": checkedItem = 3; break; case "transparent_light": checkedItem = 4; break; } builder.setCancelable(true); // Create the dialog builder.setSingleChoiceItems(themeNames, checkedItem, (dialog, which) -> { // Set the theme prefs.edit().putString("pref_theme", themeValues[which]).commit(); // Dismiss the dialog dialog.dismiss(); // Set the theme UiThreadHandler.handler.postDelayed(() -> { switch (prefs.getString("pref_theme", "system")) { case "light" -> setTheme(R.style.Theme_MagiskModuleManager_Monet_Light); case "dark" -> setTheme(R.style.Theme_MagiskModuleManager_Monet_Dark); case "system" -> setTheme(R.style.Theme_MagiskModuleManager_Monet); case "black" -> setTheme(R.style.Theme_MagiskModuleManager_Monet_Black); case "transparent_light" -> setTheme(R.style.Theme_MagiskModuleManager_Transparent_Light); } // restart the activity because switching to transparent pisses the rendering engine off Intent intent = new Intent(this, SetupActivity.class); finish(); // ensure intent originates from the same package intent.setPackage(getPackageName()); startActivity(intent); }, 100); }); builder.show(); }); // Setup language selector MaterialButton languageSelector = view.findViewById(R.id.setup_language_button); languageSelector.setOnClickListener(preference -> { LanguageSwitcher ls = new LanguageSwitcher(Objects.requireNonNull(getActivity(this))); ls.setSupportedStringLocales(MainApplication.supportedLocales); ls.showChangeLanguageDialog((FragmentActivity) getActivity(this)); }); // Set up the buttons // Setup button BottomNavigationItemView setupButton = view.findViewById(R.id.setup_finish); // enable finish button when user scrolls to the bottom findViewById(R.id.setupNestedScrollView).setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> { if (scrollY > oldScrollY) { setupButton.setEnabled(true); } }); setupButton.setOnClickListener(v -> { Timber.i("Setup button clicked"); // get instance of editor Timber.d("Saving preferences"); SharedPreferences.Editor editor = prefs.edit(); Timber.d("Got editor: %s", editor); // Set the Automatic update check pref editor.putBoolean("pref_background_update_check", ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_background_update_check))).isChecked()); // require wifi pref editor.putBoolean("pref_background_update_check_wifi", ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_background_update_check_require_wifi))).isChecked()); // Set the crash reporting pref editor.putBoolean("pref_crash_reporting", ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_crash_reporting))).isChecked()); // Set the crash reporting PII pref editor.putBoolean("pref_crash_reporting_pii", ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_crash_reporting_pii))).isChecked()); editor.putBoolean("pref_analytics_enabled", ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_app_analytics))).isChecked()); Timber.d("Saving preferences"); // Set the repos in the ReposList realm db RealmConfiguration realmConfig = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); boolean androidacyRepo = ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_androidacy_repo))).isChecked(); boolean magiskAltRepo = ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_magisk_alt_repo))).isChecked(); Realm realm = Realm.getInstance(realmConfig); Timber.d("Realm instance: %s", realm); if (realm.isInTransaction()) { realm.commitTransaction(); Timber.d("Committed last unfinished transaction"); } // check if instance has been closed if (realm.isClosed()) { Timber.d("Realm instance was closed, reopening"); realm = Realm.getInstance(realmConfig); } realm.executeTransactionAsync(r -> { Timber.d("Realm transaction started"); Objects.requireNonNull(r.where(ReposList.class).equalTo("id", "androidacy_repo").findFirst()).setEnabled(androidacyRepo); Objects.requireNonNull(r.where(ReposList.class).equalTo("id", "magisk_alt_repo").findFirst()).setEnabled(magiskAltRepo); Timber.d("Realm transaction committing"); // commit the changes r.commitTransaction(); r.close(); Timber.d("Realm transaction committed"); }); editor.putString("last_shown_setup", "v1"); // Commit the changes editor.commit(); // sleep to allow the realm transaction to finish try { Thread.sleep(250); } catch (InterruptedException e) { e.printStackTrace(); } // Log the changes Timber.d("Setup finished. Preferences: %s", prefs.getAll()); Timber.d("Androidacy repo: %s", androidacyRepo); Timber.d("Magisk Alt repo: %s", magiskAltRepo); // log last shown setup Timber.d("Last shown setup: %s", prefs.getString("last_shown_setup", "v0")); // Restart the activity MainActivity.doSetupRestarting = true; PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), PendingIntent.FLAG_IMMUTABLE); try { pendingIntent.send(); } catch (PendingIntent.CanceledException e) { e.printStackTrace(); } android.os.Process.killProcess(android.os.Process.myPid()); }); // Cancel button BottomNavigationItemView cancelButton = view.findViewById(R.id.cancel_setup); // unselect the cancel button because it's selected by default cancelButton.setSelected(false); cancelButton.setOnClickListener(v -> { Timber.i("Cancel button clicked"); // close the app finish(); }); } @Override public Resources.Theme getTheme() { Resources.Theme theme = super.getTheme(); // try cached value if (cachedTheme != 0) { theme.applyStyle(cachedTheme, true); return theme; } // Set the theme SharedPreferences prefs = MainApplication.getSharedPreferences("mmm"); String themePref = prefs.getString("pref_theme", "system"); switch (themePref) { case "light" -> { theme.applyStyle(R.style.Theme_MagiskModuleManager_Monet_Light, true); cachedTheme = R.style.Theme_MagiskModuleManager_Monet_Light; } case "dark" -> { theme.applyStyle(R.style.Theme_MagiskModuleManager_Monet_Dark, true); cachedTheme = R.style.Theme_MagiskModuleManager_Monet_Dark; } case "system" -> { theme.applyStyle(R.style.Theme_MagiskModuleManager_Monet, true); cachedTheme = R.style.Theme_MagiskModuleManager_Monet; } case "black" -> { theme.applyStyle(R.style.Theme_MagiskModuleManager_Monet_Black, true); cachedTheme = R.style.Theme_MagiskModuleManager_Monet_Black; } case "transparent_light" -> { theme.applyStyle(R.style.Theme_MagiskModuleManager_Transparent_Light, true); cachedTheme = R.style.Theme_MagiskModuleManager_Transparent_Light; } } return theme; } @Override @SuppressLint({"InlinedApi", "RestrictedApi"}) public void refreshRosettaX() { // refresh app language runOnUiThread(() -> { // refresh activity Intent intent = new Intent(this, SetupActivity.class); finish(); startActivity(intent); }); } // creates the realm database private void createRealmDatabase() { if (realmDatabasesCreated) { Timber.d("Realm databases already created"); return; } Timber.d("Creating Realm databases"); long startTime = System.currentTimeMillis(); // create encryption key Timber.d("Creating encryption key"); byte[] key = MainApplication.getINSTANCE().getKey(); // create the realm database for ReposList // create the realm configuration RealmConfiguration config = new RealmConfiguration.Builder().name("ReposList.realm").directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).encryptionKey(key).build(); // get the instance Realm.getInstanceAsync(config, new Realm.Callback() { @Override public void onSuccess(@NonNull Realm realm) { Timber.d("Realm instance: %s", realm); realm.beginTransaction(); // create the ReposList realm database Timber.d("Creating ReposList realm database"); if (realm.where(ReposList.class).equalTo("id", "androidacy_repo").findFirst() == null) { Timber.d("Creating androidacy_repo"); // create the androidacy_repo row // cant use createObject because it crashes because reasons. use copyToRealm instead ReposList androidacy_repo = realm.createObject(ReposList.class, "androidacy_repo"); Timber.d("Created androidacy_repo object"); androidacy_repo.setName("Androidacy Repo"); Timber.d("Set androidacy_repo name"); androidacy_repo.setDonate("https://www.androidacy.com/membership-account/membership-join/?utm_source=fox-app&utm_medium=app&utm_campaign=app"); Timber.d("Set androidacy_repo donate"); androidacy_repo.setSupport("https://t.me/androidacy_discussions"); Timber.d("Set androidacy_repo support"); androidacy_repo.setSubmitModule("https://www.androidacy.com/module-repository-applications/?utm_source=fox-app&utm_medium=app&utm_campaign=app"); Timber.d("Set androidacy_repo submit module"); androidacy_repo.setUrl(RepoManager.ANDROIDACY_MAGISK_REPO_ENDPOINT); Timber.d("Set androidacy_repo url"); androidacy_repo.setEnabled(true); Timber.d("Set androidacy_repo enabled"); androidacy_repo.setLastUpdate(0); Timber.d("Set androidacy_repo last update"); androidacy_repo.setWebsite(RepoManager.ANDROIDACY_MAGISK_REPO_HOMEPAGE); Timber.d("Set androidacy_repo website"); // now copy the data from the data class to the realm object using copyToRealmOrUpdate Timber.d("Copying data to realm object"); realm.copyToRealmOrUpdate(androidacy_repo); Timber.d("Created androidacy_repo"); } // create magisk_alt_repo if (realm.where(ReposList.class).equalTo("id", "magisk_alt_repo").findFirst() == null) { Timber.d("Creating magisk_alt_repo"); ReposList magisk_alt_repo = realm.createObject(ReposList.class, "magisk_alt_repo"); Timber.d("Created magisk_alt_repo object"); magisk_alt_repo.setName("Magisk Alt Repo"); magisk_alt_repo.setDonate(null); magisk_alt_repo.setWebsite(RepoManager.MAGISK_ALT_REPO_HOMEPAGE); magisk_alt_repo.setSupport(null); magisk_alt_repo.setEnabled(true); magisk_alt_repo.setUrl(RepoManager.MAGISK_ALT_REPO); magisk_alt_repo.setSubmitModule(RepoManager.MAGISK_ALT_REPO_HOMEPAGE + "/submission"); magisk_alt_repo.setLastUpdate(0); // commit the changes Timber.d("Copying data to realm object"); realm.copyToRealmOrUpdate(magisk_alt_repo); Timber.d("Created magisk_alt_repo"); } realm.commitTransaction(); realm.close(); realmDatabasesCreated = true; Timber.d("Realm transaction finished"); long endTime = System.currentTimeMillis(); Timber.d("Realm databases created in %d ms", endTime - startTime); } }); } public void createFiles() { // use cookiemanager to create the cookie database try { CookieManager.getInstance(); } catch (Exception e) { Timber.e(e); // show a toast runOnUiThread(() -> Toast.makeText(this, R.string.error_creating_cookie_database, Toast.LENGTH_LONG).show()); } // we literally only use these to create the http cache folders try { FileUtils.forceMkdir(new File(MainApplication.getINSTANCE().getDataDir() + "/cache/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")); FileUtils.forceMkdir(new File(MainApplication.getINSTANCE().getDataDir() + "/repos/magisk_alt_repo")); } catch (IOException e) { Timber.e(e); } createRealmDatabase(); } @SuppressWarnings("ConstantConditions") public void disableUpdateActivityForFdroidFlavor() { if (BuildConfig.FLAVOR.equals("fdroid")) { // check if the update activity is enabled PackageManager pm = getPackageManager(); ComponentName componentName = new ComponentName(this, UpdateActivity.class); int componentEnabledSetting = pm.getComponentEnabledSetting(componentName); if (componentEnabledSetting == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { Timber.d("Disabling update activity for fdroid flavor"); // disable update activity through package manager pm.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); } } } }