Skip to content

Commit a71b94f

Browse files
Refactor: Improve crash handling and reporting
This commit refactors the `CrashHandler` to improve how crash reports are generated, stored, and sent. Key changes in `CrashHandler.kt`: - Introduced `lastCrashUri` to store the URI of the most recent crash log. - Added `forceCrash()` for testing purposes. - Replaced `formatCustomDate` and `getDayOrdinal` with a simpler `getTimestamp` function. - Implemented `buildCrashContent` to consolidate the creation of the crash report string. - Added `saveCrashToMediaStore` to save crash logs to MediaStore for Android Q and above, storing them in `Download/[AppName]/Crash Reports` and limiting the number of stored reports to 5. - Added `getCrashFileForLegacy` to handle crash log saving for pre-Android Q devices, storing them in internal app storage (`filesDir/crash_logs`) and also limiting to 5 reports. - The `uncaughtException` method now uses `buildCrashContent` and the new saving mechanisms. It passes the crash log URI to `CrashReportActivity`. - User action timestamps are now logged using `Date().toString()`. Key changes in `CrashReportActivity.kt`: - The `sendCrashReport` function now retrieves the `crashFileUri` from `CrashHandler.lastCrashUri` instead of calling `CrashHandler.customReportSender()`.
1 parent 37d0d9f commit a71b94f

File tree

2 files changed

+123
-117
lines changed

2 files changed

+123
-117
lines changed
Lines changed: 119 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
package com.github.droidworksstudio.common
22

