From 808b0dc6ad195733e10fd604402308596b31dd03 Mon Sep 17 00:00:00 2001
From: Janne Koschinski <janne@kuschku.de>
Date: Sun, 1 Oct 2017 04:40:26 +0200
Subject: [PATCH] Performance improvements, implemented a custom crash reporter

---
 app/build.gradle.kts                          |   1 +
 app/src/main/AndroidManifest.xml              |   1 -
 .../kuschku/quasseldroid_ng/QuasseldroidNG.kt |   2 +
 .../quasseldroid_ng/service/QuasselService.kt |  10 +-
 app/src/main/res/layout/activity_main.xml     |   3 +-
 app/src/main/res/layout/content_chat_list.xml |   3 -
 app/src/main/res/layout/content_main.xml      |   1 +
 app/src/main/res/layout/content_nick_list.xml |   2 +-
 app/src/main/res/values/themes_base.xml       |   4 +
 app/src/main/res/values/themes_quassel.xml    |   6 -
 app/src/main/res/values/themes_solarized.xml  |   6 -
 .../de/kuschku/libquassel/session/Backend.kt  |   5 +-
 .../libquassel/session/SessionManager.kt      |  73 +++-
 malheur/build.gradle.kts                      | 401 ++++++++++++++++++
 malheur/proguard-rules.pro                    |  21 +
 malheur/src/main/AndroidManifest.xml          |   6 +
 .../java/de/kuschku/malheur/CrashHandler.kt   | 166 ++++++++
 .../java/de/kuschku/malheur/data/AppInfo.kt   |   7 +
 .../java/de/kuschku/malheur/data/CrashInfo.kt |   9 +
 .../de/kuschku/malheur/data/DeviceInfo.kt     |   8 +
 .../de/kuschku/malheur/data/DisplayInfo.kt    |  24 ++
 .../de/kuschku/malheur/data/ExceptionInfo.kt  |  19 +
 .../de/kuschku/malheur/data/MetricsInfo.kt    |  21 +
 .../java/de/kuschku/malheur/data/Report.kt    |  10 +
 .../de/kuschku/malheur/data/RuntimeInfo.kt    |  31 ++
 .../de/kuschku/malheur/data/ThreadInfo.kt     |  21 +
 .../de/kuschku/malheur/data/TraceElement.kt   |  17 +
 .../de/kuschku/malheur/util/DisplayHelper.kt  |  10 +
 .../de/kuschku/malheur/util/LogCatHelper.kt   |   9 +
 .../de/kuschku/malheur/util/ProcInfohelper.kt |  13 +
 .../kuschku/malheur/util/ReflectionHelper.kt  |  26 ++
 .../kuschku/malheur/util/ThrowableHelper.kt   |  11 +
 settings.gradle                               |   2 +-
 33 files changed, 899 insertions(+), 50 deletions(-)
 create mode 100644 malheur/build.gradle.kts
 create mode 100644 malheur/proguard-rules.pro
 create mode 100644 malheur/src/main/AndroidManifest.xml
 create mode 100644 malheur/src/main/java/de/kuschku/malheur/CrashHandler.kt
 create mode 100644 malheur/src/main/java/de/kuschku/malheur/data/AppInfo.kt
 create mode 100644 malheur/src/main/java/de/kuschku/malheur/data/CrashInfo.kt
 create mode 100644 malheur/src/main/java/de/kuschku/malheur/data/DeviceInfo.kt
 create mode 100644 malheur/src/main/java/de/kuschku/malheur/data/DisplayInfo.kt
 create mode 100644 malheur/src/main/java/de/kuschku/malheur/data/ExceptionInfo.kt
 create mode 100644 malheur/src/main/java/de/kuschku/malheur/data/MetricsInfo.kt
 create mode 100644 malheur/src/main/java/de/kuschku/malheur/data/Report.kt
 create mode 100644 malheur/src/main/java/de/kuschku/malheur/data/RuntimeInfo.kt
 create mode 100644 malheur/src/main/java/de/kuschku/malheur/data/ThreadInfo.kt
 create mode 100644 malheur/src/main/java/de/kuschku/malheur/data/TraceElement.kt
 create mode 100644 malheur/src/main/java/de/kuschku/malheur/util/DisplayHelper.kt
 create mode 100644 malheur/src/main/java/de/kuschku/malheur/util/LogCatHelper.kt
 create mode 100644 malheur/src/main/java/de/kuschku/malheur/util/ProcInfohelper.kt
 create mode 100644 malheur/src/main/java/de/kuschku/malheur/util/ReflectionHelper.kt
 create mode 100644 malheur/src/main/java/de/kuschku/malheur/util/ThrowableHelper.kt

diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 7f60f209e..1af10372b 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -116,6 +116,7 @@ dependencies {
   kapt("com.jakewharton:butterknife-compiler:8.7.0")
 
   implementation(project(":lib"))
+  implementation(project(":malheur"))
 
   testImplementation("android.arch.persistence.room:testing:1.0.0-alpha9")
   testImplementation("junit:junit:4.12")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4bc36f436..4e3e33c81 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,7 +3,6 @@
   package="de.kuschku.quasseldroid_ng">
 
   <uses-permission android:name="android.permission.INTERNET" />
-  <uses-permission android:name="android.permission.READ_LOGS" />
 
   <application
     android:name=".QuasseldroidNG"
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/QuasseldroidNG.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/QuasseldroidNG.kt
index 0be1ee8a0..78add5c87 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/QuasseldroidNG.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/QuasseldroidNG.kt
@@ -6,6 +6,7 @@ import android.content.pm.ShortcutInfo
 import android.content.pm.ShortcutManager
 import android.graphics.drawable.Icon
 import android.os.Build
+import de.kuschku.malheur.CrashHandler
 import de.kuschku.quasseldroid_ng.util.compatibility.AndroidCompatibilityUtils
 import de.kuschku.quasseldroid_ng.util.compatibility.AndroidLoggingHandler
 import de.kuschku.quasseldroid_ng.util.compatibility.AndroidStreamChannelFactory
@@ -17,6 +18,7 @@ class QuasseldroidNG : Application() {
   }
 
   override fun onCreate() {
+    CrashHandler.init(this, buildConfig = BuildConfig::class.java)
     super.onCreate()
 
     // Init compatibility utils
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/service/QuasselService.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/service/QuasselService.kt
index 4d0f7ceba..50a88bf38 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/service/QuasselService.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/service/QuasselService.kt
@@ -5,10 +5,7 @@ import android.arch.lifecycle.Observer
 import android.content.Intent
 import android.os.Binder
 import de.kuschku.libquassel.protocol.*
-import de.kuschku.libquassel.session.Backend
-import de.kuschku.libquassel.session.ISession
-import de.kuschku.libquassel.session.SessionManager
-import de.kuschku.libquassel.session.SocketAddress
+import de.kuschku.libquassel.session.*
 import de.kuschku.quasseldroid_ng.BuildConfig
 import de.kuschku.quasseldroid_ng.R
 import de.kuschku.quasseldroid_ng.persistence.QuasselDatabase
@@ -114,8 +111,9 @@ class QuasselService : LifecycleService() {
       supportedProtocols = listOf(Protocol.Datastream)
     )
     sessionManager.state
-      .distinctUntilChanged()
-      .debounce(50, TimeUnit.MILLISECONDS)
+      .filter { it == ConnectionState.DISCONNECTED }
+      .delay(200, TimeUnit.MILLISECONDS)
+      .throttleFirst(1, TimeUnit.SECONDS)
       .toLiveData()
       .observe(this, Observer {
         sessionManager.reconnect()
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 98d830b5d..b5f90adae 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -21,7 +21,6 @@
         android:id="@+id/toolbar"
         android:layout_width="match_parent"
         android:layout_height="?attr/actionBarSize"
-        android:background="?attr/colorPrimary"
         app:contentInsetStartWithNavigation="0dp"
         app:popupTheme="@style/Widget.PopupOverlay">
 
@@ -94,7 +93,7 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:layout_gravity="start"
-    android:background="?android:attr/windowBackground"
+    android:background="?attr/colorBackground"
     android:fitsSystemWindows="true"
     app:insetForeground="?attr/colorPrimaryDark">
 
diff --git a/app/src/main/res/layout/content_chat_list.xml b/app/src/main/res/layout/content_chat_list.xml
index a5bf5a98e..228f6f5e9 100644
--- a/app/src/main/res/layout/content_chat_list.xml
+++ b/app/src/main/res/layout/content_chat_list.xml
@@ -13,8 +13,6 @@
       android:id="@+id/chatListToolbar"
       android:layout_width="match_parent"
       android:layout_height="?attr/actionBarSize"
-      android:background="?attr/colorPrimary"
-      android:theme="?attr/actionBarTheme"
       app:contentInsetStartWithNavigation="0dp"
       app:popupTheme="@style/Widget.PopupOverlay">
 
@@ -22,7 +20,6 @@
         android:id="@+id/chatListSpinner"
         android:layout_width="fill_parent"
         android:layout_height="match_parent"
-        android:theme="?attr/actionBarTheme"
         app:popupTheme="@style/Widget.PopupOverlay" />
 
     </android.support.v7.widget.Toolbar>
diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml
index 18b4eb99c..4c0a9bf4f 100644
--- a/app/src/main/res/layout/content_main.xml
+++ b/app/src/main/res/layout/content_main.xml
@@ -2,6 +2,7 @@
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
+  android:background="?attr/colorBackground"
   tools:showIn="@layout/activity_main">
 
   <LinearLayout
diff --git a/app/src/main/res/layout/content_nick_list.xml b/app/src/main/res/layout/content_nick_list.xml
index 78c1b3ab1..eba412b14 100644
--- a/app/src/main/res/layout/content_nick_list.xml
+++ b/app/src/main/res/layout/content_nick_list.xml
@@ -4,7 +4,7 @@
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:layout_gravity="end"
-  android:background="?android:attr/windowBackground"
+  android:background="?attr/colorBackground"
   android:clipToPadding="false"
   android:fitsSystemWindows="true"
   tools:showIn="@layout/activity_main" />
diff --git a/app/src/main/res/values/themes_base.xml b/app/src/main/res/values/themes_base.xml
index 9ca5f8a3c..2c4ea184d 100644
--- a/app/src/main/res/values/themes_base.xml
+++ b/app/src/main/res/values/themes_base.xml
@@ -36,6 +36,8 @@
     <item name="actionBarTheme">@style/Widget.AppBarOverlay</item>
     <item name="formatBarTheme">@style/Widget.AppBarOverlay</item>
 
+    <item name="android:windowBackground">@null</item>
+
     <item name="windowActionModeOverlay">true</item>
 
     <item name="colorDivider">#1FFFFFFF</item>
@@ -68,6 +70,8 @@
     <item name="actionBarTheme">@style/Widget.AppBarOverlay</item>
     <item name="formatBarTheme">@style/Widget.AppBarOverlay.Light</item>
 
+    <item name="android:windowBackground">@null</item>
+
     <item name="windowActionModeOverlay">true</item>
 
     <item name="colorDivider">#1F000000</item>>
diff --git a/app/src/main/res/values/themes_quassel.xml b/app/src/main/res/values/themes_quassel.xml
index 1c6cac46f..00e32688d 100644
--- a/app/src/main/res/values/themes_quassel.xml
+++ b/app/src/main/res/values/themes_quassel.xml
@@ -31,7 +31,6 @@
     <item name="colorForegroundMirc">0x1</item>
 
     <item name="colorBackground">#FAFAFA</item>
-    <item name="android:windowBackground">@color/quasselLight_background</item>
     <item name="colorBackgroundHighlight">#ff8811</item>
     <item name="colorBackgroundSecondary">@null</item>
     <item name="colorBackgroundCard">#FFFFFF</item>
@@ -42,8 +41,6 @@
     <item name="colorTintHighlight">#ff8811</item>
   </style>
 
-  <color name="quasselLight_background">#FAFAFA</color>
-
   <string name="themeQuasselDarkName">Quasselâ„¢ (Dark)</string>
   <string name="themeQuasselDarkId">QUASSEL_DARK</string>
 
@@ -74,7 +71,6 @@
     <item name="colorForegroundMirc">0x0</item>
 
     <item name="colorBackground">#303030</item>
-    <item name="android:windowBackground">@color/quasselDark_background</item>
     <item name="colorBackgroundHighlight">#ff8811</item>
     <item name="colorBackgroundSecondary">@null</item>
     <item name="colorBackgroundCard">#424242</item>
@@ -84,6 +80,4 @@
     <item name="colorTintMessage">#2277dd</item>
     <item name="colorTintHighlight">#ff8811</item>
   </style>
-
-  <color name="quasselDark_background">#303030</color>
 </resources>
diff --git a/app/src/main/res/values/themes_solarized.xml b/app/src/main/res/values/themes_solarized.xml
index 8d4d23520..12973f3c5 100644
--- a/app/src/main/res/values/themes_solarized.xml
+++ b/app/src/main/res/values/themes_solarized.xml
@@ -37,7 +37,6 @@
     <item name="colorForegroundMirc">0xF</item>
 
     <item name="colorBackground">#FDF6E3</item>
-    <item name="android:windowBackground">@color/solarizedLight_background</item>
     <item name="colorBackgroundHighlight">#268bd2</item>
     <item name="colorBackgroundSecondary">@null</item>
     <item name="colorBackgroundCard">#EEE8D5</item>
@@ -48,8 +47,6 @@
     <item name="colorTintHighlight">#ff8811</item>
   </style>
 
-  <color name="solarizedLight_background">#FDF6E3</color>
-
   <string name="themeSolarizedDarkName">Solarized (Dark)</string>
   <string name="themeSolarizedDarkId">SOLARIZED_DARK</string>
 
@@ -84,7 +81,6 @@
     <item name="colorForegroundMirc">0xF</item>
 
     <item name="colorBackground">#002B36</item>
-    <item name="android:windowBackground">@color/solarizedDark_background</item>
     <item name="colorBackgroundHighlight">#268bd2</item>
     <item name="colorBackgroundSecondary">@null</item>
     <item name="colorBackgroundCard">#073642</item>
@@ -94,6 +90,4 @@
     <item name="colorTintMessage">#2277dd</item>
     <item name="colorTintHighlight">#ff8811</item>
   </style>
-
-  <color name="solarizedDark_background">#002B36</color>
 </resources>
diff --git a/lib/src/main/java/de/kuschku/libquassel/session/Backend.kt b/lib/src/main/java/de/kuschku/libquassel/session/Backend.kt
index 634d1da45..ff51c4acb 100644
--- a/lib/src/main/java/de/kuschku/libquassel/session/Backend.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/session/Backend.kt
@@ -1,8 +1,9 @@
 package de.kuschku.libquassel.session
 
 interface Backend {
-  fun connectUnlessConnected(address: SocketAddress, user: String, pass: String)
-  fun connect(address: SocketAddress, user: String, pass: String)
+  fun connectUnlessConnected(address: SocketAddress, user: String, pass: String, reconnect: Boolean)
+  fun connect(address: SocketAddress, user: String, pass: String, reconnect: Boolean)
+  fun reconnect()
   fun disconnect()
   fun sessionManager(): SessionManager
 }
diff --git a/lib/src/main/java/de/kuschku/libquassel/session/SessionManager.kt b/lib/src/main/java/de/kuschku/libquassel/session/SessionManager.kt
index 0729334f2..cb4006c0a 100644
--- a/lib/src/main/java/de/kuschku/libquassel/session/SessionManager.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/session/SessionManager.kt
@@ -13,35 +13,33 @@ import io.reactivex.Observable
 import io.reactivex.subjects.BehaviorSubject
 import javax.net.ssl.X509TrustManager
 
-class SessionManager(
-  private val offlineSession: ISession
-) : ISession {
+class SessionManager(offlineSession: ISession) : ISession {
   override val aliasManager: AliasManager?
-    get() = session.or(offlineSession).aliasManager
+    get() = session.or(lastSession).aliasManager
   override val backlogManager: BacklogManager?
-    get() = session.or(offlineSession).backlogManager
+    get() = session.or(lastSession).backlogManager
   override val bufferSyncer: BufferSyncer?
-    get() = session.or(offlineSession).bufferSyncer
+    get() = session.or(lastSession).bufferSyncer
   override val bufferViewManager: BufferViewManager?
-    get() = session.or(offlineSession).bufferViewManager
+    get() = session.or(lastSession).bufferViewManager
   override val certManagers: Map<IdentityId, CertManager>
-    get() = session.or(offlineSession).certManagers
+    get() = session.or(lastSession).certManagers
   override val coreInfo: CoreInfo?
-    get() = session.or(offlineSession).coreInfo
+    get() = session.or(lastSession).coreInfo
   override val dccConfig: DccConfig?
-    get() = session.or(offlineSession).dccConfig
+    get() = session.or(lastSession).dccConfig
   override val identities: Map<IdentityId, Identity>
-    get() = session.or(offlineSession).identities
+    get() = session.or(lastSession).identities
   override val ignoreListManager: IgnoreListManager?
-    get() = session.or(offlineSession).ignoreListManager
+    get() = session.or(lastSession).ignoreListManager
   override val ircListHelper: IrcListHelper?
-    get() = session.or(offlineSession).ircListHelper
+    get() = session.or(lastSession).ircListHelper
   override val networks: Map<NetworkId, Network>
-    get() = session.or(offlineSession).networks
+    get() = session.or(lastSession).networks
   override val networkConfig: NetworkConfig?
-    get() = session.or(offlineSession).networkConfig
+    get() = session.or(lastSession).networkConfig
 
-  override fun close() = session.or(offlineSession).close()
+  override fun close() = session.or(lastSession).close()
 
   init {
     log(LoggingHandler.LogLevel.INFO, "Session", "Session created")
@@ -60,25 +58,56 @@ class SessionManager(
     clientData: ClientData,
     trustManager: X509TrustManager,
     address: SocketAddress,
-    handlerService: HandlerService,
-    userData: Pair<String, String>
+    handlerService: () -> HandlerService,
+    userData: Pair<String, String>,
+    shouldReconnect: Boolean = false
   ) {
     inProgressSession.value.close()
-    inProgressSession.onNext(Session(clientData, trustManager, address, handlerService, userData))
+    lastClientData = clientData
+    lastTrustManager = trustManager
+    lastAddress = address
+    lastHandlerService = handlerService
+    lastUserData = userData
+    lastShouldReconnect = shouldReconnect
+    inProgressSession.onNext(Session(clientData, trustManager, address, handlerService(), userData))
+  }
+
+  private var lastClientData: ClientData? = null
+  private var lastTrustManager: X509TrustManager? = null
+  private var lastAddress: SocketAddress? = null
+  private var lastHandlerService: (() -> HandlerService)? = null
+  private var lastUserData: Pair<String, String>? = null
+  private var lastShouldReconnect = false
+
+  fun reconnect() {
+    if (lastShouldReconnect) {
+      val clientData = lastClientData
+      val trustManager = lastTrustManager
+      val address = lastAddress
+      val handlerService = lastHandlerService
+      val userData = lastUserData
+
+      if (clientData != null && trustManager != null && address != null && handlerService != null && userData != null) {
+        ifDisconnected {
+          connect(clientData, trustManager, address, handlerService, userData)
+        }
+      }
+    }
   }
 
   fun disconnect() {
     inProgressSession.value
     inProgressSession.value.close()
-    inProgressSession.onNext(offlineSession)
+    inProgressSession.onNext(ISession.NULL)
   }
 
-  private var inProgressSession = BehaviorSubject.createDefault(offlineSession)
+  private var inProgressSession = BehaviorSubject.createDefault(ISession.NULL)
+  private var lastSession: ISession = offlineSession
   override val state: Observable<ConnectionState> = inProgressSession.switchMap { it.state }
   val session: Observable<ISession> = state.map { connectionState ->
     if (connectionState == ConnectionState.CONNECTED)
       inProgressSession.value
     else
-      offlineSession
+      lastSession
   }
 }
diff --git a/malheur/build.gradle.kts b/malheur/build.gradle.kts
new file mode 100644
index 000000000..558449777
--- /dev/null
+++ b/malheur/build.gradle.kts
@@ -0,0 +1,401 @@
+import com.android.build.gradle.LibraryExtension
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.*
+
+apply {
+  plugin("com.android.library")
+  plugin("kotlin-android")
+  plugin("kotlin-kapt")
+}
+
+android {
+  compileSdkVersion(26)
+  buildToolsVersion("26.0.1")
+
+  defaultConfig {
+    minSdkVersion(9)
+    targetSdkVersion(26)
+
+    consumerProguardFiles("proguard-rules.pro")
+  }
+}
+
+dependencies {
+  implementation(kotlin("stdlib"))
+
+  implementation("com.google.code.gson:gson:2.2.4")
+}
+
+fun Project.android(f: LibraryExtension.() -> Unit)
+  = configure(f)
+
+fun DependencyHandlerScope.androidJacocoAgent(dependencyNotation: Any)
+  = "androidJacocoAgent"(dependencyNotation)
+
+fun DependencyHandlerScope.androidJacocoAgent(dependencyNotation: String,
+                                              dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "androidJacocoAgent"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.androidJacocoAnt(dependencyNotation: Any)
+  = "androidJacocoAnt"(dependencyNotation)
+
+fun DependencyHandlerScope.androidJacocoAnt(dependencyNotation: String,
+                                            dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "androidJacocoAnt"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.androidTestAnnotationProcessor(dependencyNotation: Any)
+  = "androidTestAnnotationProcessor"(dependencyNotation)
+
+fun DependencyHandlerScope.androidTestAnnotationProcessor(dependencyNotation: String,
+                                                          dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "androidTestAnnotationProcessor"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.androidTestApk(dependencyNotation: Any)
+  = "androidTestApk"(dependencyNotation)
+
+fun DependencyHandlerScope.androidTestApk(dependencyNotation: String,
+                                          dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "androidTestApk"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.androidTestImplementation(dependencyNotation: Any)
+  = "androidTestImplementation"(dependencyNotation)
+
+fun DependencyHandlerScope.androidTestImplementation(dependencyNotation: String,
+                                                     dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "androidTestImplementation"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.androidTestJackPlugin(dependencyNotation: Any)
+  = "androidTestJackPlugin"(dependencyNotation)
+
+fun DependencyHandlerScope.androidTestJackPlugin(dependencyNotation: String,
+                                                 dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "androidTestJackPlugin"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.androidTestProvided(dependencyNotation: Any)
+  = "androidTestProvided"(dependencyNotation)
+
+fun DependencyHandlerScope.androidTestProvided(dependencyNotation: String,
+                                               dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "androidTestProvided"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.androidTestWearApp(dependencyNotation: Any)
+  = "androidTestWearApp"(dependencyNotation)
+
+fun DependencyHandlerScope.androidTestWearApp(dependencyNotation: String,
+                                              dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "androidTestWearApp"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.annotationProcessor(dependencyNotation: Any)
+  = "annotationProcessor"(dependencyNotation)
+
+fun DependencyHandlerScope.annotationProcessor(dependencyNotation: String,
+                                               dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "annotationProcessor"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.apk(dependencyNotation: Any)
+  = "apk"(dependencyNotation)
+
+fun DependencyHandlerScope.apk(dependencyNotation: String,
+                               dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "apk"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.archives(dependencyNotation: Any)
+  = "archives"(dependencyNotation)
+
+fun DependencyHandlerScope.archives(dependencyNotation: String,
+                                    dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "archives"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.implementation(dependencyNotation: Any)
+  = "implementation"(dependencyNotation)
+
+fun DependencyHandlerScope.implementation(dependencyNotation: String,
+                                          dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "implementation"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.debugAnnotationProcessor(dependencyNotation: Any)
+  = "debugAnnotationProcessor"(dependencyNotation)
+
+fun DependencyHandlerScope.debugAnnotationProcessor(dependencyNotation: String,
+                                                    dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "debugAnnotationProcessor"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.debugApk(dependencyNotation: Any)
+  = "debugApk"(dependencyNotation)
+
+fun DependencyHandlerScope.debugApk(dependencyNotation: String,
+                                    dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "debugApk"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.debugImplementation(dependencyNotation: Any)
+  = "debugImplementation"(dependencyNotation)
+
+fun DependencyHandlerScope.debugImplementation(dependencyNotation: String,
+                                               dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "debugImplementation"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.debugJackPlugin(dependencyNotation: Any)
+  = "debugJackPlugin"(dependencyNotation)
+
+fun DependencyHandlerScope.debugJackPlugin(dependencyNotation: String,
+                                           dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "debugJackPlugin"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.debugProvided(dependencyNotation: Any)
+  = "debugProvided"(dependencyNotation)
+
+fun DependencyHandlerScope.debugProvided(dependencyNotation: String,
+                                         dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "debugProvided"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.debugWearApp(dependencyNotation: Any)
+  = "debugWearApp"(dependencyNotation)
+
+fun DependencyHandlerScope.debugWearApp(dependencyNotation: String,
+                                        dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "debugWearApp"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.default(dependencyNotation: Any)
+  = "default"(dependencyNotation)
+
+fun DependencyHandlerScope.default(dependencyNotation: String,
+                                   dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "default"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.jackPlugin(dependencyNotation: Any)
+  = "jackPlugin"(dependencyNotation)
+
+fun DependencyHandlerScope.jackPlugin(dependencyNotation: String,
+                                      dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "jackPlugin"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.kapt(dependencyNotation: Any)
+  = "kapt"(dependencyNotation)
+
+fun DependencyHandlerScope.kapt(dependencyNotation: String,
+                                dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "kapt"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.kaptAndroidTest(dependencyNotation: Any)
+  = "kaptAndroidTest"(dependencyNotation)
+
+fun DependencyHandlerScope.kaptAndroidTest(dependencyNotation: String,
+                                           dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "kaptAndroidTest"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.kaptDebug(dependencyNotation: Any)
+  = "kaptDebug"(dependencyNotation)
+
+fun DependencyHandlerScope.kaptDebug(dependencyNotation: String,
+                                     dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "kaptDebug"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.kaptRelease(dependencyNotation: Any)
+  = "kaptRelease"(dependencyNotation)
+
+fun DependencyHandlerScope.kaptRelease(dependencyNotation: String,
+                                       dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "kaptRelease"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.kaptTest(dependencyNotation: Any)
+  = "kaptTest"(dependencyNotation)
+
+fun DependencyHandlerScope.kaptTest(dependencyNotation: String,
+                                    dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "kaptTest"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.kaptTestDebug(dependencyNotation: Any)
+  = "kaptTestDebug"(dependencyNotation)
+
+fun DependencyHandlerScope.kaptTestDebug(dependencyNotation: String,
+                                         dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "kaptTestDebug"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.kaptTestRelease(dependencyNotation: Any)
+  = "kaptTestRelease"(dependencyNotation)
+
+fun DependencyHandlerScope.kaptTestRelease(dependencyNotation: String,
+                                           dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "kaptTestRelease"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.provided(dependencyNotation: Any)
+  = "provided"(dependencyNotation)
+
+fun DependencyHandlerScope.provided(dependencyNotation: String,
+                                    dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "provided"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.releaseAnnotationProcessor(dependencyNotation: Any)
+  = "releaseAnnotationProcessor"(dependencyNotation)
+
+fun DependencyHandlerScope.releaseAnnotationProcessor(dependencyNotation: String,
+                                                      dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "releaseAnnotationProcessor"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.releaseApk(dependencyNotation: Any)
+  = "releaseApk"(dependencyNotation)
+
+fun DependencyHandlerScope.releaseApk(dependencyNotation: String,
+                                      dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "releaseApk"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.releaseImplementation(dependencyNotation: Any)
+  = "releaseImplementation"(dependencyNotation)
+
+fun DependencyHandlerScope.releaseImplementation(dependencyNotation: String,
+                                                 dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "releaseImplementation"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.releaseJackPlugin(dependencyNotation: Any)
+  = "releaseJackPlugin"(dependencyNotation)
+
+fun DependencyHandlerScope.releaseJackPlugin(dependencyNotation: String,
+                                             dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "releaseJackPlugin"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.releaseProvided(dependencyNotation: Any)
+  = "releaseProvided"(dependencyNotation)
+
+fun DependencyHandlerScope.releaseProvided(dependencyNotation: String,
+                                           dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "releaseProvided"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.releaseWearApp(dependencyNotation: Any)
+  = "releaseWearApp"(dependencyNotation)
+
+fun DependencyHandlerScope.releaseWearApp(dependencyNotation: String,
+                                          dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "releaseWearApp"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.testAnnotationProcessor(dependencyNotation: Any)
+  = "testAnnotationProcessor"(dependencyNotation)
+
+fun DependencyHandlerScope.testAnnotationProcessor(dependencyNotation: String,
+                                                   dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "testAnnotationProcessor"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.testApk(dependencyNotation: Any)
+  = "testApk"(dependencyNotation)
+
+fun DependencyHandlerScope.testApk(dependencyNotation: String,
+                                   dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "testApk"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.testImplementation(dependencyNotation: Any)
+  = "testImplementation"(dependencyNotation)
+
+fun DependencyHandlerScope.testImplementation(dependencyNotation: String,
+                                              dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "testImplementation"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.testDebugAnnotationProcessor(dependencyNotation: Any)
+  = "testDebugAnnotationProcessor"(dependencyNotation)
+
+fun DependencyHandlerScope.testDebugAnnotationProcessor(dependencyNotation: String,
+                                                        dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "testDebugAnnotationProcessor"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.testDebugApk(dependencyNotation: Any)
+  = "testDebugApk"(dependencyNotation)
+
+fun DependencyHandlerScope.testDebugApk(dependencyNotation: String,
+                                        dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "testDebugApk"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.testDebugImplementation(dependencyNotation: Any)
+  = "testDebugImplementation"(dependencyNotation)
+
+fun DependencyHandlerScope.testDebugImplementation(dependencyNotation: String,
+                                                   dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "testDebugImplementation"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.testDebugJackPlugin(dependencyNotation: Any)
+  = "testDebugJackPlugin"(dependencyNotation)
+
+fun DependencyHandlerScope.testDebugJackPlugin(dependencyNotation: String,
+                                               dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "testDebugJackPlugin"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.testDebugProvided(dependencyNotation: Any)
+  = "testDebugProvided"(dependencyNotation)
+
+fun DependencyHandlerScope.testDebugProvided(dependencyNotation: String,
+                                             dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "testDebugProvided"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.testDebugWearApp(dependencyNotation: Any)
+  = "testDebugWearApp"(dependencyNotation)
+
+fun DependencyHandlerScope.testDebugWearApp(dependencyNotation: String,
+                                            dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "testDebugWearApp"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.testJackPlugin(dependencyNotation: Any)
+  = "testJackPlugin"(dependencyNotation)
+
+fun DependencyHandlerScope.testJackPlugin(dependencyNotation: String,
+                                          dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "testJackPlugin"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.testProvided(dependencyNotation: Any)
+  = "testProvided"(dependencyNotation)
+
+fun DependencyHandlerScope.testProvided(dependencyNotation: String,
+                                        dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "testProvided"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.testReleaseAnnotationProcessor(dependencyNotation: Any)
+  = "testReleaseAnnotationProcessor"(dependencyNotation)
+
+fun DependencyHandlerScope.testReleaseAnnotationProcessor(dependencyNotation: String,
+                                                          dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "testReleaseAnnotationProcessor"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.testReleaseApk(dependencyNotation: Any)
+  = "testReleaseApk"(dependencyNotation)
+
+fun DependencyHandlerScope.testReleaseApk(dependencyNotation: String,
+                                          dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "testReleaseApk"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.testReleaseImplementation(dependencyNotation: Any)
+  = "testReleaseImplementation"(dependencyNotation)
+
+fun DependencyHandlerScope.testReleaseImplementation(dependencyNotation: String,
+                                                     dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "testReleaseImplementation"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.testReleaseJackPlugin(dependencyNotation: Any)
+  = "testReleaseJackPlugin"(dependencyNotation)
+
+fun DependencyHandlerScope.testReleaseJackPlugin(dependencyNotation: String,
+                                                 dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "testReleaseJackPlugin"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.testReleaseProvided(dependencyNotation: Any)
+  = "testReleaseProvided"(dependencyNotation)
+
+fun DependencyHandlerScope.testReleaseProvided(dependencyNotation: String,
+                                               dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "testReleaseProvided"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.testReleaseWearApp(dependencyNotation: Any)
+  = "testReleaseWearApp"(dependencyNotation)
+
+fun DependencyHandlerScope.testReleaseWearApp(dependencyNotation: String,
+                                              dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "testReleaseWearApp"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.testWearApp(dependencyNotation: Any)
+  = "testWearApp"(dependencyNotation)
+
+fun DependencyHandlerScope.testWearApp(dependencyNotation: String,
+                                       dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "testWearApp"(dependencyNotation, dependencyConfiguration)
+
+fun DependencyHandlerScope.wearApp(dependencyNotation: Any)
+  = "wearApp"(dependencyNotation)
+
+fun DependencyHandlerScope.wearApp(dependencyNotation: String,
+                                   dependencyConfiguration: ExternalModuleDependency.() -> Unit)
+  = "wearApp"(dependencyNotation, dependencyConfiguration)
diff --git a/malheur/proguard-rules.pro b/malheur/proguard-rules.pro
new file mode 100644
index 000000000..8c9db7a6d
--- /dev/null
+++ b/malheur/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Gson uses generic type information stored in a class file when working with fields. Proguard
+# removes such information by default, so configure it to keep all of it.
+-keepattributes Signature
+
+# For using GSON @Expose annotation
+-keepattributes *Annotation*
+
+# Gson specific classes
+-dontwarn sun.misc.**
+#-keep class com.google.gson.stream.** { *; }
+
+# Application classes that will be serialized/deserialized over Gson
+-keep class com.google.gson.examples.android.model.** { *; }
+
+# Prevent proguard from stripping interface information from TypeAdapterFactory,
+# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
+-keep class * implements com.google.gson.TypeAdapterFactory
+-keep class * implements com.google.gson.JsonSerializer
+-keep class * implements com.google.gson.JsonDeserializer
+
+-keep class **.BuildConfig { *; }
diff --git a/malheur/src/main/AndroidManifest.xml b/malheur/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..89c8fa0f2
--- /dev/null
+++ b/malheur/src/main/AndroidManifest.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+  package="de.kuschku.malheur">
+
+  <uses-permission android:name="android.permission.READ_LOGS" />
+</manifest>
diff --git a/malheur/src/main/java/de/kuschku/malheur/CrashHandler.kt b/malheur/src/main/java/de/kuschku/malheur/CrashHandler.kt
new file mode 100644
index 000000000..415e19ddc
--- /dev/null
+++ b/malheur/src/main/java/de/kuschku/malheur/CrashHandler.kt
@@ -0,0 +1,166 @@
+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 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
+  )
+
+  @SuppressLint("HardwareIds")
+  fun init(application: Application, configuration: ReportConfiguration = ReportConfiguration(),
+           buildConfig: Class<*>?) {
+    val startTime = Date()
+    packageManager = application.packageManager
+    originalHandler = Thread.getDefaultUncaughtExceptionHandler()
+
+    config = configuration
+
+    Thread.setDefaultUncaughtExceptionHandler { 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)
+          println(json)
+        } catch (e: Throwable) {
+          originalHandler.uncaughtException(activeThread, throwable)
+        }
+      }.start()
+    }
+  }
+
+  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
+  }
+}
diff --git a/malheur/src/main/java/de/kuschku/malheur/data/AppInfo.kt b/malheur/src/main/java/de/kuschku/malheur/data/AppInfo.kt
new file mode 100644
index 000000000..d31bef282
--- /dev/null
+++ b/malheur/src/main/java/de/kuschku/malheur/data/AppInfo.kt
@@ -0,0 +1,7 @@
+package de.kuschku.malheur.data
+
+data class AppInfo(
+  val versionName: String?,
+  val versionCode: Int?,
+  val buildConfig: Map<String, Any?>?
+)
diff --git a/malheur/src/main/java/de/kuschku/malheur/data/CrashInfo.kt b/malheur/src/main/java/de/kuschku/malheur/data/CrashInfo.kt
new file mode 100644
index 000000000..cae838453
--- /dev/null
+++ b/malheur/src/main/java/de/kuschku/malheur/data/CrashInfo.kt
@@ -0,0 +1,9 @@
+package de.kuschku.malheur.data
+
+data class CrashInfo(
+  val cause: ExceptionInfo?,
+  val exception: String?,
+  val activeThread: ThreadInfo?,
+  val startTime: Long?,
+  val crashTime: Long?
+)
diff --git a/malheur/src/main/java/de/kuschku/malheur/data/DeviceInfo.kt b/malheur/src/main/java/de/kuschku/malheur/data/DeviceInfo.kt
new file mode 100644
index 000000000..bd9a07ac5
--- /dev/null
+++ b/malheur/src/main/java/de/kuschku/malheur/data/DeviceInfo.kt
@@ -0,0 +1,8 @@
+package de.kuschku.malheur.data
+
+data class DeviceInfo(
+  val build: Map<String, Any?>?,
+  val version: Map<String, Any?>?,
+  val installationId: String?,
+  val processor: String?
+)
diff --git a/malheur/src/main/java/de/kuschku/malheur/data/DisplayInfo.kt b/malheur/src/main/java/de/kuschku/malheur/data/DisplayInfo.kt
new file mode 100644
index 000000000..38e1b7a2d
--- /dev/null
+++ b/malheur/src/main/java/de/kuschku/malheur/data/DisplayInfo.kt
@@ -0,0 +1,24 @@
+package de.kuschku.malheur.data
+
+import android.view.Display
+import de.kuschku.malheur.util.getMetrics
+
+data class DisplayInfo(
+  val width: Int,
+  val height: Int,
+  val pixelFormat: Int,
+  val refreshRate: Float,
+  val isHdr: Boolean,
+  val isWideGamut: Boolean,
+  val metrics: MetricsInfo
+) {
+  constructor(display: Display) : this(
+    width = display.width,
+    height = display.height,
+    pixelFormat = display.pixelFormat,
+    refreshRate = display.refreshRate,
+    isHdr = display.isHdr,
+    isWideGamut = display.isWideColorGamut,
+    metrics = MetricsInfo(display.getMetrics())
+  )
+}
diff --git a/malheur/src/main/java/de/kuschku/malheur/data/ExceptionInfo.kt b/malheur/src/main/java/de/kuschku/malheur/data/ExceptionInfo.kt
new file mode 100644
index 000000000..73befa667
--- /dev/null
+++ b/malheur/src/main/java/de/kuschku/malheur/data/ExceptionInfo.kt
@@ -0,0 +1,19 @@
+package de.kuschku.malheur.data
+
+data class ExceptionInfo(
+  val type: String?,
+  val message: String?,
+  val localizedMessage: String?,
+  val stackTrace: List<TraceElement>?,
+  val suppressed: List<ExceptionInfo>?,
+  val cause: ExceptionInfo?
+) {
+  constructor(throwable: Throwable) : this(
+    type = throwable.javaClass.canonicalName,
+    message = throwable.message,
+    localizedMessage = throwable.localizedMessage,
+    stackTrace = throwable.stackTrace?.map(::TraceElement),
+    suppressed = throwable.suppressed?.map(::ExceptionInfo),
+    cause = throwable.cause?.let(::ExceptionInfo)
+  )
+}
diff --git a/malheur/src/main/java/de/kuschku/malheur/data/MetricsInfo.kt b/malheur/src/main/java/de/kuschku/malheur/data/MetricsInfo.kt
new file mode 100644
index 000000000..359f86302
--- /dev/null
+++ b/malheur/src/main/java/de/kuschku/malheur/data/MetricsInfo.kt
@@ -0,0 +1,21 @@
+package de.kuschku.malheur.data
+
+import android.util.DisplayMetrics
+
+data class MetricsInfo(
+  val density: Float,
+  val scaledDensity: Float,
+  val widthPixels: Int,
+  val heightPixels: Int,
+  val xdpi: Float,
+  val ydpi: Float
+) {
+  constructor(metrics: DisplayMetrics) : this(
+    density = metrics.density,
+    scaledDensity = metrics.scaledDensity,
+    widthPixels = metrics.widthPixels,
+    heightPixels = metrics.heightPixels,
+    xdpi = metrics.xdpi,
+    ydpi = metrics.ydpi
+  )
+}
diff --git a/malheur/src/main/java/de/kuschku/malheur/data/Report.kt b/malheur/src/main/java/de/kuschku/malheur/data/Report.kt
new file mode 100644
index 000000000..d0dfbb9dd
--- /dev/null
+++ b/malheur/src/main/java/de/kuschku/malheur/data/Report.kt
@@ -0,0 +1,10 @@
+package de.kuschku.malheur.data
+
+data class Report(
+  val crash: CrashInfo?,
+  val threads: List<ThreadInfo>?,
+  val logcat: Map<String, List<String>?>?,
+  val application: AppInfo?,
+  val device: DeviceInfo?,
+  val environment: Map<String, Any?>?
+)
diff --git a/malheur/src/main/java/de/kuschku/malheur/data/RuntimeInfo.kt b/malheur/src/main/java/de/kuschku/malheur/data/RuntimeInfo.kt
new file mode 100644
index 000000000..033fdffe9
--- /dev/null
+++ b/malheur/src/main/java/de/kuschku/malheur/data/RuntimeInfo.kt
@@ -0,0 +1,31 @@
+package de.kuschku.malheur.data
+
+import android.os.Debug
+
+data class MemoryInfo(
+  var dalvikPss: Int?,
+  var dalvikPrivateDirty: Int?,
+  var dalvikSharedDirty: Int?,
+
+  var nativePss: Int?,
+  var nativePrivateDirty: Int?,
+  var nativeSharedDirty: Int?,
+
+  var otherPss: Int?,
+  var otherPrivateDirty: Int?,
+  var otherSharedDirty: Int?
+) {
+  constructor(memoryInfo: Debug.MemoryInfo?) : this(
+    dalvikPss = memoryInfo?.dalvikPss,
+    dalvikPrivateDirty = memoryInfo?.dalvikPrivateDirty,
+    dalvikSharedDirty = memoryInfo?.dalvikSharedDirty,
+
+    nativePss = memoryInfo?.nativePss,
+    nativePrivateDirty = memoryInfo?.nativePrivateDirty,
+    nativeSharedDirty = memoryInfo?.nativeSharedDirty,
+
+    otherPss = memoryInfo?.otherPss,
+    otherPrivateDirty = memoryInfo?.otherPrivateDirty,
+    otherSharedDirty = memoryInfo?.otherSharedDirty
+  )
+}
diff --git a/malheur/src/main/java/de/kuschku/malheur/data/ThreadInfo.kt b/malheur/src/main/java/de/kuschku/malheur/data/ThreadInfo.kt
new file mode 100644
index 000000000..e80b4fc76
--- /dev/null
+++ b/malheur/src/main/java/de/kuschku/malheur/data/ThreadInfo.kt
@@ -0,0 +1,21 @@
+package de.kuschku.malheur.data
+
+data class ThreadInfo(
+  val id: Long?,
+  val name: String?,
+  val group: String?,
+  val status: String?,
+  val stackTrace: List<TraceElement>?,
+  val isDaemon: Boolean?,
+  val priority: Int?
+) {
+  constructor(thread: Thread, stackTrace: Array<StackTraceElement> = thread.stackTrace) : this(
+    id = thread.id,
+    name = thread.name,
+    group = thread.threadGroup?.name,
+    status = thread.state?.name,
+    stackTrace = ArrayList(stackTrace.map(::TraceElement)),
+    isDaemon = thread.isDaemon,
+    priority = thread.priority
+  )
+}
diff --git a/malheur/src/main/java/de/kuschku/malheur/data/TraceElement.kt b/malheur/src/main/java/de/kuschku/malheur/data/TraceElement.kt
new file mode 100644
index 000000000..da84bbdd2
--- /dev/null
+++ b/malheur/src/main/java/de/kuschku/malheur/data/TraceElement.kt
@@ -0,0 +1,17 @@
+package de.kuschku.malheur.data
+
+data class TraceElement(
+  val className: String?,
+  val methodName: String?,
+  val fileName: String?,
+  val lineNumber: Int?,
+  val isNative: Boolean?
+) {
+  constructor(element: StackTraceElement) : this(
+    className = element.className,
+    methodName = element.methodName,
+    fileName = element.fileName,
+    lineNumber = element.lineNumber,
+    isNative = element.isNativeMethod
+  )
+}
diff --git a/malheur/src/main/java/de/kuschku/malheur/util/DisplayHelper.kt b/malheur/src/main/java/de/kuschku/malheur/util/DisplayHelper.kt
new file mode 100644
index 000000000..71cc22a4e
--- /dev/null
+++ b/malheur/src/main/java/de/kuschku/malheur/util/DisplayHelper.kt
@@ -0,0 +1,10 @@
+package de.kuschku.malheur.util
+
+import android.util.DisplayMetrics
+import android.view.Display
+
+fun Display.getMetrics(): DisplayMetrics {
+  val metrics = DisplayMetrics()
+  getMetrics(metrics)
+  return metrics
+}
diff --git a/malheur/src/main/java/de/kuschku/malheur/util/LogCatHelper.kt b/malheur/src/main/java/de/kuschku/malheur/util/LogCatHelper.kt
new file mode 100644
index 000000000..8837570c4
--- /dev/null
+++ b/malheur/src/main/java/de/kuschku/malheur/util/LogCatHelper.kt
@@ -0,0 +1,9 @@
+package de.kuschku.malheur.util
+
+fun readLogCat(since: String, buffer: String, pid: String) = ProcessBuilder()
+  .command("logcat", "-t", since, "-b", buffer, "--pid", pid)
+  .redirectErrorStream(true)
+  .start()
+  .inputStream
+  .bufferedReader(Charsets.UTF_8)
+  .readLines()
diff --git a/malheur/src/main/java/de/kuschku/malheur/util/ProcInfohelper.kt b/malheur/src/main/java/de/kuschku/malheur/util/ProcInfohelper.kt
new file mode 100644
index 000000000..0940b565a
--- /dev/null
+++ b/malheur/src/main/java/de/kuschku/malheur/util/ProcInfohelper.kt
@@ -0,0 +1,13 @@
+package de.kuschku.malheur.util
+
+import java.io.File
+
+fun readProcInfo() = File("/proc/cpuinfo")
+  .bufferedReader(Charsets.UTF_8)
+  .lineSequence()
+  .map { line -> line.split(":") }
+  .filter { split -> split.size == 2 }
+  .map { (key, value) -> key.trim() to value.trim() }
+  .filter { (key, _) -> key == "Hardware" }
+  .map { (_, value) -> value }
+  .firstOrNull()
diff --git a/malheur/src/main/java/de/kuschku/malheur/util/ReflectionHelper.kt b/malheur/src/main/java/de/kuschku/malheur/util/ReflectionHelper.kt
new file mode 100644
index 000000000..669bb8a73
--- /dev/null
+++ b/malheur/src/main/java/de/kuschku/malheur/util/ReflectionHelper.kt
@@ -0,0 +1,26 @@
+package de.kuschku.malheur.util
+
+fun reflectionCollectConstants(klass: Class<*>?) = klass?.declaredFields
+  ?.mapNotNull {
+    var result: Pair<String, Any?>? = null
+    try {
+      result = it.name to it.get(null)
+    } catch (e: IllegalAccessException) {
+    } catch (e: IllegalArgumentException) {
+    }
+    result
+  }?.toMap()
+
+fun <T> reflectionCollectGetters(klass: Class<T>?) = klass?.declaredMethods
+  ?.filter { it.parameterTypes.isEmpty() && it.returnType != Void::class.java }
+  ?.filter { it.name != "getClass" }
+  ?.filter { it.name.startsWith("get") || it.name.startsWith("is") }
+  ?.mapNotNull {
+    var result: Pair<String, Any?>? = null
+    try {
+      result = it.name to it.invoke(it)
+    } catch (e: IllegalAccessException) {
+    } catch (e: IllegalArgumentException) {
+    }
+    result
+  }?.toMap()
diff --git a/malheur/src/main/java/de/kuschku/malheur/util/ThrowableHelper.kt b/malheur/src/main/java/de/kuschku/malheur/util/ThrowableHelper.kt
new file mode 100644
index 000000000..c797860ed
--- /dev/null
+++ b/malheur/src/main/java/de/kuschku/malheur/util/ThrowableHelper.kt
@@ -0,0 +1,11 @@
+package de.kuschku.malheur.util
+
+import java.io.PrintWriter
+import java.io.StringWriter
+
+fun Throwable.printStackTraceToString(): String? {
+  val result = StringWriter()
+  val printWriter = PrintWriter(result)
+  printStackTrace(printWriter)
+  return result.toString()
+}
diff --git a/settings.gradle b/settings.gradle
index 6c91f3880..f0ccacf73 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1 @@
-include ':invokerannotations', ':invokergenerator', ':lib', ':app'
+include ':invokerannotations', ':invokergenerator', ':lib', ':app', ':malheur'
-- 
GitLab