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.
FoxMagiskModuleManager/app/src/main/java/com/fox2code/mmm/SetupActivity.java

443 lines
24 KiB
Java

package com.fox2code.mmm;
import static com.fox2code.mmm.utils.IntentHelper.getActivity;
import android.annotation.SuppressLint;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.os.Bundle;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.util.Base64;
import android.view.View;
import android.view.WindowManager;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.fragment.app.FragmentActivity;
import androidx.security.crypto.EncryptedFile;
import androidx.security.crypto.MasterKey;
import com.fox2code.foxcompat.app.FoxActivity;
import com.fox2code.mmm.androidacy.AndroidacyRepoData;
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.button.MaterialButton;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.materialswitch.MaterialSwitch;
import com.topjohnwu.superuser.internal.UiThreadHandler;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.Objects;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import io.realm.Realm;
import io.realm.RealmConfiguration;
import timber.log.Timber;
public class SetupActivity extends FoxActivity implements LanguageActivity {
MasterKey mainKeyAlias;
@SuppressLint({"ApplySharedPref", "RestrictedApi"})
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setTitle(R.string.setup_title);
// set action bar
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
// back button is close button
actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_USE_LOGO | ActionBar.DISPLAY_SHOW_HOME);
actionBar.setLogo(R.drawable.ic_foreground);
// set title
actionBar.setTitle(R.string.setup_title);
actionBar.show();
}
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);
// 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_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
MaterialButton setupButton = view.findViewById(R.id.setup_continue);
setupButton.setOnClickListener(v -> {
// Set first launch to false
// get instance of editor
SharedPreferences.Editor editor = prefs.edit();
editor.putString("last_shown_setup", "v1");
// Set the Automatic update check pref
editor.putBoolean("pref_background_update_check", ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_background_update_check))).isChecked());
// Set the crash reporting pref
editor.putBoolean("pref_crash_reporting", ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_crash_reporting))).isChecked());
// Set the repos in the ReposList realm db
RealmConfiguration realmConfig = new RealmConfiguration.Builder().name("ReposList.realm").directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).encryptionKey(MainApplication.getINSTANCE().getExistingKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).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);
Objects.requireNonNull(realm.where(ReposList.class).equalTo("id", "androidacy_repo").findFirst()).setEnabled(androidacyRepo);
Objects.requireNonNull(realm.where(ReposList.class).equalTo("id", "magisk_alt_repo").findFirst()).setEnabled(magiskAltRepo);
// commit the changes
realm.commitTransaction();
realm.close();
// Commit the changes
editor.commit();
// Sleep for 1 second to allow the user to see the changes
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// Log the changes if debug
if (BuildConfig.DEBUG) {
Timber.d("Automatic update check: %s", prefs.getBoolean("pref_background_update_check", false));
Timber.i("Crash reporting: %s", prefs.getBoolean("pref_crash_reporting", false));
}
// Restart the activity
MainActivity.doSetupRestarting = true;
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
finish();
});
// Cancel button
MaterialButton cancelButton = view.findViewById(R.id.setup_cancel);
cancelButton.setText(R.string.cancel);
cancelButton.setOnClickListener(v -> {
// Set first launch to false and restart the activity
prefs.edit().putString("last_shown_setup", "v1").commit();
MainActivity.doSetupRestarting = true;
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
finish();
});
}
@Override
public Resources.Theme getTheme() {
Resources.Theme theme = super.getTheme();
// Set the theme
SharedPreferences prefs = MainApplication.getSharedPreferences("mmm");
switch (prefs.getString("pref_theme", "system")) {
case "light" -> theme.applyStyle(R.style.Theme_MagiskModuleManager_Monet_Light, true);
case "dark" -> theme.applyStyle(R.style.Theme_MagiskModuleManager_Monet_Dark, true);
case "system" -> theme.applyStyle(R.style.Theme_MagiskModuleManager_Monet, true);
case "black" -> theme.applyStyle(R.style.Theme_MagiskModuleManager_Monet_Black, true);
case "transparent_light" ->
theme.applyStyle(R.style.Theme_MagiskModuleManager_Transparent_Light, true);
}
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() {
Timber.d("Creating Realm databases");
// create encryption key
Timber.d("Creating encryption key");
// generate the encryption key and store it in the prefs
byte[] encryptionKey = getNewKey();
// create the realm database for ReposList
// next, create the realm database for ReposList
RealmConfiguration config2 = new RealmConfiguration.Builder().name("ReposList.realm").allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).encryptionKey(MainApplication.getINSTANCE().getExistingKey()).encryptionKey(encryptionKey).build();
// get the instance
Realm.getInstanceAsync(config2, new Realm.Callback() {
@Override
public void onSuccess(@NonNull Realm realm1) {
// create androidacy_repo and magisk_alt_repo if they don't exist under ReposList
// each has id, name, donate, website, support, enabled, and lastUpdate and name
// create androidacy_repo
realm1.beginTransaction();
if (realm1.where(ReposList.class).equalTo("id", "androidacy_repo").findFirst() == null) {
// cant use createObject because it crashes because reasons. use copyToRealm instead
ReposList androidacy_repo = realm1.createObject(ReposList.class, "androidacy_repo");
androidacy_repo.setName("Androidacy Repo");
androidacy_repo.setDonate(AndroidacyRepoData.getInstance().getDonate());
androidacy_repo.setSupport(AndroidacyRepoData.getInstance().getSupport());
androidacy_repo.setSubmitModule(AndroidacyRepoData.getInstance().getSubmitModule());
androidacy_repo.setUrl(RepoManager.ANDROIDACY_MAGISK_REPO_ENDPOINT);
androidacy_repo.setEnabled(true);
androidacy_repo.setLastUpdate(0);
androidacy_repo.setWebsite(RepoManager.ANDROIDACY_MAGISK_REPO_HOMEPAGE);
// now copy the data from the data class to the realm object using copyToRealmOrUpdate
realm1.insertOrUpdate(androidacy_repo);
}
// create magisk_alt_repo
if (realm1.where(ReposList.class).equalTo("id", "magisk_alt_repo").findFirst() == null) {
ReposList magisk_alt_repo = realm1.createObject(ReposList.class, "magisk_alt_repo");
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
realm1.insertOrUpdate(magisk_alt_repo);
}
realm1.commitTransaction();
}
});
}
public void createFiles() {
try {
String cookieFileName = "cookies";
// initial set of cookies, only really used to create the keypair and encrypted file
String initialCookie = "is_foxmmm=true; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/; domain=production-api.androidacy.com; SameSite=None; Secure;|foxmmm_version=" + BuildConfig.VERSION_CODE + "; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/; domain=production-api.androidacy.com; SameSite=None; Secure;";
Context context = getApplicationContext();
mainKeyAlias = new MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build();
EncryptedFile encryptedFile = new EncryptedFile.Builder(context, new File(MainApplication.getINSTANCE().getFilesDir(), cookieFileName), mainKeyAlias, EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB).build();
InputStream inputStream;
try {
inputStream = encryptedFile.openFileInput();
} catch (FileNotFoundException e) {
Timber.d("Cookie file not found, creating new file");
OutputStream outputStream = encryptedFile.openFileOutput();
outputStream.write(initialCookie.getBytes());
outputStream.close();
outputStream.flush();
inputStream = encryptedFile.openFileInput();
}
byte[] buffer = new byte[1024];
int bytesRead;
StringBuilder outputString = new StringBuilder();
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputString.append(new String(buffer, 0, bytesRead));
}
inputStream.close();
if (outputString.toString().isEmpty()) {
Timber.d("Cookie file is empty, writing initial cookie");
OutputStream outputStream = encryptedFile.openFileOutput();
outputStream.write(initialCookie.getBytes());
outputStream.close();
outputStream.flush();
}
} catch (GeneralSecurityException | IOException e) {
Timber.e(e);
}
// we literally only use these to create the http cache folders
File httpCacheDir = MainApplication.getINSTANCE().getDataDirWithPath("cache/WebView/Default/HTTP Cache/Code Cache/js");
File httpCacheDir2 = MainApplication.getINSTANCE().getDataDirWithPath("cache/WebView/Default/HTTP Cache/Code Cache/wasm");
if (!httpCacheDir.exists()) {
if (httpCacheDir.mkdirs()) {
Timber.d("Created http cache dir");
}
}
if (!httpCacheDir2.exists()) {
if (httpCacheDir2.mkdirs()) {
Timber.d("Created http cache dir");
}
}
createRealmDatabase();
}
@SuppressWarnings("ConstantConditions")
public void disableUpdateActivityForFdroidFlavor() {
if (BuildConfig.FLAVOR.equals("fdroid")) {
Timber.d("Disabling update activity for fdroid flavor");
// disable update activity through package manager
PackageManager pm = getPackageManager();
ComponentName componentName = new ComponentName(this, UpdateActivity.class);
pm.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
}
}
@SuppressLint("NewApi")
public byte[] getNewKey() {
if (MainApplication.getSharedPreferences("mmm").getBoolean("keygen", false)) {
Timber.d("Key already generated, returning");
return MainApplication.getINSTANCE().getExistingKey();
}
// open a connection to the android keystore
KeyStore keyStore;
try {
keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
} catch (java.security.KeyStoreException | NoSuchAlgorithmException | CertificateException |
IOException e) {
throw new RuntimeException(e);
}
// create a securely generated random asymmetric RSA key
byte[] realmKey = new byte[Realm.ENCRYPTION_KEY_LENGTH];
new SecureRandom().nextBytes(realmKey);
// create a cipher that uses AES encryption -- we'll use this to encrypt our key
Cipher cipher;
try {
cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new RuntimeException(e);
}
// generate secret key
KeyGenerator keyGenerator;
try {
keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
} catch (NoSuchAlgorithmException | NoSuchProviderException e) {
throw new RuntimeException(e);
}
KeyGenParameterSpec keySpec = new KeyGenParameterSpec.Builder("realm_key", KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT).setBlockModes(KeyProperties.BLOCK_MODE_CBC).setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7).setUserAuthenticationRequired(false).build();
try {
keyGenerator.init(keySpec);
} catch (InvalidAlgorithmParameterException e) {
throw new RuntimeException(e);
}
keyGenerator.generateKey();
// access the generated key in the android keystore, then
// use the cipher to create an encrypted version of the key
byte[] initializationVector;
byte[] encryptedKeyForRealm;
try {
SecretKey secretKey = (SecretKey) keyStore.getKey("realm_key", null);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
encryptedKeyForRealm = cipher.doFinal(realmKey);
initializationVector = cipher.getIV();
} catch (InvalidKeyException | UnrecoverableKeyException | NoSuchAlgorithmException |
KeyStoreException | BadPaddingException | IllegalBlockSizeException e) {
throw new RuntimeException(e);
}
// keep the encrypted key in shared preferences
// to persist it across application runs
byte[] initializationVectorAndEncryptedKey = new byte[Integer.BYTES + initializationVector.length + encryptedKeyForRealm.length];
ByteBuffer buffer = ByteBuffer.wrap(initializationVectorAndEncryptedKey);
buffer.order(ByteOrder.BIG_ENDIAN);
buffer.putInt(initializationVector.length);
buffer.put(initializationVector);
buffer.put(encryptedKeyForRealm);
MainApplication.getSharedPreferences("realm_key").edit().putString("iv_and_encrypted_key", Base64.encodeToString(initializationVectorAndEncryptedKey, Base64.NO_WRAP)).apply();
MainApplication.getSharedPreferences("mmm").edit().putBoolean("keygen", true).apply();
return realmKey; // pass to a realm configuration via encryptionKey()
}
}