3+
import android.content.ContentUris
4+
import android.content.ContentValues
35
import android.content.Context
46
import android.content.Intent
5-
import android.content.pm.PackageInfo
7+
import android.content.pm.PackageManager
68
import android.net.Uri
9+
import android.os.Build
10+
import android.provider.MediaStore
711
import androidx.core.content.FileProvider
812
import com.github.droidworksstudio.mlauncher.CrashReportActivity
913
import com.github.droidworksstudio.mlauncher.helper.getDeviceInfo
1014
import java.io.BufferedReader
1115
import java.io.File
12-
import java.io.FileInputStream
13-
import java.io.FileWriter
1416
import java.io.InputStreamReader
1517
import java.io.PrintWriter
18+
import java.io.StringWriter
1619
import java.text.SimpleDateFormat
1720
import java.util.Date
1821
import java.util.Locale
@@ -22,144 +25,146 @@ import kotlin.system.exitProcess
2225
class CrashHandler(private val context: Context) : Thread.UncaughtExceptionHandler {
2326

2427
companion object {
25-
private val userActions = LinkedBlockingQueue<String>(50) // Stores last 50 user actions
28+
private val userActions = LinkedBlockingQueue<String>(50)
29+
var lastCrashUri: Uri? = null // store the latest crash file URI
2630

2731
fun logUserAction(action: String) {
28-
val timeStamp = formatCustomDate(Date())
32+
val timeStamp = Date().toString()
2933
userActions.offer("$timeStamp - $action")
30-
if (userActions.size > 50) userActions.poll() // Remove oldest if over limit
34+
if (userActions.size > 50) userActions.poll()
3135
}
3236

33-
fun formatCustomDate(date: Date): String {
34-
val dayOfWeek = SimpleDateFormat("EEE", Locale.getDefault()).format(date) // e.g. Fri
35-
val day = SimpleDateFormat("d", Locale.getDefault()).format(date).toInt() // e.g. 13
36-
val monthYear = SimpleDateFormat("MMMM yyyy", Locale.getDefault()).format(date) // e.g. June 2025
37-
val time = SimpleDateFormat("hh:mm a", Locale.getDefault()).format(date) // e.g. 12:32 AM
38-
39-
val ordinal = getDayOrdinal(day)
40-
41-
return "$dayOfWeek ${day}$ordinal $monthYear - $time"
42-
}
43-
44-
fun getDayOrdinal(day: Int): String {
45-
return when {
46-
day in 11..13 -> "th"
47-
day % 10 == 1 -> "st"
48-
day % 10 == 2 -> "nd"
49-
day % 10 == 3 -> "rd"
50-
else -> "th"
51-
}
37+
fun forceCrash() {
38+
throw RuntimeException("💥 Forced crash for testing CrashHandler")
5239
}
40+
}
5341

54-
fun customReportSender(context: Context): Uri? {
55-
val logFile: File = try {
56-
val packageManager = context.packageManager
57-
val packageInfo = packageManager.getPackageInfo(context.packageName, 0)
58-
59-
// Use internal storage for saving the crash log
60-
val crashDir = File(context.filesDir, "crash_logs") // Internal storage
61-
if (!crashDir.exists()) crashDir.mkdirs()
62-
63-
val crashFile = File(crashDir, "${packageInfo.packageName}-crash-report.txt")
64-
65-
// Check if the file exists before attempting to read
66-
if (crashFile.exists()) {
67-
// Read the content of the file
68-
val fileInputStream = FileInputStream(crashFile)
69-
val inputStreamReader = InputStreamReader(fileInputStream)
70-
val stringBuilder = StringBuilder()
71-
72-
// Read the file line by line
73-
inputStreamReader.forEachLine { stringBuilder.append(it).append("\n") }
74-
75-
// Log the content of the crash report file
76-
AppLogger.d("CrashHandler", "Crash Report Content:\n${stringBuilder}")
77-
} else {
78-
AppLogger.e("CrashHandler", "Crash report file does not exist.")
79-
}
80-
81-
File(crashDir, "${packageInfo.packageName}-crash-report.txt")
82-
} catch (e: Exception) {
83-
AppLogger.e("CrashHandler", "Error determining crash log file location: ${e.message}")
84-
return null // Return null if something goes wrong
85-
}
42+
private fun getTimestamp(): String {
43+
val sdf = SimpleDateFormat("EEE, d MMM yyyy - hh:mm a", Locale.getDefault())
44+
return sdf.format(Date())
45+
}
8646

87-
// Ensure the file exists
88-
if (!logFile.exists()) {
89-
return null
47+
private fun buildCrashContent(exception: Throwable): String {
48+
val runtime = Runtime.getRuntime()
49+
val usedMemInMB = (runtime.totalMemory() - runtime.freeMemory()) / 1048576L
50+
val maxHeapSizeInMB = runtime.maxMemory() / 1048576L
51+
52+
return buildString {
53+
appendLine("Crash Report - ${Date()}")
54+
appendLine("Thread: ${Thread.currentThread().name}")
55+
appendLine("\n=== Device Info ===")
56+
appendLine(getDeviceInfo(context))
57+
appendLine("\n=== Memory Info ===")
58+
appendLine("Used Memory (MB): $usedMemInMB")
59+
appendLine("Max Heap Size (MB): $maxHeapSizeInMB")
60+
appendLine("\n=== Recent User Actions ===")
61+
userActions.forEach { appendLine(it) }
62+
appendLine("\n=== Crash LogCat ===")
63+
try {
64+
val process = Runtime.getRuntime().exec("logcat -d -t 100 AndroidRuntime:E *:S")
65+
BufferedReader(InputStreamReader(process.inputStream)).forEachLine { appendLine(it) }
66+
} catch (_: Exception) {
9067
}
91-
92-
// Use FileProvider to get a content Uri for the file
93-
return FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", logFile)
68+
appendLine("\n=== Crash Stack Trace ===")
69+
val sw = StringWriter()
70+
val pw = PrintWriter(sw)
71+
exception.printStackTrace(pw)
72+
appendLine(sw.toString())
9473
}
9574
}
9675

97-
override fun uncaughtException(thread: Thread, exception: Throwable) {
98-
AppLogger.e("CrashHandler", "Caught exception: ${exception.message}", exception)
99-
100-
// Step 1: Save custom crash log
101-
saveCrashLog(exception)
76+
private fun saveCrashToMediaStore(content: String): Uri? {
77+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return null
10278

103-
// Step 2: Start CrashReportActivity with the crash details
104-
val intent = Intent(context, CrashReportActivity::class.java).apply {
105-
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
79+
val packageManager = context.packageManager
80+
val packageName = context.packageName
81+
val appName = try {
82+
val appInfo = packageManager.getApplicationInfo(packageName, 0)
83+
packageManager.getApplicationLabel(appInfo).toString()
84+
} catch (_: PackageManager.NameNotFoundException) {
85+
packageName
10686
}
107-
context.startActivity(intent)
108-
109-
// Kill the process
110-
android.os.Process.killProcess(android.os.Process.myPid())
111-
exitProcess(1)
112-
}
11387

114-
private fun saveCrashLog(exception: Throwable): File {
115-
val logFile: File = try {
116-
val packageManager = context.packageManager
117-
val packageInfo: PackageInfo = packageManager.getPackageInfo(context.packageName, 0)
118-
119-
// Use internal storage for saving the crash log
120-
val crashDir = File(context.filesDir, "crash_logs") // This is internal storage
121-
if (!crashDir.exists()) crashDir.mkdirs()
88+
val timestamp = getTimestamp()
89+
val displayName = "$appName Crash Report_$timestamp.log"
90+
val relativePath = "Download/$appName/Crash Reports"
91+
val resolver = context.contentResolver
92+
val collection = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
93+
94+
// Insert new crash file
95+
val values = ContentValues().apply {
96+
put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
97+
put(MediaStore.MediaColumns.MIME_TYPE, "application/octet-stream")
98+
put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
99+
}
100+
val uri = resolver.insert(collection, values)
122101

123-
File(crashDir, "${packageInfo.packageName}-crash-report.txt")
124-
} catch (e: Exception) {
125-
AppLogger.e("CrashHandler", "Error determining crash log file location: ${e.message}")
126-
// In case of error, use a default file name
127-
File(context.filesDir, "default-crash-report.txt")
102+
uri?.let {
103+
resolver.openOutputStream(it, "w")?.use { outputStream ->
104+
outputStream.write(content.toByteArray())
105+
}
128106
}
129107

108+
// Maintain maximum 5 files
130109
try {
131-
val runtime = Runtime.getRuntime()
132-
val usedMemInMB = (runtime.totalMemory() - runtime.freeMemory()) / 1048576L
133-
val maxHeapSizeInMB = runtime.maxMemory() / 1048576L
110+
val cursor = resolver.query(
111+
collection,
112+
arrayOf(MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DISPLAY_NAME),
113+
"${MediaStore.MediaColumns.RELATIVE_PATH}=?",
114+
arrayOf("$relativePath/"),
115+
"${MediaStore.MediaColumns.DATE_ADDED} DESC"
116+
)
117+
cursor?.use {
118+
var count = 0
119+
while (it.moveToNext()) {
120+
count++
121+
if (count > 5) {
122+
val id = it.getLong(it.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
123+
resolver.delete(ContentUris.withAppendedId(collection, id), null, null)
124+
}
125+
}
126+
}
127+
} catch (_: Exception) {
128+
}
134129

135-
FileWriter(logFile).use { writer ->
136-
PrintWriter(writer).use { printWriter ->
137-
printWriter.println("Crash Report - ${Date()}")
138-
printWriter.println("Thread: ${Thread.currentThread().name}")
130+
return uri
131+
}
139132

140-
printWriter.println("\n=== Device Info ===")
141-
printWriter.println(getDeviceInfo(context))
133+
private fun getCrashFileForLegacy(): File {
134+
val crashDir = File(context.filesDir, "crash_logs")
135+
crashDir.mkdirs()
136+
val timestamp = getTimestamp()
137+
val file = File(crashDir, "${context.packageName}-crash-report_$timestamp.txt")
142138

143-
printWriter.println("\n=== Memory Info ===")
144-
printWriter.println("Used Memory (MB): $usedMemInMB")
145-
printWriter.println("Max Heap Size (MB): $maxHeapSizeInMB")
139+
// Remove old logs if more than 5
140+
val oldFiles = crashDir.listFiles()?.sortedByDescending { it.lastModified() } ?: emptyList()
141+
oldFiles.drop(5).forEach { it.delete() }
146142

147-
printWriter.println("\n=== Recent User Actions ===")
148-
userActions.forEach { printWriter.println(it) }
143+
return file
144+
}
149145

150-
printWriter.println("\n=== Crash LogCat ===")
151-
val process = Runtime.getRuntime().exec("logcat -d -t 100 AndroidRuntime:E *:S")
152-
val reader = BufferedReader(InputStreamReader(process.inputStream))
153-
reader.forEachLine { printWriter.println(it) }
146+
override fun uncaughtException(thread: Thread, exception: Throwable) {
147+
try {
148+
val content = buildCrashContent(exception)
149+
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
150+
saveCrashToMediaStore(content)
151+
} else {
152+
val file = getCrashFileForLegacy()
153+
file.writeText(content)
154+
FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file)
155+
}
154156

155-
printWriter.println("\n=== Crash Stack Trace ===")
156-
exception.printStackTrace(printWriter)
157+
lastCrashUri = uri
157158

158-
}
159+
val intent = Intent(context, CrashReportActivity::class.java).apply {
160+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
161+
putExtra("crash_log_uri", uri.toString())
159162
}
160-
} catch (e: Exception) {
161-
AppLogger.e("CrashHandler", "Error writing crash log: ${e.message}")
163+
context.startActivity(intent)
164+
} catch (_: Exception) {
165+
} finally {
166+
android.os.Process.killProcess(android.os.Process.myPid())
167+
exitProcess(1)
162168
}
163-
return logFile
164169
}
165170
}

app/src/main/java/com/github/droidworksstudio/mlauncher/CrashReportActivity.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,15 @@ class CrashReportActivity : AppCompatActivity() {
3939
}
4040

4141
private fun sendCrashReport(context: Context) {
42-
val crashFileUri: Uri? = CrashHandler.customReportSender(applicationContext)
42+
// Use the latest crash log URI generated by CrashHandler
43+
val crashFileUri: Uri? = CrashHandler.lastCrashUri // We'll add this to store the last URI
4344
val crashFileUris: List<Uri> = crashFileUri?.let { listOf(it) } ?: emptyList()
4445

45-
val emailSender = SimpleEmailSender() // Create an instance
46+
val emailSender = SimpleEmailSender()
4647
val deviceInfo = getDeviceInfo(context)
4748
val crashReportContent = getLocalizedString(R.string.acra_mail_body, deviceInfo)
4849
val subject = String.format("Crash Report %s - %s", pkgName, pkgVersion)
49-
val recipient = getLocalizedString(R.string.acra_email) // Replace with your email
50+
val recipient = getLocalizedString(R.string.acra_email)
5051

5152
emailSender.sendCrashReport(context, crashReportContent, crashFileUris, subject, recipient)
5253
}

0 commit comments

Comments
 (0)