diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/clientsettings/crash/CrashFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/clientsettings/crash/CrashFragment.kt
index 7e85fad2b9998b3a38909bb4fcbdb1030f9955e9..f8bf8c8b244976271effd64fc6855db444dfa32f 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/clientsettings/crash/CrashFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/clientsettings/crash/CrashFragment.kt
@@ -34,6 +34,7 @@ import butterknife.BindView
 import butterknife.ButterKnife
 import com.google.gson.Gson
 import dagger.android.support.DaggerFragment
+import de.kuschku.malheur.CrashHandler
 import de.kuschku.malheur.data.Report
 import de.kuschku.quasseldroid.BuildConfig
 import de.kuschku.quasseldroid.R
@@ -70,6 +71,29 @@ class CrashFragment : DaggerFragment() {
     handlerThread.quit()
   }
 
+  private fun reload() {
+    val crashDir = this.crashDir
+    val gson = this.gson
+    val context = this.context
+
+    if (crashDir != null && context != null) {
+      crashDir.mkdirs()
+      val list: List<Pair<Report, Uri>> = crashDir.listFiles()
+        .orEmpty()
+        .map {
+          Pair<Report, Uri>(
+            gson.fromJson(it.readText()),
+            FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.fileprovider", it)
+          )
+        }
+        .sortedByDescending { it.first.environment?.crashTime }
+
+      activity?.runOnUiThread {
+        this.adapter?.submitList(list)
+      }
+    }
+  }
+
   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                             savedInstanceState: Bundle?): View? {
     val view = inflater.inflate(R.layout.preferences_crash, container, false)
@@ -89,49 +113,33 @@ class CrashFragment : DaggerFragment() {
       crashesEmpty.visibleIf(it.isEmpty())
     }
 
-    handler.post {
-      val crashDir = this.crashDir
-      val gson = this.gson
-      val context = this.context
-
-      if (crashDir != null && context != null) {
-        crashDir.mkdirs()
-        val list: List<Pair<Report, Uri>> = crashDir.listFiles()
-          .orEmpty()
-          .map {
-            Pair<Report, Uri>(
-              gson.fromJson(it.readText()),
-              FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.fileprovider", it)
-            )
-          }
-          .sortedByDescending { it.first.environment?.crashTime }
-
-        activity?.runOnUiThread {
-          this.adapter?.submitList(list)
-        }
-      }
-    }
+    handler.post(this::reload)
     return view
   }
 
   override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
     inflater.inflate(R.menu.activity_crashes, menu)
+    menu.findItem(R.id.action_generate_crash_report).isVisible = BuildConfig.DEBUG
     super.onCreateOptionsMenu(menu, inflater)
   }
 
   override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
-    R.id.action_delete_all -> {
+    R.id.action_generate_crash_report -> {
+      handler.post {
+        CrashHandler.handleSync(Exception("User requested generation of report"))
+        reload()
+      }
+    }
+    R.id.action_delete_all            -> {
       handler.post {
         crashDir?.mkdirs()
         crashDir?.listFiles()?.forEach {
           it.delete()
         }
-        activity?.runOnUiThread {
-          this.adapter?.submitList(emptyList())
-        }
+        reload()
       }
       true
     }
-    else                   -> super.onOptionsItemSelected(item)
+    else                              -> super.onOptionsItemSelected(item)
   }
 }
diff --git a/app/src/main/res/menu/activity_crashes.xml b/app/src/main/res/menu/activity_crashes.xml
index 727490b9c6e7cdbc7296cbc1d67fef6092b73e04..1805ac59ab396c8b5568f030d3866c0aeb2bdc7e 100644
--- a/app/src/main/res/menu/activity_crashes.xml
+++ b/app/src/main/res/menu/activity_crashes.xml
@@ -18,6 +18,9 @@
   -->
 
 <menu xmlns:android="http://schemas.android.com/apk/res/android">
+  <item
+    android:id="@+id/action_generate_crash_report"
+    android:title="@string/label_generate_crash_report" />
   <item
     android:id="@+id/action_delete_all"
     android:title="@string/label_delete_all" />
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index a45eef6b23ac30963de0ffa637fee56e9118a733..1ad475ad628c724f1026e181863c9a8e425f029e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -57,6 +57,7 @@
   <string name="label_edit_topic_long">Open dialog to change the channel topic</string>
   <string name="label_filter_messages">Filter Messages</string>
   <string name="label_finish">Finish</string>
+  <string name="label_generate_crash_report">Generate Crash Report</string>
   <string name="label_hide_perm">Hide Permanently</string>
   <string name="label_hide_temp">Hide Temporarily</string>
   <string name="label_ignore">Ignore</string>
