From 36307bb386121cdc46ca79750c3fd7f6e1f40628 Mon Sep 17 00:00:00 2001 From: Alibek Omarov Date: Thu, 7 May 2026 17:58:25 +0500 Subject: [PATCH] android: handle crash logs created by the engine, keep them for 30 days for current version --- android/app/src/main/AndroidManifest.xml | 9 +++ .../main/java/su/xash/engine/MainActivity.kt | 33 ++++++++ .../java/su/xash/engine/XashActivity.java | 5 ++ .../settings/AppSettingsPreferenceFragment.kt | 7 ++ .../engine/ui/settings/CrashLogsFragment.kt | 66 +++++++++++++++ .../java/su/xash/engine/util/CrashReports.kt | 80 +++++++++++++++++++ .../app/src/main/res/navigation/nav_graph.xml | 10 ++- .../app/src/main/res/values-ru/strings.xml | 7 ++ android/app/src/main/res/values/strings.xml | 8 ++ .../app/src/main/res/xml/app_preferences.xml | 4 + .../app/src/main/res/xml/provider_paths.xml | 4 + 11 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 android/app/src/main/java/su/xash/engine/ui/settings/CrashLogsFragment.kt create mode 100644 android/app/src/main/java/su/xash/engine/util/CrashReports.kt create mode 100644 android/app/src/main/res/xml/provider_paths.xml diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 25112818..5fcf21d7 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -68,6 +68,15 @@ + + + CrashReports.sendByEmail(this, content) } + .setNeutralButton(R.string.crash_share) { _, _ -> CrashReports.share(this, archived) } + .setNegativeButton(R.string.crash_dismiss, null) + .show() + } } diff --git a/android/app/src/main/java/su/xash/engine/XashActivity.java b/android/app/src/main/java/su/xash/engine/XashActivity.java index fa8b698e..df39d7f8 100644 --- a/android/app/src/main/java/su/xash/engine/XashActivity.java +++ b/android/app/src/main/java/su/xash/engine/XashActivity.java @@ -15,6 +15,7 @@ import org.libsdl.app.SDLActivity; import su.xash.engine.util.AndroidBug5497Workaround; +import java.io.File; import java.util.Arrays; import java.util.List; @@ -123,6 +124,10 @@ public class XashActivity extends SDLActivity { // TODO: REMOVE LATER, temporary launchers support? @Override protected String[] getArguments() { + File crashDir = new File(getFilesDir(), "crashes"); + crashDir.mkdirs(); + nativeSetenv("XASH3D_CRASH_DIR", crashDir.getAbsolutePath()); + String gamedir = getIntent().getStringExtra("gamedir"); if (gamedir == null) gamedir = "valve"; nativeSetenv("XASH3D_GAME", gamedir); diff --git a/android/app/src/main/java/su/xash/engine/ui/settings/AppSettingsPreferenceFragment.kt b/android/app/src/main/java/su/xash/engine/ui/settings/AppSettingsPreferenceFragment.kt index 3d61560f..8bfd23fb 100644 --- a/android/app/src/main/java/su/xash/engine/ui/settings/AppSettingsPreferenceFragment.kt +++ b/android/app/src/main/java/su/xash/engine/ui/settings/AppSettingsPreferenceFragment.kt @@ -1,6 +1,8 @@ package su.xash.engine.ui.settings import android.os.Bundle +import androidx.navigation.fragment.findNavController +import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import su.xash.engine.R @@ -8,5 +10,10 @@ class AppSettingsPreferenceFragment() : PreferenceFragmentCompat() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { preferenceManager.sharedPreferencesName = "app_preferences"; setPreferencesFromResource(R.xml.app_preferences, rootKey); + + findPreference("crash_logs")?.setOnPreferenceClickListener { + findNavController().navigate(R.id.action_appSettingsFragment_to_crashLogsFragment) + true + } } } diff --git a/android/app/src/main/java/su/xash/engine/ui/settings/CrashLogsFragment.kt b/android/app/src/main/java/su/xash/engine/ui/settings/CrashLogsFragment.kt new file mode 100644 index 00000000..b1163903 --- /dev/null +++ b/android/app/src/main/java/su/xash/engine/ui/settings/CrashLogsFragment.kt @@ -0,0 +1,66 @@ +package su.xash.engine.ui.settings + +import android.os.Bundle +import androidx.appcompat.app.AlertDialog +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import su.xash.engine.R +import su.xash.engine.util.CrashReports +import java.io.File +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +class CrashLogsFragment : PreferenceFragmentCompat() { + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + preferenceScreen = preferenceManager.createPreferenceScreen(requireContext()) + populate() + } + + override fun onResume() { + super.onResume() + populate() + } + + private fun populate() { + val ctx = requireContext() + preferenceScreen.removeAll() + + val files = CrashReports.historyDir(ctx).listFiles()?.sortedByDescending { it.lastModified() } ?: emptyList() + + if (files.isEmpty()) { + preferenceScreen.addPreference(Preference(ctx).apply { + setTitle(R.string.crash_logs_empty) + isSelectable = false + }) + return + } + + val fmt = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US) + files.forEach { file -> + preferenceScreen.addPreference(Preference(ctx).apply { + title = fmt.format(Date(file.lastModified())) + summary = file.name + setOnPreferenceClickListener { + showCrashLog(file) + true + } + }) + } + } + + private fun showCrashLog(file: File) { + val ctx = requireContext() + val content = file.readText() + AlertDialog.Builder(ctx) + .setTitle(file.name) + .setView(CrashReports.buildContentView(ctx, content)) + .setPositiveButton(R.string.crash_send_to_developers) { _, _ -> CrashReports.sendByEmail(ctx, content) } + .setNeutralButton(R.string.crash_share) { _, _ -> CrashReports.share(ctx, file) } + .setNegativeButton(R.string.crash_log_delete) { _, _ -> + file.delete() + populate() + } + .show() + } +} diff --git a/android/app/src/main/java/su/xash/engine/util/CrashReports.kt b/android/app/src/main/java/su/xash/engine/util/CrashReports.kt new file mode 100644 index 00000000..ed37ab8f --- /dev/null +++ b/android/app/src/main/java/su/xash/engine/util/CrashReports.kt @@ -0,0 +1,80 @@ +package su.xash.engine.util + +import android.content.Context +import android.content.Intent +import android.graphics.Typeface +import android.net.Uri +import android.util.TypedValue +import android.view.View +import android.widget.ScrollView +import android.widget.TextView +import androidx.core.content.FileProvider +import su.xash.engine.BuildConfig +import su.xash.engine.R +import java.io.File + +object CrashReports { + private const val PREFS = "crash_reports" + private const val KEY_LAST_VERSION = "last_version_code" + private const val MAX_AGE_MS = 30L * 24L * 60L * 60L * 1000L // 30 days + + private const val D = "9c8d9e8c97bf9988988cd1989e86" + + fun pendingFile(ctx: Context): File = File(ctx.filesDir, "crashes/crash.log") + fun historyDir(ctx: Context): File = File(ctx.filesDir, "crashes/history") + + // wipe everything on app update; otherwise drop logs older than 30 days + fun prune(ctx: Context) { + val prefs = ctx.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + val lastVersion = prefs.getInt(KEY_LAST_VERSION, -1) + val currentVersion = BuildConfig.VERSION_CODE + + if (lastVersion != currentVersion) { + historyDir(ctx).listFiles()?.forEach { it.delete() } + pendingFile(ctx).delete() + prefs.edit().putInt(KEY_LAST_VERSION, currentVersion).apply() + return + } + + val cutoff = System.currentTimeMillis() - MAX_AGE_MS + historyDir(ctx).listFiles()?.forEach { f -> + if (f.lastModified() < cutoff) f.delete() + } + } + + fun sendByEmail(ctx: Context, content: String) { + val addr = D.chunked(2) { (it.toString().toInt(16) xor 0xFF).toChar() }.joinToString("") + val intent = Intent(Intent.ACTION_SENDTO).apply { + data = Uri.fromParts("mailto", addr, null) + putExtra(Intent.EXTRA_SUBJECT, ctx.getString(R.string.crash_email_subject)) + putExtra(Intent.EXTRA_TEXT, content) + } + if (intent.resolveActivity(ctx.packageManager) != null) { + ctx.startActivity(intent) + } + } + + fun buildContentView(ctx: Context, content: String): View { + val pad = (16 * ctx.resources.displayMetrics.density).toInt() + val text = TextView(ctx).apply { + text = content + typeface = Typeface.MONOSPACE + setTextIsSelectable(true) + setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f) + setPadding(pad, pad, pad, pad) + } + return ScrollView(ctx).apply { addView(text) } + } + + fun share(ctx: Context, file: File) { + val authority = "${BuildConfig.APPLICATION_ID}.fileprovider" + val uri = FileProvider.getUriForFile(ctx, authority, file) + val intent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_SUBJECT, ctx.getString(R.string.crash_email_subject)) + putExtra(Intent.EXTRA_STREAM, uri) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + ctx.startActivity(Intent.createChooser(intent, ctx.getString(R.string.crash_share))) + } +} diff --git a/android/app/src/main/res/navigation/nav_graph.xml b/android/app/src/main/res/navigation/nav_graph.xml index f24610bc..2e4dbf98 100644 --- a/android/app/src/main/res/navigation/nav_graph.xml +++ b/android/app/src/main/res/navigation/nav_graph.xml @@ -22,5 +22,13 @@ + android:label="@string/app_settings"> + + + diff --git a/android/app/src/main/res/values-ru/strings.xml b/android/app/src/main/res/values-ru/strings.xml index 8171d755..c240818d 100644 --- a/android/app/src/main/res/values-ru/strings.xml +++ b/android/app/src/main/res/values-ru/strings.xml @@ -23,4 +23,11 @@ Требуется APK с игровыми библиотеками Чтобы запустить эту игру, нужно установить дополнительный APK Скачать + Xash3D FWGS аварийно завершился в прошлый раз + Отправить разработчикам + Поделиться + Закрыть + Журналы сбоев + Нет сохранённых журналов сбоев + Удалить diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 6cd8bf40..3d82752d 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -24,4 +24,12 @@ Game code APK required To launch this game, you need additional APK installed Download + Xash3D FWGS crashed last time + Send to developers + Share + Dismiss + Crash logs + No crash logs recorded + Delete + Xash3D FWGS crash report diff --git a/android/app/src/main/res/xml/app_preferences.xml b/android/app/src/main/res/xml/app_preferences.xml index b5d94e96..bbaaf241 100644 --- a/android/app/src/main/res/xml/app_preferences.xml +++ b/android/app/src/main/res/xml/app_preferences.xml @@ -11,4 +11,8 @@ app:key="use_icons" app:layout="@layout/switch_preference" app:title="@string/preferences_use_icons" /> + + diff --git a/android/app/src/main/res/xml/provider_paths.xml b/android/app/src/main/res/xml/provider_paths.xml new file mode 100644 index 00000000..c6a2b50b --- /dev/null +++ b/android/app/src/main/res/xml/provider_paths.xml @@ -0,0 +1,4 @@ + + + +