Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ android {
}

dependencies {
implementation(project(":exceptionreport"))

implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
Expand Down Expand Up @@ -137,7 +138,6 @@ dependencies {
// Google Play Review
implementation(libs.play.review)
implementation(libs.play.review.ktx)
implementation(libs.sentry.android)

// Coil for image and GIF loading
implementation("io.coil-kt:coil-compose:2.6.0")
Expand Down
21 changes: 17 additions & 4 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,7 @@
android:theme="@style/Theme.AirSync"
tools:targetApi="36">

<!-- Disable Sentry auto-init as it's handled manually in AirSyncApp -->
<meta-data
android:name="io.sentry.auto-init"
android:value="false" />


<activity
android:name=".MainActivity"
Expand Down Expand Up @@ -263,6 +260,22 @@
</intent-filter>
</receiver>

<!-- Crash Reporting System Components -->
<receiver
android:name=".crash.CrashNotificationReceiver"
android:exported="false">
<intent-filter>
<action android:name="com.sameerasw.airsync.action.DISMISS_CRASH_NOTIFICATION" />
</intent-filter>
</receiver>

<activity
android:name=".crash.CrashReportActivity"
android:exported="false"
android:theme="@style/Theme.AirSync.Transparent"
android:launchMode="singleInstance"
android:excludeFromRecents="true"
android:taskAffinity="" />

</application>

Expand Down
20 changes: 3 additions & 17 deletions app/src/main/java/com/sameerasw/airsync/AirSyncApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ import android.app.Activity
import android.app.Application
import android.os.Bundle
import com.sameerasw.airsync.data.local.DataStoreManager
import io.sentry.android.core.SentryAndroid
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import com.sameerasw.airsync.crash.CrashNotificationHelper

class AirSyncApp : Application() {
private var activityCount = 0
Expand All @@ -22,7 +20,8 @@ class AirSyncApp : Application() {
override fun onCreate() {
super.onCreate()
instance = this
initSentry()

CrashNotificationHelper.createChannel(this)

bleConnectionManager = com.sameerasw.airsync.data.ble.BleConnectionManager(this)
bleConnectionManager.start()
Expand All @@ -44,17 +43,4 @@ class AirSyncApp : Application() {
}

private fun isForeground(): Boolean = activityCount > 0

private fun initSentry() {
val dataStoreManager = DataStoreManager.getInstance(this)
val isEnabled = runBlocking { dataStoreManager.getSentryReportingEnabled().first() }

if (!isEnabled) return

SentryAndroid.init(this) { options ->
options.dsn =
"https://cb9b0ead9e88e0818269e773cb662141@o4510996760887296.ingest.de.sentry.io/4511002261389392"
options.isEnabled = true
}
}
}
7 changes: 7 additions & 0 deletions app/src/main/java/com/sameerasw/airsync/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ import com.sameerasw.airsync.utils.PermissionUtil
import com.sameerasw.airsync.utils.ShortcutUtil
import com.sameerasw.airsync.utils.UDPDiscoveryManager
import com.sameerasw.airsync.utils.WebSocketUtil
import com.canerture.exceptionreport.handler.ExceptionReport
import com.sameerasw.airsync.crash.CrashHandler
import com.sameerasw.airsync.crash.CrashReportActivity
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import java.net.URLDecoder
Expand Down Expand Up @@ -175,6 +178,10 @@ class MainActivity : ComponentActivity() {

super.onCreate(savedInstanceState)

ExceptionReport(this) { deviceInfo, stackTrace ->
CrashHandler.onCrash(applicationContext, deviceInfo, stackTrace)
}.setCustomActivity(CrashReportActivity::class.java)

// Dynamically set the splash screen icon based on last connected device
splashScreen.setOnExitAnimationListener { splashScreenViewProvider ->
try {
Expand Down
29 changes: 29 additions & 0 deletions app/src/main/java/com/sameerasw/airsync/crash/CrashHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.sameerasw.airsync.crash

import android.content.Context
import com.sameerasw.airsync.data.local.DataStoreManager
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import java.io.File

object CrashHandler {
fun onCrash(context: Context, deviceInfo: String, stackTrace: String) {
try {
// Write report to last_crash.log
val logFile = File(context.filesDir, "last_crash.log")
logFile.writeText("$deviceInfo\n\n$stackTrace")

// Read preference from DataStore synchronously
val dataStoreManager = DataStoreManager.getInstance(context)
val notifyEnabled = runBlocking {
dataStoreManager.getNotifyOnCrashEnabled().first()
}

if (notifyEnabled) {
CrashNotificationHelper.postNotification(context)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.sameerasw.airsync.crash

import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.sameerasw.airsync.R

object CrashNotificationHelper {
private const val CHANNEL_ID = "crash_report"
const val NOTIFICATION_ID = 9999

fun createChannel(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val name = context.getString(R.string.crash_notification_title)
val descriptionText = context.getString(R.string.crash_notification_body)
val importance = NotificationManager.IMPORTANCE_LOW
val channel = NotificationChannel(CHANNEL_ID, name, importance).apply {
description = descriptionText
setSound(null, null)
enableLights(false)
enableVibration(false)
}
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}

fun postNotification(context: Context) {
// Broadcast pending intent for Dismiss
val dismissIntent = Intent(context, CrashNotificationReceiver::class.java).apply {
action = "com.sameerasw.airsync.action.DISMISS_CRASH_NOTIFICATION"
}
val dismissPendingIntent = PendingIntent.getBroadcast(
context,
1,
dismissIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)

// Activity pending intent for Report
val reportIntent = Intent(context, CrashReportActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra("show_report", true)
}
val reportPendingIntent = PendingIntent.getActivity(
context,
2,
reportIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)

val builder = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.rounded_bug_report_24)
.setContentTitle(context.getString(R.string.crash_notification_title))
.setContentText(context.getString(R.string.crash_notification_body))
.setPriority(NotificationCompat.PRIORITY_LOW)
.setSilent(true)
.setAutoCancel(true)
.setContentIntent(reportPendingIntent)
.addAction(
0,
context.getString(R.string.crash_action_dismiss),
dismissPendingIntent
)
.addAction(
0,
context.getString(R.string.crash_action_report),
reportPendingIntent
)

try {
val notificationManager = NotificationManagerCompat.from(context)
notificationManager.notify(NOTIFICATION_ID, builder.build())
} catch (e: SecurityException) {
e.printStackTrace()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.sameerasw.airsync.crash

import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent

class CrashNotificationReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (context != null && intent?.action == "com.sameerasw.airsync.action.DISMISS_CRASH_NOTIFICATION") {
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.cancel(CrashNotificationHelper.NOTIFICATION_ID)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.sameerasw.airsync.crash

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import com.sameerasw.airsync.ui.theme.AirSyncTheme
import java.io.File

class CrashReportActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val showReport = intent.getBooleanExtra("show_report", false)
if (!showReport) {
finish()
return
}

val reportFile = File(filesDir, "last_crash.log")
val reportText = if (reportFile.exists()) {
reportFile.readText()
} else {
"No crash log found."
}

setContent {
val viewModel: com.sameerasw.airsync.presentation.viewmodel.AirSyncViewModel =
androidx.lifecycle.viewmodel.compose.viewModel {
com.sameerasw.airsync.presentation.viewmodel.AirSyncViewModel.create(this@CrashReportActivity)
}
val uiState by viewModel.uiState.collectAsState()

AirSyncTheme(pitchBlackTheme = uiState.isPitchBlackThemeEnabled) {
CrashReportBottomSheet(
report = reportText,
onDismiss = {
finish()
}
)
}
}
}
}
Loading