Files
nfsmw-online/app/src/main/java/com/ea/ironmonkey/CrashReportActivity.kt
T
megboyzzandClaude 725ffbd8ed arm64: flat guest mapping, audio, crash reporting, self-extracting data
The run of work that took the translated build from "boots" to "playable".

Engine:
- Flat guest mapping replaces the software MMU on aarch64 hosts. A 4 GiB
  PROT_NONE reservation lets a 32-bit guest address zero-extend safely, so
  tcg_out_qemu_ld/st short-circuit before tcg_out_tlb_read and the prologue
  materialises the base into X28. Measured 1.24x (51 vs 41 fps, interleaved
  A/B). Note the base must be set BEFORE UC_INIT - Unicorn inits lazily, and
  setting it after uc_open silently falls back to softmmu.
- num_get<char> facet implemented, which was the real cause of the crash after
  the prologue race; a full race is now playable end to end.
- Thread-stack free list + ReleaseThreadEngine, fixing the arena leak that
  showed up as a black screen when entering a race. kMaxGuestThreads 16 -> 64.
- Real ARM32 FMOD now runs in-engine via the Java FMODAudioDevice bridge, with
  a per-thread JNIEnv. Two of the three blockers were our own single-image-era
  guards.

Host/app:
- Native crash handler: async-signal-safe, decodes the host fault back to a
  guest address, writes a report file and nothing else. CrashReportActivity
  picks it up on the NEXT launch, zips it, and offers to share. No backend, no
  automatic upload.
- Game data ships inside the APK and self-extracts on first launch, so a tester
  installs one file and plays. Copy-to-.part-then-rename, with a free-space
  check up front.
- EGL context preserved across pause, fixing black textures on resume.
- Navigation bar hidden and re-hidden on focus gain; volume keys reported as
  system keys, checked before the loading-state gate.
- x86_64 added to abiFilters: the ARM32 guest runs under tcg/i386 with no
  houdini in the path. The flat mapping is aarch64-only, so that host falls
  back to the software MMU - commented at the abiFilters line.

Ignore rules added for app/translated/ (611 MB of signed release APK, which
also carries the bundled OBB) and ostream_repro/build/.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-22 23:17:47 +03:00