diff --git a/malheur/build.gradle.kts b/malheur/build.gradle.kts
index 5b48600e33377dbd30873414443fa6243a20d134..ff4b6cd11f620a756b9da74eda5e3e1dcc3fc00d 100644
--- a/malheur/build.gradle.kts
+++ b/malheur/build.gradle.kts
@@ -47,4 +47,5 @@ dependencies {
   implementation(kotlin("stdlib", "1.3.30"))
 
   implementation("com.google.code.gson", "gson", "2.8.5")
+  implementation("androidx.annotation", "annotation", "1.0.1")
 }
diff --git a/malheur/src/main/java/de/kuschku/malheur/CrashHandler.kt b/malheur/src/main/java/de/kuschku/malheur/CrashHandler.kt
index 5f68831167d29c86eacedc0bbfd52666048b344e..c3de2421d8ab1360ec3c5f18847db350098a1f20 100644
--- a/malheur/src/main/java/de/kuschku/malheur/CrashHandler.kt
+++ b/malheur/src/main/java/de/kuschku/malheur/CrashHandler.kt
@@ -36,6 +36,7 @@ object CrashHandler {
 
   private val startTime = Date()
   private var originalHandler: Thread.UncaughtExceptionHandler? = null
+  private var myHandler: ((Thread, Throwable) -> Unit)? = null
 
   private lateinit var handler: Handler
 
@@ -54,64 +55,71 @@ object CrashHandler {
     handler = Handler(handlerThread.looper)
 
     val reportCollector = ReportCollector(application)
-    myHandler = Thread.UncaughtExceptionHandler { activeThread, throwable ->
+    myHandler = { activeThread, throwable ->
       val crashTime = Date()
       val stackTraces = Thread.getAllStackTraces()
       Log.e("Malheur", "Creating crash report")
       handler.post {
         Toast.makeText(application, "Creating crash report", Toast.LENGTH_LONG).show()
       }
-      Thread {
-        try {
-          val json = gson.toJson(
-            reportCollector.collect(
-              CrashContext(
-                application = application,
-                config = config,
-                crashingThread = activeThread,
-                throwable = throwable,
-                startTime = startTime,
-                crashTime = crashTime,
-                buildConfig = buildConfig,
-                stackTraces = stackTraces
-              ), config
-            )
+      try {
+        val json = gson.toJson(
+          reportCollector.collect(
+            CrashContext(
+              application = application,
+              config = config,
+              crashingThread = activeThread,
+              throwable = throwable,
+              startTime = startTime,
+              crashTime = crashTime,
+              buildConfig = buildConfig,
+              stackTraces = stackTraces
+            ), config
           )
-          val crashDirectory = File(application.cacheDir, "crashes")
-          crashDirectory.mkdirs()
-          val crashFile = File(crashDirectory, "${System.currentTimeMillis()}.json")
-          crashFile.createNewFile()
-          crashFile.writeText(json)
-          Log.e("Malheur", "Crash report saved: $crashFile", throwable)
-          handler.post {
-            Toast.makeText(
-              application, "Crash report saved: ${crashFile.name}", Toast.LENGTH_LONG
-            ).show()
-          }
-        } catch (e: Throwable) {
-          e.printStackTrace()
-          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
-            throwable.addSuppressed(e)
-          }
+        )
+        val crashDirectory = File(application.cacheDir, "crashes")
+        crashDirectory.mkdirs()
+        val crashFile = File(crashDirectory, "${System.currentTimeMillis()}.json")
+        crashFile.createNewFile()
+        crashFile.writeText(json)
+        Log.e("Malheur", "Crash report saved: $crashFile", throwable)
+        handler.post {
+          Toast.makeText(
+            application, "Crash report saved: ${crashFile.name}", Toast.LENGTH_LONG
+          ).show()
         }
-      }.start()
+      } catch (e: Throwable) {
+        e.printStackTrace()
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+          throwable.addSuppressed(e)
+        }
+      }
     }
 
     Thread.setDefaultUncaughtExceptionHandler { currentThread, throwable ->
-      myHandler?.uncaughtException(currentThread, throwable)
-      originalHandler?.uncaughtException(currentThread, throwable)
+      Thread {
+        myHandler?.invoke(currentThread, throwable)
+        originalHandler?.uncaughtException(currentThread, throwable)
+      }.start()
     }
 
     val oldHandler = Thread.currentThread().uncaughtExceptionHandler
     Thread.currentThread().setUncaughtExceptionHandler { currentThread, throwable ->
-      myHandler?.uncaughtException(currentThread, throwable)
-      oldHandler?.uncaughtException(currentThread, throwable)
+      Thread {
+        myHandler?.invoke(currentThread, throwable)
+        oldHandler?.uncaughtException(currentThread, throwable)
+      }.start()
     }
   }
 
   fun handle(throwable: Throwable) {
-    myHandler?.uncaughtException(Thread.currentThread(), throwable)
+    val thread = Thread.currentThread()
+    Thread {
+      myHandler?.invoke(thread, throwable)
+    }.start()
   }
 
-  private var myHandler: Thread.UncaughtExceptionHandler? = null
+  fun handleSync(throwable: Throwable) {
+    myHandler?.invoke(Thread.currentThread(), throwable)
+  }
 }
diff --git a/malheur/src/main/java/de/kuschku/malheur/collectors/AppCollector.kt b/malheur/src/main/java/de/kuschku/malheur/collectors/AppCollector.kt
index 42bd4cf6253ad317028e4ec18c995c9da2b4c402..26721baa1e377d4254a81d7ae57506c7465aab46 100644
--- a/malheur/src/main/java/de/kuschku/malheur/collectors/AppCollector.kt
+++ b/malheur/src/main/java/de/kuschku/malheur/collectors/AppCollector.kt
@@ -43,6 +43,9 @@ class AppCollector(private val application: Application) : Collector<AppInfo, Ap
       reflectionCollectConstants(
         context.buildConfig ?: getBuildConfigClass(application.packageName)
       )
+    },
+    installationSource = collectIf(config.installationSource) {
+      application.packageManager.getInstallerPackageName(application.packageName)
     }
   )
 
diff --git a/malheur/src/main/java/de/kuschku/malheur/collectors/DisplayCollector.kt b/malheur/src/main/java/de/kuschku/malheur/collectors/DisplayCollector.kt
index 212e74abfb3c46dcc22f6ab73c295874591b7d2d..d17e207e144b871034c446632bc66b699bfbe6c5 100644
--- a/malheur/src/main/java/de/kuschku/malheur/collectors/DisplayCollector.kt
+++ b/malheur/src/main/java/de/kuschku/malheur/collectors/DisplayCollector.kt
@@ -26,6 +26,7 @@ import android.os.Build
 import android.util.SparseArray
 import android.view.Display
 import android.view.WindowManager
+import androidx.annotation.RequiresApi
 import de.kuschku.malheur.CrashContext
 import de.kuschku.malheur.data.DisplayInfo
 import de.kuschku.malheur.data.MetricsInfo
@@ -69,6 +70,7 @@ class DisplayCollector(application: Application) :
     )
   }
 
+  @RequiresApi(Build.VERSION_CODES.N)
   private fun getHdrCapabilitiesEnum(): SparseArray<String> {
     val hdrCapabilityEnums = SparseArray<String>()
     Display.HdrCapabilities::class.java.declaredFields.filter {
diff --git a/malheur/src/main/java/de/kuschku/malheur/config/AppConfig.kt b/malheur/src/main/java/de/kuschku/malheur/config/AppConfig.kt
index 994ff8fd32c98b2211af570b9c2695140cbdfb0f..c028832e0d019acccbc42ffc2a52d9ea6bf1aa02 100644
--- a/malheur/src/main/java/de/kuschku/malheur/config/AppConfig.kt
+++ b/malheur/src/main/java/de/kuschku/malheur/config/AppConfig.kt
@@ -22,5 +22,6 @@ package de.kuschku.malheur.config
 data class AppConfig(
   val versionName: Boolean = true,
   val versionCode: Boolean = true,
-  val buildConfig: Boolean = true
+  val buildConfig: Boolean = true,
+  val installationSource: Boolean = true
 )
diff --git a/malheur/src/main/java/de/kuschku/malheur/data/AppInfo.kt b/malheur/src/main/java/de/kuschku/malheur/data/AppInfo.kt
index c06791382bd98ad9fa5c60a6bec7b6144c045b53..928a0a50e18c2cdeb71790d55317e966e0dad209 100644
--- a/malheur/src/main/java/de/kuschku/malheur/data/AppInfo.kt
+++ b/malheur/src/main/java/de/kuschku/malheur/data/AppInfo.kt
@@ -22,5 +22,6 @@ package de.kuschku.malheur.data
 data class AppInfo(
   val versionName: String?,
   val versionCode: Long?,
-  val buildConfig: Map<String, Any?>?
+  val buildConfig: Map<String, Any?>?,
+  val installationSource: String?
 )