mirror of
https://github.com/FWGS/xash3d-fwgs.git
synced 2026-08-05 03:24:56 +08:00
android: pack crash log to ZIP file, put system and intent info alongside it. Properly handle if multple email clients are installed on device (tested with gmail, outlook and k-9).
This commit is contained in:
@@ -95,5 +95,9 @@
|
|||||||
<intent>
|
<intent>
|
||||||
<action android:name="su.xash.engine.MOD" />
|
<action android:name="su.xash.engine.MOD" />
|
||||||
</intent>
|
</intent>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.SENDTO" />
|
||||||
|
<data android:scheme="mailto" />
|
||||||
|
</intent>
|
||||||
</queries>
|
</queries>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -43,26 +43,36 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun showPendingCrashReport() {
|
private fun showPendingCrashReport() {
|
||||||
val pending = CrashReports.pendingFile(this)
|
val pending = CrashReports.pendingStacktrace(this)
|
||||||
if (!pending.exists() || pending.length() == 0L)
|
if (!pending.exists() || pending.length() == 0L)
|
||||||
return
|
return
|
||||||
|
|
||||||
val historyDir = CrashReports.historyDir(this).apply { mkdirs() }
|
val historyDir = CrashReports.historyDir(this).apply { mkdirs() }
|
||||||
val ts = SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US).format(Date())
|
val ts = SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US).format(Date())
|
||||||
val archived = File(historyDir, "crash-$ts.log")
|
val entryDir = File(historyDir, "crash-$ts").apply { mkdirs() }
|
||||||
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()
|
moveOrCopy(pending, File(entryDir, CrashReports.STACKTRACE_NAME))
|
||||||
|
moveOrCopy(CrashReports.pendingSysinfo(this), File(entryDir, CrashReports.SYSINFO_NAME))
|
||||||
|
moveOrCopy(CrashReports.pendingIntent(this), File(entryDir, CrashReports.INTENT_NAME))
|
||||||
|
|
||||||
|
val entry = CrashReports.Entry(entryDir)
|
||||||
AlertDialog.Builder(this)
|
AlertDialog.Builder(this)
|
||||||
.setTitle(R.string.crash_dialog_title)
|
.setTitle(R.string.crash_dialog_title)
|
||||||
.setView(CrashReports.buildContentView(this, content))
|
.setView(CrashReports.buildContentView(this, entry.summary()))
|
||||||
.setPositiveButton(R.string.crash_send_to_developers) { _, _ -> CrashReports.sendByEmail(this, content) }
|
.setPositiveButton(R.string.crash_send_to_developers) { _, _ -> CrashReports.sendByEmail(this, entry) }
|
||||||
.setNeutralButton(R.string.crash_share) { _, _ -> CrashReports.share(this, archived) }
|
.setNeutralButton(R.string.crash_share) { _, _ -> CrashReports.share(this, entry) }
|
||||||
.setNegativeButton(R.string.crash_dismiss, null)
|
.setNegativeButton(R.string.crash_dismiss, null)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun moveOrCopy(src: File, dst: File) {
|
||||||
|
if (!src.exists())
|
||||||
|
return
|
||||||
|
|
||||||
|
if (src.renameTo(dst))
|
||||||
|
return
|
||||||
|
|
||||||
|
dst.writeText(src.readText())
|
||||||
|
src.delete()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package su.xash.engine;
|
package su.xash.engine;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Intent;
|
||||||
import android.content.pm.ActivityInfo;
|
import android.content.pm.ActivityInfo;
|
||||||
import android.content.res.AssetManager;
|
import android.content.res.AssetManager;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
@@ -14,6 +15,7 @@ import android.view.WindowManager;
|
|||||||
import org.libsdl.app.SDLActivity;
|
import org.libsdl.app.SDLActivity;
|
||||||
|
|
||||||
import su.xash.engine.util.AndroidBug5497Workaround;
|
import su.xash.engine.util.AndroidBug5497Workaround;
|
||||||
|
import su.xash.engine.util.CrashReports;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@@ -121,6 +123,45 @@ public class XashActivity extends SDLActivity {
|
|||||||
return getWindow().superDispatchKeyEvent(event);
|
return getWindow().superDispatchKeyEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void appendStringExtra(StringBuilder sb, Intent intent, String key) {
|
||||||
|
String value = intent.getStringExtra(key);
|
||||||
|
if (value != null)
|
||||||
|
sb.append(" ").append(key).append(" = ").append(value).append('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// record intent info, so that it could be consumed later for crash reporting
|
||||||
|
private void recordLaunchInfo() {
|
||||||
|
// do not overwrite current launch info with pending crash log, shouldn't happen but might
|
||||||
|
File pendingCrash = new File(getFilesDir(), "crashes/" + CrashReports.STACKTRACE_NAME);
|
||||||
|
if (pendingCrash.exists() && pendingCrash.length() > 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// write Android version, fingerprint, supported abis, etc
|
||||||
|
CrashReports.writeSystemInfo(this);
|
||||||
|
|
||||||
|
// now create intent info and pass it to crash reporting
|
||||||
|
Intent intent = getIntent();
|
||||||
|
if (intent == null)
|
||||||
|
return;
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("Action: ").append(intent.getAction()).append('\n');
|
||||||
|
sb.append("Data: ").append(intent.getDataString()).append('\n');
|
||||||
|
sb.append("Calling package: ").append(getCallingPackage()).append('\n');
|
||||||
|
sb.append("Extras:\n");
|
||||||
|
// only write intent extras that we care about
|
||||||
|
appendStringExtra(sb, intent, "gamedir");
|
||||||
|
appendStringExtra(sb, intent, "gamelibdir");
|
||||||
|
appendStringExtra(sb, intent, "pakfile");
|
||||||
|
appendStringExtra(sb, intent, "basedir");
|
||||||
|
appendStringExtra(sb, intent, "package");
|
||||||
|
appendStringExtra(sb, intent, "argv");
|
||||||
|
sb.append(" usevolume = ").append(intent.getBooleanExtra("usevolume", false)).append('\n');
|
||||||
|
String[] env = intent.getStringArrayExtra("env");
|
||||||
|
if (env != null)
|
||||||
|
sb.append(" env = ").append(Arrays.toString(env)).append('\n');
|
||||||
|
CrashReports.writeIntentInfo(this, sb.toString());
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: REMOVE LATER, temporary launchers support?
|
// TODO: REMOVE LATER, temporary launchers support?
|
||||||
@Override
|
@Override
|
||||||
protected String[] getArguments() {
|
protected String[] getArguments() {
|
||||||
@@ -128,6 +169,8 @@ public class XashActivity extends SDLActivity {
|
|||||||
crashDir.mkdirs();
|
crashDir.mkdirs();
|
||||||
nativeSetenv("XASH3D_CRASH_DIR", crashDir.getAbsolutePath());
|
nativeSetenv("XASH3D_CRASH_DIR", crashDir.getAbsolutePath());
|
||||||
|
|
||||||
|
recordLaunchInfo();
|
||||||
|
|
||||||
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);
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import androidx.preference.Preference
|
|||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import su.xash.engine.R
|
import su.xash.engine.R
|
||||||
import su.xash.engine.util.CrashReports
|
import su.xash.engine.util.CrashReports
|
||||||
import java.io.File
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@@ -26,9 +25,12 @@ class CrashLogsFragment : PreferenceFragmentCompat() {
|
|||||||
val ctx = requireContext()
|
val ctx = requireContext()
|
||||||
preferenceScreen.removeAll()
|
preferenceScreen.removeAll()
|
||||||
|
|
||||||
val files = CrashReports.historyDir(ctx).listFiles()?.sortedByDescending { it.lastModified() } ?: emptyList()
|
val dirs = CrashReports.historyDir(ctx).listFiles()
|
||||||
|
?.filter { it.isDirectory }
|
||||||
|
?.sortedByDescending { it.lastModified() }
|
||||||
|
?: emptyList()
|
||||||
|
|
||||||
if (files.isEmpty()) {
|
if (dirs.isEmpty()) {
|
||||||
preferenceScreen.addPreference(Preference(ctx).apply {
|
preferenceScreen.addPreference(Preference(ctx).apply {
|
||||||
setTitle(R.string.crash_logs_empty)
|
setTitle(R.string.crash_logs_empty)
|
||||||
isSelectable = false
|
isSelectable = false
|
||||||
@@ -37,28 +39,27 @@ class CrashLogsFragment : PreferenceFragmentCompat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val fmt = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
|
val fmt = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
|
||||||
files.forEach { file ->
|
dirs.forEach { dir ->
|
||||||
preferenceScreen.addPreference(Preference(ctx).apply {
|
preferenceScreen.addPreference(Preference(ctx).apply {
|
||||||
title = fmt.format(Date(file.lastModified()))
|
title = fmt.format(Date(dir.lastModified()))
|
||||||
summary = file.name
|
summary = dir.name
|
||||||
setOnPreferenceClickListener {
|
setOnPreferenceClickListener {
|
||||||
showCrashLog(file)
|
showCrashLog(CrashReports.Entry(dir))
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showCrashLog(file: File) {
|
private fun showCrashLog(entry: CrashReports.Entry) {
|
||||||
val ctx = requireContext()
|
val ctx = requireContext()
|
||||||
val content = file.readText()
|
|
||||||
AlertDialog.Builder(ctx)
|
AlertDialog.Builder(ctx)
|
||||||
.setTitle(file.name)
|
.setTitle(entry.name)
|
||||||
.setView(CrashReports.buildContentView(ctx, content))
|
.setView(CrashReports.buildContentView(ctx, entry.summary()))
|
||||||
.setPositiveButton(R.string.crash_send_to_developers) { _, _ -> CrashReports.sendByEmail(ctx, content) }
|
.setPositiveButton(R.string.crash_send_to_developers) { _, _ -> CrashReports.sendByEmail(ctx, entry) }
|
||||||
.setNeutralButton(R.string.crash_share) { _, _ -> CrashReports.share(ctx, file) }
|
.setNeutralButton(R.string.crash_share) { _, _ -> CrashReports.share(ctx, entry) }
|
||||||
.setNegativeButton(R.string.crash_log_delete) { _, _ ->
|
.setNegativeButton(R.string.crash_log_delete) { _, _ ->
|
||||||
file.delete()
|
entry.dir.deleteRecursively()
|
||||||
populate()
|
populate()
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
|
|||||||
@@ -4,14 +4,20 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ScrollView
|
import android.widget.ScrollView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import su.xash.engine.BuildConfig
|
import su.xash.engine.BuildConfig
|
||||||
import su.xash.engine.R
|
import su.xash.engine.R
|
||||||
|
import java.io.BufferedOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.util.zip.ZipEntry
|
||||||
|
import java.util.zip.ZipOutputStream
|
||||||
|
|
||||||
object CrashReports {
|
object CrashReports {
|
||||||
private const val PREFS = "crash_reports"
|
private const val PREFS = "crash_reports"
|
||||||
@@ -20,7 +26,37 @@ object CrashReports {
|
|||||||
|
|
||||||
private const val D = "9c8d9e8c97bf9988988cd1989e86"
|
private const val D = "9c8d9e8c97bf9988988cd1989e86"
|
||||||
|
|
||||||
fun pendingFile(ctx: Context): File = File(ctx.filesDir, "crashes/crash.log")
|
const val STACKTRACE_NAME = "crash.log"
|
||||||
|
const val SYSINFO_NAME = "sysinfo.txt"
|
||||||
|
const val INTENT_NAME = "intent.txt"
|
||||||
|
|
||||||
|
class Entry(val dir: File) {
|
||||||
|
val name: String get() = dir.name
|
||||||
|
val timestamp: Long get() = dir.lastModified()
|
||||||
|
val stacktrace: File get() = File(dir, STACKTRACE_NAME)
|
||||||
|
val sysinfo: File get() = File(dir, SYSINFO_NAME)
|
||||||
|
val intent: File get() = File(dir, INTENT_NAME)
|
||||||
|
|
||||||
|
fun attachments(): List<File> = listOf(stacktrace, sysinfo, intent).filter { it.exists() && it.length() > 0 }
|
||||||
|
|
||||||
|
fun summary(): String = buildString {
|
||||||
|
if (stacktrace.exists())
|
||||||
|
append(stacktrace.readText())
|
||||||
|
|
||||||
|
if (sysinfo.exists()) {
|
||||||
|
append("\n--- System info ---\n").append(sysinfo.readText())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (intent.exists()) {
|
||||||
|
append("\n--- XashActivity intent ---\n").append(intent.readText())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pendingDir(ctx: Context): File = File(ctx.filesDir, "crashes")
|
||||||
|
fun pendingStacktrace(ctx: Context): File = File(pendingDir(ctx), STACKTRACE_NAME)
|
||||||
|
fun pendingSysinfo(ctx: Context): File = File(pendingDir(ctx), SYSINFO_NAME)
|
||||||
|
fun pendingIntent(ctx: Context): File = File(pendingDir(ctx), INTENT_NAME)
|
||||||
fun historyDir(ctx: Context): File = File(ctx.filesDir, "crashes/history")
|
fun historyDir(ctx: Context): File = File(ctx.filesDir, "crashes/history")
|
||||||
|
|
||||||
// wipe everything on app update; otherwise drop logs older than 30 days
|
// wipe everything on app update; otherwise drop logs older than 30 days
|
||||||
@@ -30,28 +66,102 @@ object CrashReports {
|
|||||||
val currentVersion = BuildConfig.VERSION_CODE
|
val currentVersion = BuildConfig.VERSION_CODE
|
||||||
|
|
||||||
if (lastVersion != currentVersion) {
|
if (lastVersion != currentVersion) {
|
||||||
historyDir(ctx).listFiles()?.forEach { it.delete() }
|
historyDir(ctx).deleteRecursively()
|
||||||
pendingFile(ctx).delete()
|
pendingStacktrace(ctx).delete()
|
||||||
|
pendingSysinfo(ctx).delete()
|
||||||
|
pendingIntent(ctx).delete()
|
||||||
prefs.edit().putInt(KEY_LAST_VERSION, currentVersion).apply()
|
prefs.edit().putInt(KEY_LAST_VERSION, currentVersion).apply()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val cutoff = System.currentTimeMillis() - MAX_AGE_MS
|
val cutoff = System.currentTimeMillis() - MAX_AGE_MS
|
||||||
historyDir(ctx).listFiles()?.forEach { f ->
|
historyDir(ctx).listFiles()?.forEach { entry ->
|
||||||
if (f.lastModified() < cutoff) f.delete()
|
if (entry.lastModified() < cutoff)
|
||||||
|
entry.deleteRecursively()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendByEmail(ctx: Context, content: String) {
|
@JvmStatic
|
||||||
|
fun writeSystemInfo(ctx: Context) {
|
||||||
|
val text = buildString {
|
||||||
|
append("App version: ").append(BuildConfig.VERSION_NAME).append(" (code ").append(BuildConfig.VERSION_CODE).append(")\n")
|
||||||
|
append("Application ID: ").append(BuildConfig.APPLICATION_ID).append('\n')
|
||||||
|
append("Android: ").append(Build.VERSION.RELEASE).append(" (SDK ").append(Build.VERSION.SDK_INT).append(")\n")
|
||||||
|
append("Manufacturer: ").append(Build.MANUFACTURER).append('\n')
|
||||||
|
append("Brand: ").append(Build.BRAND).append('\n')
|
||||||
|
append("Model: ").append(Build.MODEL).append('\n')
|
||||||
|
append("Device: ").append(Build.DEVICE).append('\n')
|
||||||
|
append("Product: ").append(Build.PRODUCT).append('\n')
|
||||||
|
append("Hardware: ").append(Build.HARDWARE).append('\n')
|
||||||
|
append("Fingerprint: ").append(Build.FINGERPRINT).append('\n')
|
||||||
|
append("Supported ABIs: ").append(Build.SUPPORTED_ABIS.joinToString(", ")).append('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
runCatching {
|
||||||
|
pendingDir(ctx).mkdirs()
|
||||||
|
pendingSysinfo(ctx).writeText(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun writeIntentInfo(ctx: Context, text: String) {
|
||||||
|
runCatching {
|
||||||
|
pendingDir(ctx).mkdirs()
|
||||||
|
pendingIntent(ctx).writeText(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun zipUri(ctx: Context, entry: Entry): Uri {
|
||||||
|
val zipDir = File(ctx.cacheDir, "crashes").apply { mkdirs() }
|
||||||
|
val zip = File(zipDir, "${entry.name}.zip")
|
||||||
|
ZipOutputStream(BufferedOutputStream(FileOutputStream(zip))).use { zos ->
|
||||||
|
entry.attachments().forEach { f ->
|
||||||
|
zos.putNextEntry(ZipEntry(f.name))
|
||||||
|
f.inputStream().use { it.copyTo(zos) }
|
||||||
|
zos.closeEntry()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val authority = "${BuildConfig.APPLICATION_ID}.fileprovider"
|
||||||
|
return FileProvider.getUriForFile(ctx, authority, zip)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendByEmail(ctx: Context, entry: Entry) {
|
||||||
val addr = D.chunked(2) { (it.toString().toInt(16) xor 0xFF).toChar() }.joinToString("")
|
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)
|
val mailtoProbe = Intent(Intent.ACTION_SENDTO, Uri.fromParts("mailto", addr, null))
|
||||||
|
val mailApps = ctx.packageManager.queryIntentActivities(mailtoProbe, 0)
|
||||||
|
if (mailApps.isEmpty()) {
|
||||||
|
AlertDialog.Builder(ctx)
|
||||||
|
.setTitle(R.string.crash_no_mail_app_title)
|
||||||
|
.setMessage(R.string.crash_no_mail_app_message)
|
||||||
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
|
.show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val uri = zipUri(ctx, entry)
|
||||||
|
val baseSend = Intent(Intent.ACTION_SEND).apply {
|
||||||
|
type = "application/zip"
|
||||||
|
putExtra(Intent.EXTRA_EMAIL, arrayOf(addr))
|
||||||
putExtra(Intent.EXTRA_SUBJECT, ctx.getString(R.string.crash_email_subject))
|
putExtra(Intent.EXTRA_SUBJECT, ctx.getString(R.string.crash_email_subject))
|
||||||
putExtra(Intent.EXTRA_TEXT, content)
|
putExtra(Intent.EXTRA_TEXT, ctx.getString(R.string.crash_email_body))
|
||||||
|
putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
}
|
}
|
||||||
if (intent.resolveActivity(ctx.packageManager) != null) {
|
|
||||||
ctx.startActivity(intent)
|
val targeted = mailApps.map { ri ->
|
||||||
|
Intent(baseSend).apply { setPackage(ri.activityInfo.packageName) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (targeted.size == 1) {
|
||||||
|
ctx.startActivity(targeted[0])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiple mail apps — show a chooser limited to them, no other share targets
|
||||||
|
val chooser = Intent.createChooser(targeted[0], ctx.getString(R.string.crash_send_to_developers))
|
||||||
|
chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, targeted.drop(1).toTypedArray())
|
||||||
|
ctx.startActivity(chooser)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildContentView(ctx: Context, content: String): View {
|
fun buildContentView(ctx: Context, content: String): View {
|
||||||
@@ -66,11 +176,10 @@ object CrashReports {
|
|||||||
return ScrollView(ctx).apply { addView(text) }
|
return ScrollView(ctx).apply { addView(text) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun share(ctx: Context, file: File) {
|
fun share(ctx: Context, entry: Entry) {
|
||||||
val authority = "${BuildConfig.APPLICATION_ID}.fileprovider"
|
val uri = zipUri(ctx, entry)
|
||||||
val uri = FileProvider.getUriForFile(ctx, authority, file)
|
|
||||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||||
type = "text/plain"
|
type = "application/zip"
|
||||||
putExtra(Intent.EXTRA_SUBJECT, ctx.getString(R.string.crash_email_subject))
|
putExtra(Intent.EXTRA_SUBJECT, ctx.getString(R.string.crash_email_subject))
|
||||||
putExtra(Intent.EXTRA_STREAM, uri)
|
putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
|||||||
@@ -30,6 +30,9 @@
|
|||||||
<string name="crash_logs">Журналы сбоев</string>
|
<string name="crash_logs">Журналы сбоев</string>
|
||||||
<string name="crash_logs_empty">Нет сохранённых журналов сбоев</string>
|
<string name="crash_logs_empty">Нет сохранённых журналов сбоев</string>
|
||||||
<string name="crash_log_delete">Удалить</string>
|
<string name="crash_log_delete">Удалить</string>
|
||||||
|
<string name="crash_email_body">К письму прикреплены подробности сбоя. Опишите, пожалуйста, как можно подробнее, что вы делали, когда игра вылетела — любая мелочь поможет нам исправить ошибку!</string>
|
||||||
|
<string name="crash_no_mail_app_title">Почтовое приложение не найдено</string>
|
||||||
|
<string name="crash_no_mail_app_message">На этом устройстве нет почтового клиента. Установите его (или воспользуйтесь кнопкой «Поделиться», чтобы отправить отчёт другим способом) и попробуйте снова.</string>
|
||||||
<string name="downloading_game_libs">Загрузка игровых библиотек</string>
|
<string name="downloading_game_libs">Загрузка игровых библиотек</string>
|
||||||
<string name="downloading">Загрузка…</string>
|
<string name="downloading">Загрузка…</string>
|
||||||
<string name="download_progress">Загрузка… %1$d%%</string>
|
<string name="download_progress">Загрузка… %1$d%%</string>
|
||||||
|
|||||||
@@ -32,6 +32,9 @@
|
|||||||
<string name="crash_logs_empty">No crash logs recorded</string>
|
<string name="crash_logs_empty">No crash logs recorded</string>
|
||||||
<string name="crash_log_delete">Delete</string>
|
<string name="crash_log_delete">Delete</string>
|
||||||
<string name="crash_email_subject" translatable="false">Xash3D FWGS crash report</string>
|
<string name="crash_email_subject" translatable="false">Xash3D FWGS crash report</string>
|
||||||
|
<string name="crash_email_body">Crash details are attached. Please describe in as much detail as possible what you were doing when the game crashed — every little thing helps us fix it!</string>
|
||||||
|
<string name="crash_no_mail_app_title">No mail app found</string>
|
||||||
|
<string name="crash_no_mail_app_message">No email client is installed on this device. Install one (or use Share to send the report another way) and try again.</string>
|
||||||
<string name="downloading_game_libs">Downloading Game Libraries</string>
|
<string name="downloading_game_libs">Downloading Game Libraries</string>
|
||||||
<string name="downloading">Downloading…</string>
|
<string name="downloading">Downloading…</string>
|
||||||
<string name="download_progress">Downloading… %1$d%%</string>
|
<string name="download_progress">Downloading… %1$d%%</string>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<paths>
|
<paths>
|
||||||
<files-path name="crashes" path="crashes/" />
|
<cache-path name="crash_zips" path="crashes/" />
|
||||||
</paths>
|
</paths>
|
||||||
|
|||||||
Reference in New Issue
Block a user