package eu.kanade.tachiyomi.util import android.content.Context import android.os.Build import dev.zacsweers.metro.Inject import eu.kanade.domain.base.BasePreferences import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.network.NetworkPreferences import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.system.WebViewUtil import eu.kanade.tachiyomi.util.system.createFileInCacheDir import eu.kanade.tachiyomi.util.system.toShareIntent import eu.kanade.tachiyomi.util.system.toast import kotlinx.datetime.TimeZone import kotlinx.datetime.offsetAt import kotlinx.datetime.toLocalDateTime import tachiyomi.core.common.util.lang.withNonCancellableContext import tachiyomi.core.common.util.lang.withUIContext import kotlin.time.Clock @Inject class CrashLogUtil( private val context: Context, private val extensionManager: ExtensionManager, private val preferences: BasePreferences, private val networkPreferences: NetworkPreferences, ) { suspend fun dumpLogs(exception: Throwable? = null) = withNonCancellableContext { try { val file = context.createFileInCacheDir("mihon_crash_logs.txt") file.appendText(getDebugInfo() + "\n\n") getExtensionsInfo()?.let { file.appendText("$it\n\n") } exception?.let { file.appendText("$it\n\n") } val logPriority = if (networkPreferences.verboseLogging.get()) "V" else "E" Runtime.getRuntime().exec("logcat *:$logPriority -d -v year -v zone -f ${file.absolutePath}").waitFor() val uri = file.getUriCompat(context) context.startActivity(uri.toShareIntent(context, "text/plain")) } catch (_: Throwable) { withUIContext { context.toast("Failed to get logs") } } } fun getDebugInfo(): String { val now = Clock.System.now() val tz = TimeZone.currentSystemDefault() return """ App ID: ${BuildConfig.APPLICATION_ID} App version: ${BuildConfig.VERSION_NAME} (${BuildConfig.COMMIT_SHA}, ${BuildConfig.VERSION_CODE}, ${BuildConfig.BUILD_TIME}) Installation ID: ${preferences.installationId.get()} Android version: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT}; build ${Build.DISPLAY}) Device brand: ${Build.BRAND} Device manufacturer: ${Build.MANUFACTURER} Device name: ${Build.DEVICE} (${Build.PRODUCT}) Device model: ${Build.MODEL} WebView: ${WebViewUtil.getVersion(context)} Current time: ${now.toLocalDateTime(tz)}${tz.offsetAt(now)} """.trimIndent() } private suspend fun getExtensionsInfo(): String? { val availableExtensions = extensionManager.availableExtensionsFlow.value.associateBy { it.pkgName } val outdatedInfoList = extensionManager.getLoadedExtensions() .sortedBy { it.name } .mapNotNull { val availableExtension = availableExtensions[it.pkgName] val hasUpdate = (availableExtension?.versionCode ?: 0) > it.versionCode if (!hasUpdate && !it.isObsolete) return@mapNotNull null """ - ${it.name} Installed: ${it.versionName} / Available: ${availableExtension?.versionName ?: "?"} Orphaned: ${it.isObsolete} """.trimIndent() } val notLoadedInfoList = extensionManager.getNotLoadedExtensions() .sortedBy { it.name } .map { extension -> buildString { appendLine("- ${extension.name}") appendLine(" Installed: ${extension.versionName} (lib ${extension.libVersion ?: "?"})") append(" Not loaded: ${extension.reason.description}") val reason = extension.reason if (reason is Extension.NotLoaded.Reason.Failed) { appendLine() append(reason.stackTrace.trimEnd().prependIndent(" ")) } } } val extensionInfoList = outdatedInfoList + notLoadedInfoList return if (extensionInfoList.isNotEmpty()) { (listOf("Problematic extensions:") + extensionInfoList) .joinToString("\n") } else { null } } } private val Extension.NotLoaded.Reason.description: String get() = when (this) { is Extension.NotLoaded.Reason.Untrusted -> "Untrusted" Extension.NotLoaded.Reason.Filtered -> "Filtered by content warning" Extension.NotLoaded.Reason.Unsigned -> "Unsigned" Extension.NotLoaded.Reason.UnsupportedLibVersion -> "Unsupported lib version" Extension.NotLoaded.Reason.Malformed -> "Malformed" is Extension.NotLoaded.Reason.Failed -> "Failed ($message)" }