Improve error reporting from notifications

master
Koitharu 10 months ago
parent 679b1fd2f2
commit 957b12f338
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -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()

@ -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 ->

@ -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,
)
} }
} }

@ -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)

@ -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? = 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,
)
}
fun getPendingIntent(context: Context, e: Throwable): PendingIntent? = try { 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()

@ -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()

Loading…
Cancel
Save