Backup restorng fixes

This commit is contained in:
Koitharu
2025-06-14 15:04:10 +03:00
parent b9d4c070eb
commit 437e6809bf
4 changed files with 85 additions and 78 deletions

View File

@@ -20,7 +20,7 @@ class BookmarkBackup(
@SerialName("chapter_id") val chapterId: Long, @SerialName("chapter_id") val chapterId: Long,
@SerialName("page") val page: Int, @SerialName("page") val page: Int,
@SerialName("scroll") val scroll: Int, @SerialName("scroll") val scroll: Int,
@SerialName("image") val imageUrl: String, @SerialName("image_url") val imageUrl: String,
@SerialName("created_at") val createdAt: Long, @SerialName("created_at") val createdAt: Long,
@SerialName("percent") val percent: Float, @SerialName("percent") val percent: Float,
) { ) {

View File

@@ -1,20 +1,25 @@
package org.koitharu.kotatsu.backups.ui package org.koitharu.kotatsu.backups.ui
import android.app.Notification import android.net.Uri
import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat import androidx.core.app.PendingIntentCompat
import androidx.core.app.ShareCompat
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.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.CompositeResult
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.getFileDisplayName
import androidx.appcompat.R as appcompatR
abstract class BaseBackupRestoreService : CoroutineIntentService() { abstract class BaseBackupRestoreService : CoroutineIntentService() {
protected abstract val notificationTag: String protected abstract val notificationTag: String
protected abstract val isRestoreService: Boolean
protected lateinit var notificationManager: NotificationManagerCompat protected lateinit var notificationManager: NotificationManagerCompat
private set private set
@@ -26,10 +31,7 @@ abstract class BaseBackupRestoreService : CoroutineIntentService() {
} }
override fun IntentJobContext.onError(error: Throwable) { override fun IntentJobContext.onError(error: Throwable) {
if (applicationContext.checkNotificationPermission(CHANNEL_ID)) { showResultNotification(null, CompositeResult.failure(error))
val notification = createErrorNotification(error)
notificationManager.notify(notificationTag, startId, notification)
}
} }
private fun createNotificationChannel() { private fun createNotificationChannel() {
@@ -43,33 +45,93 @@ abstract class BaseBackupRestoreService : CoroutineIntentService() {
notificationManager.createNotificationChannel(channel) notificationManager.createNotificationChannel(channel)
} }
protected fun createErrorNotification(error: Throwable): Notification { protected fun IntentJobContext.showResultNotification(
fileUri: Uri?,
result: CompositeResult,
) {
if (!applicationContext.checkNotificationPermission(CHANNEL_ID)) {
return
}
val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID) val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
.setPriority(NotificationCompat.PRIORITY_HIGH) .setPriority(NotificationCompat.PRIORITY_HIGH)
.setDefaults(0) .setDefaults(0)
.setSilent(true) .setSilent(true)
.setAutoCancel(true) .setAutoCancel(true)
.setContentText(error.getDisplayMessage(resources)) .setSubText(fileUri?.let { contentResolver.getFileDisplayName(it) })
.setSmallIcon(android.R.drawable.stat_notify_error) when {
ErrorReporterReceiver.getPendingIntent(applicationContext, error)?.let { reportIntent -> result.isAllSuccess -> {
notification.addAction( if (isRestoreService) {
R.drawable.ic_alert_outline, notification
applicationContext.getString(R.string.report), .setContentTitle(getString(R.string.restoring_backup))
reportIntent, .setContentText(getString(R.string.data_restored_success))
) } else {
notification
.setContentTitle(getString(R.string.backup_saved))
.setContentText(fileUri?.let { contentResolver.getFileDisplayName(it) })
.setSubText(null)
}
notification.setSmallIcon(R.drawable.ic_stat_done)
}
result.isAllFailed || !isRestoreService -> {
val title = getString(if (isRestoreService) R.string.data_not_restored else R.string.error_occurred)
val message = result.failures.joinToString("\n") {
it.getDisplayMessage(applicationContext.resources)
}
notification
.setContentText(if (isRestoreService) getString(R.string.data_not_restored_text) else message)
.setBigText(title, message)
.setSmallIcon(android.R.drawable.stat_notify_error)
result.failures.firstNotNullOfOrNull { error ->
ErrorReporterReceiver.getPendingIntent(applicationContext, error)
}?.let { reportIntent ->
notification.addAction(
R.drawable.ic_alert_outline,
applicationContext.getString(R.string.report),
reportIntent,
)
}
}
else -> {
notification
.setContentTitle(getString(R.string.restoring_backup))
.setContentText(getString(R.string.data_restored_with_errors))
.setSmallIcon(R.drawable.ic_stat_done)
}
} }
notification.setContentIntent( notification.setContentIntent(
PendingIntentCompat.getActivity( PendingIntentCompat.getActivity(
applicationContext, applicationContext,
0, 0,
AppRouter.homeIntent(this), AppRouter.homeIntent(this@BaseBackupRestoreService),
0, 0,
false, false,
), ),
) )
return notification.build() if (!isRestoreService && fileUri != null) {
val shareIntent = ShareCompat.IntentBuilder(this@BaseBackupRestoreService)
.setStream(fileUri)
.setType("application/zip")
.setChooserTitle(R.string.share_backup)
.createChooserIntent()
notification.addAction(
appcompatR.drawable.abc_ic_menu_share_mtrl_alpha,
getString(R.string.share),
PendingIntentCompat.getActivity(this@BaseBackupRestoreService, 0, shareIntent, 0, false),
)
}
notificationManager.notify(notificationTag, startId, notification.build())
} }
protected fun NotificationCompat.Builder.setBigText(title: String, text: CharSequence) = setStyle(
NotificationCompat.BigTextStyle()
.bigText(text)
.setSummaryText(text)
.setBigContentTitle(title),
)
protected companion object { protected companion object {
const val CHANNEL_ID = "backup_restore" const val CHANNEL_ID = "backup_restore"

View File

@@ -8,8 +8,6 @@ import android.content.pm.ServiceInfo
import android.net.Uri import android.net.Uri
import androidx.annotation.CheckResult import androidx.annotation.CheckResult
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.PendingIntentCompat
import androidx.core.app.ShareCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.cancelAndJoin
@@ -19,6 +17,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.backups.data.BackupRepository import org.koitharu.kotatsu.backups.data.BackupRepository
import org.koitharu.kotatsu.backups.ui.BaseBackupRestoreService import org.koitharu.kotatsu.backups.ui.BaseBackupRestoreService
import org.koitharu.kotatsu.core.nav.AppRouter import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.util.CompositeResult
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
import org.koitharu.kotatsu.core.util.ext.powerManager import org.koitharu.kotatsu.core.util.ext.powerManager
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
@@ -35,6 +34,7 @@ import androidx.appcompat.R as appcompatR
class BackupService : BaseBackupRestoreService() { class BackupService : BaseBackupRestoreService() {
override val notificationTag = TAG override val notificationTag = TAG
override val isRestoreService = false
@Inject @Inject
lateinit var repository: BackupRepository lateinit var repository: BackupRepository
@@ -63,9 +63,7 @@ class BackupService : BaseBackupRestoreService() {
} }
progressUpdateJob?.cancelAndJoin() progressUpdateJob?.cancelAndJoin()
contentResolver.notifyChange(destination, null) contentResolver.notifyChange(destination, null)
if (checkNotificationPermission(CHANNEL_ID)) { showResultNotification(destination, CompositeResult.success())
notificationManager.notify(notificationTag, startId, createResultNotification(destination))
}
} }
} }
@@ -98,31 +96,6 @@ class BackupService : BaseBackupRestoreService() {
).build() ).build()
} }
private fun createResultNotification(uri: Uri): Notification {
val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setDefaults(0)
.setSilent(true)
.setAutoCancel(true)
.setContentText(getString(R.string.backup_saved))
.setSmallIcon(R.drawable.ic_stat_done)
val shareIntent = ShareCompat.IntentBuilder(this)
.setStream(uri)
.setType(contentResolver.getType(uri) ?: "application/zip")
.setChooserTitle(R.string.share_backup)
.createChooserIntent()
notification.setContentIntent(
PendingIntentCompat.getActivity(
applicationContext,
0,
shareIntent,
0,
false,
),
)
return notification.build()
}
companion object { companion object {
private const val TAG = "BACKUP" private const val TAG = "BACKUP"

View File

@@ -8,8 +8,6 @@ import android.content.pm.ServiceInfo
import android.net.Uri import android.net.Uri
import androidx.annotation.CheckResult import androidx.annotation.CheckResult
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.PendingIntentCompat
import androidx.core.app.ShareCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.cancelAndJoin
@@ -37,6 +35,7 @@ import androidx.appcompat.R as appcompatR
class RestoreService : BaseBackupRestoreService() { class RestoreService : BaseBackupRestoreService() {
override val notificationTag = TAG override val notificationTag = TAG
override val isRestoreService = true
@Inject @Inject
lateinit var repository: BackupRepository lateinit var repository: BackupRepository
@@ -62,13 +61,11 @@ class RestoreService : BaseBackupRestoreService() {
} else { } else {
null null
} }
ZipInputStream(contentResolver.openInputStream(source)).use { input -> val result = ZipInputStream(contentResolver.openInputStream(source)).use { input ->
repository.restoreBackup(input, sections, progress) repository.restoreBackup(input, sections, progress)
} }
progressUpdateJob?.cancelAndJoin() progressUpdateJob?.cancelAndJoin()
if (checkNotificationPermission(CHANNEL_ID)) { showResultNotification(source, result)
notificationManager.notify(notificationTag, startId, createResultNotification(source))
}
} }
} }
@@ -101,31 +98,6 @@ class RestoreService : BaseBackupRestoreService() {
).build() ).build()
} }
private fun createResultNotification(uri: Uri): Notification {
val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setDefaults(0)
.setSilent(true)
.setAutoCancel(true)
.setContentText(getString(R.string.backup_saved))
.setSmallIcon(R.drawable.ic_stat_done)
val shareIntent = ShareCompat.IntentBuilder(this)
.setStream(uri)
.setType(contentResolver.getType(uri) ?: "application/zip")
.setChooserTitle(R.string.share_backup)
.createChooserIntent()
notification.setContentIntent(
PendingIntentCompat.getActivity(
applicationContext,
0,
shareIntent,
0,
false,
),
)
return notification.build()
}
companion object { companion object {
private const val TAG = "RESTORE" private const val TAG = "RESTORE"