mirror of
https://github.com/FWGS/xash3d-fwgs.git
synced 2026-08-05 03:24:56 +08:00
android: handle crash logs created by the engine, keep them for 30 days for current version
This commit is contained in:
@@ -68,6 +68,15 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/provider_paths" />
|
||||||
|
</provider>
|
||||||
<activity
|
<activity
|
||||||
android:name=".XashActivity"
|
android:name=".XashActivity"
|
||||||
android:alwaysRetainTaskState="true"
|
android:alwaysRetainTaskState="true"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package su.xash.engine
|
package su.xash.engine
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.fragment.NavHostFragment
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
@@ -8,6 +9,11 @@ import androidx.navigation.ui.AppBarConfiguration
|
|||||||
import androidx.navigation.ui.navigateUp
|
import androidx.navigation.ui.navigateUp
|
||||||
import androidx.navigation.ui.setupActionBarWithNavController
|
import androidx.navigation.ui.setupActionBarWithNavController
|
||||||
import su.xash.engine.databinding.ActivityMainBinding
|
import su.xash.engine.databinding.ActivityMainBinding
|
||||||
|
import su.xash.engine.util.CrashReports
|
||||||
|
import java.io.File
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
private lateinit var binding: ActivityMainBinding
|
private lateinit var binding: ActivityMainBinding
|
||||||
@@ -27,9 +33,36 @@ class MainActivity : AppCompatActivity() {
|
|||||||
navController = navHostFragment.navController
|
navController = navHostFragment.navController
|
||||||
appBarConfiguration = AppBarConfiguration(navController.graph)
|
appBarConfiguration = AppBarConfiguration(navController.graph)
|
||||||
setupActionBarWithNavController(navController, appBarConfiguration)
|
setupActionBarWithNavController(navController, appBarConfiguration)
|
||||||
|
|
||||||
|
CrashReports.prune(this)
|
||||||
|
showPendingCrashReport()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSupportNavigateUp(): Boolean {
|
override fun onSupportNavigateUp(): Boolean {
|
||||||
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
|
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showPendingCrashReport() {
|
||||||
|
val pending = CrashReports.pendingFile(this)
|
||||||
|
if (!pending.exists() || pending.length() == 0L)
|
||||||
|
return
|
||||||
|
|
||||||
|
val historyDir = CrashReports.historyDir(this).apply { mkdirs() }
|
||||||
|
val ts = SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US).format(Date())
|
||||||
|
val archived = File(historyDir, "crash-$ts.log")
|
||||||
|
if (!pending.renameTo(archived)) {
|
||||||
|
// fall back to in-place read if move failed; still consume the file
|
||||||
|
archived.writeText(pending.readText())
|
||||||
|
pending.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
val content = archived.readText()
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle(R.string.crash_dialog_title)
|
||||||
|
.setView(CrashReports.buildContentView(this, content))
|
||||||
|
.setPositiveButton(R.string.crash_send_to_developers) { _, _ -> CrashReports.sendByEmail(this, content) }
|
||||||
|
.setNeutralButton(R.string.crash_share) { _, _ -> CrashReports.share(this, archived) }
|
||||||
|
.setNegativeButton(R.string.crash_dismiss, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import org.libsdl.app.SDLActivity;
|
|||||||
|
|
||||||
import su.xash.engine.util.AndroidBug5497Workaround;
|
import su.xash.engine.util.AndroidBug5497Workaround;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -123,6 +124,10 @@ public class XashActivity extends SDLActivity {
|
|||||||
// TODO: REMOVE LATER, temporary launchers support?
|
// TODO: REMOVE LATER, temporary launchers support?
|
||||||
@Override
|
@Override
|
||||||
protected String[] getArguments() {
|
protected String[] getArguments() {
|
||||||
|
File crashDir = new File(getFilesDir(), "crashes");
|
||||||
|
crashDir.mkdirs();
|
||||||
|
nativeSetenv("XASH3D_CRASH_DIR", crashDir.getAbsolutePath());
|
||||||
|
|
||||||
String gamedir = getIntent().getStringExtra("gamedir");
|
String gamedir = getIntent().getStringExtra("gamedir");
|
||||||
if (gamedir == null) gamedir = "valve";
|
if (gamedir == null) gamedir = "valve";
|
||||||
nativeSetenv("XASH3D_GAME", gamedir);
|
nativeSetenv("XASH3D_GAME", gamedir);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package su.xash.engine.ui.settings
|
package su.xash.engine.ui.settings
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import su.xash.engine.R
|
import su.xash.engine.R
|
||||||
|
|
||||||
@@ -8,5 +10,10 @@ class AppSettingsPreferenceFragment() : PreferenceFragmentCompat() {
|
|||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
preferenceManager.sharedPreferencesName = "app_preferences";
|
preferenceManager.sharedPreferencesName = "app_preferences";
|
||||||
setPreferencesFromResource(R.xml.app_preferences, rootKey);
|
setPreferencesFromResource(R.xml.app_preferences, rootKey);
|
||||||
|
|
||||||
|
findPreference<Preference>("crash_logs")?.setOnPreferenceClickListener {
|
||||||
|
findNavController().navigate(R.id.action_appSettingsFragment_to_crashLogsFragment)
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,5 +22,13 @@
|
|||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/appSettingsFragment"
|
android:id="@+id/appSettingsFragment"
|
||||||
android:name="su.xash.engine.ui.settings.AppSettingsFragment"
|
android:name="su.xash.engine.ui.settings.AppSettingsFragment"
|
||||||
android:label="@string/app_settings" />
|
android:label="@string/app_settings">
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_appSettingsFragment_to_crashLogsFragment"
|
||||||
|
app:destination="@id/crashLogsFragment" />
|
||||||
|
</fragment>
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/crashLogsFragment"
|
||||||
|
android:name="su.xash.engine.ui.settings.CrashLogsFragment"
|
||||||
|
android:label="@string/crash_logs" />
|
||||||
</navigation>
|
</navigation>
|
||||||
|
|||||||
@@ -23,4 +23,11 @@
|
|||||||
<string name="game_apk_required">Требуется APK с игровыми библиотеками</string>
|
<string name="game_apk_required">Требуется APK с игровыми библиотеками</string>
|
||||||
<string name="game_apk_message">Чтобы запустить эту игру, нужно установить дополнительный APK</string>
|
<string name="game_apk_message">Чтобы запустить эту игру, нужно установить дополнительный APK</string>
|
||||||
<string name="game_apk_install">Скачать</string>
|
<string name="game_apk_install">Скачать</string>
|
||||||
|
<string name="crash_dialog_title">Xash3D FWGS аварийно завершился в прошлый раз</string>
|
||||||
|
<string name="crash_send_to_developers">Отправить разработчикам</string>
|
||||||
|
<string name="crash_share">Поделиться</string>
|
||||||
|
<string name="crash_dismiss">Закрыть</string>
|
||||||
|
<string name="crash_logs">Журналы сбоев</string>
|
||||||
|
<string name="crash_logs_empty">Нет сохранённых журналов сбоев</string>
|
||||||
|
<string name="crash_log_delete">Удалить</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -24,4 +24,12 @@
|
|||||||
<string name="game_apk_required">Game code APK required</string>
|
<string name="game_apk_required">Game code APK required</string>
|
||||||
<string name="game_apk_message">To launch this game, you need additional APK installed</string>
|
<string name="game_apk_message">To launch this game, you need additional APK installed</string>
|
||||||
<string name="game_apk_install">Download</string>
|
<string name="game_apk_install">Download</string>
|
||||||
|
<string name="crash_dialog_title">Xash3D FWGS crashed last time</string>
|
||||||
|
<string name="crash_send_to_developers">Send to developers</string>
|
||||||
|
<string name="crash_share">Share</string>
|
||||||
|
<string name="crash_dismiss">Dismiss</string>
|
||||||
|
<string name="crash_logs">Crash logs</string>
|
||||||
|
<string name="crash_logs_empty">No crash logs recorded</string>
|
||||||
|
<string name="crash_log_delete">Delete</string>
|
||||||
|
<string name="crash_email_subject" translatable="false">Xash3D FWGS crash report</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -11,4 +11,8 @@
|
|||||||
app:key="use_icons"
|
app:key="use_icons"
|
||||||
app:layout="@layout/switch_preference"
|
app:layout="@layout/switch_preference"
|
||||||
app:title="@string/preferences_use_icons" />
|
app:title="@string/preferences_use_icons" />
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
app:key="crash_logs"
|
||||||
|
app:title="@string/crash_logs" />
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
||||||
|
|||||||
4
android/app/src/main/res/xml/provider_paths.xml
Normal file
4
android/app/src/main/res/xml/provider_paths.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths>
|
||||||
|
<files-path name="crashes" path="crashes/" />
|
||||||
|
</paths>
|
||||||
Reference in New Issue
Block a user