238 lines
9.3 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.ea.ironmonkey
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.darkColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.FileProvider
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
/**
* Shown on the launch AFTER a crash, never during one.
*
* The native handler (crash_handler.cpp) can only write a file - a signal
* handler runs on an already-broken process where JNI and Activities are not
* legal. So it drops `crash_pending.txt` and dies; this screen is what the
* tester sees next time they open the game.
*
* Everything it produces stays on the device unless the tester presses send.
* There is no backend and no automatic upload - see BETA_TELEMETRY_PLAN.md.
*
* Built with Compose/Material3 and its own dark colour scheme rather than the
* game's `Theme.AppCompat.NoActionBar`: this screen must render correctly no
* matter what the rest of the app's theming does, and it is the one screen a
* tester sees when everything else has already gone wrong.
*/
class CrashReportActivity : ComponentActivity() {
companion object {
private const val TAG = "CrashReport"
private const val PENDING_NAME = "crash_pending.txt"
/** Reports live here: Android/data/<pkg>/files/crashes - reachable over USB, no permission needed. */
fun crashDir(activity: android.app.Activity): File =
File(activity.getExternalFilesDir(null), "crashes")
/** The file the native handler writes. Null if there is nothing waiting. */
fun pendingReport(activity: android.app.Activity): File? =
File(crashDir(activity), PENDING_NAME).takeIf { it.isFile && it.length() > 0 }
}
private var bundle: File? = null
override fun onCreate(savedInstanceState: Bundle?) {
// Android 15 draws every app edge to edge whether it asks or not, so
// the choice is between handling insets or having text sit under the
// status bar. Declaring it explicitly and letting Scaffold apply the
// padding is the supported way; the first version did neither and the
// content ran under the system bars.
enableEdgeToEdge()
super.onCreate(savedInstanceState)
val pending = pendingReport(this)
if (pending == null) {
// Nothing to report - never block the tester on an empty screen.
finish()
return
}
// Rename out of the way FIRST, before anything that could fail. The
// native handler always writes the same fixed name (it cannot safely
// format a timestamp inside a signal handler), so leaving it in place
// would let the next crash overwrite a report not yet sent.
val stamp = SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US).format(Date())
val kept = File(crashDir(this), "crash-$stamp.txt")
if (!pending.renameTo(kept)) {
Log.w(TAG, "could not rename $pending - reporting it in place")
}
val report = if (kept.isFile) kept else pending
val details = buildString {
append(deviceSummary())
append("\n\n")
append(runCatching { report.readText() }.getOrElse { "(could not read the report: $it)" })
}
bundle = runCatching { zipReport(report, details, stamp) }
.onFailure { Log.w(TAG, "could not build the zip", it) }
.getOrNull()
setContent {
MaterialTheme(colorScheme = darkColorScheme()) {
CrashReportScreen(
details = details,
bundlePath = bundle?.absolutePath,
canSend = bundle != null,
onSend = ::share,
onContinue = ::startGame,
)
}
}
}
private fun deviceSummary(): String = buildString {
append("device: ${Build.MANUFACTURER} ${Build.MODEL}\n")
append("android: ${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})\n")
append("soc: ${Build.HARDWARE}\n")
append("abis: ${Build.SUPPORTED_ABIS.joinToString(", ")}\n")
append("app: ${appVersion()}")
}
private fun appVersion(): String = runCatching {
val p = packageManager.getPackageInfo(packageName, 0)
"${p.versionName} (${p.longVersionCode})"
}.getOrElse { "(unknown)" }
private fun zipReport(report: File, details: String, stamp: String): File {
val out = File(crashDir(this), "crash-$stamp.zip")
ZipOutputStream(out.outputStream().buffered()).use { zip ->
zip.putNextEntry(ZipEntry("crash.txt"))
zip.write(details.toByteArray())
zip.closeEntry()
if (report.isFile) {
zip.putNextEntry(ZipEntry(report.name))
report.inputStream().use { it.copyTo(zip) }
zip.closeEntry()
}
}
return out
}
private fun share() {
val file = bundle ?: return
val uri = runCatching {
FileProvider.getUriForFile(this, "$packageName.crashreports", file)
}.getOrElse {
Log.w(TAG, "FileProvider failed for $file", it)
return
}
val send = Intent(Intent.ACTION_SEND).apply {
type = "application/zip"
putExtra(Intent.EXTRA_STREAM, uri)
putExtra(Intent.EXTRA_SUBJECT, "NFSMW arm64 - отчёт о сбое")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
startActivity(Intent.createChooser(send, "Отправить отчёт"))
}
private fun startGame() {
startActivity(Intent(this, GameActivityMain::class.java))
finish()
}
}
@Composable
private fun CrashReportScreen(
details: String,
bundlePath: String?,
canSend: Boolean,
onSend: () -> Unit,
onContinue: () -> Unit,
) {
// Scaffold's innerPadding carries the system-bar insets, so nothing ends up
// under the status bar or the gesture handle.
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(16.dp)
) {
Text(
text = "Игра аварийно завершилась",
style = MaterialTheme.typography.headlineSmall,
)
Text(
// Say plainly what is in the file before offering to send it.
// The tester is the one sending it; they should know what it
// contains.
text = "Отчёт сохранён на устройстве. В нём модель телефона, версия Android, " +
"версия сборки и технические данные о сбое. Личных данных и игрового " +
"аккаунта в нём нет.",
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(top = 8.dp),
)
if (bundlePath != null) {
Text(
text = bundlePath,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 8.dp),
)
}
Card(modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp)
.weight(1f)
) {
Text(
text = details,
fontSize = 11.sp,
lineHeight = 15.sp,
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(12.dp),
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),
) {
OutlinedButton(onClick = onContinue) { Text("Продолжить") }
Button(onClick = onSend, enabled = canSend) { Text("Отправить отчёт") }
}
}
}
}