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 @@ + +