Skip to content
Snippets Groups Projects
Commit 51e8c775 authored by Janne Mareike Koschinski's avatar Janne Mareike Koschinski
Browse files

Cleaned up the CrashReporter

parent 808b0dc6
No related branches found
No related tags found
No related merge requests found
Showing
with 391 additions and 148 deletions
package de.kuschku.malheur
import android.app.Application
import de.kuschku.malheur.config.ReportConfig
import java.util.*
data class CrashContext(
val application: Application,
val config: ReportConfig,
val crashingThread: Thread,
val throwable: Throwable,
val startTime: Date,
val crashTime: Date,
val buildConfig: Class<*>?
)
package de.kuschku.malheur
import android.annotation.SuppressLint
import android.app.Application
import android.content.pm.PackageManager
import android.os.Build
import android.os.Debug
import android.os.Environment
import android.os.Process
import android.provider.Settings
import com.google.gson.GsonBuilder
import de.kuschku.malheur.data.*
import de.kuschku.malheur.util.*
import java.io.File
import java.text.SimpleDateFormat
import de.kuschku.malheur.collectors.ReportCollector
import de.kuschku.malheur.config.ReportConfig
import java.util.*
object CrashHandler {
private lateinit var packageManager: PackageManager
private lateinit var config: ReportConfiguration
private lateinit var originalHandler: Thread.UncaughtExceptionHandler
private val logcatTimeFormatter = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
private val gson = GsonBuilder().disableHtmlEscaping().setPrettyPrinting().create()
data class ReportConfiguration(
val crash: Boolean = true,
val crashCause: Boolean = true,
val crashException: Boolean = true,
val crashActiveThread: Boolean = true,
val crashStartTime: Boolean = true,
val crashCrashTime: Boolean = true,
val threads: Boolean = true,
val logcat: List<String> = listOf("main", "events", "crash"),
val application: Boolean = true,
val applicationVersionName: Boolean = true,
val applicationVersionCode: Boolean = true,
val applicationBuildConfig: Boolean = true,
val device: Boolean = true,
val deviceBuild: Boolean = true,
val deviceVersion: Boolean = true,
val deviceInstallationId: Boolean = true,
val deviceProcessor: Boolean = true,
val deviceRuntime: Boolean = true,
val environment: Boolean = true,
val environmentPaths: Boolean = true,
val environmentMemory: Boolean = true
)
private val startTime = Date()
private var originalHandler: Thread.UncaughtExceptionHandler? = null
@SuppressLint("HardwareIds")
fun init(application: Application, configuration: ReportConfiguration = ReportConfiguration(),
fun init(application: Application, config: ReportConfig = ReportConfig(),
buildConfig: Class<*>?) {
val startTime = Date()
packageManager = application.packageManager
if (myHandler == null) {
originalHandler = Thread.getDefaultUncaughtExceptionHandler()
}
config = configuration
Thread.setDefaultUncaughtExceptionHandler { activeThread, throwable ->
val reportCollector = ReportCollector(application)
myHandler = Thread.UncaughtExceptionHandler { activeThread, throwable ->
Thread {
val pid = Process.myPid().toString()
val crashTime = Date()
try {
val since = logcatTimeFormatter.format(startTime)
val data = Report(
crash = CrashInfo(
cause = orNull(config.crashCause) {
ExceptionInfo(throwable)
},
exception = orNull(config.crashException) {
throwable.printStackTraceToString()
},
activeThread = orNull(config.crashActiveThread) {
ThreadInfo(activeThread)
},
startTime = orNull(config.crashStartTime) {
startTime.time
},
crashTime = orNull(config.crashCrashTime) {
crashTime.time
}
),
threads = orNull(config.threads) {
Thread.getAllStackTraces()
.filterKeys { it != Thread.currentThread() }
.map { (thread, stackTrace) ->
ThreadInfo(thread, stackTrace)
}
},
logcat = config.logcat.map { buffer ->
buffer to readLogCat(since, buffer, pid)
}.toMap(),
application = orNull(config.application) {
AppInfo(
versionName = orNull(config.applicationVersionName) {
packageManager.getPackageInfo(application.packageName, 0).versionName
},
versionCode = orNull(config.applicationVersionCode) {
packageManager.getPackageInfo(application.packageName, 0).versionCode
},
buildConfig = orNull(config.applicationBuildConfig) {
reflectionCollectConstants(
buildConfig ?: getBuildConfigClass(application.packageName))
}
)
},
device = orNull(config.device) {
DeviceInfo(
build = orNull(config.deviceBuild) {
reflectionCollectConstants(Build::class.java)
},
version = orNull(config.deviceVersion) {
reflectionCollectConstants(Build.VERSION::class.java)
},
installationId = orNull(config.deviceInstallationId) {
Settings.Secure.getString(
application.contentResolver, Settings.Secure.ANDROID_ID
)
},
processor = orNull(config.deviceProcessor) {
readProcInfo()
}
)
},
environment = orNull(config.environment) {
mapOf(
"paths" to orNull(config.environmentPaths) {
reflectionCollectGetters(Environment::class.java)?.map { (key, value) ->
key to if (value is File) {
value.canonicalPath
} else {
value
}
}?.toMap()
},
"memory" to orNull(config.environmentMemory) {
val memoryInfo = Debug.MemoryInfo()
Debug.getMemoryInfo(memoryInfo)
MemoryInfo(memoryInfo)
}
)
}
)
val json = gson.toJson(data)
val json = gson.toJson(reportCollector.collect(CrashContext(
application = application,
config = config,
crashingThread = activeThread,
throwable = throwable,
startTime = startTime,
crashTime = Date(),
buildConfig = buildConfig
), config))
println(json)
} catch (e: Throwable) {
originalHandler.uncaughtException(activeThread, throwable)
originalHandler?.uncaughtException(activeThread, throwable)
}
}.start()
}
Thread.setDefaultUncaughtExceptionHandler(myHandler)
}
private fun getBuildConfigClass(packageName: String) = try {
Class.forName("$packageName.BuildConfig")
} catch (e: ClassNotFoundException) {
null
}
private fun <T> orNull(condition: Boolean, closure: (() -> T)) = if (condition) {
closure()
} else {
null
}
var myHandler: Thread.UncaughtExceptionHandler? = null
}
package de.kuschku.malheur.collectors
import android.app.Application
import de.kuschku.malheur.CrashContext
import de.kuschku.malheur.config.AppConfig
import de.kuschku.malheur.data.AppInfo
import de.kuschku.malheur.util.reflectionCollectConstants
class AppCollector(private val application: Application) : Collector<AppInfo, AppConfig> {
override fun collect(context: CrashContext, config: AppConfig) = AppInfo(
versionName = collectIf(config.versionName) {
application.packageManager.getPackageInfo(application.packageName, 0).versionName
},
versionCode = collectIf(config.versionCode) {
application.packageManager.getPackageInfo(application.packageName, 0).versionCode
},
buildConfig = collectIf(config.buildConfig) {
reflectionCollectConstants(
context.buildConfig ?: getBuildConfigClass(application.packageName)
)
}
)
private fun getBuildConfigClass(packageName: String) = try {
Class.forName("$packageName.BuildConfig")
} catch (e: ClassNotFoundException) {
null
}
}
package de.kuschku.malheur.collectors
import de.kuschku.malheur.CrashContext
interface Collector<out DataType, ConfigType> {
fun collect(context: CrashContext, config: ConfigType): DataType?
}
inline fun <DataType, ConfigType> Collector<DataType, ConfigType>.collectIf(
context: CrashContext,
config: ConfigType?
) = if (config != null)
collect(context, config)
else
null
inline fun <DataType> collectIf(enabled: Boolean, closure: () -> DataType?) = if (enabled)
closure()
else
null
package de.kuschku.malheur.collectors
import android.app.Application
import android.content.res.Configuration
import android.util.SparseArray
import de.kuschku.malheur.CrashContext
import java.lang.reflect.Modifier
class ConfigurationCollector(private val application: Application) :
Collector<Map<String, Any?>, Boolean> {
private val configValueInfo = mutableMapOf<String, SparseArray<String>>()
private val configurationFields = listOf(
FieldDefinition(
fieldName = "mcc"
),
FieldDefinition(
fieldName = "mnc"
),
FieldDefinition(
fieldName = "screenHeightDp"
),
FieldDefinition(
fieldName = "screenWidthDp"
),
FieldDefinition(
fieldName = "smallestScreenWidthDp"
),
FieldDefinition(
fieldName = "navigation",
enumPrefix = "NAVIGATION"
),
FieldDefinition(
fieldName = "navigationHidden",
enumPrefix = "NAVIGATIONHIDDEN"
),
FieldDefinition(
fieldName = "orientation",
enumPrefix = "ORIENTATION"
),
FieldDefinition(
fieldName = "screenLayout",
enumPrefix = "SCREENLAYOUT",
isFlag = true
),
FieldDefinition(
fieldName = "touchscreen",
enumPrefix = "TOUCHSCREEN"
),
FieldDefinition(
fieldName = "uiMode",
enumPrefix = "UI_MODE",
isFlag = true
)
)
init {
val configurationFieldPrefixes = configurationFields.map(FieldDefinition::enumPrefix)
Configuration::class.java.declaredFields.filter {
Modifier.isStatic(it.modifiers)
}.filter {
it.type == Int::class.java
}.filterNot {
it.name.endsWith("_MASK")
}.forEach { field ->
val group = configurationFieldPrefixes.find { field.name.startsWith(it + "_") }
if (group != null) {
val value = field.name.substring(group.length + 1)
configValueInfo.getOrPut(group, ::SparseArray).put(field.getInt(null), value)
}
}
}
override fun collect(context: CrashContext, config: Boolean) = configurationFields.map {
it to Configuration::class.java.getDeclaredField(it.fieldName)
}.filter { (_, field) ->
field != null && !Modifier.isStatic(field.modifiers)
}.map { (info, field) ->
val groupInfo = configValueInfo[info.enumPrefix]
if (groupInfo != null) {
val value = field.getInt(application.resources.configuration)
if (info.isFlag) {
info.fieldName to (0 until groupInfo.size()).map { idx ->
groupInfo.keyAt(idx) to groupInfo.valueAt(idx)
}.filter { (key, _) ->
value and key != 0
}.map { (_, value) ->
value
}.toList()
} else {
val valueConstant = groupInfo[value]
if (valueConstant == null) {
info.fieldName to value
} else {
info.fieldName to valueConstant
}
}
} else {
val value = field.getInt(application.resources.configuration)
info.fieldName to value
}
}.toMap()
class FieldDefinition(
val fieldName: String,
val enumPrefix: String? = null,
val isFlag: Boolean = false
)
}
package de.kuschku.malheur.collectors
import android.app.Application
import de.kuschku.malheur.CrashContext
import de.kuschku.malheur.config.CrashConfig
import de.kuschku.malheur.data.CrashInfo
import de.kuschku.malheur.data.ExceptionInfo
import de.kuschku.malheur.data.ThreadInfo
import de.kuschku.malheur.util.printStackTraceToString
import java.text.SimpleDateFormat
import java.util.*
class CrashCollector(application: Application) : Collector<CrashInfo, CrashConfig> {
private val configurationCollector = ConfigurationCollector(application)
private val isoFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX", Locale.US)
override fun collect(context: CrashContext, config: CrashConfig) = CrashInfo(
cause = collectIf(config.cause) {
ExceptionInfo(context.throwable)
},
exception = collectIf(config.exception) {
context.throwable.printStackTraceToString()
},
activeThread = collectIf(config.activeThread) {
ThreadInfo(context.crashingThread)
},
startTime = collectIf(config.startTime) {
isoFormatter.format(context.startTime)
},
crashTime = collectIf(config.crashTime) {
isoFormatter.format(context.crashTime)
},
configuration = configurationCollector.collectIf(context, config.configuration)
)
}
package de.kuschku.malheur.collectors
import android.annotation.SuppressLint
import android.app.Application
import android.os.Build
import android.provider.Settings
import de.kuschku.malheur.CrashContext
import de.kuschku.malheur.config.DeviceConfig
import de.kuschku.malheur.data.DeviceInfo
import de.kuschku.malheur.util.readProcInfo
import de.kuschku.malheur.util.reflectionCollectConstants
class DeviceCollector(private val application: Application) : Collector<DeviceInfo, DeviceConfig> {
@SuppressLint("HardwareIds")
override fun collect(context: CrashContext, config: DeviceConfig) = DeviceInfo(
build = collectIf(config.build) {
reflectionCollectConstants(Build::class.java)
},
version = collectIf(config.version) {
reflectionCollectConstants(Build.VERSION::class.java)
},
installationId = collectIf(config.installationId) {
Settings.Secure.getString(application.contentResolver, Settings.Secure.ANDROID_ID)
},
processor = collectIf(config.processor) {
readProcInfo()
}
)
}
package de.kuschku.malheur.collectors
import android.app.Application
import android.os.Debug
import android.os.Environment
import de.kuschku.malheur.CrashContext
import de.kuschku.malheur.config.EnvConfig
import de.kuschku.malheur.data.MemoryInfo
import de.kuschku.malheur.util.reflectionCollectGetters
import java.io.File
class EnvCollector(application: Application) : Collector<Map<String, Any?>, EnvConfig> {
override fun collect(context: CrashContext, config: EnvConfig) = mapOf(
"paths" to collectIf(config.paths) {
reflectionCollectGetters(
Environment::class.java)?.map { (key, value) ->
key to if (value is File) {
value.canonicalPath
} else {
value
}
}?.toMap()
},
"memory" to collectIf(config.memory) {
val memoryInfo = Debug.MemoryInfo()
Debug.getMemoryInfo(memoryInfo)
MemoryInfo(memoryInfo)
}
)
}
package de.kuschku.malheur.collectors
import android.app.Application
import android.os.Process
import de.kuschku.malheur.CrashContext
import de.kuschku.malheur.config.LogConfig
import de.kuschku.malheur.util.readLogCat
import java.text.SimpleDateFormat
import java.util.*
class LogCollector(application: Application) : Collector<Map<String, List<String>>, LogConfig> {
private val logcatTimeFormatter = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
private val pid = Process.myPid().toString()
override fun collect(context: CrashContext, config: LogConfig): Map<String, List<String>> {
val since = logcatTimeFormatter.format(context.startTime)
return config.buffers.map { buffer ->
buffer to readLogCat(since, buffer, pid)
}.toMap()
}
}
package de.kuschku.malheur.collectors
import android.app.Application
import de.kuschku.malheur.CrashContext
import de.kuschku.malheur.config.ReportConfig
import de.kuschku.malheur.data.Report
import de.kuschku.malheur.data.ThreadInfo
class ReportCollector(application: Application) : Collector<Report, ReportConfig> {
private val logcatCollector = LogCollector(application)
private val crashCollector = CrashCollector(application)
private val applicationCollector = AppCollector(application)
private val deviceCollector = DeviceCollector(application)
private val environmentCollector = EnvCollector(application)
override fun collect(context: CrashContext, config: ReportConfig) = Report(
crash = crashCollector.collectIf(context, config.crash),
threads = collectIf(config.threads) {
Thread.getAllStackTraces()
.filterKeys { it != Thread.currentThread() }
.map { (thread, stackTrace) ->
ThreadInfo(thread, stackTrace)
}
},
logcat = logcatCollector.collectIf(context, config.logcat),
application = applicationCollector.collectIf(context, config.application),
device = deviceCollector.collectIf(context, config.device),
environment = environmentCollector.collectIf(context, config.environment)
)
}
package de.kuschku.malheur.config
data class AppConfig(
val versionName: Boolean = true,
val versionCode: Boolean = true,
val buildConfig: Boolean = true
)
package de.kuschku.malheur.config
data class CrashConfig(
val cause: Boolean = true,
val exception: Boolean = true,
val activeThread: Boolean = true,
val startTime: Boolean = true,
val crashTime: Boolean = true,
val configuration: Boolean = true
)
package de.kuschku.malheur.config
data class DeviceConfig(
val build: Boolean = true,
val version: Boolean = true,
val installationId: Boolean = true,
val processor: Boolean = true,
val runtime: Boolean = true
)
package de.kuschku.malheur.config
data class EnvConfig(
val paths: Boolean = true,
val memory: Boolean = true
)
package de.kuschku.malheur.config
data class LogConfig(
val buffers: List<String> = listOf("main", "events", "crash")
)
package de.kuschku.malheur.config
data class ReportConfig(
val crash: CrashConfig? = CrashConfig(),
val threads: Boolean = true,
val logcat: LogConfig? = LogConfig(),
val application: AppConfig? = AppConfig(),
val device: DeviceConfig? = DeviceConfig(),
val environment: EnvConfig? = EnvConfig()
)
......@@ -4,6 +4,7 @@ data class CrashInfo(
val cause: ExceptionInfo?,
val exception: String?,
val activeThread: ThreadInfo?,
val startTime: Long?,
val crashTime: Long?
val startTime: String?,
val crashTime: String?,
val configuration: Map<String, Any?>?
)
......@@ -5,7 +5,6 @@ data class ExceptionInfo(
val message: String?,
val localizedMessage: String?,
val stackTrace: List<TraceElement>?,
val suppressed: List<ExceptionInfo>?,
val cause: ExceptionInfo?
) {
constructor(throwable: Throwable) : this(
......@@ -13,7 +12,6 @@ data class ExceptionInfo(
message = throwable.message,
localizedMessage = throwable.localizedMessage,
stackTrace = throwable.stackTrace?.map(::TraceElement),
suppressed = throwable.suppressed?.map(::ExceptionInfo),
cause = throwable.cause?.let(::ExceptionInfo)
)
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment