huge refactoring

Signed-off-by: androidacy-user <opensource@androidacy.com>
pull/277/head
androidacy-user 1 year ago
parent 24ec7f6cc6
commit 8898d0674c

@ -35,7 +35,7 @@ android {
minSdk 24
targetSdk 33
versionCode 64
versionName "1.2.1"
versionName "2.0.0-beta.1"
signingConfig signingConfigs.release
archivesBaseName = "FoxMMM-v$versionName"
}
@ -84,6 +84,8 @@ android {
flavorDimensions "type"
productFlavors {
"default" {
// debug http requests. do not set this to true if you care about performance!!!!!
buildConfigField "boolean", "DEBUG_HTTP", "false"
// Latest commit hash as BuildConfig.COMMIT_HASH
def gitCommitHash = 'git rev-parse --short HEAD'.execute([], project.rootDir).text.trim()
buildConfigField "String", "COMMIT_HASH", "\"${gitCommitHash}\""
@ -122,7 +124,7 @@ android {
} else {
buildConfigField("java.util.List<String>",
"ENABLED_REPOS",
"java.util.Arrays.asList(\"magisk_alt_repo\", \"androidacy_repo\")",)
"java.util.Arrays.asList(\"androidacy_repo\")",)
}
}
@ -130,7 +132,8 @@ android {
fdroid {
dimension "type"
applicationIdSuffix ".fdroid"
// debug http requests. do not set this to true if you care about performance!!!!!
buildConfigField "boolean", "DEBUG_HTTP", "false"
// Latest commit hash as BuildConfig.COMMIT_HASH
def gitCommitHash = 'git rev-parse --short HEAD'.execute([], project.rootDir).text.trim()
buildConfigField "String", "COMMIT_HASH", "\"${gitCommitHash}\""
@ -160,10 +163,10 @@ android {
}
// Repo with ads or tracking feature are disabled by default for the
// F-Droid flavor.
// F-Droid flavor. at the same time, the alt repo isn't particularly trustworthy
buildConfigField("java.util.List<String>",
"ENABLED_REPOS",
"java.util.Arrays.asList(\"magisk_alt_repo\")",)
"java.util.Arrays.asList(\"\")",)
// Get the androidacy client ID from the androidacy.properties
Properties properties = new Properties()

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:enableOnBackInvokedCallback="true"
tools:ignore="QueryAllPackagesPermission"
tools:targetApi="tiramisu">
@ -24,6 +23,7 @@
<application
android:name=".MainApplication"
android:enableOnBackInvokedCallback="true"
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/full_backup_content"
@ -37,9 +37,10 @@
android:usesCleartextTraffic="false"
tools:ignore="ManifestResource"
tools:replace="android:supportsRtl"
tools:targetApi="s">
tools:targetApi="tiramisu">
<activity
android:name=".CrashHandler"
android:process=":crash"
android:exported="false" />
<activity
android:name=".SetupActivity"

@ -1,14 +1,132 @@
package com.fox2code.mmm;
import android.annotation.SuppressLint;
import android.content.ClipboardManager;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
import com.fox2code.foxcompat.app.FoxActivity;
import com.google.android.material.textview.MaterialTextView;
import java.io.StringWriter;
import io.sentry.Sentry;
import io.sentry.UserFeedback;
import io.sentry.protocol.SentryId;
public class CrashHandler extends FoxActivity {
@SuppressLint("RestrictedApi")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_crash_handler);
// set crash_details MaterialTextView to the exception passed in the intent or unknown if null
// convert stacktrace from array to string, and pretty print it (first line is the exception, the rest is the stacktrace, with each line indented by 4 spaces)
// first line is the exception, the rest is the stacktrace, with each line indented by 4 spaces. empty out the material text view first
MaterialTextView crashDetails = findViewById(R.id.crash_details);
crashDetails.setText("");
// get the exception from the intent
Throwable exception = (Throwable) getIntent().getSerializableExtra("exception");
// if the exception is null, set the crash details to "Unknown"
if (exception == null) {
crashDetails.setText(R.string.crash_details);
} else {
// if the exception is not null, set the crash details to the exception and stacktrace
// stacktrace is an StacktraceElement, so convert it to a string and replace the commas with newlines
StringWriter stringWriter = new StringWriter();
exception.printStackTrace(new java.io.PrintWriter(stringWriter));
String stacktrace = stringWriter.toString();
stacktrace = stacktrace.replace(",", "\n ");
crashDetails.setText(getString(R.string.crash_full_stacktrace, stacktrace));
}
// disable feedback if sentry is disabled
if (MainApplication.isCrashReportingEnabled()) {
SharedPreferences preferences = getSharedPreferences("sentry", MODE_PRIVATE);
// get lastEventId from intent
SentryId lastEventId = Sentry.captureException((Throwable) getIntent().getSerializableExtra("exception"));
// get name, email, and message fields
EditText name = findViewById(R.id.feedback_name);
EditText email = findViewById(R.id.feedback_email);
EditText description = findViewById(R.id.feedback_message);
// get submit button
findViewById(R.id.feedback_submit).setOnClickListener(v -> {
// require the feedback_message, rest is optional
if (description.getText().toString().equals("")) {
Toast.makeText(this, R.string.sentry_dialogue_empty_message, Toast.LENGTH_LONG).show();
return;
}
// if email or name is empty, use "Anonymous"
String nameString = name.getText().toString().equals("") ? "Anonymous" : name.getText().toString();
String emailString = email.getText().toString().equals("") ? "Anonymous" : email.getText().toString();
// Prevent strict mode violation
new Thread(() -> {
// create sentry userFeedback request
UserFeedback userFeedback = new UserFeedback(lastEventId);
userFeedback.setName(nameString);
userFeedback.setEmail(emailString);
userFeedback.setComments(description.getText().toString());
// send the request
Sentry.captureUserFeedback(userFeedback);
}).start();
// Close the activity
finish();
// start the main activity
startActivity(getPackageManager().getLaunchIntentForPackage(getPackageName()));
});
// get restart button
findViewById(R.id.restart).setOnClickListener(v -> {
// Save the user's name and email
preferences.edit().putString("name", name.getText().toString()).putString("email", email.getText().toString()).apply();
// Restart the app
finish();
startActivity(getPackageManager().getLaunchIntentForPackage(getPackageName()));
});
} else {
// disable feedback if sentry is disabled
findViewById(R.id.feedback_name).setEnabled(false);
findViewById(R.id.feedback_email).setEnabled(false);
findViewById(R.id.feedback_message).setEnabled(false);
// fade out all the fields
findViewById(R.id.feedback_name).setAlpha(0.5f);
findViewById(R.id.feedback_email).setAlpha(0.5f);
findViewById(R.id.feedback_message).setAlpha(0.5f);
// fade out the submit button
findViewById(R.id.feedback_submit).setAlpha(0.5f);
// set feedback_text to "Crash reporting is disabled"
((MaterialTextView) findViewById(R.id.feedback_text)).setText(R.string.sentry_enable_nag);
findViewById(R.id.feedback_submit).setOnClickListener(v -> Toast.makeText(this, R.string.sentry_dialogue_disabled, Toast.LENGTH_LONG).show());
// handle restart button
// we have to explicitly enable it because it's disabled by default
findViewById(R.id.restart).setEnabled(true);
findViewById(R.id.restart).setOnClickListener(v -> {
// Restart the app
finish();
startActivity(getPackageManager().getLaunchIntentForPackage(getPackageName()));
});
}
}
public void copyCrashDetails(View view) {
// change view to a checkmark
view.setBackgroundResource(R.drawable.baseline_check_24);
// copy crash_details to clipboard
ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
String crashDetails = ((MaterialTextView) findViewById(R.id.crash_details)).getText().toString();
clipboard.setPrimaryClip(android.content.ClipData.newPlainText("crash_details", crashDetails));
// show a toast
Toast.makeText(this, R.string.crash_details_copied, Toast.LENGTH_LONG).show();
// after 1 second, change the view back to a copy button
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
runOnUiThread(() -> view.setBackgroundResource(R.drawable.baseline_copy_all_24));
}).start();
}
}

