Skip to content

Commit

Permalink
Uncaugh exception handlers for both platforms (#169)
Browse files Browse the repository at this point in the history
- full implementation for both android apps: placeholder for potential UI
 - partial implementation for iOS: crash still propagates but gets logged
 - both implementation linked to shared code
  • Loading branch information
rodvar authored Jan 20, 2025
1 parent 170755e commit fda3b3a
Show file tree
Hide file tree
Showing 8 changed files with 87 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ class MainActivity : ComponentActivity() {
// TODO probably better to handle from presenter once the user reach home
private lateinit var requestPermissionLauncher: ActivityResultLauncher<String>

init {
MainPresenter.init()
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
presenter.attachView(this)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,23 @@ import network.bisq.mobile.domain.di.domainModule
import network.bisq.mobile.domain.di.serviceModule
import network.bisq.mobile.presentation.di.presentationModule
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.GlobalContext

import org.koin.core.context.startKoin

class MainApplication: Application() {
override fun onCreate() {
super.onCreate()

startKoin {
androidContext(this@MainApplication)
modules(listOf(domainModule, serviceModule, presentationModule, clientModule, androidClientModule))
setupKoinDI()
}

private fun setupKoinDI() {
if (GlobalContext.getOrNull() == null) {
startKoin {
androidContext(this@MainApplication)
modules(listOf(domainModule, serviceModule, presentationModule, clientModule, androidClientModule))
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ class MainActivity : ComponentActivity() {
// TODO probably better to handle from presenter once the user reach home
private lateinit var requestPermissionLauncher: ActivityResultLauncher<String>

init {
MainPresenter.init()
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
presenter.attachView(this)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class LifecycleAwareComposeViewController: UIViewController {

init(presenter: MainPresenter) {
self.presenter = presenter
MainPresenter.companion.doInit()
super.init(nibName: nil, bundle: nil)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ actual fun getDeviceLanguageCode(): String {
return Locale.getDefault().language
}

actual fun setupUncaughtExceptionHandler(onCrash: () -> Unit) {
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
println("Uncaught exception on thread: ${thread.name}")
throwable.printStackTrace()

// TODO report to some sort non-survaillant crashlytics?

// Let the UI react
onCrash()
}
}

class AndroidUrlLauncher(private val context: Context) : UrlLauncher {
override fun openUrl(url: String) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ interface UrlLauncher {
fun openUrl(url: String)
}

expect fun setupUncaughtExceptionHandler(onCrash: () -> Unit)

expect fun getDeviceLanguageCode(): String

expect fun getPlatformInfo(): PlatformInfo
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,22 @@ package network.bisq.mobile.domain
import com.russhwolf.settings.ExperimentalSettingsImplementation
import com.russhwolf.settings.KeychainSettings
import com.russhwolf.settings.Settings
import kotlinx.cinterop.BetaInteropApi
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.MemScope
import kotlinx.cinterop.addressOf
import kotlinx.cinterop.refTo
import kotlinx.cinterop.usePinned
import kotlinx.cinterop.*
import kotlinx.serialization.Serializable
import platform.Foundation.NSBundle
import platform.Foundation.NSData
import platform.Foundation.NSDictionary
import platform.Foundation.NSLocale
import platform.Foundation.NSString
import platform.Foundation.NSURL
import platform.Foundation.allKeys
import platform.Foundation.create
import platform.Foundation.currentLocale
import platform.Foundation.dictionaryWithContentsOfFile
import platform.Foundation.languageCode
import platform.Foundation.stringWithFormat
import platform.Foundation.*
import platform.UIKit.UIApplication
import platform.UIKit.UIDevice
import platform.UIKit.UIImage
import platform.UIKit.UIImagePNGRepresentation
import platform.posix.memcpy
import kotlin.collections.set

import platform.Foundation.NSException
import platform.Foundation.NSSetUncaughtExceptionHandler
import platform.darwin.dispatch_async
import platform.darwin.dispatch_get_main_queue
import kotlin.experimental.ExperimentalNativeApi

@OptIn(ExperimentalSettingsImplementation::class)
actual fun getPlatformSettings(): Settings {
// TODO we might get away just using normal Settings() KMP agnostic implementation,
Expand All @@ -42,6 +32,40 @@ actual fun getDeviceLanguageCode(): String {
return NSLocale.currentLocale.languageCode ?: "en"
}

private var globalOnCrash: (() -> Unit)? = null
@OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class)
@Throws(Exception::class)
actual fun setupUncaughtExceptionHandler(onCrash: () -> Unit) {
// TODO this catches the exceptions but let them go through crashing the app, whether in android it will stop the propagation
globalOnCrash = onCrash
NSSetUncaughtExceptionHandler(staticCFunction { exception: NSException? ->
if (exception != null) {
println("Uncaught exception: ${exception.name}, reason: ${exception.reason}")
println("Stack trace: ${exception.callStackSymbols.joinToString("\n")}")

// TODO report to some sort non-survaillant crashlytics?

// Let the UI react
globalOnCrash?.invoke()

// needed on iOS
dispatch_async(dispatch_get_main_queue()) {
println("Performing cleanup after uncaught exception")
}
}
// setUnhandledExceptionHook { throwable ->
// println("Uncaught Kotlin exception: ${throwable.message}")
// throwable.printStackTrace()
//
// // Perform cleanup on the main thread
// dispatch_async(dispatch_get_main_queue()) {
// println("Performing cleanup after uncaught Kotlin exception")
// globalOnCrash?.invoke()
// }
// }
})
}

class IOSUrlLauncher : UrlLauncher {
override fun openUrl(url: String) {
val nsUrl = NSURL.URLWithString(url)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import network.bisq.mobile.domain.UrlLauncher
import network.bisq.mobile.domain.getDeviceLanguageCode
import network.bisq.mobile.domain.getPlatformInfo
import network.bisq.mobile.domain.service.controller.NotificationServiceController
import network.bisq.mobile.domain.setupUncaughtExceptionHandler
import network.bisq.mobile.presentation.ui.AppPresenter
import kotlin.jvm.JvmStatic
import kotlin.random.Random


Expand All @@ -29,6 +31,16 @@ open class MainPresenter(
// it will push a notification every 60 sec
const val testNotifications = false
const val PUSH_DELAY = 60000L

// TODO based on this flag show user a modal explaining internal crash, devs reporte,d with a button to quit the app
val _systemCrashed = MutableStateFlow(false)

@JvmStatic
fun init() {
setupUncaughtExceptionHandler({
_systemCrashed.value = true
})
}
}

override lateinit var navController: NavHostController
Expand Down

0 comments on commit fda3b3a

Please sign in to comment.