Improve error reporting from notifications

This commit is contained in:
Koitharu
2025-06-27 18:39:10 +03:00
parent 679b1fd2f2
commit 957b12f338
6 changed files with 91 additions and 32 deletions

View File

@@ -19,6 +19,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.alternatives.domain.AutoFixUseCase import org.koitharu.kotatsu.alternatives.domain.AutoFixUseCase
import org.koitharu.kotatsu.core.ErrorReporterReceiver import org.koitharu.kotatsu.core.ErrorReporterReceiver
import org.koitharu.kotatsu.core.model.getTitle import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.nav.AppRouter import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.ui.CoroutineIntentService import org.koitharu.kotatsu.core.ui.CoroutineIntentService
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
@@ -58,7 +59,7 @@ class AutoFixService : CoroutineIntentService() {
autoFixUseCase.invoke(mangaId) autoFixUseCase.invoke(mangaId)
} }
if (applicationContext.checkNotificationPermission(CHANNEL_ID)) { if (applicationContext.checkNotificationPermission(CHANNEL_ID)) {
val notification = buildNotification(result) val notification = buildNotification(startId, result)
notificationManager.notify(TAG, startId, notification) notificationManager.notify(TAG, startId, notification)
} }
} }
@@ -67,7 +68,7 @@ class AutoFixService : CoroutineIntentService() {
override fun IntentJobContext.onError(error: Throwable) { override fun IntentJobContext.onError(error: Throwable) {
if (applicationContext.checkNotificationPermission(CHANNEL_ID)) { if (applicationContext.checkNotificationPermission(CHANNEL_ID)) {
val notification = runBlocking { buildNotification(Result.failure(error)) } val notification = runBlocking { buildNotification(startId, Result.failure(error)) }
notificationManager.notify(TAG, startId, notification) notificationManager.notify(TAG, startId, notification)
} }
} }
@@ -108,7 +109,7 @@ class AutoFixService : CoroutineIntentService() {
) )
} }
private suspend fun buildNotification(result: Result<Pair<Manga, Manga?>>): Notification { private suspend fun buildNotification(startId: Int, result: Result<Pair<Manga, Manga?>>): Notification {
val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID) val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
.setPriority(NotificationCompat.PRIORITY_DEFAULT) .setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setDefaults(0) .setDefaults(0)
@@ -135,7 +136,11 @@ class AutoFixService : CoroutineIntentService() {
false, false,
), ),
).setVisibility( ).setVisibility(
if (replacement.isNsfw) NotificationCompat.VISIBILITY_SECRET else NotificationCompat.VISIBILITY_PUBLIC, if (replacement.isNsfw()) {
NotificationCompat.VISIBILITY_SECRET
} else {
NotificationCompat.VISIBILITY_PUBLIC
},
) )
notification notification
.setContentTitle(applicationContext.getString(R.string.fixed)) .setContentTitle(applicationContext.getString(R.string.fixed))
@@ -165,12 +170,13 @@ class AutoFixService : CoroutineIntentService() {
error.getDisplayMessage(applicationContext.resources) error.getDisplayMessage(applicationContext.resources)
}, },
).setSmallIcon(android.R.drawable.stat_notify_error) ).setSmallIcon(android.R.drawable.stat_notify_error)
ErrorReporterReceiver.getPendingIntent(applicationContext, error)?.let { reportIntent -> ErrorReporterReceiver.getNotificationAction(
notification.addAction( context = applicationContext,
R.drawable.ic_alert_outline, e = error,
applicationContext.getString(R.string.report), notificationId = startId,
reportIntent, notificationTag = TAG,
) )?.let { action ->
notification.addAction(action)
} }
} }
return notification.build() return notification.build()

View File

@@ -183,8 +183,10 @@ class BackupRepository @Inject constructor(
data.onStart { data.onStart {
putNextEntry(ZipEntry(section.entryName)) putNextEntry(ZipEntry(section.entryName))
write("[") write("[")
}.onCompletion { }.onCompletion { error ->
write("]") if (error == null) {
write("]")
}
closeEntry() closeEntry()
flush() flush()
}.collectIndexed { index, value -> }.collectIndexed { index, value ->

View File

@@ -84,13 +84,9 @@ abstract class BaseBackupRestoreService : CoroutineIntentService() {
.setBigText(title, message) .setBigText(title, message)
.setSmallIcon(android.R.drawable.stat_notify_error) .setSmallIcon(android.R.drawable.stat_notify_error)
result.failures.firstNotNullOfOrNull { error -> result.failures.firstNotNullOfOrNull { error ->
ErrorReporterReceiver.getPendingIntent(applicationContext, error) ErrorReporterReceiver.getNotificationAction(applicationContext, error, startId, notificationTag)
}?.let { reportIntent -> }?.let { action ->
notification.addAction( notification.addAction(action)
R.drawable.ic_alert_outline,
applicationContext.getString(R.string.report),
reportIntent,
)
} }
} }

View File

@@ -10,6 +10,7 @@ import android.widget.Toast
import androidx.annotation.CheckResult import androidx.annotation.CheckResult
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.cancelAndJoin
@@ -61,8 +62,17 @@ class BackupService : BaseBackupRestoreService() {
} else { } else {
null null
} }
ZipOutputStream(contentResolver.openOutputStream(destination)).use { output -> try {
repository.createBackup(output, progress) ZipOutputStream(contentResolver.openOutputStream(destination)).use { output ->
repository.createBackup(output, progress)
}
} catch (e: Throwable) {
try {
DocumentFile.fromSingleUri(applicationContext, destination)?.delete()
} catch (e2: Throwable) {
e.addSuppressed(e2)
}
throw e
} }
progressUpdateJob?.cancelAndJoin() progressUpdateJob?.cancelAndJoin()
contentResolver.notifyChange(destination, null) contentResolver.notifyChange(destination, null)

View File

@@ -5,9 +5,12 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.BadParcelableException import android.os.BadParcelableException
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat import androidx.core.app.PendingIntentCompat
import androidx.core.net.toUri import androidx.core.net.toUri
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.nav.AppRouter import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
@@ -17,18 +20,58 @@ class ErrorReporterReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
val e = intent?.getSerializableExtraCompat<Throwable>(AppRouter.KEY_ERROR) ?: return val e = intent?.getSerializableExtraCompat<Throwable>(AppRouter.KEY_ERROR) ?: return
val notificationId = intent.getIntExtra(EXTRA_NOTIFICATION_ID, 0)
if (notificationId != 0 && context != null) {
val notificationTag = intent.getStringExtra(EXTRA_NOTIFICATION_TAG)
NotificationManagerCompat.from(context).cancel(notificationTag, notificationId)
}
e.report() e.report()
} }
companion object { companion object {
private const val ACTION_REPORT = "${BuildConfig.APPLICATION_ID}.action.REPORT_ERROR" private const val ACTION_REPORT = "${BuildConfig.APPLICATION_ID}.action.REPORT_ERROR"
private const val EXTRA_NOTIFICATION_ID = "notify.id"
private const val EXTRA_NOTIFICATION_TAG = "notify.tag"
fun getPendingIntent(context: Context, e: Throwable): PendingIntent? = try { fun getPendingIntent(context: Context, e: Throwable): PendingIntent? = getPendingIntentInternal(
context = context,
e = e,
notificationId = 0,
notificationTag = null,
)
fun getNotificationAction(
context: Context,
e: Throwable,
notificationId: Int,
notificationTag: String?,
): NotificationCompat.Action? {
val intent = getPendingIntentInternal(
context = context,
e = e,
notificationId = notificationId,
notificationTag = notificationTag,
) ?: return null
return NotificationCompat.Action(
R.drawable.ic_alert_outline,
context.getString(R.string.report),
intent,
)
}
private fun getPendingIntentInternal(
context: Context,
e: Throwable,
notificationId: Int,
notificationTag: String?,
): PendingIntent? = try {
val intent = Intent(context, ErrorReporterReceiver::class.java) val intent = Intent(context, ErrorReporterReceiver::class.java)
intent.setAction(ACTION_REPORT) intent.setAction(ACTION_REPORT)
intent.setData("err://${e.hashCode()}".toUri()) intent.setData("err://${e.hashCode()}".toUri())
intent.putExtra(AppRouter.KEY_ERROR, e) intent.putExtra(AppRouter.KEY_ERROR, e)
intent.putExtra(EXTRA_NOTIFICATION_ID, notificationId)
intent.putExtra(EXTRA_NOTIFICATION_TAG, notificationTag)
PendingIntentCompat.getBroadcast(context, 0, intent, 0, false) PendingIntentCompat.getBroadcast(context, 0, intent, 0, false)
} catch (e: BadParcelableException) { } catch (e: BadParcelableException) {
e.printStackTraceDebug() e.printStackTraceDebug()

View File

@@ -18,6 +18,7 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ErrorReporterReceiver import org.koitharu.kotatsu.core.ErrorReporterReceiver
import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.nav.AppRouter import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.ui.CoroutineIntentService import org.koitharu.kotatsu.core.ui.CoroutineIntentService
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
@@ -57,7 +58,7 @@ class ImportService : CoroutineIntentService() {
importer.import(uri).manga importer.import(uri).manga
} }
if (applicationContext.checkNotificationPermission(CHANNEL_ID)) { if (applicationContext.checkNotificationPermission(CHANNEL_ID)) {
val notification = buildNotification(result) val notification = buildNotification(startId, result)
notificationManager.notify(TAG, startId, notification) notificationManager.notify(TAG, startId, notification)
} }
} }
@@ -65,7 +66,7 @@ class ImportService : CoroutineIntentService() {
override fun IntentJobContext.onError(error: Throwable) { override fun IntentJobContext.onError(error: Throwable) {
if (applicationContext.checkNotificationPermission(CHANNEL_ID)) { if (applicationContext.checkNotificationPermission(CHANNEL_ID)) {
val notification = runBlocking { buildNotification(Result.failure(error)) } val notification = runBlocking { buildNotification(startId, Result.failure(error)) }
notificationManager.notify(TAG, startId, notification) notificationManager.notify(TAG, startId, notification)
} }
} }
@@ -101,7 +102,7 @@ class ImportService : CoroutineIntentService() {
) )
} }
private suspend fun buildNotification(result: Result<Manga>): Notification { private suspend fun buildNotification(startId: Int, result: Result<Manga>): Notification {
val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID) val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
.setPriority(NotificationCompat.PRIORITY_DEFAULT) .setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setDefaults(0) .setDefaults(0)
@@ -127,7 +128,7 @@ class ImportService : CoroutineIntentService() {
false, false,
), ),
).setVisibility( ).setVisibility(
if (manga.isNsfw) NotificationCompat.VISIBILITY_SECRET else NotificationCompat.VISIBILITY_PUBLIC, if (manga.isNsfw()) NotificationCompat.VISIBILITY_SECRET else NotificationCompat.VISIBILITY_PUBLIC,
) )
notification.setContentTitle(applicationContext.getString(R.string.import_completed)) notification.setContentTitle(applicationContext.getString(R.string.import_completed))
.setContentText(applicationContext.getString(R.string.import_completed_hint)) .setContentText(applicationContext.getString(R.string.import_completed_hint))
@@ -138,12 +139,13 @@ class ImportService : CoroutineIntentService() {
notification.setContentTitle(applicationContext.getString(R.string.error_occurred)) notification.setContentTitle(applicationContext.getString(R.string.error_occurred))
.setContentText(error.getDisplayMessage(applicationContext.resources)) .setContentText(error.getDisplayMessage(applicationContext.resources))
.setSmallIcon(android.R.drawable.stat_notify_error) .setSmallIcon(android.R.drawable.stat_notify_error)
ErrorReporterReceiver.getPendingIntent(applicationContext, error)?.let { reportIntent -> ErrorReporterReceiver.getNotificationAction(
notification.addAction( context = applicationContext,
R.drawable.ic_alert_outline, e = error,
applicationContext.getString(R.string.report), notificationId = startId,
reportIntent, notificationTag = TAG,
) )?.let { action ->
notification.addAction(action)
} }
} }
return notification.build() return notification.build()