11package com.github.droidworksstudio.common
22
3+ import android.content.ContentUris
4+ import android.content.ContentValues
35import android.content.Context
46import android.content.Intent
5- import android.content.pm.PackageInfo
7+ import android.content.pm.PackageManager
68import android.net.Uri
9+ import android.os.Build
10+ import android.provider.MediaStore
711import androidx.core.content.FileProvider
812import com.github.droidworksstudio.mlauncher.CrashReportActivity
913import com.github.droidworksstudio.mlauncher.helper.getDeviceInfo
1014import java.io.BufferedReader
1115import java.io.File
12- import java.io.FileInputStream
13- import java.io.FileWriter
1416import java.io.InputStreamReader
1517import java.io.PrintWriter
18+ import java.io.StringWriter
1619import java.text.SimpleDateFormat
1720import java.util.Date
1821import java.util.Locale
@@ -22,144 +25,146 @@ import kotlin.system.exitProcess
2225class 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}
0 commit comments