diff --git a/apps/student/src/main/java/com/instructure/student/mobius/common/MobiusExceptionLogger.kt b/apps/student/src/main/java/com/instructure/student/mobius/common/MobiusExceptionLogger.kt new file mode 100644 index 0000000000..cd81944c05 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/mobius/common/MobiusExceptionLogger.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2019 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.mobius.common + +import com.crashlytics.android.Crashlytics +import com.instructure.student.BuildConfig +import com.spotify.mobius.First +import com.spotify.mobius.Next +import com.spotify.mobius.android.AndroidLogger +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch + +/** + * Intercepts exceptions in the mobius loop's update and init operations and logs them to Crashlytics. + * For debug builds the exception will be logged locally and then thrown. + */ +class MobiusExceptionLogger : AndroidLogger("Mobius") { + override fun afterUpdate(model: MODEL, event: EVENT, result: Next) { + if (BuildConfig.DEBUG) super.afterUpdate(model, event, result) + } + + override fun afterInit(model: MODEL, result: First) { + if (BuildConfig.DEBUG) super.afterInit(model, result) + } + + override fun beforeInit(model: MODEL) { + if (BuildConfig.DEBUG) super.beforeInit(model) + } + + override fun beforeUpdate(model: MODEL, event: EVENT) { + if (BuildConfig.DEBUG) super.beforeUpdate(model, event) + } + + override fun exceptionDuringInit(model: MODEL, exception: Throwable) { + if (BuildConfig.DEBUG) { + super.exceptionDuringInit(model, exception) + // Must throw as a separate message, otherwise Mobius might consume the exception + GlobalScope.launch(Dispatchers.Main) { throw exception } + } else { + Crashlytics.logException(exception) + } + } + + override fun exceptionDuringUpdate(model: MODEL, event: EVENT, exception: Throwable) { + if (BuildConfig.DEBUG) { + super.exceptionDuringUpdate(model, event, exception) + // Must throw as a separate message, otherwise Mobius might consume the exception + GlobalScope.launch(Dispatchers.Main) { throw exception } + } else { + Crashlytics.logException(exception) + } + } +} diff --git a/apps/student/src/main/java/com/instructure/student/mobius/common/ui/MobiusFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/common/ui/MobiusFragment.kt index 9122b2927c..b2cdc4541c 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/common/ui/MobiusFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/common/ui/MobiusFragment.kt @@ -22,14 +22,19 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import com.crashlytics.android.Crashlytics import com.instructure.interactions.FragmentInteractions import com.instructure.interactions.Navigation +import com.instructure.student.BuildConfig import com.instructure.student.mobius.common.* import com.spotify.mobius.* import com.spotify.mobius.android.MobiusAndroid import com.spotify.mobius.android.runners.MainThreadWorkRunner import com.spotify.mobius.functions.Consumer import kotlinx.android.extensions.LayoutContainer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch abstract class MobiusFragment, VIEW_STATE> : Fragment(), FragmentInteractions { var overrideInitModel: MODEL? = null @@ -89,6 +94,7 @@ abstract class MobiusFragment : CoroutineConnection( protected var consumer = ConsumerQueueWrapper() + private val connectionWrapper by lazy { ExceptionLoggerConnectionWrapper(this) } + override fun connect(output: Consumer): Connection { consumer.attach(output) - return this + return connectionWrapper } override fun dispose() { @@ -213,4 +221,28 @@ abstract class EffectHandler : CoroutineConnection( fun logEvent(eventName: String) { // TODO } + + /** + * Catches exceptions in the [accept] function of the provided [connection] and logs them to Crashlytics. + * For debug builds the exception will be logged locally and then thrown. + */ + private class ExceptionLoggerConnectionWrapper(private val connection: Connection) : Connection { + override fun accept(value: T) { + try { + connection.accept(value) + } catch (e: Throwable) { + if (BuildConfig.DEBUG) { + e.printStackTrace() + // Must throw as a separate message, otherwise Mobius might silently consume the exception + GlobalScope.launch(Dispatchers.Main) { throw e } + } else { + Crashlytics.logException(e) + } + } + } + + override fun dispose() { + connection.dispose() + } + } }