From ac164c5b064e3015a490b44015ab1c1b04a17a57 Mon Sep 17 00:00:00 2001 From: damontecres <154766448+damontecres@users.noreply.github.com> Date: Fri, 2 Aug 2024 19:17:06 -0400 Subject: [PATCH] Send logs & crash reports to stash server (#339) Because gathering logs and information about app crashes is so difficult, this PR adds the ability to gather that information and send it to the user's Stash server if the companion plugin is installed. The logs are only ever sent to the user's own stash server to help protect privacy. If the app crashes, a dialog will appear asking whether or not to send a crash report to the server. The report will be logged server-side in Log tab of Settings (eg http://192.168.1.5:9999/settings?tab=logs). This can be disabled in advanced settings (ie the dialog will never appear). PR to add the plugin to the community scripts: https://github.com/stashapp/CommunityScripts/pull/381 --- app/build.gradle.kts | 12 ++ app/src/main/graphql/Configuration.graphql | 10 ++ app/src/main/graphql/RunPluginTask.graphql | 3 + .../damontecres/stashapp/DebugActivity.kt | 78 ++++++++++++- .../damontecres/stashapp/MainFragment.kt | 70 ++++++----- .../damontecres/stashapp/SettingsFragment.kt | 52 ++++++--- .../damontecres/stashapp/StashApplication.kt | 42 +++++++ .../stashapp/util/MutationEngine.kt | 2 +- .../damontecres/stashapp/util/QueryEngine.kt | 6 + .../stashapp/util/ServerPreferences.kt | 54 +++++---- .../damontecres/stashapp/util/StashClient.kt | 8 +- .../stashapp/util/plugin/CompanionPlugin.kt | 109 ++++++++++++++++++ .../util/plugin/CrashReportSenderFactory.kt | 58 ++++++++++ .../stashapp/util/plugin/LogcatTags.kt | 83 +++++++++++++ app/src/main/res/layout/debug.xml | 42 +++++++ app/src/main/res/xml/advanced_preferences.xml | 6 + app/src/main/res/xml/root_preferences.xml | 3 + 17 files changed, 551 insertions(+), 87 deletions(-) create mode 100644 app/src/main/graphql/RunPluginTask.graphql create mode 100644 app/src/main/java/com/github/damontecres/stashapp/util/plugin/CompanionPlugin.kt create mode 100644 app/src/main/java/com/github/damontecres/stashapp/util/plugin/CrashReportSenderFactory.kt create mode 100644 app/src/main/java/com/github/damontecres/stashapp/util/plugin/LogcatTags.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bbca5640..d64a0363 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -41,6 +41,10 @@ android { } } + buildFeatures { + buildConfig = true + } + defaultConfig { applicationId = "com.github.damontecres.stashapp" minSdk = 23 @@ -130,6 +134,7 @@ tasks.preBuild.dependsOn("generateStrings") val mediaVersion = "1.3.1" val glideVersion = "4.16.0" +val acraVersion = "5.11.3" dependencies { implementation("androidx.core:core-ktx:1.13.1") @@ -139,6 +144,7 @@ dependencies { implementation("com.github.bumptech.glide:glide:$glideVersion") implementation("com.github.bumptech.glide:okhttp3-integration:$glideVersion") implementation("androidx.leanback:leanback-tab:1.1.0-beta01") + implementation("androidx.test.ext:junit-ktx:1.2.1") kapt("com.github.bumptech.glide:compiler:$glideVersion") implementation("com.caverock:androidsvg-aar:1.4") @@ -162,6 +168,12 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") implementation("io.noties.markwon:core:4.6.2") + implementation("ch.acra:acra-http:$acraVersion") + implementation("ch.acra:acra-dialog:$acraVersion") + implementation("ch.acra:acra-limiter:$acraVersion") + compileOnly("com.google.auto.service:auto-service-annotations:1.1.1") + kapt("com.google.auto.service:auto-service:1.1.1") + testImplementation("androidx.test:core-ktx:1.6.1") testImplementation("junit:junit:4.13.2") testImplementation("org.mockito:mockito-core:5.9.0") diff --git a/app/src/main/graphql/Configuration.graphql b/app/src/main/graphql/Configuration.graphql index c73a33eb..ad5a0a40 100644 --- a/app/src/main/graphql/Configuration.graphql +++ b/app/src/main/graphql/Configuration.graphql @@ -31,6 +31,16 @@ query Configuration { } ui } + version { + version + hash + build_time + } + plugins { + id + name + version + } } query ConfigurationUI { diff --git a/app/src/main/graphql/RunPluginTask.graphql b/app/src/main/graphql/RunPluginTask.graphql new file mode 100644 index 00000000..eea3cb89 --- /dev/null +++ b/app/src/main/graphql/RunPluginTask.graphql @@ -0,0 +1,3 @@ +mutation RunPluginTask($plugin_id: ID!, $task_name: String!, $args_map: Map) { + runPluginTask(plugin_id: $plugin_id, task_name: $task_name, args_map: $args_map) +} diff --git a/app/src/main/java/com/github/damontecres/stashapp/DebugActivity.kt b/app/src/main/java/com/github/damontecres/stashapp/DebugActivity.kt index e1bce59e..4f9deb64 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/DebugActivity.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/DebugActivity.kt @@ -1,17 +1,25 @@ package com.github.damontecres.stashapp import android.graphics.Color +import android.graphics.Typeface import android.os.Bundle import android.view.Gravity import android.view.View import android.widget.TableLayout import android.widget.TableRow import android.widget.TextView +import android.widget.Toast import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager import com.github.damontecres.stashapp.playback.CodecSupport import com.github.damontecres.stashapp.util.ServerPreferences +import com.github.damontecres.stashapp.util.StashClient +import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler +import com.github.damontecres.stashapp.util.StashServer +import com.github.damontecres.stashapp.util.plugin.CompanionPlugin +import kotlinx.coroutines.launch class DebugActivity : FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -34,6 +42,8 @@ class DebugActivity : FragmentActivity() { val prefTable = view.findViewById(R.id.preferences_table) val serverPrefTable = view.findViewById(R.id.server_prefs_table) val formatSupportedTable = view.findViewById(R.id.supported_formats_table) + val otherTable = view.findViewById(R.id.other_table) + val logTextView = view.findViewById(R.id.logs) val prefManager = PreferenceManager.getDefaultSharedPreferences(requireContext()).all prefManager.keys.sorted().forEach { @@ -42,9 +52,10 @@ class DebugActivity : FragmentActivity() { } prefTable.isStretchAllColumns = true - val serverPrefs = ServerPreferences(requireContext()).preferences.all - serverPrefs.keys.sorted().forEach { - val row = createRow(it, serverPrefs[it].toString()) + val serverPrefs = ServerPreferences(requireContext()) + val serverPrefsRaw = serverPrefs.preferences.all + serverPrefsRaw.keys.sorted().forEach { + val row = createRow(it, serverPrefsRaw[it].toString()) serverPrefTable.addView(row) } serverPrefTable.isStretchAllColumns = true @@ -69,6 +80,54 @@ class DebugActivity : FragmentActivity() { ), ) formatSupportedTable.isStretchAllColumns = true + + val server = StashServer.getCurrentStashServer(requireContext()) + if (server != null) { + otherTable.addView( + createRow( + "Current server URL", + server.url, + ), + ) + otherTable.addView( + createRow( + "Current server API Key", + server.apiKey, + ), + ) + otherTable.addView( + createRow( + "Current server URL (resolved endpoint)", + StashClient.cleanServerUrl(server.url), + ), + ) + otherTable.addView( + createRow( + "Current server URL (root)", + StashClient.getServerRoot(server.url), + ), + ) + } + otherTable.addView( + createRow( + "User-Agent", + StashClient.createUserAgent(requireContext()), + ), + ) + otherTable.isStretchAllColumns = true + + viewLifecycleOwner.lifecycleScope.launch( + StashCoroutineExceptionHandler { + Toast.makeText( + requireContext(), + "Exception getting logs: ${it.message}", + Toast.LENGTH_LONG, + ) + }, + ) { + val logs = CompanionPlugin.getLogCatLines(true).joinToString("\n") + logTextView.text = logs + } } private fun createRow( @@ -95,8 +154,19 @@ class DebugActivity : FragmentActivity() { // keyView.maxLines=8 // keyView.maxWidth=400 + val isApiKey = key.contains("apikey", true) || key.contains("api key", true) + val valueView = TextView(requireContext()) - valueView.text = if (key.contains("apikey", true)) "*****" else value + valueView.text = + if (isApiKey && value != null + ) { + value.take(4) + "..." + value.takeLast(8) + } else { + value + } + if (isApiKey) { + valueView.typeface = Typeface.MONOSPACE + } valueView.textSize = TABLE_TEXT_SIZE valueView.setTextColor(Color.WHITE) valueView.textAlignment = TextView.TEXT_ALIGNMENT_VIEW_START diff --git a/app/src/main/java/com/github/damontecres/stashapp/MainFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/MainFragment.kt index 97763eae..2cef2629 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/MainFragment.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/MainFragment.kt @@ -17,7 +17,6 @@ import androidx.leanback.widget.ListRowPresenter import androidx.leanback.widget.SparseArrayObjectAdapter import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager -import com.github.damontecres.stashapp.api.ConfigurationQuery import com.github.damontecres.stashapp.api.ServerInfoQuery import com.github.damontecres.stashapp.api.fragment.ImageData import com.github.damontecres.stashapp.api.fragment.SlimSceneData @@ -295,44 +294,41 @@ class MainFragment : BrowseSupportFragment() { ?: Version.MINIMUM_STASH_VERSION, ) - val query = ConfigurationQuery() - val config = queryEngine.executeQuery(query).data?.configuration - ServerPreferences(requireContext()).updatePreferences(config, serverInfo) + val config = queryEngine.getServerConfiguration() + ServerPreferences(requireContext()).updatePreferences(config) - if (config?.ui != null) { - val ui = config.ui - val frontPageContent = - (ui as Map).getCaseInsensitive("frontPageContent") as List> - val pageSize = - PreferenceManager.getDefaultSharedPreferences(requireContext()) - .getInt(getString(R.string.pref_key_page_size), 25) - val frontPageParser = - FrontPageParser(queryEngine, filterParser, pageSize) - val jobs = frontPageParser.parse(frontPageContent) - jobs.forEachIndexed { index, job -> - job.await().let { row -> - if (row.successful) { - val rowData = row.data!! - filterList.set(index, rowData.filter) + val ui = config.configuration.ui + val frontPageContent = + (ui as Map).getCaseInsensitive("frontPageContent") as List> + val pageSize = + PreferenceManager.getDefaultSharedPreferences(requireContext()) + .getInt(getString(R.string.pref_key_page_size), 25) + val frontPageParser = + FrontPageParser(queryEngine, filterParser, pageSize) + val jobs = frontPageParser.parse(frontPageContent) + jobs.forEachIndexed { index, job -> + job.await().let { row -> + if (row.successful) { + val rowData = row.data!! + filterList.set(index, rowData.filter) - val adapter = ArrayObjectAdapter(StashPresenter.SELECTOR) - adapter.addAll(0, rowData.data) - adapter.add(rowData.filter) - adapters.add(adapter) - withContext(Dispatchers.Main) { - rowsAdapter.set( - index, - ListRow(HeaderItem(rowData.name), adapter), - ) - } - } else if (row.result == FrontPageParser.FrontPageRowResult.ERROR) { - withContext(Dispatchers.Main) { - Toast.makeText( - requireContext(), - "Error loading row $index on front page", - Toast.LENGTH_SHORT, - ).show() - } + val adapter = ArrayObjectAdapter(StashPresenter.SELECTOR) + adapter.addAll(0, rowData.data) + adapter.add(rowData.filter) + adapters.add(adapter) + withContext(Dispatchers.Main) { + rowsAdapter.set( + index, + ListRow(HeaderItem(rowData.name), adapter), + ) + } + } else if (row.result == FrontPageParser.FrontPageRowResult.ERROR) { + withContext(Dispatchers.Main) { + Toast.makeText( + requireContext(), + "Error loading row $index on front page", + Toast.LENGTH_SHORT, + ).show() } } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/SettingsFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/SettingsFragment.kt index 6e740244..f79b3555 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/SettingsFragment.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/SettingsFragment.kt @@ -34,6 +34,7 @@ import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler import com.github.damontecres.stashapp.util.StashServer import com.github.damontecres.stashapp.util.UpdateChecker import com.github.damontecres.stashapp.util.cacheDurationPrefToDuration +import com.github.damontecres.stashapp.util.plugin.CompanionPlugin import com.github.damontecres.stashapp.util.testStashConnection import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.Dispatchers @@ -286,41 +287,54 @@ class SettingsFragment : LeanbackSettingsFragmentCompat() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (savedInstanceState == null) { - val serverPref = findPreference(PREF_STASH_URL)!! requireActivity().supportFragmentManager.addOnBackStackChangedListener { - val currentServer = StashServer.getCurrentStashServer(requireContext()) - serverPref.summary = currentServer?.url + if (requireActivity().supportFragmentManager.backStackEntryCount == 0) { + refresh() + } } } } override fun onResume() { super.onResume() + refresh() + } + private fun refresh() { + Log.v(TAG, "refresh") val currentServer = StashServer.getCurrentStashServer(requireContext()) findPreference(PREF_STASH_URL)!!.summary = currentServer?.url + val sendLogsPref = findPreference("sendLogs")!! + sendLogsPref.isEnabled = false + sendLogsPref.summary = "Checking for companion plugin..." + viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler()) { - ServerPreferences(requireContext()).updatePreferences() + val serverPrefs = ServerPreferences(requireContext()).updatePreferences() + if (serverPrefs.companionPluginInstalled) { + sendLogsPref.isEnabled = true + sendLogsPref.summary = "Send a copy of recent app logs to your current server" + sendLogsPref.setOnPreferenceClickListener { + viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler()) { + CompanionPlugin.sendLogCat(requireContext(), false) + } + true + } + sendLogsPref.setOnLongClickListener { + viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler()) { + CompanionPlugin.sendLogCat(requireContext(), true) + } + true + } + } else { + sendLogsPref.isEnabled = false + sendLogsPref.summary = "Companion plugin not installed" + } } } - override fun onStop() { - super.onStop() - } - - private fun setServers() { - val manager = PreferenceManager.getDefaultSharedPreferences(requireContext()) - val keys = - manager.all.keys.filter { it.startsWith(SERVER_PREF_PREFIX) }.sorted().toList() - val values = keys.map { manager.all[it].toString() }.toList() - - serverKeys = values - serverValues = keys - } - companion object { - const val TAG = "SettingsFragment" + private const val TAG = "SettingsFragment" const val SERVER_PREF_PREFIX = "server_" const val SERVER_APIKEY_PREF_PREFIX = "apikey_" diff --git a/app/src/main/java/com/github/damontecres/stashapp/StashApplication.kt b/app/src/main/java/com/github/damontecres/stashapp/StashApplication.kt index 50bbc816..5a2d9174 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/StashApplication.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/StashApplication.kt @@ -4,6 +4,7 @@ import android.app.Activity import android.app.Application import android.content.Intent import android.graphics.Typeface +import android.os.Build import android.os.Bundle import android.util.Log import android.view.WindowManager @@ -21,6 +22,11 @@ import com.github.damontecres.stashapp.util.Version import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import org.acra.ACRA +import org.acra.ReportField +import org.acra.config.dialog +import org.acra.data.StringFormat +import org.acra.ktx.initAcra class StashApplication : Application() { private var wasEnterBackground = false @@ -34,6 +40,42 @@ class StashApplication : Application() { Log.v(TAG, "onCreate wasEnterBackground=$wasEnterBackground, mainDestroyed=$mainDestroyed") + initAcra { + buildConfigClass = BuildConfig::class.java + reportFormat = StringFormat.JSON + excludeMatchingSharedPreferencesKeys = + listOf("^stashApiKey$", "^stashUrl$", "^server_.*", "^apikey_.*", "^pinCode$") + 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 = + "StashAppAndroidTV has crashed! Would you like to attempt to send a crash report to your Stash server?" + + "\n\nThis will only work if you have already installed the companion plugin." + title = "StashAppAndroidTV Crash Report" + positiveButtonText = "Send" + negativeButtonText = "Do not send" + } + reportSendFailureToast = "Crash report failed to send" + reportSendSuccessToast = "Attempted to send crash report!" + } + ACRA.errorReporter.putCustomData("SDK_INT", Build.VERSION.SDK_INT.toString()) + registerActivityLifecycleCallbacks(ActivityLifecycleCallbacksImpl()) ProcessLifecycleOwner.get().lifecycle.addObserver(LifecycleObserverImpl()) diff --git a/app/src/main/java/com/github/damontecres/stashapp/util/MutationEngine.kt b/app/src/main/java/com/github/damontecres/stashapp/util/MutationEngine.kt index ca926d32..c05d46d3 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/util/MutationEngine.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/util/MutationEngine.kt @@ -63,7 +63,7 @@ class MutationEngine( private val writeLock = lock?.writeLock() - private suspend fun executeMutation(mutation: Mutation): ApolloResponse { + suspend fun executeMutation(mutation: Mutation): ApolloResponse { val mutationName = mutation.name() val id = MUTATION_ID.getAndIncrement() try { diff --git a/app/src/main/java/com/github/damontecres/stashapp/util/QueryEngine.kt b/app/src/main/java/com/github/damontecres/stashapp/util/QueryEngine.kt index 1da7af20..6c159345 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/util/QueryEngine.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/util/QueryEngine.kt @@ -11,6 +11,7 @@ import com.apollographql.apollo3.api.Query import com.apollographql.apollo3.exception.ApolloException import com.apollographql.apollo3.exception.ApolloHttpException import com.apollographql.apollo3.exception.ApolloNetworkException +import com.github.damontecres.stashapp.api.ConfigurationQuery import com.github.damontecres.stashapp.api.FindDefaultFilterQuery import com.github.damontecres.stashapp.api.FindGalleriesQuery import com.github.damontecres.stashapp.api.FindImageQuery @@ -330,6 +331,11 @@ class QueryEngine( return executeQuery(query).data?.findDefaultFilter?.savedFilterData } + suspend fun getServerConfiguration(): ConfigurationQuery.Data { + val query = ConfigurationQuery() + return executeQuery(query).data!! + } + /** * Updates a FindFilterType if needed * diff --git a/app/src/main/java/com/github/damontecres/stashapp/util/ServerPreferences.kt b/app/src/main/java/com/github/damontecres/stashapp/util/ServerPreferences.kt index 810fc04e..a891d897 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/util/ServerPreferences.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/util/ServerPreferences.kt @@ -5,7 +5,7 @@ import android.content.SharedPreferences import android.util.Log import androidx.core.content.edit import com.github.damontecres.stashapp.api.ConfigurationQuery -import com.github.damontecres.stashapp.api.ServerInfoQuery +import com.github.damontecres.stashapp.util.plugin.CompanionPlugin /** * Represents configuration that users have set server-side @@ -38,26 +38,38 @@ class ServerPreferences(private val context: Context) { val alwaysStartFromBeginning get() = preferences.getBoolean(PREF_ALWAYS_START_BEGINNING, false) + val companionPluginVersion + get() = + preferences.getString( + PREF_COMPANION_PLUGIN_VERSION, + null, + ) + + val companionPluginInstalled + get() = companionPluginVersion != null + suspend fun updatePreferences(): ServerPreferences { val queryEngine = QueryEngine(context) - val query = ConfigurationQuery() - val config = queryEngine.executeQuery(query).data?.configuration - val serverInfo = queryEngine.executeQuery(ServerInfoQuery()).data - updatePreferences(config, serverInfo) + val result = queryEngine.getServerConfiguration() + updatePreferences(result) return this } /** * Update the local preferences from the server configuration */ - fun updatePreferences( - config: ConfigurationQuery.Configuration?, - serverInfo: ServerInfoQuery.Data?, - ) { - val serverVersion = - Version.tryFromString(serverInfo?.version?.version) - if (config != null) { - val ui = config.ui as Map + fun updatePreferences(config: ConfigurationQuery.Data) { + val serverVersion = Version.tryFromString(config.version.version) + + val companionPluginVersion = + config.plugins?.firstOrNull { it.id == CompanionPlugin.PLUGIN_ID }?.version + + preferences.edit { + putString(PREF_SERVER_VERSION, config.version.version) + putString(PREF_COMPANION_PLUGIN_VERSION, companionPluginVersion) + } + if (config.configuration.ui is Map<*, *>) { + val ui = config.configuration.ui as Map preferences.edit(true) { ui.getCaseInsensitive(PREF_TRACK_ACTIVITY).also { if (it != null) { @@ -115,7 +127,7 @@ class ServerPreferences(private val context: Context) { ) } - val scan = config.defaults.scan + val scan = config.configuration.defaults.scan if (scan != null) { putBoolean(PREF_SCAN_GENERATE_COVERS, scan.scanGenerateCovers) putBoolean(PREF_SCAN_GENERATE_PREVIEWS, scan.scanGeneratePreviews) @@ -126,7 +138,7 @@ class ServerPreferences(private val context: Context) { putBoolean(PREF_SCAN_GENERATE_CLIP_PREVIEWS, scan.scanGenerateClipPreviews) } - val generate = config.defaults.generate + val generate = config.configuration.defaults.generate if (generate != null) { putBoolean(PREF_GEN_CLIP_PREVIEWS, generate.clipPreviews ?: false) putBoolean(PREF_GEN_COVERS, generate.covers ?: false) @@ -147,21 +159,16 @@ class ServerPreferences(private val context: Context) { putBoolean(PREF_GEN_TRANSCODES, generate.transcodes ?: false) } - val menuItems = config.`interface`.menuItems?.map(String::lowercase)?.toSet() + val menuItems = + config.configuration.`interface`.menuItems?.map(String::lowercase)?.toSet() putStringSet(PREF_INTERFACE_MENU_ITEMS, menuItems) putBoolean( PREF_INTERFACE_STUDIOS_AS_TEXT, - config.`interface`.showStudioAsText ?: false, + config.configuration.`interface`.showStudioAsText ?: false, ) } } - - if (serverInfo != null) { - preferences.edit(true) { - putString(PREF_SERVER_VERSION, serverInfo.version.version) - } - } } companion object { @@ -179,6 +186,7 @@ class ServerPreferences(private val context: Context) { ) const val PREF_SERVER_VERSION = "serverInfo.version" + const val PREF_COMPANION_PLUGIN_VERSION = "companionPlugin.version" const val PREF_TRACK_ACTIVITY = "trackActivity" const val PREF_MINIMUM_PLAY_PERCENT = "minimumPlayPercent" diff --git a/app/src/main/java/com/github/damontecres/stashapp/util/StashClient.kt b/app/src/main/java/com/github/damontecres/stashapp/util/StashClient.kt index 52a5ee29..6d6305f3 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/util/StashClient.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/util/StashClient.kt @@ -225,6 +225,9 @@ class StashClient private constructor() { return apolloClient!! } + /** + * Create a new [ApolloClient]. Using [getApolloClient] is preferred. + */ private fun createApolloClient(context: Context): ApolloClient { val stashUrl = PreferenceManager.getDefaultSharedPreferences(context).getString( @@ -242,7 +245,7 @@ class StashClient private constructor() { .build() } - private fun cleanServerUrl(stashUrl: String): String { + fun cleanServerUrl(stashUrl: String): String { var cleanedStashUrl = stashUrl.trim() if (!cleanedStashUrl.startsWith("http://") && !cleanedStashUrl.startsWith("https://")) { // Assume http @@ -257,14 +260,13 @@ class StashClient private constructor() { url.buildUpon() .path(pathSegments.joinToString("/")) // Ensure the URL is the graphql endpoint .build() - Log.d(TAG, "StashUrl: $stashUrl => $url") return url.toString() } /** * Get the server URL excluding the (unlikely) `/graphql` last path segment */ - private fun getServerRoot(stashUrl: String?): String? { + fun getServerRoot(stashUrl: String?): String? { if (stashUrl == null) { return null } diff --git a/app/src/main/java/com/github/damontecres/stashapp/util/plugin/CompanionPlugin.kt b/app/src/main/java/com/github/damontecres/stashapp/util/plugin/CompanionPlugin.kt new file mode 100644 index 00000000..40a5f7c6 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/util/plugin/CompanionPlugin.kt @@ -0,0 +1,109 @@ +package com.github.damontecres.stashapp.util.plugin + +import android.content.Context +import android.util.Log +import android.widget.Toast +import com.github.damontecres.stashapp.api.RunPluginTaskMutation +import com.github.damontecres.stashapp.util.MutationEngine +import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.BufferedReader +import java.io.InputStreamReader + +/** + * Class for interacting with the server-side companion plugin + */ +class CompanionPlugin { + companion object { + private const val TAG = "CompanionPlugin" + + const val PLUGIN_ID = "stashAppAndroidTvCompanion" + + const val CRASH_TASK_NAME = "crash_report" + const val LOGCAT_TASK_NAME = "logcat" + + fun getLogCatLines(verbose: Boolean): List { + val lineCount = if (verbose) 500 else 200 + val args = + buildList { + add("logcat") + add("-d") + add("-t") + add(lineCount.toString()) + if (verbose) { + addAll(THIRD_PARTY_TAGS) + add("*:V") + } else { + add("-s") + addAll(LOGCAT_TAGS) + addAll(THIRD_PARTY_TAGS) + add("*:E") + } + } + val process = ProcessBuilder().command(args).redirectErrorStream(true).start() + val logLines = mutableListOf() + try { + val reader = BufferedReader(InputStreamReader(process.inputStream)) + var count = 0 + + while (count < lineCount) { + val line = reader.readLine() + if (line != null) { + logLines.add(line) + } else { + break + } + count++ + } + } finally { + process.destroy() + } + return logLines + } + + suspend fun sendLogCat( + context: Context, + verbose: Boolean, + ) = withContext(Dispatchers.IO + StashCoroutineExceptionHandler()) { + try { + val lines = getLogCatLines(verbose) + val sb = StringBuilder("** LOGCAT START **\n") + sb.append(lines.joinToString("\n")) + sb.append("\n** LOGCAT END **") + // Avoid individual lines being logged server-side + val logcat = sb.replace(Regex("\n"), "") + + val mutationEngine = MutationEngine(context, true) + val mutation = + RunPluginTaskMutation( + plugin_id = PLUGIN_ID, + task_name = LOGCAT_TASK_NAME, + args_map = mapOf(LOGCAT_TASK_NAME to logcat), + ) + mutationEngine.executeMutation(mutation) + val msg = + buildString { + if (verbose) { + append("Verbose logs sent!") + } else { + append("Logs sent!") + } + append(" Check the server's log page.") + } + withContext(Dispatchers.Main) { + Toast.makeText(context, msg, Toast.LENGTH_LONG).show() + } + } catch (ex: Exception) { + Log.e(TAG, "Error sending logs", ex) + withContext(Dispatchers.Main) { + Toast.makeText( + context, + "Error sending logs: ${ex.message}", + Toast.LENGTH_LONG, + ).show() + } + } + } + } +} diff --git a/app/src/main/java/com/github/damontecres/stashapp/util/plugin/CrashReportSenderFactory.kt b/app/src/main/java/com/github/damontecres/stashapp/util/plugin/CrashReportSenderFactory.kt new file mode 100644 index 00000000..7bece65d --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/util/plugin/CrashReportSenderFactory.kt @@ -0,0 +1,58 @@ +package com.github.damontecres.stashapp.util.plugin + +import android.content.Context +import android.util.Log +import com.github.damontecres.stashapp.api.RunPluginTaskMutation +import com.github.damontecres.stashapp.util.StashClient +import com.google.auto.service.AutoService +import kotlinx.coroutines.runBlocking +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 + +@AutoService(ReportSenderFactory::class) +class CrashReportSenderFactory : ReportSenderFactory { + override fun create( + context: Context, + config: CoreConfiguration, + ): ReportSender { + return CrashReportSender() + } + + override fun enabled(config: CoreConfiguration): Boolean { + return true + } + + class CrashReportSender : ReportSender { + override fun send( + context: Context, + errorContent: CrashReportData, + ) { + Log.w(TAG, "Sending crash report") + try { + val client = StashClient.getApolloClient(context) + val mutation = + RunPluginTaskMutation( + plugin_id = CompanionPlugin.PLUGIN_ID, + task_name = CompanionPlugin.CRASH_TASK_NAME, + args_map = mapOf(CompanionPlugin.CRASH_TASK_NAME to errorContent.toJSON()), + ) + runBlocking { + val response = client.mutation(mutation).execute() + if (response.hasErrors()) { + throw ReportSenderException(response.errors.toString()) + } + } + } catch (ex: Exception) { + Log.e(TAG, "Error while sending crash report", ex) + throw ReportSenderException("Error while sending crash report ", ex) + } + } + } + + companion object { + const val TAG = "CrashReportSenderFactory" + } +} diff --git a/app/src/main/java/com/github/damontecres/stashapp/util/plugin/LogcatTags.kt b/app/src/main/java/com/github/damontecres/stashapp/util/plugin/LogcatTags.kt new file mode 100644 index 00000000..1d8d35a2 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/util/plugin/LogcatTags.kt @@ -0,0 +1,83 @@ +package com.github.damontecres.stashapp.util.plugin + +val THIRD_PARTY_TAGS = + listOf( + "libc:F", + "ExoPlayerImpl:W", + // FireTV + "Codec2Client:E", + "CCodecBuffers:E", + "CCodecConfig:E", + "okhttp.Http2:W", + "okhttp.TaskRunner:W", + "LruBitmapPool:W", + "FragmentManager:W", + "ConfigStore:W", + "GlideRequest:W", + "FactoryPools:W", + "ViewTarget:W", + "Engine:W", + "Downsampler:W", + "TransformationUtils:W", + "DecodeJob:W", + "BufferPoolAccessor2.0:W", + "ExifInterface:W", + "MediaCodec:W", + "SurfaceUtils:W", + "ByteArrayPool:W", + "HardwareConfig:W", + "DfltImageHeaderParser:W", + ) + +// TODO Add a method to auto generating this +// rg 'TAG = "(\w+)"' app/src/main/java/com/github/damontecres/stashapp/ -or '$1' --no-filename -N | sort | uniq | while read -r tag; do echo -n "\"$tag:V\","; done +val LOGCAT_TAGS = + listOf( + "ActionPresenter:V", + "AppUpgradeHandler:V", + "CardPresenter:V", + "CodecSupport:V", + "Constants:V", + "CrashReportSenderFactory:V", + "FilterListActivity:V", + "FilterParser:V", + "GalleryPresenter:V", + "ImageActivity:V", + "ImageGridClickedListener:V", + "ImagePresenter:V", + "MainFragment:V", + "MainPageParser:V", + "MarkerDetailsFragment:V", + "MarkerPresenter:V", + "MutationEngine:V", + "PerformerFragment:V", + "PinActivity:V", + "PlaybackActivity:V", + "PlaybackExoFragment:V", + "PlaybackFragment:V", + "PlaybackMarkersFragment:V", + "QueryEngine:V", + "ScenePresenter:V", + "SearchForFragment:V", + "ServerPreferences:V", + "SettingsFragment:V", + "SetupStep5Ssl:V", + "StashApplication:V", + "StashClient:V", + "StashCoroutineExceptionHandler:V", + "StashFilterPager:V", + "StashFilterPresenter:V", + "StashGlide:V", + "StashGridFragment:V", + "StashImageCardView:V", + "StashItemViewClickListener:V", + "StashPlayerView:V", + "StashPresenter:V", + "StashPreviewLoader:V", + "StashSearchFragment:V", + "StudioPresenter:V", + "TagPresenter:V", + "UpdateBroadcastReceiver:V", + "UpdateChecker:V", + "VideoDetailsFragment:V", + ) diff --git a/app/src/main/res/layout/debug.xml b/app/src/main/res/layout/debug.xml index 49ab1b4b..d579b8f6 100644 --- a/app/src/main/res/layout/debug.xml +++ b/app/src/main/res/layout/debug.xml @@ -65,6 +65,48 @@ android:layout_width="match_parent" android:layout_height="wrap_content" /> + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/advanced_preferences.xml b/app/src/main/res/xml/advanced_preferences.xml index 25b9efa5..3d1652e6 100644 --- a/app/src/main/res/xml/advanced_preferences.xml +++ b/app/src/main/res/xml/advanced_preferences.xml @@ -157,6 +157,12 @@ + +