Skip to content

Commit 4d750cb

Browse files
feat(crash): Implement native crash reporting service
This commit introduces a native crash reporting service that automatically sends crash data to a remote endpoint. This provides a more robust and immediate way to capture and analyze crashes compared to the previous email-based reporting system. Key changes include: - A new `sendCrashReportNative` function in `CrashReportActivity` that sends a POST request with the crash details to a worker endpoint. - Crash data, including stack trace and device information, is now formatted as a JSON payload. - New helper functions `getDeviceInfoObject` and `getDeviceInfoJson` have been added to `SystemUtils.kt` to gather detailed device and application information using Moshi for JSON serialization. - An internet connectivity check is now performed before attempting to send the crash report. - Moshi dependencies have been added to facilitate JSON handling.
1 parent 84c797e commit 4d750cb

File tree

2 files changed

+158
-0
lines changed

2 files changed

+158
-0
lines changed

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

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,29 @@ package com.github.droidworksstudio.mlauncher
22

33
import android.content.Context
44
import android.content.Intent
5+
import android.net.ConnectivityManager
6+
import android.net.NetworkCapabilities
57
import android.net.Uri
68
import android.os.Bundle
9+
import android.util.Base64
710
import androidx.appcompat.app.AppCompatActivity
811
import androidx.core.net.toUri
12+
import androidx.lifecycle.lifecycleScope
913
import com.github.droidworksstudio.common.getLocalizedString
1014
import com.github.droidworksstudio.mlauncher.helper.emptyString
1115
import com.github.droidworksstudio.mlauncher.helper.getDeviceInfo
16+
import com.github.droidworksstudio.mlauncher.helper.getDeviceInfoJson
1217
import com.github.droidworksstudio.mlauncher.helper.utils.SimpleEmailSender
1318
import com.google.android.material.dialog.MaterialAlertDialogBuilder
19+
import com.squareup.moshi.JsonAdapter
20+
import com.squareup.moshi.Moshi
21+
import com.squareup.moshi.Types
22+
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
23+
import kotlinx.coroutines.Dispatchers
24+
import kotlinx.coroutines.launch
25+
import org.json.JSONObject
26+
import java.net.HttpURLConnection
27+
import java.net.URL
1428

