mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Send crash reports to the server (#65)
Adds ability to send app crash reports to the last connected server. The app prompts you before sending. There is an advanced settings to disable this as well. Additionally, there is an advanced settings button to send the app logs to the current server. The resulting reports can be found on the server's web UI under Dashboard->Logs If sharing the logs, make sure there's no private information present!
This commit is contained in:
parent
05f3811149
commit
2667090485
11 changed files with 278 additions and 41 deletions
|
|
@ -231,6 +231,11 @@ dependencies {
|
|||
implementation(libs.multiplatform.markdown.renderer)
|
||||
implementation(libs.multiplatform.markdown.renderer.m3)
|
||||
implementation(libs.programguide)
|
||||
implementation(libs.acra.http)
|
||||
implementation(libs.acra.dialog)
|
||||
implementation(libs.acra.limiter)
|
||||
compileOnly(libs.auto.service.annotations)
|
||||
ksp(libs.auto.service.ksp)
|
||||
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,20 @@
|
|||
package com.github.damontecres.wholphin
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Composer
|
||||
import androidx.compose.runtime.ExperimentalComposeRuntimeApi
|
||||
import androidx.compose.runtime.tooling.ComposeStackTraceMode
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import org.acra.ACRA
|
||||
import org.acra.ReportField
|
||||
import org.acra.config.dialog
|
||||
import org.acra.data.StringFormat
|
||||
import org.acra.ktx.initAcra
|
||||
import timber.log.Timber
|
||||
|
||||
@OptIn(ExperimentalComposeRuntimeApi::class)
|
||||
@HiltAndroidApp
|
||||
class WholphinApplication : Application() {
|
||||
init {
|
||||
|
|
@ -31,6 +41,48 @@ class WholphinApplication : Application() {
|
|||
},
|
||||
)
|
||||
}
|
||||
|
||||
Composer.setDiagnosticStackTraceMode(
|
||||
if (BuildConfig.DEBUG) ComposeStackTraceMode.SourceInformation else ComposeStackTraceMode.GroupKeys,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
initAcra {
|
||||
buildConfigClass = BuildConfig::class.java
|
||||
reportFormat = StringFormat.JSON
|
||||
excludeMatchingSharedPreferencesKeys = listOf()
|
||||
reportContent =
|
||||
listOf(
|
||||
ReportField.ANDROID_VERSION,
|
||||
ReportField.APP_VERSION_CODE,
|
||||
ReportField.APP_VERSION_NAME,
|
||||
ReportField.BRAND,
|
||||
// ReportField.BUILD_CONFIG,
|
||||
// ReportField.BUILD,
|
||||
ReportField.CUSTOM_DATA,
|
||||
ReportField.LOGCAT,
|
||||
ReportField.PHONE_MODEL,
|
||||
ReportField.PRODUCT,
|
||||
ReportField.REPORT_ID,
|
||||
ReportField.SHARED_PREFERENCES,
|
||||
ReportField.STACK_TRACE,
|
||||
ReportField.USER_COMMENT,
|
||||
ReportField.USER_CRASH_DATE,
|
||||
)
|
||||
dialog {
|
||||
text =
|
||||
"Wholphin has crashed! Would you like to attempt to " +
|
||||
"send a crash report to your Jellyfin server?"
|
||||
title = "Wholphin Crash Report"
|
||||
positiveButtonText = "Send"
|
||||
negativeButtonText = "Do not send"
|
||||
}
|
||||
reportSendFailureToast = "Crash report failed to send"
|
||||
reportSendSuccessToast = "Sent crash report!"
|
||||
}
|
||||
ACRA.errorReporter.putCustomData("SDK_INT", Build.VERSION.SDK_INT.toString())
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
package com.github.damontecres.wholphin.data
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.content.edit
|
||||
import androidx.datastore.core.DataStore
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.ui.toServerString
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.Jellyfin
|
||||
|
|
@ -27,11 +31,14 @@ import javax.inject.Singleton
|
|||
class ServerRepository
|
||||
@Inject
|
||||
constructor(
|
||||
@param:ApplicationContext private val context: Context,
|
||||
val jellyfin: Jellyfin,
|
||||
val serverDao: JellyfinServerDao,
|
||||
val apiClient: ApiClient,
|
||||
val userPreferencesDataStore: DataStore<AppPreferences>,
|
||||
) {
|
||||
private val sharedPreferences = getServerSharedPreferences(context)
|
||||
|
||||
private var _currentServer by mutableStateOf<JellyfinServer?>(null)
|
||||
val currentServer get() = _currentServer
|
||||
private var _currentUser by mutableStateOf<JellyfinUser?>(null)
|
||||
|
|
@ -102,6 +109,10 @@ class ServerRepository
|
|||
_currentServer = updatedServer
|
||||
_currentUser = updatedUser
|
||||
}
|
||||
sharedPreferences.edit(true) {
|
||||
putString(SERVER_URL_KEY, updatedServer.url)
|
||||
putString(ACCESS_TOKEN_KEY, updatedUser.accessToken)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -200,4 +211,15 @@ class ServerRepository
|
|||
serverDao.deleteServer(server.id)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getServerSharedPreferences(context: Context): SharedPreferences =
|
||||
context.getSharedPreferences(
|
||||
"${context.packageName}_server",
|
||||
Context.MODE_PRIVATE,
|
||||
)
|
||||
|
||||
const val SERVER_URL_KEY = "current.server"
|
||||
const val ACCESS_TOKEN_KEY = "current.accessToken"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ package com.github.damontecres.wholphin.preferences
|
|||
import android.content.Context
|
||||
import androidx.annotation.ArrayRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.WholphinApplication
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.preferences.PreferenceGroup
|
||||
import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption
|
||||
|
|
@ -533,6 +536,34 @@ sealed interface AppPreference<T> {
|
|||
getter = { },
|
||||
setter = { prefs, _ -> prefs },
|
||||
)
|
||||
|
||||
val SendCrashReports =
|
||||
AppSwitchPreference(
|
||||
title = R.string.send_crash_reports,
|
||||
defaultValue = true,
|
||||
getter = {
|
||||
PreferenceManager
|
||||
.getDefaultSharedPreferences(WholphinApplication.instance)
|
||||
.getBoolean("acra.enable", true)
|
||||
},
|
||||
setter = { prefs, value ->
|
||||
PreferenceManager
|
||||
.getDefaultSharedPreferences(WholphinApplication.instance)
|
||||
.edit {
|
||||
putBoolean("acra.enable", value)
|
||||
}
|
||||
prefs.update { sendCrashReports = value }
|
||||
},
|
||||
summary = R.string.send_crash_reports_summary,
|
||||
)
|
||||
|
||||
val SendAppLogs =
|
||||
AppClickablePreference(
|
||||
title = R.string.send_app_logs,
|
||||
summary = R.string.send_app_logs_summary,
|
||||
getter = { },
|
||||
setter = { prefs, _ -> prefs },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -626,6 +657,8 @@ val advancedPreferences =
|
|||
title = R.string.more,
|
||||
preferences =
|
||||
listOf(
|
||||
AppPreference.SendAppLogs,
|
||||
AppPreference.SendCrashReports,
|
||||
AppPreference.ClearImageCache,
|
||||
AppPreference.OssLicenseInfo,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ class DebugViewModel
|
|||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getLogCatLines(): List<LogcatLine> {
|
||||
val lineCount = 500
|
||||
val args =
|
||||
|
|
@ -110,6 +111,7 @@ class DebugViewModel
|
|||
return logLines
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class LogcatLine(
|
||||
val level: Int,
|
||||
|
|
|
|||
|
|
@ -283,6 +283,19 @@ fun PreferencesContent(
|
|||
}
|
||||
}
|
||||
|
||||
AppPreference.SendAppLogs -> {
|
||||
ClickPreference(
|
||||
title = stringResource(pref.title),
|
||||
onClick = {
|
||||
viewModel.sendAppLogs()
|
||||
},
|
||||
modifier = Modifier,
|
||||
summary = pref.summary(context, null),
|
||||
onLongClick = {},
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val value = pref.getter.invoke(preferences)
|
||||
ComposablePreference(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.github.damontecres.wholphin.ui.preferences
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
|
@ -11,25 +12,36 @@ import com.github.damontecres.wholphin.data.isPinned
|
|||
import com.github.damontecres.wholphin.data.model.NavDrawerPinnedItem
|
||||
import com.github.damontecres.wholphin.data.model.NavPinType
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.ui.detail.DebugViewModel
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.nav.NavDrawerItem
|
||||
import com.github.damontecres.wholphin.ui.nav.NavigationManager
|
||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||
import com.github.damontecres.wholphin.ui.showToast
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
import com.github.damontecres.wholphin.util.RememberTabManager
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.clientLogApi
|
||||
import org.jellyfin.sdk.model.ClientInfo
|
||||
import org.jellyfin.sdk.model.DeviceInfo
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class PreferencesViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
@param:ApplicationContext private val context: Context,
|
||||
private val api: ApiClient,
|
||||
val preferenceDataStore: DataStore<AppPreferences>,
|
||||
val navigationManager: NavigationManager,
|
||||
val rememberTabManager: RememberTabManager,
|
||||
val serverRepository: ServerRepository,
|
||||
val navDrawerItemRepository: NavDrawerItemRepository,
|
||||
val serverPreferencesDao: ServerPreferencesDao,
|
||||
private val rememberTabManager: RememberTabManager,
|
||||
private val serverRepository: ServerRepository,
|
||||
private val navDrawerItemRepository: NavDrawerItemRepository,
|
||||
private val serverPreferencesDao: ServerPreferencesDao,
|
||||
private val deviceInfo: DeviceInfo,
|
||||
private val clientInfo: ClientInfo,
|
||||
) : ViewModel(),
|
||||
RememberTabManager by rememberTabManager {
|
||||
private lateinit var allNavDrawerItems: List<NavDrawerItem>
|
||||
|
|
@ -77,4 +89,19 @@ class PreferencesViewModel
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun sendAppLogs() {
|
||||
viewModelScope.launchIO(ExceptionHandler(true)) {
|
||||
val logcat = DebugViewModel.getLogCatLines().joinToString("\n") { it.text }
|
||||
val body =
|
||||
"""
|
||||
Send App Logs
|
||||
$clientInfo
|
||||
$deviceInfo
|
||||
|
||||
""".trimIndent() + logcat
|
||||
val response by api.clientLogApi.logFile(body)
|
||||
showToast(context, "Sent! Filename=${response.fileName}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
package com.github.damontecres.wholphin.util
|
||||
|
||||
import android.content.Context
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.hilt.AppModule
|
||||
import com.google.auto.service.AutoService
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.OkHttpClient
|
||||
import org.acra.config.CoreConfiguration
|
||||
import org.acra.data.CrashReportData
|
||||
import org.acra.sender.ReportSender
|
||||
import org.acra.sender.ReportSenderException
|
||||
import org.acra.sender.ReportSenderFactory
|
||||
import org.jellyfin.sdk.Jellyfin
|
||||
import org.jellyfin.sdk.api.client.extensions.clientLogApi
|
||||
import org.jellyfin.sdk.api.okhttp.OkHttpFactory
|
||||
import org.jellyfin.sdk.createJellyfin
|
||||
import timber.log.Timber
|
||||
|
||||
@AutoService(ReportSenderFactory::class)
|
||||
class CrashReportSenderFactory : ReportSenderFactory {
|
||||
override fun create(
|
||||
context: Context,
|
||||
config: CoreConfiguration,
|
||||
): ReportSender = CrashReportSender()
|
||||
|
||||
override fun enabled(config: CoreConfiguration): Boolean = true
|
||||
}
|
||||
|
||||
class CrashReportSender : ReportSender {
|
||||
override fun send(
|
||||
context: Context,
|
||||
errorContent: CrashReportData,
|
||||
) {
|
||||
Timber.v("Attempting to send crash report")
|
||||
val prefs = ServerRepository.getServerSharedPreferences(context)
|
||||
val serverUrl = prefs.getString(ServerRepository.SERVER_URL_KEY, null)
|
||||
val accessToken = prefs.getString(ServerRepository.ACCESS_TOKEN_KEY, null)
|
||||
if (serverUrl != null && accessToken != null) {
|
||||
try {
|
||||
val okHttpClient =
|
||||
OkHttpClient
|
||||
.Builder()
|
||||
.build()
|
||||
val okHttpFactory = OkHttpFactory(okHttpClient)
|
||||
val api =
|
||||
createJellyfin {
|
||||
this.context = context
|
||||
clientInfo = AppModule.clientInfo()
|
||||
deviceInfo = AppModule.deviceInfo(context)
|
||||
apiClientFactory = okHttpFactory
|
||||
socketConnectionFactory = okHttpFactory
|
||||
minimumServerVersion = Jellyfin.minimumVersion
|
||||
}.createApi(baseUrl = serverUrl, accessToken = accessToken)
|
||||
|
||||
runBlocking {
|
||||
val filename =
|
||||
api.clientLogApi
|
||||
.logFile(errorContent.toJSON())
|
||||
.content.fileName
|
||||
Timber.i("Sent report to $serverUrl, filename=$filename")
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
throw ReportSenderException("Exception while sending crash report", ex)
|
||||
}
|
||||
} else {
|
||||
throw ReportSenderException("Could not find valid server and/or credentials to use")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -77,4 +77,5 @@ message AppPreferences {
|
|||
|
||||
bool auto_check_for_updates = 6;
|
||||
string update_url = 7;
|
||||
bool send_crash_reports = 8;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,5 +92,9 @@
|
|||
<string name="profile_specific_settings">User Profile Settings</string>
|
||||
<string name="nav_drawer_pins_summary">Choose the default items to show, others will be hidden</string>
|
||||
<string name="combine_continue_next_summary">Applies to TV Series only</string>
|
||||
<string name="send_crash_reports">Send Crash Reports</string>
|
||||
<string name="send_crash_reports_summary">Will try to send to last connected server</string>
|
||||
<string name="send_app_logs">Send app logs to current server</string>
|
||||
<string name="send_app_logs_summary">Useful for debugging</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
[versions]
|
||||
aboutLibraries = "12.2.4"
|
||||
acra = "5.13.1"
|
||||
agp = "8.13.0"
|
||||
auto-service = "1.1.1"
|
||||
autoServiceKsp = "1.2.0"
|
||||
desugar_jdk_libs = "2.1.5"
|
||||
hiltNavigationCompose = "1.3.0"
|
||||
kotlin = "2.2.20"
|
||||
|
|
@ -36,6 +39,9 @@ roomTesting = "2.8.2"
|
|||
[libraries]
|
||||
aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutLibraries" }
|
||||
aboutlibraries-compose-m3 = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutLibraries" }
|
||||
acra-dialog = { module = "ch.acra:acra-dialog", version.ref = "acra" }
|
||||
acra-http = { module = "ch.acra:acra-http", version.ref = "acra" }
|
||||
acra-limiter = { module = "ch.acra:acra-limiter", version.ref = "acra" }
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||
|
|
@ -55,6 +61,8 @@ androidx-tv-material = { group = "androidx.tv", name = "tv-material", version.re
|
|||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
||||
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||
androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" }
|
||||
auto-service-annotations = { module = "com.google.auto.service:auto-service-annotations", version.ref = "auto-service" }
|
||||
auto-service-ksp = { module = "dev.zacsweers.autoservice:auto-service-ksp", version.ref = "autoServiceKsp" }
|
||||
desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
|
||||
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
|
||||
hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue