diff --git a/.gitignore b/.gitignore index faf530b..21e8403 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.iml .gradle +/sentry.properties /local.properties /.idea/ .DS_Store diff --git a/app/build.gradle b/app/build.gradle index 48f09f7..53bede2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,7 @@ plugins { id 'com.android.application' id 'com.mikepenz.aboutlibraries.plugin' + id "io.sentry.android.gradle" version "3.1.5" } android { @@ -78,8 +79,25 @@ aboutLibraries { additionalLicenses = ["LGPL_3_0_only"] } +sentry { + ignoredBuildTypes = ["debug"] + + includeProguardMapping = true + + autoUploadProguardMapping = isLocalSentry + + tracingInstrumentation { + enabled = false + } + + autoInstallation { + enabled = false + } +} + configurations { implementation.exclude group: 'org.jetbrains' , module: 'annotations' + implementation.exclude group: 'io.sentry' , module: 'sentry-android-okhttp' } dependencies { @@ -109,8 +127,8 @@ dependencies { implementation 'com.github.Fox2Code:AndroidANSI:1.0.1' // Error reporting - implementation 'io.sentry:sentry-android:6.4.0' - implementation 'io.sentry:sentry-android-fragment:6.4.0' + implementation 'io.sentry:sentry-android:6.4.1' + implementation 'io.sentry:sentry-android-fragment:6.4.1' // Markdown implementation "io.noties.markwon:core:4.6.2" diff --git a/app/src/main/java/com/fox2code/mmm/MainApplication.java b/app/src/main/java/com/fox2code/mmm/MainApplication.java index 6614bb8..fcb7d84 100644 --- a/app/src/main/java/com/fox2code/mmm/MainApplication.java +++ b/app/src/main/java/com/fox2code/mmm/MainApplication.java @@ -9,8 +9,6 @@ import android.content.res.Resources; import android.graphics.Color; import android.os.Build; import android.os.SystemClock; -import android.system.ErrnoException; -import android.system.Os; import android.text.SpannableStringBuilder; import android.util.Log; @@ -50,8 +48,8 @@ import io.noties.prism4j.annotations.PrismBundle; import io.sentry.JsonObjectWriter; import io.sentry.NoOpLogger; import io.sentry.TypeCheckHint; -import io.sentry.UncaughtExceptionHandlerIntegration; import io.sentry.android.core.SentryAndroid; +import io.sentry.android.fragment.FragmentLifecycleIntegration; import io.sentry.hints.DiskFlushNotification; @PrismBundle( @@ -371,6 +369,7 @@ public class MainApplication extends FoxApplication } SentryAndroid.init(this, options -> { + options.addIntegration(new FragmentLifecycleIntegration(this, true, false)); // Note: Sentry library only take a screenshot of Fox Magisk Module Manager. // The screen shot doesn't and cannot contain other applications (if in multi windows) // status bar and notifications (even if notification shade is pulled down) @@ -381,6 +380,10 @@ public class MainApplication extends FoxApplication options.setAttachScreenshot(true); // User interaction tracing is not needed to get context of crash options.setEnableUserInteractionTracing(false); + // Send client reports has nothing to do with error reporting + options.setSendClientReports(false); + // Auto session tracking has nothing to do with error reporting + options.setEnableAutoSessionTracking(false); // Add a callback that will be used before the event is sent to Sentry. // With this callback, you can modify the event or, when returning null, also discard the event. options.setBeforeSend((event, hint) -> { diff --git a/app/src/main/java/com/fox2code/mmm/XHooks.java b/app/src/main/java/com/fox2code/mmm/XHooks.java index f459b88..4104594 100644 --- a/app/src/main/java/com/fox2code/mmm/XHooks.java +++ b/app/src/main/java/com/fox2code/mmm/XHooks.java @@ -50,11 +50,11 @@ public class XHooks { @Keep public static XRepo addXRepo(String url, String fallbackName) { - return RepoManager.getINSTANCE().addOrGet(url, fallbackName); + return RepoManager.getINSTANCE_UNSAFE().addOrGet(url, fallbackName); } @Keep public static XRepo getXRepo(String url) { - return RepoManager.getINSTANCE().get(url); + return RepoManager.getINSTANCE_UNSAFE().get(url); } } diff --git a/app/src/main/java/com/fox2code/mmm/installer/InstallerActivity.java b/app/src/main/java/com/fox2code/mmm/installer/InstallerActivity.java index 28acaa6..a30d48e 100644 --- a/app/src/main/java/com/fox2code/mmm/installer/InstallerActivity.java +++ b/app/src/main/java/com/fox2code/mmm/installer/InstallerActivity.java @@ -49,6 +49,10 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipInputStream; +import io.sentry.Breadcrumb; +import io.sentry.Sentry; +import io.sentry.SentryLevel; + public class InstallerActivity extends FoxActivity { private static final String TAG = "InstallerActivity"; public LinearProgressIndicator progressIndicator; @@ -99,6 +103,17 @@ public class InstallerActivity extends FoxActivity { return; } Log.i(TAG, "Install link: " + target); + // Note: Sentry only send this info on crash. + if (MainApplication.isCrashReportingEnabled()) { + Breadcrumb breadcrumb = new Breadcrumb(); + breadcrumb.setType("install"); + breadcrumb.setData("target", target); + breadcrumb.setData("name", name); + breadcrumb.setData("checksum", checksum); + breadcrumb.setCategory("app.action.preinstall"); + breadcrumb.setLevel(SentryLevel.INFO); + Sentry.addBreadcrumb(breadcrumb); + } boolean urlMode = target.startsWith("http://") || target.startsWith("https://"); getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); setTitle(name); @@ -138,9 +153,10 @@ public class InstallerActivity extends FoxActivity { !new SuFile(moduleCache.getAbsolutePath()).delete()) Log.e(TAG, "Failed to delete module cache"); String errMessage = "Failed to download module zip"; + byte[] rawModule; try { Log.i(TAG, (urlMode ? "Downloading: " : "Loading: ") + target); - byte[] rawModule = urlMode ? Http.doHttpGet(target, (progress, max, done) -> { + rawModule = urlMode ? Http.doHttpGet(target, (progress, max, done) -> { if (max <= 0 && this.progressIndicator.isIndeterminate()) return; this.runOnUiThread(() -> { @@ -227,6 +243,14 @@ public class InstallerActivity extends FoxActivity { Log.e(TAG, errMessage, e); this.setInstallStateFinished(false, "! " + errMessage, ""); + } catch (OutOfMemoryError e) { + //noinspection UnusedAssignment (Important to avoid OutOfMemoryError) + rawModule = null; // Because reference is kept when calling setInstallStateFinished + if ("Failed to install module zip".equals(errMessage)) + throw e; // Ignore if in installation state. + Log.e(TAG, "Module too large", e); + this.setInstallStateFinished(false, + "! Module is too large to be loaded on this device", ""); } }, "Module install Thread").start(); } @@ -416,6 +440,19 @@ public class InstallerActivity extends FoxActivity { "cd \"" + this.moduleCache.getAbsolutePath() + "\"", installCommand).to(installerController, installerMonitor); } + // Note: Sentry only send this info on crash. + if (MainApplication.isCrashReportingEnabled()) { + Breadcrumb breadcrumb = new Breadcrumb(); + breadcrumb.setType("install"); + breadcrumb.setData("moduleId", moduleId == null ? "" : moduleId); + breadcrumb.setData("isAnyKernel3", anyKernel3 ? "true" : "false"); + breadcrumb.setData("noExtensions", noExtensions ? "true" : "false"); + breadcrumb.setData("ansi", this.installerTerminal + .isAnsiEnabled() ? "enabled" : "disabled"); + breadcrumb.setCategory("app.action.install"); + breadcrumb.setLevel(SentryLevel.INFO); + Sentry.addBreadcrumb(breadcrumb); + } } boolean success = installJob.exec().isSuccess(); // Wait one UI cycle before disabling controller or processing results 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 2543406..d5aed00 100644 --- a/app/src/main/java/com/fox2code/mmm/repo/RepoManager.java +++ b/app/src/main/java/com/fox2code/mmm/repo/RepoManager.java @@ -57,6 +57,23 @@ public final class RepoManager extends SyncManager { private static volatile RepoManager INSTANCE; public static RepoManager getINSTANCE() { + if (INSTANCE == null || !INSTANCE.initialized) { + synchronized (lock) { + if (INSTANCE == null) { + MainApplication mainApplication = MainApplication.getINSTANCE(); + if (mainApplication != null) { + INSTANCE = new RepoManager(mainApplication); + XHooks.onRepoManagerInitialized(); + } else { + throw new RuntimeException("Getting RepoManager too soon!"); + } + } + } + } + return INSTANCE; + } + + public static RepoManager getINSTANCE_UNSAFE() { if (INSTANCE == null) { synchronized (lock) { if (INSTANCE == null) { diff --git a/app/src/main/java/com/fox2code/mmm/utils/Hashes.java b/app/src/main/java/com/fox2code/mmm/utils/Hashes.java index b470b55..c346af6 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/Hashes.java +++ b/app/src/main/java/com/fox2code/mmm/utils/Hashes.java @@ -2,6 +2,8 @@ package com.fox2code.mmm.utils; import android.util.Log; +import java.io.IOException; +import java.io.InputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Locale; @@ -88,6 +90,35 @@ public class Hashes { return hash.equals(checksum.toLowerCase(Locale.ROOT)); } + /** + * Check if the checksum match a file by picking the correct + * hashing algorithm depending on the length of the checksum + */ + public static boolean checkSumMatch(InputStream data, String checksum) throws IOException { + String hash; + if (checksum == null) return false; + String checksumAlgorithm = checkSumName(checksum); + if (checksumAlgorithm == null) { + Log.e(TAG, "No hash algorithm for " + + checksum.length() * 8 + "bit checksums"); + return false; + } + try { + MessageDigest md = MessageDigest.getInstance(checksumAlgorithm); + + byte[] bytes = new byte[2048]; + int nRead; + while ((nRead = data.read(bytes)) > 0) { + md.update(bytes, 0, nRead); + } + hash = bytesToHex(md.digest()); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + Log.d(TAG, "Checksum result (data: " + hash + ",expected: " + checksum + ")"); + return hash.equals(checksum.toLowerCase(Locale.ROOT)); + } + public static boolean checkSumValid(String checksum) { if (checksum == null) return false; switch (checksum.length()) { diff --git a/build.gradle b/build.gradle index 2a9f4e3..9b1aeff 100644 --- a/build.gradle +++ b/build.gradle @@ -6,6 +6,8 @@ buildscript { gradlePluginPortal() } project.ext.latestAboutLibsRelease = "10.4.1-a01" + project.ext.isLocalSentry = new File( + rootProject.rootDir, "sentry.properties").exists() dependencies { classpath 'com.android.tools.build:gradle:7.3.0-rc01' classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:${latestAboutLibsRelease}"