@ -13,17 +13,13 @@ import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.text.InputType;
import android.util.Log;
import android.util.TypedValue;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.SearchView;
@ -49,18 +45,14 @@ import com.fox2code.mmm.repo.RepoManager;
import com.fox2code.mmm.settings.SettingsActivity;
import com.fox2code.mmm.utils.BlurUtils;
import com.fox2code.mmm.utils.ExternalHelper;
import com.fox2code.mmm.utils.io.Http;
import com.fox2code.mmm.utils.IntentHelper;
import com.fox2code.mmm.utils.io.Http;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.progressindicator.LinearProgressIndicator;
import org.chromium.net.ExperimentalCronetEngine;
import org.chromium.net.urlconnection.CronetURLStreamHandlerFactory;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import eightbitlab.com.blurview.BlurView;
@ -307,93 +299,6 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
}, true);
ExternalHelper.INSTANCE.refreshHelper(this);
this.initMode = false;
// Show an material alert dialog if lastEventId is not "" or null in the private sentry shared preferences
//noinspection ConstantConditions
if (MainApplication.isCrashReportingEnabled() && !BuildConfig.SENTRY_TOKEN.isEmpty()) {
SharedPreferences preferences = getSharedPreferences("sentry", MODE_PRIVATE);
String lastExitReason = preferences.getString("lastExitReason", "");
if (BuildConfig.DEBUG)
Log.i("NoodleDebug", "Last Exit Reason: " + lastExitReason);
if (lastExitReason.equals("crash")) {
String lastEventId = preferences.getString("lastEventId", "");
if (BuildConfig.DEBUG)
Log.i("NoodleDebug", "Last Event ID: " + lastEventId);
if (!lastEventId.equals("")) {
// Three edit texts for the user to enter their email, name and a description of the issue
EditText email = new EditText(this);
email.setHint(R.string.email);
email.setInputType(InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);
EditText name = new EditText(this);
name.setHint(R.string.name);
name.setInputType(InputType.TYPE_TEXT_VARIATION_PERSON_NAME);
EditText description = new EditText(this);
description.setHint(R.string.additional_info);
// Set description to be multiline and auto resize
description.setSingleLine(false);
description.setMaxHeight(1000);
// Make description required-
new MaterialAlertDialogBuilder(this).setCancelable(false).setTitle(R.string.sentry_dialogue_title).setMessage(R.string.sentry_dialogue_message).setView(new LinearLayout(this) {{
setOrientation(LinearLayout.VERTICAL);
setPadding(40, 20, 40, 10);
addView(email);
addView(name);
addView(description);
}}).setPositiveButton(R.string.submit, (dialog, which) -> {
// Make sure the user has entered a description
if (description.getText().toString().equals("")) {
Toast.makeText(this, R.string.sentry_dialogue_no_description, Toast.LENGTH_LONG).show();
dialog.cancel();
}
preferences.edit().remove("lastEventId").apply();
preferences.edit().putString("lastExitReason", "").apply();
// Prevent strict mode violation
new Thread(() -> {
try {
HttpURLConnection connection = (HttpURLConnection) new URL("https" + "://sentry.io/api/0/projects/androidacy-i6/foxmmm/user-feedback/").openConnection();
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("Authorization", "Bearer " + BuildConfig.SENTRY_TOKEN);
// Setups the JSON body
String nameString = name.getText().toString();
String emailString = email.getText().toString();
if (nameString.equals(""))
nameString = "Anonymous";
if (emailString.equals(""))
emailString = "Anonymous";
JSONObject body = new JSONObject();
body.put("event_id", lastEventId);
body.put("name", nameString);
body.put("email", emailString);
body.put("comments", description.getText().toString());
// Send the request
connection.setDoOutput(true);
connection.getOutputStream().write(body.toString().getBytes());
connection.connect();
// For debug builds, log the response code and response body
if (BuildConfig.DEBUG) {
Log.d("NoodleDebug", "Response Code: " + connection.getResponseCode());
}
// Check if the request was successful
if (connection.getResponseCode() == 200) {
runOnUiThread(() -> Toast.makeText(this, R.string.sentry_dialogue_success, Toast.LENGTH_LONG).show());
} else {
runOnUiThread(() -> Toast.makeText(this, R.string.sentry_dialogue_failed_toast, Toast.LENGTH_LONG).show());
}
} catch (
IOException |
JSONException ignored) {
// Show a toast if the user feedback could not be submitted
runOnUiThread(() -> Toast.makeText(this, R.string.sentry_dialogue_failed_toast, Toast.LENGTH_LONG).show());
}
}).start();
}).setNegativeButton(R.string.cancel, (dialog, which) -> {
preferences.edit().remove("lastEventId").apply();
preferences.edit().putString("lastExitReason", "").apply();
Log.w(TAG, "User cancelled sentry dialog");
}).show();
}
}
}
}
private void cardIconifyUpdate() {

@ -398,6 +398,22 @@ public class MainApplication extends FoxApplication implements androidx.work.Con
super.onConfigurationChanged(newConfig);
}
// getDataDir wrapper with optional path parameter
public File getDataDirWithPath(String path) {
File dataDir = this.getDataDir();
if (path != null) {
dataDir = new File(dataDir, path);
}
// create the directory if it doesn't exist
if (!dataDir.exists()) {
if (!dataDir.mkdirs()) {
if (BuildConfig.DEBUG)
Log.w("MainApplication", "Failed to create directory " + dataDir);
}
}
return dataDir;
}
public void clearAppData() {
// Clear app data
try {

@ -6,14 +6,12 @@ import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.WindowManager;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.ActionBar;
import androidx.fragment.app.FragmentActivity;
@ -54,9 +52,7 @@ public class SetupActivity extends FoxActivity implements LanguageActivity {
actionBar.show();
}
this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, 0);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
createRealmDatabase();
}
createRealmDatabase();
// Set theme
SharedPreferences prefs = MainApplication.getSharedPreferences();
switch (prefs.getString("theme", "system")) {
@ -85,6 +81,11 @@ public class SetupActivity extends FoxActivity implements LanguageActivity {
setContentView(view);
((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"));
@ -178,7 +179,7 @@ public class SetupActivity extends FoxActivity implements LanguageActivity {
// 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").schemaVersion(1).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).build();
RealmConfiguration realmConfig = new RealmConfiguration.Builder().name("ReposList.realm").directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).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.getInstanceAsync(realmConfig, new Realm.Callback() {
@ -188,24 +189,16 @@ public class SetupActivity extends FoxActivity implements LanguageActivity {
ReposList androidacyRepoDB = realm1.where(ReposList.class).equalTo("id", "androidacy_repo").findFirst();
if (androidacyRepoDB != null) {
androidacyRepoDB.setEnabled(androidacyRepo);
// set remaining fields from the existing db entry
androidacyRepoDB.setName(androidacyRepoDB.getName());
androidacyRepoDB.setUrl(androidacyRepoDB.getUrl());
androidacyRepoDB.setLastUpdate(androidacyRepoDB.getLastUpdate());
androidacyRepoDB.setDonate(androidacyRepoDB.getDonate());
androidacyRepoDB.setSupport(androidacyRepoDB.getSupport());
}
ReposList magiskAltRepoDB = realm1.where(ReposList.class).equalTo("id", "magisk_alt_repo").findFirst();
if (magiskAltRepoDB != null) {
magiskAltRepoDB.setEnabled(magiskAltRepo);
// set remaining fields from the existing db entry
magiskAltRepoDB.setName(magiskAltRepoDB.getName());
magiskAltRepoDB.setUrl(magiskAltRepoDB.getUrl());
magiskAltRepoDB.setLastUpdate(magiskAltRepoDB.getLastUpdate());
magiskAltRepoDB.setDonate(magiskAltRepoDB.getDonate());
magiskAltRepoDB.setSupport(magiskAltRepoDB.getSupport());
}
// commit the changes
realm1.commitTransaction();
realm1.close();
});
realm.commitTransaction();
realm.close();
}
});
@ -280,7 +273,6 @@ public class SetupActivity extends FoxActivity implements LanguageActivity {
}
// creates the realm database
@RequiresApi(api = Build.VERSION_CODES.N)
private void createRealmDatabase() {
if (BuildConfig.DEBUG) {
Log.d("Realm", "Creating Realm databases");
@ -298,7 +290,7 @@ public class SetupActivity extends FoxActivity implements LanguageActivity {
});
// create the realm database for ReposList
// next, create the realm database for ReposList
RealmConfiguration config2 = new RealmConfiguration.Builder().name("ReposList.realm").schemaVersion(1).build();
RealmConfiguration config2 = new RealmConfiguration.Builder().name("ReposList.realm").directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build();
// get the instance
Realm.getInstanceAsync(config2, new Realm.Callback() {
@Override
@ -310,17 +302,19 @@ public class SetupActivity extends FoxActivity implements LanguageActivity {
// 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");
String name = getString(R.string.androidacy_repo_name);
String website = AndroidacyRepoData.getInstance().website;
String donate = AndroidacyRepoData.getInstance().donate;
String support = AndroidacyRepoData.getInstance().support;
androidacy_repo.setName(name);
androidacy_repo.setDonate(donate);
androidacy_repo.setWebsite(website);
androidacy_repo.setSupport(support);
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.setWebsite(AndroidacyRepoData.getInstance().getWebsite());
androidacy_repo.setUrl(AndroidacyRepoData.getInstance().getWebsite());
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.copyToRealmOrUpdate(androidacy_repo);
}
// create magisk_alt_repo
if (realm1.where(ReposList.class).equalTo("id", "magisk_alt_repo").findFirst() == null) {
@ -330,7 +324,11 @@ public class SetupActivity extends FoxActivity implements LanguageActivity {
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_HOMEPAGE);
magisk_alt_repo.setSubmitModule(RepoManager.MAGISK_ALT_REPO_HOMEPAGE + "/submission");
magisk_alt_repo.setLastUpdate(0);
// commit the changes
realm1.copyToRealmOrUpdate(magisk_alt_repo);
}
realm1.commitTransaction();
realm1.close();

@ -4,12 +4,10 @@ import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.MainApplication;
@ -21,7 +19,6 @@ import com.fox2code.mmm.repo.RepoModule;
import com.fox2code.mmm.utils.io.Http;
import com.fox2code.mmm.utils.io.HttpException;
import com.fox2code.mmm.utils.io.PropUtils;
import com.fox2code.mmm.utils.realm.ReposList;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.topjohnwu.superuser.Shell;
@ -80,7 +77,6 @@ public final class AndroidacyRepoData extends RepoData {
this.testMode = testMode;
}
@RequiresApi(api = Build.VERSION_CODES.N)
public static AndroidacyRepoData getInstance() {
return RepoManager.getINSTANCE().getAndroidacyRepoData();
}
@ -182,23 +178,6 @@ public final class AndroidacyRepoData extends RepoData {
@SuppressLint("RestrictedApi")
@Override
protected boolean prepare() throws NoSuchAlgorithmException {
// insert metadata into database
RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ReposListCache.realm").allowWritesOnUiThread(true).allowWritesOnUiThread(true).directory(cacheRoot).build();
Realm realm = Realm.getInstance(realmConfiguration);
realm.beginTransaction();
ReposList repo = realm.where(ReposList.class).equalTo("id", this.id).findFirst();
if (repo == null) {
repo = realm.createObject(ReposList.class, this.id);
}
repo.setName(this.defaultName);
repo.setWebsite(this.defaultWebsite);
repo.setSupport(this.defaultSupport);
repo.setDonate(this.defaultDonate);
repo.setSubmitModule(this.defaultSubmitModule);
repo.setLastUpdate(0);
// close realm
realm.commitTransaction();
realm.close();
// If ANDROIDACY_CLIENT_ID is not set or is empty, disable this repo and return
if (Objects.equals(BuildConfig.ANDROIDACY_CLIENT_ID, "")) {
SharedPreferences.Editor editor = MainApplication.getSharedPreferences().edit();
@ -222,7 +201,7 @@ public final class AndroidacyRepoData extends RepoData {
new MaterialAlertDialogBuilder(MainApplication.getINSTANCE()).setTitle(R.string.androidacy_update_needed).setMessage(R.string.androidacy_update_needed_message).setPositiveButton(R.string.update, (dialog, which) -> {
// Open the app's page on the Play Store
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://github.com/Fox2Code/FoxMagiskModuleManager/releases/latest"));
intent.setData(Uri.parse("https://www.androidacy.com/downloads/?view=FoxMMM&utm_source=foxmnm&utm_medium=app&utm_campaign=android-app"));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
MainApplication.getINSTANCE().startActivity(intent);
}).setNegativeButton(R.string.cancel, null).show();
@ -238,7 +217,7 @@ public final class AndroidacyRepoData extends RepoData {
long time = System.currentTimeMillis();
if (this.androidacyBlockade > time)
return true; // fake it till you make it. Basically,
// don'e fail just becaue we're rate limited. API and web rate limits are different.
// don't fail just becaue we're rate limited. API and web rate limits are different.
this.androidacyBlockade = time + 30_000L;
try {
if (token == null) {

@ -113,7 +113,7 @@ public class RepoData extends XRepo {
Realm realm = Realm.getInstance(realmConfiguration);
ReposList reposList = realm.where(ReposList.class).equalTo("id", this.id).findFirst();
if (BuildConfig.DEBUG) {
Log.d("RepoData", "RepoData: " + this.id + ". record in database: " + (reposList != null ? reposList.toString() : null));
Log.d("RepoData", "RepoData: " + this.id + ". record in database: " + (reposList != null ? reposList.toString() : "none"));
}
this.enabled = (!this.forceHide && reposList != null && reposList.isEnabled());
this.enabled = (!this.forceHide) && MainApplication.getSharedPreferences().getBoolean("pref_" + this.getPreferenceId() + "_enabled", true);
@ -122,7 +122,7 @@ public class RepoData extends XRepo {
// load metadata from realm database
if (this.enabled) {
try {
RealmConfiguration realmConfiguration2 = new RealmConfiguration.Builder().name("ReposList.realm").allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(cacheRoot).build();
RealmConfiguration realmConfiguration2 = new RealmConfiguration.Builder().name("ReposList.realm").allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).build();
// load metadata from realm database
Realm.getInstance(realmConfiguration2);
this.metaDataCache = ModuleListCache.getRepoModulesAsJson(this.id);
@ -135,7 +135,7 @@ public class RepoData extends XRepo {
this.submitModule = this.defaultSubmitModule;
} else {
// get everything from ReposList realm database
RealmConfiguration realmConfiguration3 = new RealmConfiguration.Builder().name("ReposList.realm").allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(cacheRoot).build();
RealmConfiguration realmConfiguration3 = new RealmConfiguration.Builder().name("ReposList.realm").allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).build();
// load metadata from realm database
Realm.getInstance(realmConfiguration3);
this.name = ReposList.getRepo(this.id).getName();

@ -4,14 +4,12 @@ import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.MainActivity;
@ -28,8 +26,10 @@ import com.fox2code.mmm.utils.io.Http;
import com.fox2code.mmm.utils.io.PropUtils;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
@ -38,6 +38,7 @@ import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.stream.Collectors;
public final class RepoManager extends SyncManager {
public static final String MAGISK_REPO = "https://raw.githubusercontent.com/Magisk-Modules-Repo/submission/modules/modules.json";
@ -65,7 +66,6 @@ public final class RepoManager extends SyncManager {
private boolean initialized;
private boolean repoLastSuccess;
@RequiresApi(api = Build.VERSION_CODES.N)
private RepoManager(MainApplication mainApplication) {
INSTANCE = this; // Set early fox XHooks
this.initialized = false;
@ -92,7 +92,6 @@ public final class RepoManager extends SyncManager {
this.initialized = true;
}
@RequiresApi(api = Build.VERSION_CODES.N)
public static RepoManager getINSTANCE() {
if (INSTANCE == null || !INSTANCE.initialized) {
synchronized (lock) {
@ -110,7 +109,6 @@ public final class RepoManager extends SyncManager {
return INSTANCE;
}
@RequiresApi(api = Build.VERSION_CODES.N)
public static RepoManager getINSTANCE_UNSAFE() {
if (INSTANCE == null) {
synchronized (lock) {
@ -161,7 +159,6 @@ public final class RepoManager extends SyncManager {
return INSTANCE != null && INSTANCE.androidacyRepoData != null && INSTANCE.androidacyRepoData.isEnabled();
}
@RequiresApi(api = Build.VERSION_CODES.N)
@SuppressWarnings("StatementWithEmptyBody")
private void populateDefaultCache(RepoData repoData) {
for (RepoModule repoModule : repoData.moduleHashMap.values()) {
@ -191,12 +188,10 @@ public final class RepoManager extends SyncManager {
return this.repoData.get(url);
}
@RequiresApi(api = Build.VERSION_CODES.N)
public RepoData addOrGet(String url) {
return this.addOrGet(url, null);
}
@RequiresApi(api = Build.VERSION_CODES.N)
public RepoData addOrGet(String url, String fallBackName) {
if (MAGISK_ALT_REPO_JSDELIVR.equals(url))
url = MAGISK_ALT_REPO;
@ -220,7 +215,6 @@ public final class RepoManager extends SyncManager {
}
@SuppressWarnings("StatementWithEmptyBody")
@RequiresApi(api = Build.VERSION_CODES.N)
@SuppressLint("StringFormatInvalid")
protected void scanInternal(@NonNull UpdateListener updateListener) {
// Refuse to start if first_launch is not false in shared preferences
@ -250,14 +244,20 @@ public final class RepoManager extends SyncManager {
urlConnection.setReadTimeout(1000);
urlConnection.setUseCaches(false);
urlConnection.getInputStream().close();
// should return a 200 and the content should be "OK"
if (urlConnection.getResponseCode() == 200 && urlConnection.getContentLength() == 3) {
if (BuildConfig.DEBUG)
Log.i("RepoManager", "Internet connection detected");
// should return a 200 and the content should contain "visit_scheme=https" and ip=<some ip>
if (BuildConfig.DEBUG) {
Log.d(TAG, "Response code: " + urlConnection.getResponseCode());
}
// get the response body
String responseBody = new BufferedReader(new InputStreamReader(urlConnection.getInputStream())).lines().collect(Collectors.joining("\n"));
if (BuildConfig.DEBUG) {
Log.d(TAG, "Response body: " + responseBody);
}
// check if the response body contains the expected content
if (urlConnection.getResponseCode() == 200 && responseBody.contains("visit_scheme=https") && responseBody.contains("ip=")) {
this.hasInternet = true;
} else {
if (BuildConfig.DEBUG)
Log.w("RepoManager", "Internet connection not detected");
Log.e(TAG, "Failed to check internet connection");
}
} catch (
IOException e) {
@ -389,7 +389,6 @@ public final class RepoManager extends SyncManager {
return this.hasInternet;
}
@RequiresApi(api = Build.VERSION_CODES.N)
private RepoData addRepoData(String url, String fallBackName) {
String id = internalIdOfUrl(url);
File cacheRoot = new File(this.mainApplication.getDataDir(), id);
@ -416,7 +415,6 @@ public final class RepoManager extends SyncManager {
return repoData;
}
@RequiresApi(api = Build.VERSION_CODES.N)
private AndroidacyRepoData addAndroidacyRepoData() {
// cache dir is actually under app data
File cacheRoot = new File(this.mainApplication.getDataDir(), "androidacy_repo");

@ -1,14 +1,13 @@
package com.fox2code.mmm.repo;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import com.fox2code.mmm.utils.io.Http;
import com.fox2code.mmm.utils.realm.ModuleListCache;
import org.json.JSONException;
import org.json.JSONObject;
import java.nio.charset.StandardCharsets;
@ -31,7 +30,6 @@ public class RepoUpdater {
this.repoData = repoData;
}
@RequiresApi(api = Build.VERSION_CODES.N)
public int fetchIndex() {
if (!RepoManager.getINSTANCE().hasConnectivity()) {
this.indexRaw = null;
@ -113,103 +111,120 @@ public class RepoUpdater {
//installedVersionCode=<int> (only if installed)
//
// all except first six can be null
for (JSONObject module : new JSONObject[]{new JSONObject(new String(this.indexRaw, StandardCharsets.UTF_8))}) {
// get module id
String id = module.getString("id");
// get module name
String name = module.getString("name");
// get module version
String version = module.getString("version");
// get module version code
int versionCode = module.getInt("versionCode");
// get module author
String author = module.getString("author");
// get module description
String description = module.getString("description");
// get module min api
int minApi = module.getInt("minApi");
// get module max api
int maxApi = module.getInt("maxApi");
// get module min magisk
int minMagisk = module.getInt("minMagisk");
// get module need ramdisk
boolean needRamdisk = module.getBoolean("needRamdisk");
// get module support
String support = module.getString("support");
// get module donate
String donate = module.getString("donate");
// get module config
String config = module.getString("config");
// get module change boot
boolean changeBoot = module.getBoolean("changeBoot");
// get module mmt reborn
boolean mmtReborn = module.getBoolean("mmtReborn");
// get module repo id
String repoId = this.repoData.id;
// get module installed
boolean installed = false;
// get module installed version code
int installedVersionCode = 0;
// insert module to realm
// first create a collection of all the properties
// then insert to realm
// then commit
// then close
Realm.getInstanceAsync(realmConfiguration, new Realm.Callback() {
@Override
public void onSuccess(@NonNull Realm realm) {
realm.executeTransactionAsync(r -> {
// create a new module
ModuleListCache moduleListCache = r.createObject(ModuleListCache.class);
// set module id
moduleListCache.setId(id);
// set module name
moduleListCache.setName(name);
// set module version
moduleListCache.setVersion(version);
// set module version code
moduleListCache.setVersionCode(versionCode);
// set module author
moduleListCache.setAuthor(author);
// set module description
moduleListCache.setDescription(description);
// set module min api
moduleListCache.setMinApi(minApi);
// set module max api
moduleListCache.setMaxApi(maxApi);
// set module min magisk
moduleListCache.setMinMagisk(minMagisk);
// set module need ramdisk
moduleListCache.setNeedRamdisk(needRamdisk);
// set module support
moduleListCache.setSupport(support);
// set module donate
moduleListCache.setDonate(donate);
// set module config
moduleListCache.setConfig(config);
// set module change boot
moduleListCache.setChangeBoot(changeBoot);
// set module mmt reborn
moduleListCache.setMmtReborn(mmtReborn);
// set module repo id
moduleListCache.setRepoId(repoId);
// set module installed
moduleListCache.setInstalled(installed);
// set module installed version code
moduleListCache.setInstalledVersionCode(installedVersionCode);
}, () -> {
// Transaction was a success.
Log.d(TAG, "onSuccess: Transaction was a success.");
// close realm
realm.close();
}, error -> {
// Transaction failed and was automatically canceled.
Log.e(TAG, "onError: Transaction failed and was automatically canceled.", error);
// close realm
realm.close();
});
}
});
// this.indexRaw is the raw index file (json) and the modules can be either under the "modules" key or the "data" key
// both are arrays of modules
// try to get modules from "modules" key
JSONObject modules = new JSONObject(new String(this.indexRaw, StandardCharsets.UTF_8));
try {
// get modules
modules = modules.getJSONObject("modules");
} catch (JSONException e) {
// if it fails, try to get modules from "data" key
modules = modules.getJSONObject("data");
}
for (JSONObject module : new JSONObject[]{modules}) {
try {
// get module id
String id = module.getString("id");
// get module name
String name = module.getString("name");
// get module version
String version = module.getString("version");
// get module version code
int versionCode = module.getInt("versionCode");
// get module author
String author = module.getString("author");
// get module description
String description = module.getString("description");
// get module min api
int minApi = module.getInt("minApi");
// get module max api
int maxApi = module.getInt("maxApi");
// get module min magisk
int minMagisk = module.getInt("minMagisk");
// get module need ramdisk
boolean needRamdisk = module.getBoolean("needRamdisk");
// get module support
String support = module.getString("support");
// get module donate
String donate = module.getString("donate");
// get module config
String config = module.getString("config");
// get module change boot
boolean changeBoot = module.getBoolean("changeBoot");
// get module mmt reborn
boolean mmtReborn = module.getBoolean("mmtReborn");
// get module repo id
String repoId = this.repoData.id;
// get module installed
boolean installed = false;
// get module installed version code
int installedVersionCode = 0;
// insert module to realm
// first create a collection of all the properties
// then insert to realm
// then commit
// then close
Realm.getInstanceAsync(realmConfiguration, new Realm.Callback() {
@Override
public void onSuccess(@NonNull Realm realm) {
realm.executeTransactionAsync(r -> {
// create a new module
ModuleListCache moduleListCache = r.createObject(ModuleListCache.class);
// set module id
moduleListCache.setId(id);
// set module name
moduleListCache.setName(name);
// set module version
moduleListCache.setVersion(version);
// set module version code
moduleListCache.setVersionCode(versionCode);
// set module author
moduleListCache.setAuthor(author);
// set module description
moduleListCache.setDescription(description);
// set module min api
moduleListCache.setMinApi(minApi);
// set module max api
moduleListCache.setMaxApi(maxApi);
// set module min magisk
moduleListCache.setMinMagisk(minMagisk);
// set module need ramdisk
moduleListCache.setNeedRamdisk(needRamdisk);
// set module support
moduleListCache.setSupport(support);
// set module donate
moduleListCache.setDonate(donate);
// set module config
moduleListCache.setConfig(config);
// set module change boot
moduleListCache.setChangeBoot(changeBoot);
// set module mmt reborn
moduleListCache.setMmtReborn(mmtReborn);
// set module repo id
moduleListCache.setRepoId(repoId);
// set module installed
moduleListCache.setInstalled(installed);
// set module installed version code
moduleListCache.setInstalledVersionCode(installedVersionCode);
}, () -> {
// Transaction was a success.
Log.d(TAG, "onSuccess: Transaction was a success.");
// close realm
realm.close();
}, error -> {
// Transaction failed and was automatically canceled.
Log.e(TAG, "onError: Transaction failed and was automatically canceled.", error);
// close realm
realm.close();
});
}
});
} catch (
JSONException e) {
e.printStackTrace();
Log.w(TAG, "Failed to get module info from module " + module + " in repo " + this.repoData.id + " with error " + e.getMessage());
}
}
} catch (
Exception e) {

@ -26,7 +26,6 @@ import android.widget.Button;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.FileProvider;
@ -162,7 +161,6 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
}
public static class SettingsFragment extends PreferenceFragmentCompat implements FoxActivity.OnBackPressedCallback {
@RequiresApi(api = Build.VERSION_CODES.N)
@SuppressLint("UnspecifiedImmutableFlag")
@Override
@SuppressWarnings("ConstantConditions")
@ -385,7 +383,8 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
if (findPreference("pref_test_crash") != null && findPreference("pref_clear_data") != null) {
findPreference("pref_test_crash").setOnPreferenceClickListener(preference -> {
// Hard crash the app
throw new Error("This is a test crash");
// 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
@ -664,7 +663,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
int declaredLanguageLevel = this.getResources().getInteger(R.integer.language_support_level);
if (declaredLanguageLevel != LANGUAGE_SUPPORT_LEVEL)
return declaredLanguageLevel;
if (!this.getResources().getConfiguration().locale.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")) {
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;

@ -140,7 +140,7 @@ public class Http {
}
if (chain.request().header("Accept-Language") == null) {
request.header("Accept-Language", // Send system language to the server
mainApplication.getResources().getConfiguration().locale.toLanguageTag());
mainApplication.getResources().getConfiguration().getLocales().get(0).toLanguageTag());
}
return chain.proceed(request.build());
});
@ -226,12 +226,12 @@ public class Http {
@SuppressLint("RestrictedApi")
@SuppressWarnings("resource")
public static byte[] doHttpGet(String url, boolean allowCache) throws IOException {
if (BuildConfig.DEBUG) {
if (BuildConfig.DEBUG_HTTP) {
// Log, but set all query parameters values to "****" while keeping the keys
Log.d(TAG, "doHttpGet: " + url.replaceAll("=[^&]*", "=****"));
}
Response response = (allowCache ? getHttpClientWithCache() : getHttpClient()).newCall(new Request.Builder().url(url).get().build()).execute();
if (BuildConfig.DEBUG) {
if (BuildConfig.DEBUG_HTTP) {
Log.d(TAG, "doHttpGet: request executed");
}
// 200/204 == success, 304 == cache valid
@ -245,7 +245,7 @@ public class Http {
}
throw new HttpException(response.code());
}
if (BuildConfig.DEBUG) {
if (BuildConfig.DEBUG_HTTP) {
Log.d(TAG, "doHttpGet: " + url.replaceAll("=[^&]*", "=****") + " succeeded");
}
ResponseBody responseBody = response.body();
@ -255,7 +255,7 @@ public class Http {
if (response != null)
responseBody = response.body();
}
if (BuildConfig.DEBUG) {
if (BuildConfig.DEBUG_HTTP) {
Log.d(TAG, "doHttpGet: returning " + responseBody.contentLength() + " bytes");
}
return responseBody.bytes();

@ -39,7 +39,11 @@ public class SentryMain {
editor.apply();
// open crash handler and exit
Intent intent = new Intent(mainApplication, CrashHandler.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// pass the entire exception to the crash handler
intent.putExtra("exception", throwable);
// add stacktrace as string
intent.putExtra("stacktrace", throwable.getStackTrace());
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
mainApplication.startActivity(intent);
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(10);

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp"
android:height="24dp" android:autoMirrored="true"
android:tint="?attr/colorControlNormal" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="@android:color/white" android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4z"/>
</vector>

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp"
android:height="24dp" android:autoMirrored="true"
android:tint="?attr/colorControlNormal" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="@android:color/white" android:pathData="M18,2H9C7.9,2 7,2.9 7,4v12c0,1.1 0.9,2 2,2h9c1.1,0 2,-0.9 2,-2V4C20,2.9 19.1,2 18,2zM18,16H9V4h9V16zM3,15v-2h2v2H3zM3,9.5h2v2H3V9.5zM10,20h2v2h-2V20zM3,18.5v-2h2v2H3zM5,22c-1.1,0 -2,-0.9 -2,-2h2V22zM8.5,22h-2v-2h2V22zM13.5,22L13.5,22l0,-2h2v0C15.5,21.1 14.6,22 13.5,22zM5,6L5,6l0,2H3v0C3,6.9 3.9,6 5,6z"/>
</vector>

@ -1,48 +1,89 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent">
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical">
<!-- layout with crash_text header and crash_details body -->
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical">
<!-- layout with crash_text header and crash_details body -->
<!-- first, crash icon -->
<ImageView
android:id="@+id/crash_icon"
android:layout_width="101dp"
android:layout_height="93dp"
android:layout_gravity="center"
android:layout_margin="8dp"
android:contentDescription="@string/crash_icon"
android:src="@drawable/ic_baseline_bug_report_24" />
<!-- crash_text header -->
<!-- crash_text header -->
<com.google.android.material.textview.MaterialTextView
android:id="@+id/crash_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:gravity="center"
android:text="@string/crash_text"
android:textSize="20sp"
android:textStyle="bold" />
<!-- smaller details may be found below header -->
<com.google.android.material.textview.MaterialTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:gravity="center"
android:text="@string/more_details"
android:textSize="16sp" />
<!-- copyable crash_details body with copy button in top right corner -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<HorizontalScrollView android:layout_width="match_parent" android:layout_height="wrap_content">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/crash_text"
android:layout_width="match_parent"
android:id="@+id/crash_details"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:fontFamily="monospace"
android:gravity="start"
android:minHeight="48dp"
android:padding="12dp"
android:scrollbars="vertical|horizontal"
android:scrollHorizontally="true"
android:text="@string/crash_details"
android:textIsSelectable="false"
android:textSize="16sp" />
</HorizontalScrollView>
<com.google.android.material.button.MaterialButton
android:id="@+id/copy_button"
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_gravity="top|end"
android:layout_margin="10dp"
android:gravity="fill"
android:text="@string/crash_text"
android:textSize="20sp"
android:textStyle="bold" />
android:background="@drawable/baseline_copy_all_24"
android:contentDescription="@string/copy_button"
android:onClick="copyCrashDetails"
android:padding="4dp" />
</FrameLayout>
<com.google.android.material.textview.MaterialTextView
android:id="@+id/crash_details"
<!-- feedback form -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:fontFamily="monospace"
android:gravity="fill"
android:text="@string/crash_details"
android:textSize="16sp" />
android:orientation="vertical">
<!-- feedback form -->
<androidx.appcompat.widget.LinearLayoutCompat
<!-- feedback form header -->
<com.google.android.material.textview.MaterialTextView
android:id="@+id/feedback_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- feedback form header -->
<TextView
android:id="@+id/feedback_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:gravity="fill"
android:text="@string/please_feedback"
android:textSize="20sp"
android:textStyle="bold" />
android:layout_margin="10dp"
android:gravity="fill"
android:text="@string/please_feedback"
android:textSize="18sp" />
<LinearLayout
android:id="@+id/feedback"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
@ -53,7 +94,7 @@
android:layout_width="320dp"
android:layout_height="48dp"
android:layout_margin="10dp"
android:hint="@string/name"
android:hint="@string/feedback_name"
android:inputType="text" />
<!-- feedback form email -->
@ -62,7 +103,7 @@
android:layout_width="320dp"
android:layout_height="48dp"
android:layout_margin="10dp"
android:hint="@string/email"
android:hint="@string/feedback_email"
android:inputType="textEmailAddress" />
<!-- feedback form message -->
@ -82,6 +123,7 @@
<!-- submit feedback button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/feedback_submit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
@ -89,6 +131,7 @@
<!-- restart button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/restart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
@ -96,6 +139,6 @@
</LinearLayout>
</LinearLayout>
</androidx.appcompat.widget.LinearLayoutCompat>
</LinearLayout>
</LinearLayout>
</ScrollView>

@ -298,6 +298,19 @@
<string name="error_saving_logs">Could not save logs</string>
<string name="share_logs">Share FoxMMM logs</string>
<string name="not_official_build">This app is an unofficial FoxMMM build.</string>
<string name="crash_text">Oops! We ran into a problem.</string>
<string name="crash_details">Exception: %s</string><string name="feedback_message">Give us more details about what you were doing when this happened.</string><string name="feedback_submit">Submit and restart</string><string name="please_feedback">Please help us out by telling us what you were trying to do when this happened.</string>
<string name="crash_text">Uh-oh, we hit a snag!</string>=
<string name="feedback_message">Give us more details about what you were doing when this happened. The more, the merrier!</string>
<string name="feedback_submit">Submit and restart</string>
<string name="please_feedback">Please help us out by telling us what you were trying to do when this happened.</string>
<string name="sentry_dialogue_disabled">Crash reporting is disabled. Enable it to submit feedback.</string>
<string name="feedback_name">Name (optional)</string>
<string name="feedback_email">Email (optional)</string>
<string name="sentry_dialogue_empty_message">You didn\'t specify additional feedback.</string>
<string name="crash_icon">Crash icon</string>
<string name="copy_button">Copy text</string>
<string name="crash_details">Unknown cause</string>
<string name="more_details">More details may be found below.</string>
<string name="crash_details_copied">Copied stacktrace to clipboard!</string>
<string name="crash_full_stacktrace">Stacktrace:\n%1$s</string>
<string name="sentry_enable_nag">It looks like crash reporting is disabled. Please enable it to submit feedback.</string>
</resources>

Loading…
Cancel
Save