Skip to content
Snippets Groups Projects
Unverified Commit 500c36c4 authored by Hee Tatt Ooi's avatar Hee Tatt Ooi Committed by GitHub
Browse files

Exception handler (#176)

* global exception handler that restarts app with previous crash message. for both main thread and coroutines

* function comment

* naming

* added stackrace to intent

* removed unnecessary line

* extract constants

* call reporter for dialog show

* comments and refactoring

* enable show dialog on app crash

* cleanup and refactor

* comment

* cleanup
parent baa3e130
No related branches found
No related tags found
No related merge requests found
Showing
with 169 additions and 33 deletions
...@@ -4,6 +4,7 @@ import android.annotation.SuppressLint ...@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.content.IntentFilter
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
...@@ -12,6 +13,10 @@ import androidx.lifecycle.Lifecycle ...@@ -12,6 +13,10 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import de.rki.coronawarnapp.exception.ErrorReportReceiver
import de.rki.coronawarnapp.exception.ReportingConstants.ERROR_REPORT_LOCAL_BROADCAST_CHANNEL
import de.rki.coronawarnapp.exception.handler.GlobalExceptionHandler
import de.rki.coronawarnapp.notification.NotificationHelper import de.rki.coronawarnapp.notification.NotificationHelper
import org.conscrypt.Conscrypt import org.conscrypt.Conscrypt
import java.security.Security import java.security.Security
...@@ -34,12 +39,15 @@ class CoronaWarnApplication : Application(), LifecycleObserver, ...@@ -34,12 +39,15 @@ class CoronaWarnApplication : Application(), LifecycleObserver,
instance.applicationContext instance.applicationContext
} }
private lateinit var errorReceiver: ErrorReportReceiver
override fun onCreate() { override fun onCreate() {
super.onCreate()
GlobalExceptionHandler(this)
instance = this instance = this
NotificationHelper.createNotificationChannel() NotificationHelper.createNotificationChannel()
// Enable Conscrypt for TLS1.3 Support below API Level 29 // Enable Conscrypt for TLS1.3 Support below API Level 29
Security.insertProviderAt(Conscrypt.newProvider(), 1) Security.insertProviderAt(Conscrypt.newProvider(), 1)
super.onCreate()
ProcessLifecycleOwner.get().lifecycle.addObserver(this) ProcessLifecycleOwner.get().lifecycle.addObserver(this)
registerActivityLifecycleCallbacks(this) registerActivityLifecycleCallbacks(this)
} }
...@@ -63,7 +71,8 @@ class CoronaWarnApplication : Application(), LifecycleObserver, ...@@ -63,7 +71,8 @@ class CoronaWarnApplication : Application(), LifecycleObserver,
} }
override fun onActivityPaused(activity: Activity) { override fun onActivityPaused(activity: Activity) {
// does not override function. Empty on intention // unregisters error receiver
LocalBroadcastManager.getInstance(this).unregisterReceiver(errorReceiver)
} }
override fun onActivityStarted(activity: Activity) { override fun onActivityStarted(activity: Activity) {
...@@ -94,6 +103,8 @@ class CoronaWarnApplication : Application(), LifecycleObserver, ...@@ -94,6 +103,8 @@ class CoronaWarnApplication : Application(), LifecycleObserver,
} }
override fun onActivityResumed(activity: Activity) { override fun onActivityResumed(activity: Activity) {
// does not override function. Empty on intention errorReceiver = ErrorReportReceiver(activity)
LocalBroadcastManager.getInstance(this)
.registerReceiver(errorReceiver, IntentFilter(ERROR_REPORT_LOCAL_BROADCAST_CHANNEL))
} }
} }
...@@ -13,6 +13,7 @@ class ErrorReportReceiver(private val activity: Activity) : BroadcastReceiver() ...@@ -13,6 +13,7 @@ class ErrorReportReceiver(private val activity: Activity) : BroadcastReceiver()
companion object { companion object {
private val TAG: String = ErrorReportReceiver::class.java.simpleName private val TAG: String = ErrorReportReceiver::class.java.simpleName
} }
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
val category = ExceptionCategory val category = ExceptionCategory
.valueOf(intent.getStringExtra(ReportingConstants.ERROR_REPORT_CATEGORY_EXTRA) ?: "") .valueOf(intent.getStringExtra(ReportingConstants.ERROR_REPORT_CATEGORY_EXTRA) ?: "")
...@@ -25,25 +26,28 @@ class ErrorReportReceiver(private val activity: Activity) : BroadcastReceiver() ...@@ -25,25 +26,28 @@ class ErrorReportReceiver(private val activity: Activity) : BroadcastReceiver()
val confirm = context.resources.getString(R.string.errors_generic_button_positive) val confirm = context.resources.getString(R.string.errors_generic_button_positive)
val details = context.resources.getString(R.string.errors_generic_button_negative) val details = context.resources.getString(R.string.errors_generic_button_negative)
val detailsTitle = context.resources.getString(R.string.errors_generic_details_headline) val detailsTitle = context.resources.getString(R.string.errors_generic_details_headline)
if (CoronaWarnApplication.isAppInForeground) { if (CoronaWarnApplication.isAppInForeground) {
DialogHelper.showDialog(DialogHelper.DialogInstance( DialogHelper.showDialog(
activity, DialogHelper.DialogInstance(
title, activity,
message, title,
confirm, message,
details, confirm,
null, details,
{}, null,
{ {},
DialogHelper.showDialog( {
DialogHelper.DialogInstance( DialogHelper.showDialog(
activity, DialogHelper.DialogInstance(
title, activity,
"$detailsTitle:\n$stack", title,
confirm "$detailsTitle:\n$stack",
)).run {} confirm
} )
)) ).run {}
}
))
} }
Log.e( Log.e(
TAG, TAG,
......
...@@ -26,7 +26,7 @@ fun Throwable.report( ...@@ -26,7 +26,7 @@ fun Throwable.report(
LocalBroadcastManager.getInstance(CoronaWarnApplication.getAppContext()).sendBroadcast(intent) LocalBroadcastManager.getInstance(CoronaWarnApplication.getAppContext()).sendBroadcast(intent)
} }
fun Throwable.reportGeneric( fun reportGeneric(
stackString: String stackString: String
) { ) {
val intent = Intent(ReportingConstants.ERROR_REPORT_LOCAL_BROADCAST_CHANNEL) val intent = Intent(ReportingConstants.ERROR_REPORT_LOCAL_BROADCAST_CHANNEL)
......
package de.rki.coronawarnapp.exception.handler
import android.content.Context
import android.content.Intent
import android.util.Log
import de.rki.coronawarnapp.CoronaWarnApplication
import de.rki.coronawarnapp.ui.LauncherActivity
import java.io.PrintWriter
import java.io.StringWriter
import java.lang.reflect.InvocationTargetException
class GlobalExceptionHandler(private val application: CoronaWarnApplication) :
Thread.UncaughtExceptionHandler {
companion object {
val TAG: String? = GlobalExceptionHandler::class.simpleName
}
init {
Thread.setDefaultUncaughtExceptionHandler(this)
}
override fun uncaughtException(thread: Thread, throwable: Throwable) {
try {
Log.i(TAG, "cause caught: " + throwable)
val cause = throwable.cause
val stringWriter = StringWriter()
// Throwables from main thread are wrapped in an InvocationTargetException,
// unwrap the InvocationTargetException to get the original cause
if (cause is InvocationTargetException) {
cause.targetException.printStackTrace(PrintWriter(stringWriter))
Log.i(TAG, "InvocationTargetException caught: " + cause.targetException)
}
// for errors thrown by coroutines, these are not wrapped in InvocationTargetException
else {
Log.i(TAG, "InvocationTargetException caught: " + throwable)
throwable.printStackTrace(PrintWriter(stringWriter))
}
val stackTrace = stringWriter.toString()
triggerRestart(CoronaWarnApplication.getAppContext(), stackTrace)
} catch (e: Exception) {
Log.e(TAG, "GlobalExceptionHandler failing" + e)
}
}
/**
* Restarts the app by sending an Intent to start LauncherActivitiy and
* terminating the JVM
*
* @see de.rki.coronawarnapp.ui.LauncherActivity
*
* @param context application context
* @param stackTrace exception that caused the crash
*/
private fun triggerRestart(context: Context, stackTrace: String) {
val intent = Intent(context, LauncherActivity::class.java)
intent.addFlags(
Intent.FLAG_ACTIVITY_CLEAR_TOP
or Intent.FLAG_ACTIVITY_CLEAR_TASK
or Intent.FLAG_ACTIVITY_NEW_TASK
)
intent.putExtra(GlobalExceptionHandlerConstants.APP_CRASHED, true)
intent.putExtra(GlobalExceptionHandlerConstants.STACK_TRACE, stackTrace)
context.startActivity(intent)
Runtime.getRuntime().exit(0)
}
}
package de.rki.coronawarnapp.exception.handler
object GlobalExceptionHandlerConstants {
// name of intent extra to described that an app has crashed. Intent extra is of type boolean
const val APP_CRASHED = "appCrashed"
// name of intent extra that contains the stacktrace. Intent extra is of type boolean
const val STACK_TRACE = "stackTrace"
}
package de.rki.coronawarnapp.ui
import androidx.appcompat.app.AppCompatActivity
import de.rki.coronawarnapp.exception.handler.GlobalExceptionHandlerConstants
import de.rki.coronawarnapp.exception.reportGeneric
/**
* If the app crashed in the last instance and was restarted, the stacktrace is retrieved
* from the intent and displayed in a dialog report
*
* @see de.rki.coronawarnapp.exception.handler.GlobalExceptionHandler
*/
fun AppCompatActivity.showDialogWithStacktraceIfPreviouslyCrashed() {
val appCrashedAndWasRestarted =
intent.getBooleanExtra(GlobalExceptionHandlerConstants.APP_CRASHED, false)
if (appCrashedAndWasRestarted) {
val stackTrade = intent.getStringExtra(GlobalExceptionHandlerConstants.STACK_TRACE)
if (!stackTrade.isNullOrEmpty()) {
reportGeneric(stackTrade)
}
}
}
...@@ -5,6 +5,7 @@ import android.net.Uri ...@@ -5,6 +5,7 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import de.rki.coronawarnapp.exception.handler.GlobalExceptionHandlerConstants
import de.rki.coronawarnapp.http.DynamicURLs import de.rki.coronawarnapp.http.DynamicURLs
import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.storage.LocalData
import de.rki.coronawarnapp.ui.main.MainActivity import de.rki.coronawarnapp.ui.main.MainActivity
...@@ -17,7 +18,6 @@ class LauncherActivity : AppCompatActivity() { ...@@ -17,7 +18,6 @@ class LauncherActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
retrieveCustomURLsFromSchema(intent.data) retrieveCustomURLsFromSchema(intent.data)
if (LocalData.isOnboarded()) { if (LocalData.isOnboarded()) {
...@@ -56,12 +56,33 @@ class LauncherActivity : AppCompatActivity() { ...@@ -56,12 +56,33 @@ class LauncherActivity : AppCompatActivity() {
} }
private fun startOnboardingActivity() { private fun startOnboardingActivity() {
startActivity(Intent(this, OnboardingActivity::class.java)) val onboardingActivity = Intent(this, OnboardingActivity::class.java)
mapIntentExtras(onboardingActivity)
startActivity(onboardingActivity)
finish() finish()
} }
private fun startMainActivity() { private fun startMainActivity() {
startActivity(Intent(this, MainActivity::class.java)) val mainActivityIntent = Intent(this, MainActivity::class.java)
mapIntentExtras(mainActivityIntent)
startActivity(mainActivityIntent)
finish() finish()
} }
/**
* Maps the intentExtras for global exception handling to the next activity that is
* started
*
* @param intentForNextActivity
*/
private fun mapIntentExtras(intentForNextActivity: Intent) {
intentForNextActivity.putExtra(
GlobalExceptionHandlerConstants.APP_CRASHED,
intent.getBooleanExtra(GlobalExceptionHandlerConstants.APP_CRASHED, false)
)
intentForNextActivity.putExtra(
GlobalExceptionHandlerConstants.STACK_TRACE,
intent.getStringExtra(GlobalExceptionHandlerConstants.STACK_TRACE)
)
}
} }
package de.rki.coronawarnapp.ui.main package de.rki.coronawarnapp.ui.main
import android.content.Intent import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import de.rki.coronawarnapp.R import de.rki.coronawarnapp.R
import de.rki.coronawarnapp.exception.ErrorReportReceiver import de.rki.coronawarnapp.ui.showDialogWithStacktraceIfPreviouslyCrashed
import de.rki.coronawarnapp.ui.viewmodel.SettingsViewModel import de.rki.coronawarnapp.ui.viewmodel.SettingsViewModel
import de.rki.coronawarnapp.util.ConnectivityHelper import de.rki.coronawarnapp.util.ConnectivityHelper
import de.rki.coronawarnapp.worker.BackgroundWorkScheduler import de.rki.coronawarnapp.worker.BackgroundWorkScheduler
...@@ -32,8 +30,6 @@ class MainActivity : AppCompatActivity() { ...@@ -32,8 +30,6 @@ class MainActivity : AppCompatActivity() {
private lateinit var settingsViewModel: SettingsViewModel private lateinit var settingsViewModel: SettingsViewModel
private val errorReceiver = ErrorReportReceiver(this)
/** /**
* Register connection callback. * Register connection callback.
*/ */
...@@ -74,9 +70,9 @@ class MainActivity : AppCompatActivity() { ...@@ -74,9 +70,9 @@ class MainActivity : AppCompatActivity() {
*/ */
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
LocalBroadcastManager.getInstance(this).registerReceiver(errorReceiver, IntentFilter("error-report"))
ConnectivityHelper.registerNetworkStatusCallback(this, callbackNetwork) ConnectivityHelper.registerNetworkStatusCallback(this, callbackNetwork)
ConnectivityHelper.registerBluetoothStatusCallback(this, callbackBluetooth) ConnectivityHelper.registerBluetoothStatusCallback(this, callbackBluetooth)
showDialogWithStacktraceIfPreviouslyCrashed()
} }
/** /**
...@@ -86,8 +82,6 @@ class MainActivity : AppCompatActivity() { ...@@ -86,8 +82,6 @@ class MainActivity : AppCompatActivity() {
super.onPause() super.onPause()
ConnectivityHelper.unregisterNetworkStatusCallback(this, callbackNetwork) ConnectivityHelper.unregisterNetworkStatusCallback(this, callbackNetwork)
ConnectivityHelper.unregisterBluetoothStatusCallback(this, callbackBluetooth) ConnectivityHelper.unregisterBluetoothStatusCallback(this, callbackBluetooth)
// Unregister since the activity is about to be closed.
LocalBroadcastManager.getInstance(this).unregisterReceiver(errorReceiver)
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
......
...@@ -9,6 +9,7 @@ import androidx.lifecycle.LifecycleObserver ...@@ -9,6 +9,7 @@ import androidx.lifecycle.LifecycleObserver
import de.rki.coronawarnapp.R import de.rki.coronawarnapp.R
import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.storage.LocalData
import de.rki.coronawarnapp.ui.main.MainActivity import de.rki.coronawarnapp.ui.main.MainActivity
import de.rki.coronawarnapp.ui.showDialogWithStacktraceIfPreviouslyCrashed
/** /**
* This activity holds all the onboarding fragments and isn't used after a successful onboarding flow. * This activity holds all the onboarding fragments and isn't used after a successful onboarding flow.
...@@ -40,6 +41,11 @@ class OnboardingActivity : AppCompatActivity(), LifecycleObserver { ...@@ -40,6 +41,11 @@ class OnboardingActivity : AppCompatActivity(), LifecycleObserver {
) )
} }
override fun onResume() {
super.onResume()
showDialogWithStacktraceIfPreviouslyCrashed()
}
fun completeOnboarding() { fun completeOnboarding() {
LocalData.isOnboarded(true) LocalData.isOnboarded(true)
startActivity(Intent(this, MainActivity::class.java)) startActivity(Intent(this, MainActivity::class.java))
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment