diff --git a/android/.gitignore b/android/.gitignore index 796b96d..cc1668c 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -1 +1,2 @@ /build +acra.properties diff --git a/android/acra.properties.sample b/android/acra.properties.sample new file mode 100644 index 0000000..c0c912f --- /dev/null +++ b/android/acra.properties.sample @@ -0,0 +1,3 @@ +url=ACRA_URL +user=ACRA_USER +pass=ACRA_PASS diff --git a/android/build.gradle b/android/build.gradle index 8d41eaf..e16c285 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -3,6 +3,17 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' +def acraProperties = new Properties() +try { + def acraPropertiesFile = project.file("acra.properties") + acraProperties.load(new FileInputStream(acraPropertiesFile)) +} catch (FileNotFoundException ignored) { + logger.warn("Unable to load ACRA properties. Error reporting won't be available") + acraProperties['url'] = "" + acraProperties['user'] = "" + acraProperties['pass'] = "" +} + android { compileSdkVersion 29 compileOptions { @@ -25,6 +36,9 @@ android { // buildConfigField "String", "API_URL", "\"http://192.168.86.163:8080/\"" // buildConfigField "String", "API_URL", "\"http://10.0.2.2:8080/\"" buildConfigField "String", "API_URL", "\"https://api.twigs.brawner.dev/\"" + buildConfigField "String", "ACRA_URL", "\"${acraProperties['url']}\"" + buildConfigField "String", "ACRA_USER", "\"${acraProperties['user']}\"" + buildConfigField "String", "ACRA_PASS", "\"${acraProperties['pass']}\"" } buildTypes { release { @@ -65,6 +79,8 @@ dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0' implementation 'androidx.navigation:navigation-fragment-ktx:2.2.2' implementation 'androidx.navigation:navigation-ui-ktx:2.2.2' + implementation "ch.acra:acra-http:$acra_version" + implementation "ch.acra:acra-advanced-scheduler:$acra_version" debugImplementation "com.willowtreeapps.hyperion:hyperion-core:$hyperion" debugImplementation "com.willowtreeapps.hyperion:hyperion-attr:$hyperion" diff --git a/android/src/main/java/com/wbrawner/budget/AllowanceApplication.kt b/android/src/main/java/com/wbrawner/budget/AllowanceApplication.kt index da45f4b..128290e 100644 --- a/android/src/main/java/com/wbrawner/budget/AllowanceApplication.kt +++ b/android/src/main/java/com/wbrawner/budget/AllowanceApplication.kt @@ -14,5 +14,6 @@ class AllowanceApplication : Application() { .baseUrl(BuildConfig.API_URL) .context(this) .build() + appComponent.errorHandler.init(this) } } diff --git a/android/src/main/java/com/wbrawner/budget/AppComponent.kt b/android/src/main/java/com/wbrawner/budget/AppComponent.kt index fe08147..2ba78f0 100644 --- a/android/src/main/java/com/wbrawner/budget/AppComponent.kt +++ b/android/src/main/java/com/wbrawner/budget/AppComponent.kt @@ -1,6 +1,7 @@ package com.wbrawner.budget import android.content.Context +import com.wbrawner.budget.common.util.ErrorHandler import com.wbrawner.budget.lib.network.NetworkModule import com.wbrawner.budget.storage.StorageModule import com.wbrawner.budget.ui.SplashViewModel @@ -18,7 +19,7 @@ import javax.inject.Named import javax.inject.Singleton @Singleton -@Component(modules = [StorageModule::class, NetworkModule::class]) +@Component(modules = [AppModule::class, StorageModule::class, NetworkModule::class]) interface AppComponent { fun inject(viewModel: OverviewViewModel) fun inject(viewModel: SplashViewModel) @@ -30,6 +31,9 @@ interface AppComponent { fun inject(viewModel: TransactionListViewModel) fun inject(viewModel: TransactionFormViewModel) + @Singleton + val errorHandler: ErrorHandler + @Component.Builder interface Builder { @BindsInstance diff --git a/android/src/main/java/com/wbrawner/budget/AppModule.kt b/android/src/main/java/com/wbrawner/budget/AppModule.kt new file mode 100644 index 0000000..a1c13e5 --- /dev/null +++ b/android/src/main/java/com/wbrawner/budget/AppModule.kt @@ -0,0 +1,12 @@ +package com.wbrawner.budget + +import com.wbrawner.budget.common.util.ErrorHandler +import com.wbrawner.budget.util.AcraErrorHandler +import dagger.Module +import dagger.Provides + +@Module +class AppModule { + @Provides + fun provideErrorHandler(): ErrorHandler = AcraErrorHandler() +} \ No newline at end of file diff --git a/android/src/main/java/com/wbrawner/budget/util/AcraErrorHandler.kt b/android/src/main/java/com/wbrawner/budget/util/AcraErrorHandler.kt new file mode 100644 index 0000000..d9faf6c --- /dev/null +++ b/android/src/main/java/com/wbrawner/budget/util/AcraErrorHandler.kt @@ -0,0 +1,68 @@ +package com.wbrawner.budget.util + +import android.app.Application +import android.app.job.JobInfo +import android.util.Log +import com.wbrawner.budget.BuildConfig +import com.wbrawner.budget.common.util.ErrorHandler +import org.acra.ACRA +import org.acra.ReportField +import org.acra.config.CoreConfigurationBuilder +import org.acra.config.HttpSenderConfigurationBuilder +import org.acra.config.SchedulerConfigurationBuilder +import org.acra.data.StringFormat +import org.acra.sender.HttpSender + +class AcraErrorHandler : ErrorHandler { + + override fun init(application: Application) { + if (BuildConfig.ACRA_URL.isBlank() + || BuildConfig.ACRA_USER.isBlank() + || BuildConfig.ACRA_PASS.isBlank()) { + return + } + val builder = CoreConfigurationBuilder(application) + .setBuildConfigClass(BuildConfig::class.java) + .setReportFormat(StringFormat.JSON) + .setReportContent( + ReportField.ANDROID_VERSION, + ReportField.APP_VERSION_CODE, + ReportField.APP_VERSION_NAME, + ReportField.APPLICATION_LOG, + ReportField.AVAILABLE_MEM_SIZE, + ReportField.BRAND, + ReportField.BUILD_CONFIG, + ReportField.CRASH_CONFIGURATION, + ReportField.CUSTOM_DATA, // Not currently used, but might be useful in the future + ReportField.INITIAL_CONFIGURATION, + ReportField.PACKAGE_NAME, + ReportField.PHONE_MODEL, + ReportField.SHARED_PREFERENCES, + ReportField.STACK_TRACE, + ReportField.STACK_TRACE_HASH, + ReportField.THREAD_DETAILS, + ReportField.TOTAL_MEM_SIZE, + ReportField.USER_APP_START_DATE, + ReportField.USER_CRASH_DATE + ) + builder.getPluginConfigurationBuilder(HttpSenderConfigurationBuilder::class.java) + .setUri(BuildConfig.ACRA_URL) + .setHttpMethod(HttpSender.Method.POST) + .setBasicAuthLogin(BuildConfig.ACRA_USER) + .setBasicAuthPassword(BuildConfig.ACRA_PASS) + .setEnabled(true) + builder.getPluginConfigurationBuilder(SchedulerConfigurationBuilder::class.java) + .setRequiresNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) + .setRequiresBatteryNotLow(true) + .setEnabled(true) + ACRA.init(application, builder) + } + + override fun reportException(t: Throwable, message: String?) { + @Suppress("ConstantConditionIf") + if (BuildConfig.DEBUG) { + Log.e("AcraErrorHandler", "Caught exception: $message", t) + } + ACRA.getErrorReporter().handleException(t) + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 337769e..10b81e9 100644 --- a/build.gradle +++ b/build.gradle @@ -1,11 +1,12 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.3.72' + ext.acra_version = '5.7.0' ext.dagger = '2.23.1' + ext.hyperion = '0.9.27' + ext.kotlin_version = '1.3.72' ext.moshi = '1.8.0' ext.retrofit = '2.6.0' - ext.hyperion = '0.9.27' ext.room_version = "2.2.5" repositories { google() diff --git a/common/src/main/java/com/wbrawner/budget/common/util/ErrorHandler.kt b/common/src/main/java/com/wbrawner/budget/common/util/ErrorHandler.kt new file mode 100644 index 0000000..8318a6c --- /dev/null +++ b/common/src/main/java/com/wbrawner/budget/common/util/ErrorHandler.kt @@ -0,0 +1,8 @@ +package com.wbrawner.budget.common.util + +import android.app.Application + +interface ErrorHandler { + fun init(application: Application) + fun reportException(t: Throwable, message: String? = null) +}