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" />
|
||||
</intent-filter>
|
||||
</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
|
||||
android:name=".XashActivity"
|
||||
android:alwaysRetainTaskState="true"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package su.xash.engine
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
@@ -8,6 +9,11 @@ import androidx.navigation.ui.AppBarConfiguration
|
||||
import androidx.navigation.ui.navigateUp
|
||||
import androidx.navigation.ui.setupActionBarWithNavController
|
||||
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() {
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
@@ -27,9 +33,36 @@ class MainActivity : AppCompatActivity() {
|
||||
navController = navHostFragment.navController
|
||||
appBarConfiguration = AppBarConfiguration(navController.graph)
|
||||
setupActionBarWithNavController(navController, appBarConfiguration)
|
||||
|
||||
CrashReports.prune(this)
|
||||
showPendingCrashReport()
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
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 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);
|
||||
|
||||
@@ -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<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
|
||||
android:id="@+id/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>
|
||||
|
||||
@@ -23,4 +23,11 @@
|
||||
<string name="game_apk_required">Требуется APK с игровыми библиотеками</string>
|
||||
<string name="game_apk_message">Чтобы запустить эту игру, нужно установить дополнительный APK</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>
|
||||
|
||||
@@ -24,4 +24,12 @@
|
||||
<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_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>
|
||||
|
||||
@@ -11,4 +11,8 @@
|
||||
app:key="use_icons"
|
||||
app:layout="@layout/switch_preference"
|
||||
app:title="@string/preferences_use_icons" />
|
||||
|
||||
<Preference
|
||||
app:key="crash_logs"
|
||||
app:title="@string/crash_logs" />
|
||||
</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