1529
class CrashReportActivity : AppCompatActivity() {
1630
private var pkgName: String = emptyString()
@@ -24,6 +38,11 @@ class CrashReportActivity : AppCompatActivity() {
2438
0
2539
).versionName.toString()
2640

41+
// Check for internet connection before sending crash report
42+
if (isInternetAvailable()) {
43+
sendCrashReportNative()
44+
}
45+
2746
// Show a dialog to ask if the user wants to report the crash
2847
MaterialAlertDialogBuilder(this)
2948
.setTitle(getLocalizedString(R.string.acra_crash))
@@ -38,6 +57,88 @@ class CrashReportActivity : AppCompatActivity() {
3857
.show()
3958
}
4059

60+
// Function to check internet connectivity
61+
private fun isInternetAvailable(): Boolean {
62+
val connectivityManager = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
63+
val network = connectivityManager.activeNetwork ?: return false
64+
val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
65+
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
66+
}
67+
68+
private fun sendCrashReportNative() {
69+
lifecycleScope.launch(Dispatchers.IO) {
70+
try {
71+
val crashFileUri: Uri? = intent.getStringExtra("crash_log_uri")?.toUri()
72+
val crashFileUris: List<Uri> = crashFileUri?.let { listOf(it) } ?: emptyList()
73+
74+
val logContent = readFirstCrashFile(this@CrashReportActivity, crashFileUris)
75+
76+
// Example device info JSON string (replace with getDeviceInfoJson() if dynamic)
77+
val jsonDeviceInfo = getDeviceInfoJson(this@CrashReportActivity)
78+
79+
// Parse device info JSON into Map<String, Any>
80+
val moshi = Moshi.Builder()
81+
.add(KotlinJsonAdapterFactory())
82+
.build()
83+
84+
val type = Types.newParameterizedType(Map::class.java, String::class.java, Any::class.java)
85+
val deviceAdapter: JsonAdapter<Map<String, Any>> = moshi.adapter(type)
86+
val deviceMap: Map<String, Any> = deviceAdapter.fromJson(jsonDeviceInfo) ?: emptyMap()
87+
88+
89+
val logBase64 = logContent?.toByteArray()?.let {
90+
Base64.encodeToString(it, Base64.NO_WRAP)
91+
} ?: ""
92+
93+
// Build crash JSON
94+
val crashJson = JSONObject().apply {
95+
put("thread", "main")
96+
put("message", "App crashed")
97+
put("stackTrace", logContent ?: "No stack trace available")
98+
put("device", JSONObject(deviceMap))
99+
put("timestamp", System.currentTimeMillis())
100+
put("logFileBase64", logBase64)
101+
}.toString()
102+
103+
val url = URL("https://crash-worker.wayne6324.workers.dev")
104+
val connection = url.openConnection() as HttpURLConnection
105+
connection.requestMethod = "POST"
106+
connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8")
107+
connection.doOutput = true
108+
109+
// Send JSON
110+
connection.outputStream.use { it.write(crashJson.toByteArray(Charsets.UTF_8)) }
111+
112+
val responseCode = connection.responseCode
113+
114+
// Read inputStream if 2xx, else errorStream
115+
val responseStream = if (responseCode in 200..299) connection.inputStream else connection.errorStream
116+
val responseMessage = responseStream.bufferedReader().use { it.readText() }
117+
118+
if (responseCode in 200..299) {
119+
println("Crash report sent successfully: $responseMessage")
120+
} else {
121+
println("Failed to send crash report: $responseCode $responseMessage")
122+
}
123+
124+
connection.disconnect()
125+
} catch (e: Exception) {
126+
e.printStackTrace()
127+
}
128+
}
129+
}
130+
131+
fun readFirstCrashFile(context: Context, crashFileUris: List<Uri>): String? {
132+
val firstUri = crashFileUris.firstOrNull() ?: return null
133+
return try {
134+
context.contentResolver.openInputStream(firstUri)?.bufferedReader().use { it?.readText() }
135+
} catch (e: Exception) {
136+
e.printStackTrace()
137+
null
138+
}
139+
}
140+
141+
41142
private fun sendCrashReport(context: Context) {
42143
// Use the latest crash log URI generated by CrashHandler
43144
val crashFileUri: Uri? = intent.getStringExtra("crash_log_uri")?.toUri()

app/src/main/java/com/github/droidworksstudio/mlauncher/helper/SystemUtils.kt

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ import com.github.droidworksstudio.mlauncher.services.ActionService
5959
import com.github.droidworksstudio.mlauncher.ui.widgets.home.HomeAppsWidgetProvider
6060
import com.github.droidworksstudio.mlauncher.ui.widgets.wordoftheday.WordOfTheDayWidget
6161
import com.google.android.material.dialog.MaterialAlertDialogBuilder
62+
import com.squareup.moshi.Moshi
63+
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
6264
import org.xmlpull.v1.XmlPullParser
6365
import java.io.File
6466
import java.text.SimpleDateFormat
@@ -576,6 +578,61 @@ fun getDeviceInfo(context: Context): String {
576578
}
577579
}
578580

581+
// 1. Define the data class
582+
data class DeviceInfo(
583+
val manufacturer: String,
584+
val model: String,
585+
val brand: String,
586+
val device: String,
587+
val product: String,
588+
val androidVersion: String,
589+
val sdkInt: Int,
590+
val abi: String,
591+
val appVersionName: String,
592+
val appVersionCode: Int,
593+
val locale: String,
594+
val timezone: String,
595+
val installedFrom: String
596+
)
597+
598+
// 2. Function to get DeviceInfo object
599+
fun getDeviceInfoObject(context: Context): DeviceInfo {
600+
val packageManager = context.packageManager
601+
val installSource = getInstallSource(packageManager, context.packageName)
602+
603+
return DeviceInfo(
604+
manufacturer = Build.MANUFACTURER,
605+
model = Build.MODEL,
606+
brand = Build.BRAND,
607+
device = Build.DEVICE,
608+
product = Build.PRODUCT,
609+
androidVersion = Build.VERSION.RELEASE,
610+
sdkInt = Build.VERSION.SDK_INT,
611+
abi = Build.SUPPORTED_ABIS.joinToString(),
612+
appVersionName = BuildConfig.VERSION_NAME,
613+
appVersionCode = BuildConfig.VERSION_CODE,
614+
locale = Locale.getDefault().toString(),
615+
timezone = TimeZone.getDefault().id,
616+
installedFrom = installSource
617+
)
618+
}
619+
620+
// 3. Function to convert DeviceInfo to JSON using Moshi
621+
fun getDeviceInfoJson(context: Context): String {
622+
return try {
623+
val deviceInfo = getDeviceInfoObject(context)
624+
625+
val moshi = Moshi.Builder()
626+
.add(KotlinJsonAdapterFactory())
627+
.build()
628+
val jsonAdapter = moshi.adapter(DeviceInfo::class.java).indent(" ") // pretty print
629+
630+
jsonAdapter.toJson(deviceInfo)
631+
} catch (e: Exception) {
632+
"""{"error": "Device Info Unavailable: ${e.message}"}"""
633+
}
634+
}
635+
579636
fun getInstallSource(packageManager: PackageManager, packageName: String): String {
580637
try {
581638
val installer = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {

0 commit comments

Comments
 (0)