Install uncaught exception handler in a separate startup plugin (bitfireAT/davx5#597)

This commit is contained in:
Ricki Hirner 2024-07-18 09:03:13 +02:00
parent 3a38a06302
commit 50c13e5b6d
5 changed files with 139 additions and 41 deletions

View file

@ -5,13 +5,11 @@
package at.bitfire.davdroid
import android.app.Application
import android.os.StrictMode
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import at.bitfire.davdroid.log.LogManager
import at.bitfire.davdroid.startup.StartupPlugin
import at.bitfire.davdroid.sync.account.AccountsCleanupWorker
import at.bitfire.davdroid.ui.DebugInfoActivity
import at.bitfire.davdroid.ui.NotificationUtils
import at.bitfire.davdroid.ui.UiUtils
import dagger.hilt.android.HiltAndroidApp
@ -19,13 +17,11 @@ import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.util.logging.Level
import java.util.logging.Logger
import javax.inject.Inject
import kotlin.system.exitProcess
@HiltAndroidApp
class App: Application(), Thread.UncaughtExceptionHandler, Configuration.Provider {
class App: Application(), Configuration.Provider {
@Inject
lateinit var logger: Logger
@ -51,19 +47,7 @@ class App: Application(), Thread.UncaughtExceptionHandler, Configuration.Provide
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG)
// debug builds
StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder()
.detectActivityLeaks()
.detectFileUriExposure()
.detectLeakedClosableObjects()
.detectLeakedRegistrationObjects()
.detectLeakedSqlLiteObjects()
.penaltyLog()
.build())
else // if (BuildConfig.FLAVOR == FLAVOR_STANDARD)
// handle uncaught exceptions in non-debug standard flavor
Thread.setDefaultUncaughtExceptionHandler(this)
logger.fine("Logging using LogManager $logManager")
NotificationUtils.createChannels(this)
@ -94,16 +78,4 @@ class App: Application(), Thread.UncaughtExceptionHandler, Configuration.Provide
}
}
override fun uncaughtException(t: Thread, e: Throwable) {
logger.log(Level.SEVERE, "Unhandled exception!", e)
val intent = DebugInfoActivity.IntentBuilder(this)
.withCause(e)
.newTask()
.build()
startActivity(intent)
exitProcess(1)
}
}

View file

@ -0,0 +1,72 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.startup
import android.content.Context
import android.os.StrictMode
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.startup.StartupPlugin.Companion.PRIORITY_DEFAULT
import at.bitfire.davdroid.startup.StartupPlugin.Companion.PRIORITY_HIGHEST
import dagger.Binds
import dagger.BindsOptionalOf
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoSet
import java.util.Optional
import java.util.logging.Logger
import javax.inject.Inject
import kotlin.jvm.optionals.getOrNull
/**
* Sets up the uncaught exception (crash) handler and enables StrictMode in debug builds.
*/
class CrashHandlerSetup @Inject constructor(
@ApplicationContext private val context: Context,
private val logger: Logger,
private val crashHandler: Optional<Thread.UncaughtExceptionHandler>
): StartupPlugin {
@Module
@InstallIn(SingletonComponent::class)
interface CrashHandlerSetupModule {
// allows to inject Optional<Thread.UncaughtExceptionHandler>
@BindsOptionalOf
fun optionalDebugInfoCrashHandler(): Thread.UncaughtExceptionHandler
@Binds
@IntoSet
fun crashHandlerSetup(impl: CrashHandlerSetup): StartupPlugin
}
override fun onAppCreate() {
if (BuildConfig.DEBUG) {
logger.info("Debug build, enabling StrictMode with logging")
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectAll()
.penaltyLog()
.build()
)
}
val handler = crashHandler.getOrNull()
if (handler != null) {
logger.info("Setting uncaught exception handler: ${handler.javaClass.name}")
Thread.setDefaultUncaughtExceptionHandler(handler)
} else
logger.info("Using default uncaught exception handler")
}
override fun priority() = PRIORITY_HIGHEST
override suspend fun onAppCreateAsync() {
}
override fun priorityAsync(): Int = PRIORITY_DEFAULT
}

View file

@ -6,6 +6,11 @@ package at.bitfire.davdroid.startup
interface StartupPlugin {
companion object {
const val PRIORITY_DEFAULT = 100
const val PRIORITY_HIGHEST = 0
}
/**
* Runs synchronously during [at.bitfire.davdroid.App.onCreate]. Use only for tasks that must be completed before
* the app can run. Causes the app to start slower.

View file

@ -5,42 +5,41 @@
package at.bitfire.davdroid.startup
import android.content.Context
import at.bitfire.davdroid.startup.StartupPlugin.Companion.PRIORITY_DEFAULT
import at.bitfire.davdroid.util.TaskUtils
import at.bitfire.davdroid.util.packageChangedFlow
import at.bitfire.ical4android.TaskProvider
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoSet
import java.util.logging.Logger
import javax.inject.Inject
/**
* Watches whether a tasks app has been installed or uninstalled and updates
* the selected tasks app and task sync settings accordingly.
*/
class TasksAppWatcher private constructor(
private val context: Context,
class TasksAppWatcher @Inject constructor(
@ApplicationContext private val context: Context,
private val logger: Logger
): StartupPlugin {
@Module
@InstallIn(SingletonComponent::class)
class TasksAppWatcherModule {
@Provides
interface TasksAppWatcherModule {
@Binds
@IntoSet
fun tasksAppWatcher(
@ApplicationContext context: Context,
logger: Logger
): StartupPlugin = TasksAppWatcher(context, logger)
fun tasksAppWatcher(impl: TasksAppWatcher): StartupPlugin
}
override fun onAppCreate() {
}
override fun priority() = 100
override fun priority() = PRIORITY_DEFAULT
override suspend fun onAppCreateAsync() {
logger.info("Watching for package changes in order to detect tasks app changes")
@ -49,7 +48,7 @@ class TasksAppWatcher private constructor(
}
}
override fun priorityAsync() = 100
override fun priorityAsync() = PRIORITY_DEFAULT
private fun onPackageChanged() {

View file

@ -0,0 +1,50 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid
import android.content.Context
import at.bitfire.davdroid.ui.DebugInfoActivity
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import java.util.logging.Level
import java.util.logging.Logger
import javax.inject.Inject
class DebugInfoCrashHandler @Inject constructor(
@ApplicationContext private val context: Context,
private val logger: Logger
): Thread.UncaughtExceptionHandler {
@Module
@InstallIn(SingletonComponent::class)
interface DebugInfoCrashHandlerModule {
@Binds
fun debugInfoCrashHandler(
debugInfoCrashHandler: DebugInfoCrashHandler
): Thread.UncaughtExceptionHandler
}
// See https://developer.android.com/about/versions/oreo/android-8.0-changes#loue
val originalCrashHandler = Thread.getDefaultUncaughtExceptionHandler()
override fun uncaughtException(t: Thread, e: Throwable) {
logger.log(Level.SEVERE, "Unhandled exception in thread ${t.id}!", e)
// start debug info activity with exception (will be started in a new process)
val intent = DebugInfoActivity.IntentBuilder(context)
.withCause(e)
.newTask()
.build()
context.startActivity(intent)
// pass through to default handler to kill the process
originalCrashHandler?.uncaughtException(t, e)
}
}