diff --git a/app/build.gradle b/app/build.gradle index 4f084e2..0c526e8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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", "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", "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() diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b1f21ac..104360f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,6 @@ @@ -24,6 +23,7 @@ + tools:targetApi="tiramisu"> { + // 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(); } } \ No newline at end of file diff --git a/app/src/main/java/com/fox2code/mmm/MainActivity.java b/app/src/main/java/com/fox2code/mmm/MainActivity.java index 83d9e04..7fffabb 100644 --- a/app/src/main/java/com/fox2code/mmm/MainActivity.java +++ b/app/src/main/java/com/fox2code/mmm/MainActivity.java @@ -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() { diff --git a/app/src/main/java/com/fox2code/mmm/MainApplication.java b/app/src/main/java/com/fox2code/mmm/MainApplication.java index 7e603ab..47a97bf 100644 --- a/app/src/main/java/com/fox2code/mmm/MainApplication.java +++ b/app/src/main/java/com/fox2code/mmm/MainApplication.java @@ -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 { diff --git a/app/src/main/java/com/fox2code/mmm/SetupActivity.java b/app/src/main/java/com/fox2code/mmm/SetupActivity.java index 87afa25..81ec47e 100644 --- a/app/src/main/java/com/fox2code/mmm/SetupActivity.java +++ b/app/src/main/java/com/fox2code/mmm/SetupActivity.java @@ -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(); diff --git a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyRepoData.java b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyRepoData.java index 8565266..e8370e2 100644 --- a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyRepoData.java +++ b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyRepoData.java @@ -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) { diff --git a/app/src/main/java/com/fox2code/mmm/repo/RepoData.java b/app/src/main/java/com/fox2code/mmm/repo/RepoData.java index 97516a2..7fdaf54 100644 --- a/app/src/main/java/com/fox2code/mmm/repo/RepoData.java +++ b/app/src/main/java/com/fox2code/mmm/repo/RepoData.java @@ -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(); diff --git a/app/src/main/java/com/fox2code/mmm/repo/RepoManager.java b/app/src/main/java/com/fox2code/mmm/repo/RepoManager.java index c6d058c..4a7c7b5 100644 --- a/app/src/main/java/com/fox2code/mmm/repo/RepoManager.java +++ b/app/src/main/java/com/fox2code/mmm/repo/RepoManager.java @@ -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= + 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"); diff --git a/app/src/main/java/com/fox2code/mmm/repo/RepoUpdater.java b/app/src/main/java/com/fox2code/mmm/repo/RepoUpdater.java index ad16ffb..487fed3 100644 --- a/app/src/main/java/com/fox2code/mmm/repo/RepoUpdater.java +++ b/app/src/main/java/com/fox2code/mmm/repo/RepoUpdater.java @@ -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= (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) { diff --git a/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java b/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java index ca0e967..c1df679 100644 --- a/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java +++ b/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java @@ -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; diff --git a/app/src/main/java/com/fox2code/mmm/utils/io/Http.java b/app/src/main/java/com/fox2code/mmm/utils/io/Http.java index a4c9b3c..2f5065f 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/io/Http.java +++ b/app/src/main/java/com/fox2code/mmm/utils/io/Http.java @@ -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(); diff --git a/app/src/main/java/com/fox2code/mmm/utils/sentry/SentryMain.java b/app/src/main/java/com/fox2code/mmm/utils/sentry/SentryMain.java index d8db1e9..f04631b 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/sentry/SentryMain.java +++ b/app/src/main/java/com/fox2code/mmm/utils/sentry/SentryMain.java @@ -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); diff --git a/app/src/main/res/drawable/baseline_check_24.xml b/app/src/main/res/drawable/baseline_check_24.xml new file mode 100644 index 0000000..38d0ab7 --- /dev/null +++ b/app/src/main/res/drawable/baseline_check_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_copy_all_24.xml b/app/src/main/res/drawable/baseline_copy_all_24.xml new file mode 100644 index 0000000..e56b4b0 --- /dev/null +++ b/app/src/main/res/drawable/baseline_copy_all_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/activity_crash_handler.xml b/app/src/main/res/layout/activity_crash_handler.xml index 06354ec..cd71453 100644 --- a/app/src/main/res/layout/activity_crash_handler.xml +++ b/app/src/main/res/layout/activity_crash_handler.xml @@ -1,48 +1,89 @@ + + - - + + - + + + + + + + + + + + + + android:background="@drawable/baseline_copy_all_24" + android:contentDescription="@string/copy_button" + android:onClick="copyCrashDetails" + android:padding="4dp" /> + - + + android:orientation="vertical"> - - + - - - + android:layout_margin="10dp" + android:gravity="fill" + android:text="@string/please_feedback" + android:textSize="18sp" /> @@ -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" /> @@ -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" /> @@ -82,6 +123,7 @@ - + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d83aa36..2e72c91 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -298,6 +298,19 @@ Could not save logs Share FoxMMM logs This app is an unofficial FoxMMM build. - Oops! We ran into a problem. - Exception: %sGive us more details about what you were doing when this happened.Submit and restartPlease help us out by telling us what you were trying to do when this happened. + Uh-oh, we hit a snag!= + Give us more details about what you were doing when this happened. The more, the merrier! + Submit and restart + Please help us out by telling us what you were trying to do when this happened. + Crash reporting is disabled. Enable it to submit feedback. + Name (optional) + Email (optional) + You didn\'t specify additional feedback. + Crash icon + Copy text + Unknown cause + More details may be found below. + Copied stacktrace to clipboard! + Stacktrace:\n%1$s + It looks like crash reporting is disabled. Please enable it to submit feedback.