From 86c46de0690c4954a3f621af1784e5551be17795 Mon Sep 17 00:00:00 2001 From: androidacy-user Date: Sat, 10 Dec 2022 21:36:21 -0500 Subject: [PATCH] Misc optimizations Signed-off-by: androidacy-user --- app/build.gradle | 38 +++--- .../java/com/fox2code/mmm/MainActivity.java | 114 ++++++++++++++++++ .../mmm/settings/SettingsActivity.java | 6 +- .../java/com/fox2code/mmm/utils/Http.java | 27 +++-- app/src/main/res/values/strings.xml | 8 +- .../com/fox2code/mmm/sentry/SentryMain.java | 91 +++++++------- 6 files changed, 203 insertions(+), 81 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 5b06804..bcee40d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -38,7 +38,7 @@ android { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), -'proguard-rules.pro' + 'proguard-rules.pro' } debug { applicationIdSuffix '.debug' @@ -57,6 +57,13 @@ android { dimension "type" buildConfigField "boolean", "ENABLE_AUTO_UPDATER", "true" buildConfigField "boolean", "DEFAULT_ENABLE_CRASH_REPORTING", "true" + if (hasSentryConfig) { + Properties properties = new Properties() + try (FileInputStream fis = new FileInputStream(sentryConfigFile)) { + properties.load(fis) + } + buildConfigField "String", "SENTRY_TOKEN", '"' + properties.getProperty("auth." + "token") + '"' + } // Get the androidacy client ID from the androidacy.properties Properties properties = new Properties() // If androidacy.properties doesn't exist, use the default client ID which is heavily @@ -91,6 +98,14 @@ android { // Disable crash reporting for F-Droid flavor by default buildConfigField "boolean", "DEFAULT_ENABLE_CRASH_REPORTING", "false" + if (hasSentryConfig) { + Properties properties = new Properties() + try (FileInputStream fis = new FileInputStream(sentryConfigFile)) { + properties.load(fis) + } + buildConfigField "String", "SENTRY_TOKEN", '"' + properties.getProperty("auth." + "token") + '"' + } + // Repo with ads or tracking feature are disabled by default for the // F-Droid flavor. buildConfigField("java.util.List", @@ -128,7 +143,7 @@ aboutLibraries { // This is because gradle doesn't allow to enable/disable plugins conditionally sentry { // Disable sentry on F-Droid flavor - ignoredFlavors = hasSentryConfig ? [] : ["default", "fdroid"] + ignoredFlavors = [] // Disables or enables the handling of Proguard mapping for Sentry. // If enabled the plugin will generate a UUID and will take care of @@ -164,14 +179,14 @@ sentry { // Does auto instrumentation for specified features through bytecode manipulation. // Default is enabled. tracingInstrumentation { - enabled = hasSentryConfig + enabled = true } // Enable auto-installation of Sentry components (sentry-android SDK and okhttp, timber and fragment integrations). // Default is enabled. // Only available v3.1.0 and above. autoInstallation { - enabled = hasSentryConfig + enabled = true // Specifies a version of the sentry-android SDK and fragment, timber and okhttp integrations. // @@ -188,9 +203,6 @@ sentry { configurations { implementation.exclude group: 'org.jetbrains', module: 'annotations' - if (!hasSentryConfig) { - implementation.exclude group: 'io.sentry', module: 'sentry-android' - } } dependencies { @@ -223,11 +235,7 @@ dependencies { implementation 'com.github.topjohnwu.libsu:io:5.0.1' implementation 'com.github.Fox2Code:RosettaX:1.0.9' implementation 'com.github.Fox2Code:AndroidANSI:1.0.1' - - if (hasSentryConfig) { - // Error reporting - implementation 'io.sentry:sentry-android:6.9.2' - } + implementation 'io.sentry:sentry-android:6.9.2' // Markdown implementation "io.noties.markwon:core:4.6.2" @@ -241,6 +249,9 @@ dependencies { // Test testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.4' + + // Add okhttp logging interceptor if debug build + debugImplementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.10' } if (hasSentryConfig) { @@ -257,8 +268,7 @@ if (hasSentryConfig) { } final String sentrySrc = hasSentryConfig ? 'src/sentry/java' : 'src/sentryless/java' -final String sentryManifestSrc = hasSentryConfig ? - 'src/sentry/AndroidManifest.xml' : 'src/sentryless/AndroidManifest.xml' +final String sentryManifestSrc = hasSentryConfig ? 'src/sentry/AndroidManifest.xml' : 'src/sentryless/AndroidManifest.xml' android { sourceSets { diff --git a/app/src/main/java/com/fox2code/mmm/MainActivity.java b/app/src/main/java/com/fox2code/mmm/MainActivity.java index 4f80a54..0b0f1bd 100644 --- a/app/src/main/java/com/fox2code/mmm/MainActivity.java +++ b/app/src/main/java/com/fox2code/mmm/MainActivity.java @@ -13,13 +13,17 @@ 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; @@ -50,7 +54,19 @@ import com.fox2code.mmm.utils.NoodleDebug; 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.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; + import eightbitlab.com.blurview.BlurView; +import io.sentry.Sentry; public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRefreshListener, SearchView.OnQueryTextListener, SearchView.OnCloseListener, OverScrollManager.OverScrollHelper { private static final String TAG = "MainActivity"; @@ -241,6 +257,104 @@ 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 + if (MainApplication.isCrashReportingEnabled()) { + SharedPreferences preferences = getSharedPreferences("sentry", MODE_PRIVATE); + String lastExitReason = preferences.getString("lastExitReason", ""); + if (lastExitReason.equals("crash")) { + String lastEventId = preferences.getString("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(); + // Prevent strict mode violation + new Thread(() -> { + try { + // Use HTTPConnectionURL to send a post request to the sentry server + ExperimentalCronetEngine cronetEngine = new ExperimentalCronetEngine.Builder(this).build(); + CronetURLStreamHandlerFactory cronetURLStreamHandlerFactory = new CronetURLStreamHandlerFactory(cronetEngine); + URL.setURLStreamHandlerFactory(cronetURLStreamHandlerFactory); + 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); + connection.setDoOutput(true); + connection.setDoInput(true); + connection.setChunkedStreamingMode(0); + connection.connect(); + // Write the JSON data to the output stream + OutputStream outputStream = connection.getOutputStream(); + // Name and email are optional, so if they are empty, set them to + // Anonymous and Anonymous respectively + String nameString = name.getText().toString(); + String emailString = email.getText().toString(); + if (nameString.equals("")) nameString = "Anonymous"; + if (emailString.equals("")) emailString = "Anonymous"; + String finalNameString = nameString; + String finalEmailString = emailString; + outputStream.write(new JSONObject() {{ + put("event_id", lastEventId); + put("name", finalNameString); + put("email", finalEmailString); + put("comments", description.getText().toString()); + }}.toString().getBytes()); + outputStream.flush(); + outputStream.close(); + // Read the response + InputStream inputStream = connection.getInputStream(); + byte[] buffer = new byte[1024]; + int length; + StringBuilder stringBuilder = new StringBuilder(); + while ((length = inputStream.read(buffer)) != -1) { + stringBuilder.append(new String(buffer, 0, length)); + } + inputStream.close(); + connection.disconnect(); + if (BuildConfig.DEBUG) Log.d("Sentry", stringBuilder.toString()); + // Valid responses will be a json object with a key "id" + JSONObject jsonObject = new JSONObject(stringBuilder.toString()); + if (jsonObject.has("id")) { + // Show a toast to the user to confirm that the feedback has been sent + runOnUiThread(() -> Toast.makeText(this, R.string.sentry_dialogue_success, Toast.LENGTH_LONG).show()); + } else { + // Show a toast to the user to confirm that the feedback has not been sent + 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(); + Sentry.captureMessage("User has ignored the crash"); + }).show(); + } + } + } } private void cardIconifyUpdate() { 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 a904edd..2e27722 100644 --- a/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java +++ b/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java @@ -330,7 +330,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { } else { findPreference("pref_crash").setOnPreferenceClickListener(preference -> { // Hard crash the app - throw new Error("This is a test crash"); + throw new RuntimeException("This is a test crash"); }); } if (InstallerInitializer.peekMagiskVersion() < Constants.MAGISK_VER_CODE_INSTALL_COMMAND || !MainApplication.isDeveloper()) { @@ -645,9 +645,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { }); prefAndroidacyRepoApiKey.setPositiveButtonText(R.string.save_api_key); prefAndroidacyRepoApiKey.setOnPreferenceChangeListener((preference, newValue) -> { - if (originalApiKeyRef[0].equals(newValue)) return true; // Skip if nothing changed. - // Curious if this actually works - so crash the app on purpose - // throw new RuntimeException("This is a test crash"); + if (originalApiKeyRef[0].equals(newValue)) return true; // get original api key String apiKey = String.valueOf(newValue); // Show snack bar with indeterminate progress diff --git a/app/src/main/java/com/fox2code/mmm/utils/Http.java b/app/src/main/java/com/fox2code/mmm/utils/Http.java index 43c8c78..f8a22d1 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/Http.java +++ b/app/src/main/java/com/fox2code/mmm/utils/Http.java @@ -19,7 +19,6 @@ import com.fox2code.mmm.MainApplication; import com.fox2code.mmm.androidacy.AndroidacyUtil; import com.fox2code.mmm.installer.InstallerInitializer; import com.fox2code.mmm.repo.RepoManager; -import com.google.net.cronet.okhttptransport.CronetCallFactory; import com.google.net.cronet.okhttptransport.CronetInterceptor; import org.chromium.net.CronetEngine; @@ -54,6 +53,7 @@ import okhttp3.RequestBody; import okhttp3.Response; import okhttp3.ResponseBody; import okhttp3.dnsoverhttps.DnsOverHttps; +import okhttp3.logging.HttpLoggingInterceptor; import okio.BufferedSink; public class Http { @@ -140,13 +140,6 @@ public class Http { return chain.proceed(request.build()); }); // Add cronet interceptor - // install cronet - /*try { - // Detect if cronet is installed - CronetProviderInstaller.installProvider(mainApplication); - } catch (Exception e) { - Log.e(TAG, "Failed to install cronet", e); - }*/ // init cronet try { // Load the cronet library @@ -165,8 +158,7 @@ public class Http { } builder.setStoragePath(mainApplication.getCacheDir().getAbsolutePath() + "/cronet"); builder.enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 10 * 1024 * 1024); - CronetEngine engine = - builder.build(); + CronetEngine engine = builder.build(); httpclientBuilder.addInterceptor(CronetInterceptor.newBuilder(engine).build()); } catch (Exception e) { Log.e(TAG, "Failed to init cronet", e); @@ -179,6 +171,13 @@ public class Http { httpClient = followRedirects(httpclientBuilder, true).build(); followRedirects(httpclientBuilder, false).build(); httpclientBuilder.dns(fallbackDNS); + if (BuildConfig.DEBUG) { + // Enable logging + HttpLoggingInterceptor logging = new HttpLoggingInterceptor(s -> Log.d(TAG, s)); + logging.setLevel(HttpLoggingInterceptor.Level.BODY); + httpclientBuilder.addInterceptor(logging); + Log.d(TAG, "OkHttp logging enabled"); + } httpClientDoH = followRedirects(httpclientBuilder, true).build(); followRedirects(httpclientBuilder, false).build(); httpclientBuilder.cache(new Cache(new File(mainApplication.getCacheDir(), "http_cache"), 16L * 1024L * 1024L)); // 16Mib of cache @@ -234,8 +233,7 @@ public class Http { @SuppressWarnings("resource") public static byte[] doHttpGet(String url, boolean allowCache) throws IOException { checkNeedBlockAndroidacyRequest(url); - Response response = - (allowCache ? getHttpClientWithCache() : getHttpClient()).newCall(new Request.Builder().url(url).get().build()).execute(); + Response response = (allowCache ? getHttpClientWithCache() : getHttpClient()).newCall(new Request.Builder().url(url).get().build()).execute(); // 200/204 == success, 304 == cache valid if (response.code() != 200 && response.code() != 204 && (response.code() != 304 || !allowCache)) { Log.e(TAG, "Failed to fetch " + url + ", code: " + response.code()); @@ -263,12 +261,15 @@ public class Http { private static Object doHttpPostRaw(String url, String data, boolean allowCache) throws IOException { if (BuildConfig.DEBUG) Log.d(TAG, "POST " + url + " " + data); checkNeedBlockAndroidacyRequest(url); - Response response = (allowCache ? getHttpClientWithCache() : getHttpClient()).newCall(new Request.Builder().url(url).post(JsonRequestBody.from(data)).header("Content-Type", "application/json").build()).execute(); + Response response; + response = (allowCache ? getHttpClientWithCache() : getHttpClient()).newCall(new Request.Builder().url(url).post(JsonRequestBody.from(data)).header("Content-Type", "application/json").build()).execute(); if (response.isRedirect()) { return response.request().url().uri().toString(); } // 200/204 == success, 304 == cache valid if (response.code() != 200 && response.code() != 204 && (response.code() != 304 || !allowCache)) { + if (BuildConfig.DEBUG) + Log.e(TAG, "Failed to fetch " + url + ", code: " + response.code() + ", body: " + response.body().string()); checkNeedCaptchaAndroidacy(url, response.code()); throw new HttpException(response.code()); } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 06ac217..13a1f9b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -204,10 +204,14 @@ You are setting a transparent theme Transparent themes may have some inconsistencies and may not work on all ROMs. In additon, monet and blur will be disabled. You can change back at any time. Custom repos are always on until you remove them. - We encountered an error, please help us improve the app by adding some information about the error. - Sentry report + We encountered an error! Please help us improve the app + by adding some information about the error below. Name and email are optional. + Oops! Something went wrong. Name Email Tell us what happened Submit + Could not submit feedback + Submitted feedback + Please provide a description/comment for the issue diff --git a/app/src/sentry/java/com/fox2code/mmm/sentry/SentryMain.java b/app/src/sentry/java/com/fox2code/mmm/sentry/SentryMain.java index 5d91070..46ae270 100644 --- a/app/src/sentry/java/com/fox2code/mmm/sentry/SentryMain.java +++ b/app/src/sentry/java/com/fox2code/mmm/sentry/SentryMain.java @@ -3,26 +3,26 @@ package com.fox2code.mmm.sentry; import static io.sentry.TypeCheckHint.SENTRY_TYPE_CHECK_HINT; import android.annotation.SuppressLint; -import android.app.Activity; +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; import android.net.Uri; -import android.os.Handler; -import android.os.Looper; import android.util.Log; -import android.widget.EditText; import com.fox2code.mmm.BuildConfig; +import com.fox2code.mmm.MainActivity; import com.fox2code.mmm.MainApplication; -import com.fox2code.mmm.R; import com.fox2code.mmm.androidacy.AndroidacyUtil; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.io.IOException; import java.io.Writer; +import java.util.Objects; import io.sentry.JsonObjectWriter; import io.sentry.NoOpLogger; import io.sentry.Sentry; -import io.sentry.UserFeedback; import io.sentry.android.core.SentryAndroid; import io.sentry.android.fragment.FragmentLifecycleIntegration; import io.sentry.hints.DiskFlushNotification; @@ -36,7 +36,7 @@ public class SentryMain { * Initialize Sentry * Sentry is used for crash reporting and performance monitoring. The SDK is explcitly configured not to send PII, and server side scrubbing of sensitive data is enabled (which also removes IP addresses) */ - @SuppressLint("RestrictedApi") + @SuppressLint({"RestrictedApi", "UnspecifiedImmutableFlag"}) public static void initialize(final MainApplication mainApplication) { SentryAndroid.init(mainApplication, options -> { // If crash reporting is disabled, stop here. @@ -96,46 +96,10 @@ public class SentryMain { Log.i(TAG, stringBuilder.toString()); } if (MainApplication.isCrashReportingEnabled()) { - // Get user feedback - SentryId sentryId = event.getEventId(); - if (sentryId != null) { - UserFeedback userFeedback = new UserFeedback(sentryId); - // Get the current activity - Activity context = MainApplication.getINSTANCE().getLastCompatActivity(); - // Create a material dialog - new Handler(Looper.getMainLooper()).post(() -> { - if (context != null) { - // Show fields for name, email, and comment, and two buttons: "Submit" and "Cancel" - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); - builder.setTitle(R.string.sentry_dialogue_title); - builder.setMessage(R.string.sentry_dialogue_message); - // Add the text fields, set the text to the previously entered values - EditText name = new EditText(context); - name.setHint(R.string.name); - builder.setView(name); - EditText email = new EditText(context); - email.setHint(R.string.email); - builder.setView(email); - EditText comment = new EditText(context); - comment.setHint(R.string.additional_info); - builder.setView(comment); - // Add the buttons - builder.setPositiveButton(R.string.submit, (dialog, id) -> { - // User clicked "Submit" - userFeedback.setName(name.getText().toString()); - userFeedback.setEmail(email.getText().toString()); - userFeedback.setComments(comment.getText().toString()); - // Send the feedback - Sentry.captureUserFeedback(userFeedback); - }); - builder.setNegativeButton(R.string.cancel, (dialog, id) -> { - // User cancelled the dialog - }); - // Create and show the AlertDialog - builder.create().show(); - } - }); - } + // Save lastEventId to private shared preferences + SharedPreferences sharedPreferences = mainApplication.getSharedPreferences("sentry", Context.MODE_PRIVATE); + sharedPreferences.edit().putString("lastEventId", + Objects.requireNonNull(event.getEventId()).toString()).apply(); return event; } else { // We need to do this to avoid crash delay on crash when the event is dropped @@ -154,6 +118,37 @@ public class SentryMain { } return breadcrumb; }); + // On uncaught exception, set the lastEventId in private sentry preferences + Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> { + SentryId lastEventId = Sentry.captureException(throwable); + SharedPreferences.Editor editor = mainApplication.getSharedPreferences("sentry", 0).edit(); + editor.putString("lastExitReason", "crash"); + editor.apply(); + // Start a new instance of the main activity + // The intent flags ensure that the activity is started as a new task + // and that any existing task is cleared before the activity is started + // This ensures that the activity stack is cleared and the app is restarted + // from the root activity + Intent intent = new Intent(mainApplication, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + // Set an alarm to restart the app one second after it is killed + // This is necessary because the app is killed before the intent is started + // and the intent is ignored if the app is not running + PendingIntent pendingIntent; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + pendingIntent = PendingIntent.getActivity(mainApplication, 0, + intent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE); + } else { + pendingIntent = PendingIntent.getActivity(mainApplication, 0, + intent, PendingIntent.FLAG_CANCEL_CURRENT); + } + AlarmManager alarmManager = (AlarmManager) mainApplication.getSystemService(Context.ALARM_SERVICE); + if (alarmManager != null) { + alarmManager.set(AlarmManager.RTC, System.currentTimeMillis() + 1000, pendingIntent); + } + // Kill the app + System.exit(2); + }); } }); }