diff --git a/husky/app/src/husky/res/values/donottranslate.xml b/husky/app/src/husky/res/values/donottranslate.xml index d6c9f29..8e994be 100644 --- a/husky/app/src/husky/res/values/donottranslate.xml +++ b/husky/app/src/husky/res/values/donottranslate.xml @@ -1,15 +1,15 @@ - - husky@nixnetmail.com - Husky crashed - Please, explain the steps to reproduce the crash (english only):\n\n - husky_crash_report.txt - ACRA - This channel is used for push notifications related to crashes captured by ACRA. - Husky crashed - There was an error that crashed Husky. If you want, you can report via email this crash. - Report - Discard + + husky@nixnetmail.com + Husky crashed + Please, explain the steps to reproduce the crash (english only):\n\n + husky_crash.txt + ACRA + This channel is used for push notifications related to crashes captured by ACRA. + Husky crashed + There was an error that crashed Husky. If you want, you can report via email this crash. + Report + Discard https://husky.adol.pw diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt b/husky/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt index 72897f0..c8f1fe0 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt +++ b/husky/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt @@ -31,6 +31,7 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.github.piasy.biv.BigImageViewer import com.github.piasy.biv.loader.glide.GlideCustomImageLoader import com.keylesspalace.tusky.components.notifications.NotificationWorkerFactory +import com.keylesspalace.tusky.core.logging.CrashHandler import com.keylesspalace.tusky.core.logging.HyperlinkDebugTree import com.keylesspalace.tusky.core.utils.ApplicationUtils import com.keylesspalace.tusky.di.AppInjector @@ -58,6 +59,10 @@ class TuskyApplication : Application(), HasAndroidInjector { override fun onCreate() { super.onCreate() + val preferences = PreferenceManager.getDefaultSharedPreferences(this) + + CrashHandler.setAsDefaultHandler(this) + if(ApplicationUtils.isDebug()) { Timber.plant(HyperlinkDebugTree()) } @@ -68,7 +73,6 @@ class TuskyApplication : Application(), HasAndroidInjector { AppInjector.init(this) - val preferences = PreferenceManager.getDefaultSharedPreferences(this) // init the custom emoji fonts val emojiSelection = preferences.getInt(PrefKeys.EMOJI, 0) @@ -99,8 +103,6 @@ class TuskyApplication : Application(), HasAndroidInjector { override fun attachBaseContext(base: Context) { localeManager = LocaleManager(base) super.attachBaseContext(localeManager.setLocale(base)) - - //setupAcra() } override fun onConfigurationChanged(newConfig: Configuration) { @@ -114,42 +116,4 @@ class TuskyApplication : Application(), HasAndroidInjector { @JvmStatic lateinit var localeManager: LocaleManager } - - // TODO: Enable ACRA again after figuring out what's wrong. - /* - private fun setupAcra() { - initAcra { - buildConfigClass = BuildConfig::class.java - reportFormat = StringFormat.KEY_VALUE_LIST - reportContent = listOf( - ANDROID_VERSION, - APP_VERSION_NAME, - APP_VERSION_CODE, - BUILD_CONFIG, - STACK_TRACE - ) - - notification { - title = getString(R.string.acra_notification_title) - text = getString(R.string.acra_notification_body) - channelName = getString(R.string.acra_notification_channel_title) - channelDescription = getString(R.string.acra_notification_channel_body) - channelImportance = NotificationManagerCompat.IMPORTANCE_DEFAULT - //resIcon = R.drawable.notification_icon - sendButtonText = getString(R.string.acra_notification_report) - //resSendButtonIcon = R.drawable.notification_send - discardButtonText = getString(R.string.acra_notification_discard) - //resDiscardButtonIcon = R.drawable.notification_discard - sendOnClick = false - } - - mailSender { - mailTo = getString(R.string.acra_email) - subject = getString(R.string.acra_email_subject) - body = getString(R.string.acra_email_body) - reportAsFile = true - reportFileName = getString(R.string.acra_email_report_filename) - } - } - }*/ } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/core/logging/CrashHandler.kt b/husky/app/src/main/java/com/keylesspalace/tusky/core/logging/CrashHandler.kt new file mode 100644 index 0000000..8b6315a --- /dev/null +++ b/husky/app/src/main/java/com/keylesspalace/tusky/core/logging/CrashHandler.kt @@ -0,0 +1,144 @@ +/* + * Husky -- A Pleroma client for Android + * + * Copyright (C) 2022 The Husky Developers + * + * 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, either version 3 of the License, or + * (at your option) any later version. + * + * 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.keylesspalace.tusky.core.logging + +import android.app.Activity +import android.app.Application +import android.content.Intent +import android.net.Uri +import android.os.Build.VERSION +import android.os.Bundle +import androidx.core.content.FileProvider +import com.keylesspalace.tusky.BuildConfig +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.core.ui.callbacks.ActivityCallback +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import timber.log.Timber + +class CrashHandler( + private val defaultHandler: Thread.UncaughtExceptionHandler, + private val huskyApp: Application +) : Thread.UncaughtExceptionHandler { + + private var lastActivity: Activity? = null + + companion object { + fun setAsDefaultHandler(application: Application) { + val handler = Thread.getDefaultUncaughtExceptionHandler()?.let { + CrashHandler(it, application) + } + Thread.setDefaultUncaughtExceptionHandler(handler) + } + } + + init { + huskyApp.registerActivityLifecycleCallbacks( + object : ActivityCallback() { + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + lastActivity = activity + Timber.d("onActivityCreated[${activity::class.simpleName}]") + } + + override fun onActivityResumed(activity: Activity) { + lastActivity = activity + Timber.d("onActivityResumed[${activity::class.simpleName}]") + } + + override fun onActivityStopped(activity: Activity) { + lastActivity = null + Timber.d("onActivityStopped[${activity::class.simpleName}]") + } + } + ) + } + + override fun uncaughtException(thread: Thread, throwable: Throwable) { + try { + sendLogEmail(throwable.stackTraceToString()) + } catch(e: IOException) { + Timber.e("CrashHandler Exception[${e.message}]") + } finally { + lastActivity?.finish() + } + } + + private fun getDeviceInfo(): String { + return StringBuilder().apply { + this.appendLine("Version name: ${BuildConfig.VERSION_NAME}") + .appendLine("Version code: ${BuildConfig.VERSION_CODE}") + .appendLine("OS Version: ${VERSION.RELEASE}") + .appendLine("SDK: ${VERSION.SDK_INT}") + }.toString() + } + + private fun sendLogEmail(stacktrace: String) { + createCrashesFolder() + + lastActivity?.let { activity -> + val formattedLog = StringBuilder().apply { + this.appendLine("## Steps to reproduce the crash:") + .appendLine("1. ###") + .appendLine("2. ###") + .appendLine("...") + .appendLine() + .appendLine("## Instance: INSTANCE_DOMAIN") + .appendLine() + .appendLine("## Device details") + .appendLine(getDeviceInfo()) + //.appendLine("## Crash details") + //.appendLine(stacktrace) + }.toString() + Timber.d(formattedLog) + + val intent = Intent(Intent.ACTION_SEND).apply { + type = "message/rfc822" + putExtra(Intent.EXTRA_EMAIL, arrayOf(activity.getString(R.string.crashhandler_email))) + putExtra(Intent.EXTRA_SUBJECT, "Husky ${BuildConfig.VERSION_NAME} crash") + putExtra(Intent.EXTRA_TEXT, formattedLog) + putExtra(Intent.EXTRA_STREAM, getCrashFileUri(activity, stacktrace)) + flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + } + + if(intent.resolveActivity(activity.packageManager) != null) { + activity.startActivity(intent) + } + } + } + + private fun createCrashesFolder() { + File("${huskyApp.cacheDir}/crashes").mkdirs() + } + + private fun getCrashFileUri(activity: Activity, stacktrace: String): Uri { + val file = File("${huskyApp.cacheDir}/crashes", activity.getString(R.string.crashhandler_email_report_filename)) + FileOutputStream(file).apply { + write(stacktrace.toByteArray()) + }.also { + it.close() + } + return FileProvider.getUriForFile( + activity, + "${BuildConfig.APPLICATION_ID}.fileprovider", + file + ) + } +} diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/core/ui/callbacks/ActivityCallback.kt b/husky/app/src/main/java/com/keylesspalace/tusky/core/ui/callbacks/ActivityCallback.kt new file mode 100644 index 0000000..8f96c4b --- /dev/null +++ b/husky/app/src/main/java/com/keylesspalace/tusky/core/ui/callbacks/ActivityCallback.kt @@ -0,0 +1,48 @@ +/* + * Husky -- A Pleroma client for Android + * + * Copyright (C) 2022 The Husky Developers + * + * 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, either version 3 of the License, or + * (at your option) any later version. + * + * 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.keylesspalace.tusky.core.ui.callbacks + +import android.app.Activity +import android.app.Application.ActivityLifecycleCallbacks +import android.os.Bundle + +open class ActivityCallback : ActivityLifecycleCallbacks { + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + } + + override fun onActivityStarted(activity: Activity) { + } + + override fun onActivityResumed(activity: Activity) { + } + + override fun onActivityPaused(activity: Activity) { + } + + override fun onActivityStopped(activity: Activity) { + } + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { + } + + override fun onActivityDestroyed(activity: Activity) { + } +} diff --git a/husky/app/src/main/res/xml/file_paths.xml b/husky/app/src/main/res/xml/file_paths.xml index 61f9cde..d727b98 100644 --- a/husky/app/src/main/res/xml/file_paths.xml +++ b/husky/app/src/main/res/xml/file_paths.xml @@ -1,5 +1,12 @@ - - - \ No newline at end of file + + + +