____ _ _______ __
/ __ \ | /| / / __/ |/ /
/ /_/ / |/ |/ / _// /
\____/|__/|__/___/_||_/
owenclave / app/src/main/java/io/nekohasekai/sagernet/utils/CrashHandler.kt
/******************************************************************************
* Copyright (C) 2021 by nekohasekai *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.utils
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Build
import android.util.Log
import androidx.core.content.FileProvider
import com.jakewharton.processphoenix.ProcessPhoenix
import io.nekohasekai.sagernet.BuildConfig
import io.nekohasekai.sagernet.database.preference.PublicDatabase
import io.nekohasekai.sagernet.ktx.Logs
import io.nekohasekai.sagernet.ktx.app
import io.nekohasekai.sagernet.ktx.use
import java.io.*
import java.text.SimpleDateFormat
import java.util.*
import java.util.regex.Pattern
object CrashHandler : Thread.UncaughtExceptionHandler {
@Suppress("UNNECESSARY_SAFE_CALL")
override fun uncaughtException(thread: Thread, throwable: Throwable) {
Log.e("Owenclave", "uncaughtException", throwable)
val logFile = File.createTempFile("Owenclave Crash Report ",
".log",
File(app.externalCacheDir, "log").also { it.mkdirs() }
)
var report = buildReportHeader()
report += "\n"
report += "Thread: $thread\n\n"
report += formatThrowable(throwable) + "\n\n"
report += "Logcat: \n\n"
logFile.writeText(report)
try {
Runtime.getRuntime().exec(arrayOf("logcat", "-d")).inputStream.use(
FileOutputStream(
logFile, true
)
)
} catch (e: IOException) {
Logs.w(e)
logFile.appendText("Export logcat error: " + formatThrowable(e))
}
ProcessPhoenix.triggerRebirth(
app, Intent.createChooser(
Intent(Intent.ACTION_SEND).setType("text/plain")
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.putExtra(
Intent.EXTRA_STREAM, FileProvider.getUriForFile(
app, BuildConfig.APPLICATION_ID + ".cache", logFile
)
), app.getString(androidx.appcompat.R.string.abc_shareactionprovider_share_with)
)
)
}
fun formatThrowable(throwable: Throwable): String {
var format = throwable.javaClass.name
val message = throwable.message
if (!message.isNullOrBlank()) {
format += ": $message"
}
format += "\n"
format += throwable.stackTrace.joinToString("\n") {
" at ${it.className}.${it.methodName}(${it.fileName}:${if (it.isNativeMethod) "native" else it.lineNumber})"
}
val cause = throwable.cause
if (cause != null) {
format += "\n\nCaused by: " + formatThrowable(cause)
}
return format
}
fun buildReportHeader(): String {
var report = ""
report += "Owenclave ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE}) ${BuildConfig.FLAVOR.uppercase()}\n"
report += "Date: ${getCurrentMilliSecondUTCTimeStamp()}\n\n"
report += "OS_VERSION: ${getSystemPropertyWithAndroidAPI("os.version")}\n"
report += "SDK_INT: ${Build.VERSION.SDK_INT}\n"
report += if ("REL" == Build.VERSION.CODENAME) {
"RELEASE: ${Build.VERSION.RELEASE}"
} else {
"CODENAME: ${Build.VERSION.CODENAME}"
} + "\n"
report += "ID: ${Build.ID}\n"
report += "DISPLAY: ${Build.DISPLAY}\n"
report += "INCREMENTAL: ${Build.VERSION.INCREMENTAL}\n"
val systemProperties = getSystemProperties()
report += "SECURITY_PATCH: ${systemProperties.getProperty("ro.build.version.security_patch")}\n"
report += "IS_DEBUGGABLE: ${systemProperties.getProperty("ro.debuggable")}\n"
report += "IS_EMULATOR: ${systemProperties.getProperty("ro.boot.qemu")}\n"
report += "IS_TREBLE_ENABLED: ${systemProperties.getProperty("ro.treble.enabled")}\n"
report += "TYPE: ${Build.TYPE}\n"
report += "TAGS: ${Build.TAGS}\n\n"
report += "MANUFACTURER: ${Build.MANUFACTURER}\n"
report += "BRAND: ${Build.BRAND}\n"
report += "MODEL: ${Build.MODEL}\n"
report += "PRODUCT: ${Build.PRODUCT}\n"
report += "BOARD: ${Build.BOARD}\n"
report += "HARDWARE: ${Build.HARDWARE}\n"
report += "DEVICE: ${Build.DEVICE}\n"
report += "SUPPORTED_ABIS: ${
Build.SUPPORTED_ABIS.filter { it.isNotBlank() }.joinToString(", ")
}\n\n"
try {
report += "Settings: \n"
for (pair in PublicDatabase.kvPairDao.all()) {
report += "\n"
report += pair.key + ": " + pair.toString()
}
}catch (e: Exception) {
report += "Export settings failed: " + formatThrowable(e)
}
report += "\n\n"
return report
}
private fun getSystemProperties(): Properties {
val systemProperties = Properties()
// getprop commands returns values in the format `[key]: [value]`
// Regex matches string starting with a literal `[`,
// followed by one or more characters that do not match a closing square bracket as the key,
// followed by a literal `]: [`,
// followed by one or more characters as the value,
// followed by string ending with literal `]`
// multiline values will be ignored
val propertiesPattern = Pattern.compile("^\\[([^]]+)]: \\[(.+)]$")
try {
val process = ProcessBuilder().command("/system/bin/getprop")
.redirectErrorStream(true)
.start()
val inputStream = process.inputStream
val bufferedReader = BufferedReader(InputStreamReader(inputStream))
var line: String?
var key: String?
var value: String?
while (bufferedReader.readLine().also { line = it } != null) {
val matcher = propertiesPattern.matcher(line!!)
if (matcher.matches()) {
key = matcher.group(1)
value = matcher.group(2)
if (!key.isNullOrBlank() && !value.isNullOrBlank()) systemProperties[key] = value
}
}
bufferedReader.close()
process.destroy()
} catch (e: IOException) {
Logs.e(
"Failed to get run \"/system/bin/getprop\" to get system properties.", e
)
}
//for (String key : systemProperties.stringPropertyNames()) {
// Logger.logVerbose(key + ": " + systemProperties.get(key));
//}
return systemProperties
}
private fun getSystemPropertyWithAndroidAPI(property: String): String? {
return try {
System.getProperty(property)
} catch (e: Exception) {
Logs.e("Failed to get system property \"" + property + "\":" + e.message)
null
}
}
@SuppressLint("SimpleDateFormat")
private fun getCurrentMilliSecondUTCTimeStamp(): String {
val df = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS z")
df.timeZone = TimeZone.getTimeZone("UTC")
return df.format(Date())
}
}