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 package de.kuschku.malheur
import android.annotation.SuppressLint
import android.app.Application 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 com.google.gson.GsonBuilder
import de.kuschku.malheur.data.* import de.kuschku.malheur.collectors.ReportCollector
import de.kuschku.malheur.util.* import de.kuschku.malheur.config.ReportConfig
import java.io.File
import java.text.SimpleDateFormat
import java.util.* import java.util.*
object CrashHandler { 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() private val gson = GsonBuilder().disableHtmlEscaping().setPrettyPrinting().create()
data class ReportConfiguration( private val startTime = Date()
val crash: Boolean = true, private var originalHandler: Thread.UncaughtExceptionHandler? = null
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
)
@SuppressLint("HardwareIds") fun init(application: Application, config: ReportConfig = ReportConfig(),
fun init(application: Application, configuration: ReportConfiguration = ReportConfiguration(),
buildConfig: Class<*>?) { buildConfig: Class<*>?) {
val startTime = Date() if (myHandler == null) {
packageManager = application.packageManager
originalHandler = Thread.getDefaultUncaughtExceptionHandler() originalHandler = Thread.getDefaultUncaughtExceptionHandler()
}
config = configuration val reportCollector = ReportCollector(application)
myHandler = Thread.UncaughtExceptionHandler { activeThread, throwable ->
Thread.setDefaultUncaughtExceptionHandler { activeThread, throwable ->
Thread { Thread {
val pid = Process.myPid().toString()
val crashTime = Date()
try { try {
val since = logcatTimeFormatter.format(startTime) val json = gson.toJson(reportCollector.collect(CrashContext(
val data = Report( application = application,
crash = CrashInfo( config = config,
cause = orNull(config.crashCause) { crashingThread = activeThread,
ExceptionInfo(throwable) throwable = throwable,
}, startTime = startTime,
exception = orNull(config.crashException) { crashTime = Date(),
throwable.printStackTraceToString() buildConfig = buildConfig
}, ), config))
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)
println(json) println(json)
} catch (e: Throwable) { } catch (e: Throwable) {
originalHandler.uncaughtException(activeThread, throwable) originalHandler?.uncaughtException(activeThread, throwable)
} }
}.start() }.start()
} }
Thread.setDefaultUncaughtExceptionHandler(myHandler)
} }
private fun getBuildConfigClass(packageName: String) = try { var myHandler: Thread.UncaughtExceptionHandler? = null
Class.forName("$packageName.BuildConfig")
} catch (e: ClassNotFoundException) {
null
}
private fun <T> orNull(condition: Boolean, closure: (() -> T)) = if (condition) {
closure()
} else {
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( ...@@ -4,6 +4,7 @@ data class CrashInfo(
val cause: ExceptionInfo?, val cause: ExceptionInfo?,
val exception: String?, val exception: String?,
val activeThread: ThreadInfo?, val activeThread: ThreadInfo?,
val startTime: Long?, val startTime: String?,
val crashTime: Long? val crashTime: String?,
val configuration: Map<String, Any?>?
) )
...@@ -5,7 +5,6 @@ data class ExceptionInfo( ...@@ -5,7 +5,6 @@ data class ExceptionInfo(
val message: String?, val message: String?,
val localizedMessage: String?, val localizedMessage: String?,
val stackTrace: List<TraceElement>?, val stackTrace: List<TraceElement>?,
val suppressed: List<ExceptionInfo>?,
val cause: ExceptionInfo? val cause: ExceptionInfo?
) { ) {
constructor(throwable: Throwable) : this( constructor(throwable: Throwable) : this(
...@@ -13,7 +12,6 @@ data class ExceptionInfo( ...@@ -13,7 +12,6 @@ data class ExceptionInfo(
message = throwable.message, message = throwable.message,
localizedMessage = throwable.localizedMessage, localizedMessage = throwable.localizedMessage,
stackTrace = throwable.stackTrace?.map(::TraceElement), stackTrace = throwable.stackTrace?.map(::TraceElement),
suppressed = throwable.suppressed?.map(::ExceptionInfo),
cause = throwable.cause?.let(::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