diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 115e6d42..a40b55f1 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -65,6 +65,7 @@ android {
}
dependencies {
+ implementation(project(":exceptionreport"))
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
@@ -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")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 5cd44fcb..a6ad76b7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -65,10 +65,7 @@
android:theme="@style/Theme.AirSync"
tools:targetApi="36">
-
-
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/sameerasw/airsync/AirSyncApp.kt b/app/src/main/java/com/sameerasw/airsync/AirSyncApp.kt
index 45f8a4fe..f82b6e29 100644
--- a/app/src/main/java/com/sameerasw/airsync/AirSyncApp.kt
+++ b/app/src/main/java/com/sameerasw/airsync/AirSyncApp.kt
@@ -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
@@ -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()
@@ -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
- }
- }
}
diff --git a/app/src/main/java/com/sameerasw/airsync/MainActivity.kt b/app/src/main/java/com/sameerasw/airsync/MainActivity.kt
index b71c49da..207f5738 100644
--- a/app/src/main/java/com/sameerasw/airsync/MainActivity.kt
+++ b/app/src/main/java/com/sameerasw/airsync/MainActivity.kt
@@ -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
@@ -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 {
diff --git a/app/src/main/java/com/sameerasw/airsync/crash/CrashHandler.kt b/app/src/main/java/com/sameerasw/airsync/crash/CrashHandler.kt
new file mode 100644
index 00000000..90b3f2ed
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/airsync/crash/CrashHandler.kt
@@ -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()
+ }
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/airsync/crash/CrashNotificationHelper.kt b/app/src/main/java/com/sameerasw/airsync/crash/CrashNotificationHelper.kt
new file mode 100644
index 00000000..a73188bd
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/airsync/crash/CrashNotificationHelper.kt
@@ -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()
+ }
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/airsync/crash/CrashNotificationReceiver.kt b/app/src/main/java/com/sameerasw/airsync/crash/CrashNotificationReceiver.kt
new file mode 100644
index 00000000..832864ca
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/airsync/crash/CrashNotificationReceiver.kt
@@ -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)
+ }
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/airsync/crash/CrashReportActivity.kt b/app/src/main/java/com/sameerasw/airsync/crash/CrashReportActivity.kt
new file mode 100644
index 00000000..452a6da2
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/airsync/crash/CrashReportActivity.kt
@@ -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()
+ }
+ )
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/airsync/crash/CrashReportBottomSheet.kt b/app/src/main/java/com/sameerasw/airsync/crash/CrashReportBottomSheet.kt
new file mode 100644
index 00000000..326fadb8
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/airsync/crash/CrashReportBottomSheet.kt
@@ -0,0 +1,186 @@
+package com.sameerasw.airsync.crash
+
+import android.content.ContentValues
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.os.Environment
+import android.provider.MediaStore
+import android.widget.Toast
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.Text
+import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import com.sameerasw.airsync.R
+import com.sameerasw.airsync.presentation.ui.components.RoundedCardContainer
+import com.sameerasw.airsync.presentation.ui.components.cards.IconToggleItem
+import java.io.OutputStream
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun CrashReportBottomSheet(
+ report: String,
+ onDismiss: () -> Unit
+) {
+ val context = LocalContext.current
+
+ ModalBottomSheet(
+ onDismissRequest = onDismiss,
+ sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false),
+ containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
+ ) {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ item {
+ Text(
+ text = stringResource(R.string.crash_report_title),
+ style = MaterialTheme.typography.headlineSmall,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onSurface,
+ modifier = Modifier.padding(bottom = 8.dp, top = 8.dp)
+ )
+ }
+
+ item {
+ RoundedCardContainer {
+ // Copy report
+ IconToggleItem(
+ title = stringResource(R.string.crash_copy),
+ description = stringResource(R.string.crash_copy_desc),
+ iconRes = R.drawable.ic_clipboard_24,
+ showToggle = false,
+ onClick = {
+ copyToClipboard(context, report)
+ }
+ )
+
+ // Save to file
+ IconToggleItem(
+ title = stringResource(R.string.crash_save_file),
+ description = stringResource(R.string.crash_save_file_desc),
+ iconRes = R.drawable.outline_downloading_24,
+ showToggle = false,
+ onClick = {
+ saveLogToFile(context, report, showToast = true)
+ }
+ )
+
+ // Email report
+ IconToggleItem(
+ title = stringResource(R.string.crash_email),
+ description = stringResource(R.string.crash_email_desc),
+ iconRes = R.drawable.outline_feedback_24,
+ showToggle = false,
+ onClick = {
+ emailReport(context, report)
+ }
+ )
+
+ // Open GitHub issue
+ IconToggleItem(
+ title = stringResource(R.string.crash_github),
+ description = stringResource(R.string.crash_github_desc),
+ iconRes = R.drawable.brand_github,
+ showToggle = false,
+ onClick = {
+ openGitHubIssue(context, report)
+ }
+ )
+ }
+ }
+
+ item {
+ Box(modifier = Modifier.padding(bottom = 32.dp))
+ }
+ }
+ }
+}
+
+private fun copyToClipboard(context: Context, text: String) {
+ try {
+ val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
+ val clip = android.content.ClipData.newPlainText("AirSync Crash Report", text)
+ clipboard.setPrimaryClip(clip)
+ Toast.makeText(context, "Report copied to clipboard", Toast.LENGTH_SHORT).show()
+ } catch (e: Exception) {
+ Toast.makeText(context, "Failed to copy report", Toast.LENGTH_SHORT).show()
+ }
+}
+
+private fun saveLogToFile(context: Context, text: String, showToast: Boolean): Uri? {
+ try {
+ val filename = "airsync_crash_${System.currentTimeMillis()}.log"
+ val resolver = context.contentResolver
+ val contentValues = ContentValues().apply {
+ put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
+ put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
+ }
+ }
+
+ val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
+ if (uri != null) {
+ val outputStream: OutputStream? = resolver.openOutputStream(uri)
+ outputStream?.use {
+ it.write(text.toByteArray())
+ }
+ if (showToast) {
+ Toast.makeText(context, context.getString(R.string.crash_file_saved), Toast.LENGTH_LONG).show()
+ }
+ return uri
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ Toast.makeText(context, context.getString(R.string.crash_file_save_failed), Toast.LENGTH_SHORT).show()
+ return null
+}
+
+private fun emailReport(context: Context, text: String) {
+ try {
+ val emailIntent = Intent(Intent.ACTION_SENDTO).apply {
+ data = Uri.parse("mailto:mail@sameerasw.com")
+ putExtra(Intent.EXTRA_SUBJECT, "AirSync Android Crash Report")
+ val bodyText = if (text.length > 4000) text.substring(0, 4000) + "\n...[truncated]" else text
+ putExtra(Intent.EXTRA_TEXT, bodyText)
+ }
+ context.startActivity(Intent.createChooser(emailIntent, "Send Email"))
+ } catch (e: Exception) {
+ Toast.makeText(context, "No email client found", Toast.LENGTH_SHORT).show()
+ }
+}
+
+private fun openGitHubIssue(context: Context, text: String) {
+ try {
+ val uri = saveLogToFile(context, text, showToast = false)
+ if (uri != null) {
+ Toast.makeText(context, context.getString(R.string.crash_github_hint), Toast.LENGTH_LONG).show()
+ }
+ val url = "https://github.com/sameerasw/airsync-android/issues/new"
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
+ context.startActivity(intent)
+ } catch (e: Exception) {
+ Toast.makeText(context, "Could not open link", Toast.LENGTH_SHORT).show()
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/airsync/data/local/DataStoreManager.kt b/app/src/main/java/com/sameerasw/airsync/data/local/DataStoreManager.kt
index 1ffdc453..8fb6fb97 100644
--- a/app/src/main/java/com/sameerasw/airsync/data/local/DataStoreManager.kt
+++ b/app/src/main/java/com/sameerasw/airsync/data/local/DataStoreManager.kt
@@ -90,7 +90,6 @@ class DataStoreManager(private val context: Context) {
private val DEVICE_ID = stringPreferencesKey("device_id")
private val USE_BLUR = booleanPreferencesKey("use_blur")
private val PITCH_BLACK_THEME = booleanPreferencesKey("pitch_black_theme")
- private val SENTRY_REPORTING_ENABLED = booleanPreferencesKey("sentry_reporting_enabled")
private val QUICK_SHARE_ENABLED = booleanPreferencesKey("quick_share_enabled")
private val FILE_ACCESS_ENABLED = booleanPreferencesKey("file_access_enabled")
@@ -102,6 +101,7 @@ class DataStoreManager(private val context: Context) {
private val BLE_SYNC_ENABLED = booleanPreferencesKey("ble_sync_enabled")
private val BLE_AUTO_CONNECT_ENABLED = booleanPreferencesKey("ble_auto_connect_enabled")
+ private val NOTIFY_ON_CRASH = booleanPreferencesKey("notify_on_crash")
private const val NETWORK_DEVICES_PREFIX = "network_device_"
private const val NETWORK_CONNECTIONS_PREFIX = "network_connections_"
@@ -324,17 +324,7 @@ class DataStoreManager(private val context: Context) {
}
}
- suspend fun setSentryReportingEnabled(enabled: Boolean) {
- context.dataStore.edit { preferences ->
- preferences[SENTRY_REPORTING_ENABLED] = enabled
- }
- }
- fun getSentryReportingEnabled(): Flow {
- return context.dataStore.data.map { preferences ->
- preferences[SENTRY_REPORTING_ENABLED] ?: true // Default to enabled
- }
- }
suspend fun setQuickShareEnabled(enabled: Boolean) {
context.dataStore.edit { preferences ->
@@ -635,6 +625,18 @@ class DataStoreManager(private val context: Context) {
}
}
+ suspend fun setNotifyOnCrashEnabled(enabled: Boolean) {
+ context.dataStore.edit { preferences ->
+ preferences[NOTIFY_ON_CRASH] = enabled
+ }
+ }
+
+ fun getNotifyOnCrashEnabled(): Flow {
+ return context.dataStore.data.map { preferences ->
+ preferences[NOTIFY_ON_CRASH] != false
+ }
+ }
+
// Network-aware device connections
suspend fun saveNetworkDeviceConnection(
deviceName: String,
diff --git a/app/src/main/java/com/sameerasw/airsync/data/repository/AirSyncRepositoryImpl.kt b/app/src/main/java/com/sameerasw/airsync/data/repository/AirSyncRepositoryImpl.kt
index 397217b6..afb6a3d0 100644
--- a/app/src/main/java/com/sameerasw/airsync/data/repository/AirSyncRepositoryImpl.kt
+++ b/app/src/main/java/com/sameerasw/airsync/data/repository/AirSyncRepositoryImpl.kt
@@ -224,13 +224,7 @@ class AirSyncRepositoryImpl(
return dataStoreManager.getDefaultTab()
}
- override suspend fun setSentryReportingEnabled(enabled: Boolean) {
- dataStoreManager.setSentryReportingEnabled(enabled)
- }
- override fun getSentryReportingEnabled(): Flow {
- return dataStoreManager.getSentryReportingEnabled()
- }
override suspend fun setWidgetTransparency(alpha: Float) {
dataStoreManager.setWidgetTransparency(alpha)
@@ -303,4 +297,12 @@ class AirSyncRepositoryImpl(
override fun isFileAccessEnabled(): Flow {
return dataStoreManager.isFileAccessEnabled()
}
+
+ override suspend fun setNotifyOnCrashEnabled(enabled: Boolean) {
+ dataStoreManager.setNotifyOnCrashEnabled(enabled)
+ }
+
+ override fun getNotifyOnCrashEnabled(): Flow {
+ return dataStoreManager.getNotifyOnCrashEnabled()
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/sameerasw/airsync/domain/model/UiState.kt b/app/src/main/java/com/sameerasw/airsync/domain/model/UiState.kt
index 44bd392e..52b7d34e 100644
--- a/app/src/main/java/com/sameerasw/airsync/domain/model/UiState.kt
+++ b/app/src/main/java/com/sameerasw/airsync/domain/model/UiState.kt
@@ -46,10 +46,10 @@ data class UiState(
val isPowerSaveMode: Boolean = false,
val isPitchBlackThemeEnabled: Boolean = false,
val isBlurEnabled: Boolean = true,
- val isSentryReportingEnabled: Boolean = true,
val isOnboardingCompleted: Boolean = true,
val widgetTransparency: Float = 1f,
val isQuickShareEnabled: Boolean = false,
val isFileAccessEnabled: Boolean = true,
+ val isNotifyOnCrashEnabled: Boolean = true,
val bleConnectionState: com.sameerasw.airsync.data.ble.BleGattServer.BleConnectionState = com.sameerasw.airsync.data.ble.BleGattServer.BleConnectionState.DISCONNECTED
)
\ No newline at end of file
diff --git a/app/src/main/java/com/sameerasw/airsync/domain/repository/AirSyncRepository.kt b/app/src/main/java/com/sameerasw/airsync/domain/repository/AirSyncRepository.kt
index ab1420d3..87f23e9c 100644
--- a/app/src/main/java/com/sameerasw/airsync/domain/repository/AirSyncRepository.kt
+++ b/app/src/main/java/com/sameerasw/airsync/domain/repository/AirSyncRepository.kt
@@ -101,9 +101,7 @@ interface AirSyncRepository {
suspend fun setDefaultTab(tab: String)
fun getDefaultTab(): Flow
- // Sentry reporting settings
- suspend fun setSentryReportingEnabled(enabled: Boolean)
- fun getSentryReportingEnabled(): Flow
+
// Widget specific settings
suspend fun setWidgetTransparency(alpha: Float)
@@ -136,4 +134,7 @@ interface AirSyncRepository {
// File Access (WebDAV Server)
suspend fun setFileAccessEnabled(enabled: Boolean)
fun isFileAccessEnabled(): Flow
+
+ suspend fun setNotifyOnCrashEnabled(enabled: Boolean)
+ fun getNotifyOnCrashEnabled(): Flow
}
\ No newline at end of file
diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/SettingsView.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/SettingsView.kt
index 87782df6..dbb89ed1 100644
--- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/SettingsView.kt
+++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/SettingsView.kt
@@ -178,12 +178,12 @@ fun SettingsView(
)
IconToggleItem(
- title = androidx.compose.ui.res.stringResource(com.sameerasw.airsync.R.string.label_error_reporting),
- description = androidx.compose.ui.res.stringResource(com.sameerasw.airsync.R.string.subtitle_error_reporting),
+ title = stringResource(R.string.label_notify_on_crash),
+ description = stringResource(R.string.subtitle_notify_on_crash),
iconRes = R.drawable.rounded_bug_report_24,
- isChecked = uiState.isSentryReportingEnabled,
+ isChecked = uiState.isNotifyOnCrashEnabled,
onCheckedChange = { enabled: Boolean ->
- viewModel.setSentryReportingEnabled(enabled)
+ viewModel.setNotifyOnCrashEnabled(enabled)
}
)
}
diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/pickers/CrashReportingPicker.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/pickers/CrashReportingPicker.kt
deleted file mode 100644
index 446ed7ef..00000000
--- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/pickers/CrashReportingPicker.kt
+++ /dev/null
@@ -1,106 +0,0 @@
-package com.sameerasw.airsync.presentation.ui.components.pickers
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.material3.ButtonGroupDefaults
-import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.material3.ToggleButton
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.semantics.Role
-import androidx.compose.ui.semantics.role
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
-import com.sameerasw.airsync.R
-
-@OptIn(ExperimentalMaterial3ExpressiveApi::class)
-@Composable
-fun CrashReportingPicker(
- isReportingEnabled: Boolean,
- onReportingChanged: (Boolean) -> Unit,
- modifier: Modifier = Modifier,
- iconRes: Int = R.drawable.rounded_security_24
-) {
- val options = listOf(false, true)
- val labels = listOf(
- R.string.sentry_mode_off,
- R.string.sentry_mode_auto
- )
-
- Column(
- modifier = modifier
- .fillMaxWidth()
- .background(
- color = MaterialTheme.colorScheme.surfaceBright,
- shape = MaterialTheme.shapes.extraSmall
- )
- .padding(12.dp),
- verticalArrangement = Arrangement.spacedBy(10.dp)
- ) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(12.dp)
- ) {
- Spacer(modifier = Modifier.size(2.dp))
- Icon(
- painter = painterResource(id = iconRes),
- contentDescription = null,
- modifier = Modifier.size(24.dp),
- tint = MaterialTheme.colorScheme.primary
- )
- Spacer(modifier = Modifier.size(2.dp))
-
- Text(
- text = stringResource(R.string.sentry_report_mode_title),
- style = MaterialTheme.typography.bodyMedium,
- modifier = Modifier.weight(1f),
- color = MaterialTheme.colorScheme.onSurface
- )
- }
-
- Row(
- modifier = Modifier
- .fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween),
- ) {
- options.forEachIndexed { index, option ->
- val isChecked = isReportingEnabled == option
-
- ToggleButton(
- checked = isChecked,
- onCheckedChange = {
- onReportingChanged(option)
- },
- modifier = Modifier
- .weight(1f)
- .semantics { role = Role.RadioButton },
- shapes = when {
- index == 0 -> ButtonGroupDefaults.connectedLeadingButtonShapes()
- index == options.lastIndex -> ButtonGroupDefaults.connectedTrailingButtonShapes()
- else -> ButtonGroupDefaults.connectedMiddleButtonShapes()
- },
- ) {
- Text(
- text = stringResource(labels[index]),
- style = MaterialTheme.typography.labelLarge,
- fontWeight = if (isChecked) FontWeight.Bold else FontWeight.Normal,
- maxLines = 1
- )
- }
- }
- }
- }
-}
diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/composables/WelcomeScreen.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/composables/WelcomeScreen.kt
index d03651ed..bd6e03a4 100644
--- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/composables/WelcomeScreen.kt
+++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/composables/WelcomeScreen.kt
@@ -689,13 +689,6 @@ fun PreferencesStepContent(
onCheckedChange = { viewModel.setUseBlurEnabled(it, context) },
enabled = !isBlurProblematic
)
- IconToggleItem(
- iconRes = R.drawable.rounded_security_24,
- title = stringResource(R.string.label_error_reporting),
- description = stringResource(R.string.subtitle_error_reporting),
- isChecked = uiState.isSentryReportingEnabled,
- onCheckedChange = { viewModel.setSentryReportingEnabled(it) }
- )
}
Spacer(modifier = Modifier.height(32.dp))
diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/viewmodel/AirSyncViewModel.kt b/app/src/main/java/com/sameerasw/airsync/presentation/viewmodel/AirSyncViewModel.kt
index 66ba6ec1..ede86d00 100644
--- a/app/src/main/java/com/sameerasw/airsync/presentation/viewmodel/AirSyncViewModel.kt
+++ b/app/src/main/java/com/sameerasw/airsync/presentation/viewmodel/AirSyncViewModel.kt
@@ -211,12 +211,7 @@ class AirSyncViewModel(
}
}
- // Observe sentry reporting preference
- viewModelScope.launch {
- repository.getSentryReportingEnabled().collect { enabled ->
- _uiState.value = _uiState.value.copy(isSentryReportingEnabled = enabled)
- }
- }
+
// Observe widget transparency preference
viewModelScope.launch {
@@ -246,6 +241,13 @@ class AirSyncViewModel(
}
}
+ // Observe Notify on Crash preference
+ viewModelScope.launch {
+ repository.getNotifyOnCrashEnabled().collect { enabled ->
+ _uiState.value = _uiState.value.copy(isNotifyOnCrashEnabled = enabled)
+ }
+ }
+
// Observe BLE connection status
viewModelScope.launch {
com.sameerasw.airsync.AirSyncApp.getBleConnectionManager()?.connectionState?.collect { state ->
@@ -368,11 +370,11 @@ class AirSyncViewModel(
val isDeviceDiscoveryEnabled = repository.getDeviceDiscoveryEnabled().first()
val isBlurEnabledSetting = repository.getUseBlurEnabled().first()
val isPitchBlackThemeEnabled = repository.getPitchBlackThemeEnabled().first()
- val isSentryReportingEnabled = repository.getSentryReportingEnabled().first()
val isFirstRun = repository.getFirstRun().first()
val isPowerSaveMode = DeviceInfoUtil.isPowerSaveMode(context)
val isBlurProblematic = DeviceInfoUtil.isBlurProblematicDevice()
val isQuickShareEnabled = repository.isQuickShareEnabled().first()
+ val isNotifyOnCrashEnabled = repository.getNotifyOnCrashEnabled().first()
// Replicate Essentials logic for initial state
val isBlurEnabled = isBlurEnabledSetting && !isPowerSaveMode && !isBlurProblematic
@@ -433,9 +435,9 @@ class AirSyncViewModel(
isPowerSaveMode = isPowerSaveMode,
isPitchBlackThemeEnabled = isPitchBlackThemeEnabled,
isBlurEnabled = isBlurEnabled,
- isSentryReportingEnabled = isSentryReportingEnabled,
isOnboardingCompleted = !isFirstRun,
- isQuickShareEnabled = isQuickShareEnabled
+ isQuickShareEnabled = isQuickShareEnabled,
+ isNotifyOnCrashEnabled = isNotifyOnCrashEnabled
)
updateRatingPromptDisplay()
@@ -703,13 +705,7 @@ class AirSyncViewModel(
}
}
- fun setSentryReportingEnabled(enabled: Boolean) {
- _uiState.value = _uiState.value.copy(isSentryReportingEnabled = enabled)
- viewModelScope.launch {
- repository.setSentryReportingEnabled(enabled)
- // Note: Changes typically take effect on next launch as Sentry is initialized in Application.onCreate
- }
- }
+
fun setPitchBlackThemeEnabled(enabled: Boolean) {
_uiState.value = _uiState.value.copy(isPitchBlackThemeEnabled = enabled)
@@ -763,6 +759,13 @@ class AirSyncViewModel(
}
}
+ fun setNotifyOnCrashEnabled(enabled: Boolean) {
+ _uiState.value = _uiState.value.copy(isNotifyOnCrashEnabled = enabled)
+ viewModelScope.launch {
+ repository.setNotifyOnCrashEnabled(enabled)
+ }
+ }
+
fun manualSyncAppIcons(context: Context) {
_uiState.value = _uiState.value.copy(isIconSyncLoading = true, iconSyncMessage = "")
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 25f49035..ad2cd62e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -51,8 +51,6 @@
Disabled due to compatibility issues with this Samsung device
Pitch Black Theme
Pure black background in dark mode
- Error Reporting
- Automatically report crashes when they happen
Welcome to AirSync
@@ -75,10 +73,6 @@
How to Connect?
Back
-
- Crash reporting
- Off
- Auto
App icon credits: @Syntrop2k2 on Telegram
https://t.me/Syntrop2k2
@@ -107,4 +101,24 @@
Grant Answer Calls Access
Select apps
To be notified
+
+
+ Notify on crash
+ Send a notification if something goes wrong
+ AirSync did an oopsie!
+ Share with the developer to help improve AirSync
+ Dismiss
+ Report
+ Send Crash Report
+ Copy report
+ Copy the crash trace to clipboard
+ Save to file
+ Save log to your downloads folder
+ Email report
+ Email log to mail@sameerasw.com
+ Open GitHub issue
+ Submit an issue on our GitHub repository
+ Log saved to Downloads
+ Please attach the log from Downloads to the issue
+ Failed to save log file
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index 18318bec..ab7c9fa1 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -2,4 +2,6 @@
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.compose) apply false
+ alias(libs.plugins.android.library) apply false
+ alias(libs.plugins.kotlin.android) apply false
}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 79dd1811..c5e4dbf3 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -16,13 +16,14 @@ kotlinxCoroutines = "1.7.3"
okhttp = "4.11.0"
playReview = "2.0.2"
ksp = "2.3.5"
-sentry = "8.0.0"
protobuf = "4.28.2"
wire = "6.0.0-alpha03"
bouncycastle = "1.78.1"
ktor = "2.3.12"
+appcompat = "1.7.1"
[libraries]
+androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
@@ -52,7 +53,6 @@ okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhtt
# Google Play Review
play-review = { group = "com.google.android.play", name = "review", version.ref = "playReview" }
play-review-ktx = { group = "com.google.android.play", name = "review-ktx", version.ref = "playReview" }
-sentry-android = { group = "io.sentry", name = "sentry-android", version.ref = "sentry" }
wire-runtime = { group = "com.squareup.wire", name = "wire-runtime", version.ref = "wire" }
bouncycastle = { group = "org.bouncycastle", name = "bcprov-jdk18on", version.ref = "bouncycastle" }
@@ -64,12 +64,18 @@ ktor-server-status-pages = { group = "io.ktor", name = "ktor-server-status-pages
ktor-server-content-negotiation = { group = "io.ktor", name = "ktor-server-content-negotiation", version.ref = "ktor" }
ktor-serialization-gson = { group = "io.ktor", name = "ktor-serialization-gson", version.ref = "ktor" }
+[bundles]
+androidx = ["androidx-core-ktx", "androidx-activity-compose", "androidx-ui", "androidx-ui-graphics", "androidx-ui-tooling-preview", "androidx-material3"]
+
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
google-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
wire = { id = "com.squareup.wire", version.ref = "wire" }
+android-library = { id = "com.android.library", version.ref = "agp" }
+compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 9ff59c83..059df158 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -24,3 +24,6 @@ dependencyResolutionManagement {
rootProject.name = "AirSync"
include(":app")
+include(":exceptionreport")
+project(":exceptionreport").projectDir = File("../ExceptionReport/exceptionreport")
+