package com.example.aiapp import android.content.Context import java.io.File import java.io.PrintWriter import java.io.StringWriter import java.text.SimpleDateFormat import java.util.Date import java.util.Locale /** * The last crash, kept so the debug button can hand it over. * * The alternative is asking somebody to reproduce a crash with the phone plugged into a computer * and `logcat` running, which is the one thing nobody has set up at the moment it happens -- and a * crash report that arrives a day later, without the stack, is a guess. This costs one file write * on a process that is already dying, and it turns "it crashes when I open that chat" into the * frame it crashed in. * * Kept until it is read rather than cleared on the next launch: the app restarts before anybody can * ask about it, so a log that lives for one session is a log that is never read. */ private const val CRASH_FILE = "last-crash.txt" /** * How much of a stack is kept. * * This is pasted into a conversation, so it has a budget like any other output written for a * reader. The top of a stack is what identifies a crash and the bottom is framework plumbing, so * what gets cut is the part nobody reads. */ private const val CRASH_LIMIT = 4000 /** * Records uncaught exceptions, then lets the platform do what it was going to do. * * Chained rather than replacing: the default handler is what shows the "app has stopped" dialog and * ends the process, and an app that swallows that instead sits there in an unknown state. This only * adds a witness. */ fun installCrashLog(context: Context) { val app = context.applicationContext val previous = Thread.getDefaultUncaughtExceptionHandler() Thread.setDefaultUncaughtExceptionHandler { thread, error -> runCatching { File(app.filesDir, CRASH_FILE).writeText(describe(thread, error)) } previous?.uncaughtException(thread, error) } } private fun describe(thread: Thread, error: Throwable): String { val when_ = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).format(Date()) val stack = StringWriter().also { error.printStackTrace(PrintWriter(it)) }.toString() val kept = if (stack.length <= CRASH_LIMIT) stack else stack.take(CRASH_LIMIT) + "\n ... ${stack.length - CRASH_LIMIT} more characters" return "$when_ on thread ${thread.name}\n$kept" } /** The last crash, or null if there has not been one since it was last read. */ fun lastCrash(context: Context): String? = File(context.applicationContext.filesDir, CRASH_FILE).takeIf { it.exists() }?.readText() /** Forgets the last crash, once somebody has taken a copy of it. */ fun clearCrash(context: Context) { File(context.applicationContext.filesDir, CRASH_FILE).delete() }