diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt index 7d6e9099e..3983912cc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt @@ -9,6 +9,7 @@ import androidx.core.net.toUri import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.library.LibraryUpdateJob +import eu.kanade.tachiyomi.data.sync.SyncDataJob import eu.kanade.tachiyomi.data.updater.AppUpdateDownloadJob import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.reader.ReaderActivity @@ -70,6 +71,8 @@ class NotificationReceiver : BroadcastReceiver() { "application/x-protobuf+gzip", ) ACTION_CANCEL_RESTORE -> cancelRestore(context) + + ACTION_CANCEL_SYNC -> cancelSync(context) // Cancel library update and dismiss notification ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context) // Start downloading app update @@ -187,6 +190,15 @@ class NotificationReceiver : BroadcastReceiver() { AppUpdateDownloadJob.stop(context) } + /** + * Method called when user wants to stop a backup restore job. + * + * @param context context of application + */ + private fun cancelSync(context: Context) { + SyncDataJob.stop(context) + } + /** * Method called when user wants to mark manga chapters as read * @@ -239,6 +251,8 @@ class NotificationReceiver : BroadcastReceiver() { private const val ACTION_CANCEL_RESTORE = "$ID.$NAME.CANCEL_RESTORE" + private const val ACTION_CANCEL_SYNC = "$ID.$NAME.CANCEL_SYNC" + private const val ACTION_CANCEL_LIBRARY_UPDATE = "$ID.$NAME.CANCEL_LIBRARY_UPDATE" private const val ACTION_START_APP_UPDATE = "$ID.$NAME.ACTION_START_APP_UPDATE" @@ -615,5 +629,25 @@ class NotificationReceiver : BroadcastReceiver() { PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, ) } + + /** + * Returns [PendingIntent] that cancels a sync restore job. + * + * @param context context of application + * @param notificationId id of notification + * @return [PendingIntent] + */ + internal fun cancelSyncPendingBroadcast(context: Context, notificationId: Int): PendingIntent { + val intent = Intent(context, NotificationReceiver::class.java).apply { + action = ACTION_CANCEL_SYNC + putExtra(EXTRA_NOTIFICATION_ID, notificationId) + } + return PendingIntent.getBroadcast( + context, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncDataJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncDataJob.kt new file mode 100644 index 000000000..7680eb0a8 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncDataJob.kt @@ -0,0 +1,103 @@ +package eu.kanade.tachiyomi.data.sync + +import android.content.Context +import android.content.pm.ServiceInfo +import android.os.Build +import androidx.work.CoroutineWorker +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.ExistingWorkPolicy +import androidx.work.ForegroundInfo +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkerParameters +import eu.kanade.domain.sync.SyncPreferences +import eu.kanade.tachiyomi.data.notification.Notifications +import eu.kanade.tachiyomi.util.system.cancelNotification +import eu.kanade.tachiyomi.util.system.isRunning +import eu.kanade.tachiyomi.util.system.workManager +import logcat.LogPriority +import tachiyomi.core.util.system.logcat +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.util.concurrent.TimeUnit + +class SyncDataJob(private val context: Context, workerParams: WorkerParameters) : + CoroutineWorker(context, workerParams) { + + private val notifier = SyncNotifier(context) + + override suspend fun doWork(): Result { + try { + setForeground(getForegroundInfo()) + } catch (e: IllegalStateException) { + logcat(LogPriority.ERROR, e) { "Not allowed to run on foreground service" } + } + + return try { + //TODO: Uncomment this when the rest of sync PR is merged. +// SyncManager(context).syncData() + Result.success() + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + notifier.showSyncError(e.message) + Result.failure() + } finally { + context.cancelNotification(Notifications.ID_RESTORE_PROGRESS) + } + } + + override suspend fun getForegroundInfo(): ForegroundInfo { + return ForegroundInfo( + Notifications.ID_RESTORE_PROGRESS, + notifier.showSyncProgress().build(), + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC + } else { + 0 + }, + ) + } + + companion object { + private const val TAG_JOB = "SyncDataJob" + private const val TAG_AUTO = "$TAG_JOB:auto" + const val TAG_MANUAL = "$TAG_JOB:manual" + + private val jobTagList = listOf(TAG_AUTO, TAG_MANUAL) + + fun isAnyJobRunning(context: Context): Boolean { + return jobTagList.any { context.workManager.isRunning(it) } + } + + fun setupTask(context: Context, prefInterval: Int? = null) { + val syncPreferences = Injekt.get() + val interval = prefInterval ?: syncPreferences.syncInterval().get() + + if (interval > 0) { + val request = PeriodicWorkRequestBuilder( + interval.toLong(), + TimeUnit.MINUTES, + 10, + TimeUnit.MINUTES, + ) + .addTag(TAG_AUTO) + .build() + + context.workManager.enqueueUniquePeriodicWork(TAG_AUTO, ExistingPeriodicWorkPolicy.UPDATE, request) + } else { + context.workManager.cancelUniqueWork(TAG_AUTO) + } + } + + fun startNow(context: Context) { + val request = OneTimeWorkRequestBuilder() + .addTag(TAG_MANUAL) + .build() + context.workManager.enqueueUniqueWork(TAG_MANUAL, ExistingWorkPolicy.KEEP, request) + } + + fun stop(context: Context) { + context.workManager.cancelUniqueWork(TAG_MANUAL) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncNotifier.kt new file mode 100644 index 000000000..695597af5 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncNotifier.kt @@ -0,0 +1,86 @@ +package eu.kanade.tachiyomi.data.sync + +import android.content.Context +import android.graphics.BitmapFactory +import androidx.core.app.NotificationCompat +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.core.security.SecurityPreferences +import eu.kanade.tachiyomi.data.notification.NotificationReceiver +import eu.kanade.tachiyomi.data.notification.Notifications +import eu.kanade.tachiyomi.util.system.cancelNotification +import eu.kanade.tachiyomi.util.system.notificationBuilder +import eu.kanade.tachiyomi.util.system.notify +import uy.kohesive.injekt.injectLazy + +class SyncNotifier(private val context: Context) { + + private val preferences: SecurityPreferences by injectLazy() + + private val progressNotificationBuilder = context.notificationBuilder( + Notifications.CHANNEL_BACKUP_RESTORE_PROGRESS, + ) { + setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)) + setSmallIcon(R.drawable.ic_mihon) + setAutoCancel(false) + setOngoing(true) + setOnlyAlertOnce(true) + } + + private val completeNotificationBuilder = context.notificationBuilder( + Notifications.CHANNEL_BACKUP_RESTORE_PROGRESS, + ) { + setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)) + setSmallIcon(R.drawable.ic_mihon) + setAutoCancel(false) + } + + private fun NotificationCompat.Builder.show(id: Int) { + context.notify(id, build()) + } + + fun showSyncProgress(content: String = "", progress: Int = 0, maxAmount: Int = 100): NotificationCompat.Builder { + val builder = with(progressNotificationBuilder) { + setContentTitle(context.getString(R.string.syncing_library)) + + if (!preferences.hideNotificationContent().get()) { + setContentText(content) + } + + setProgress(maxAmount, progress, true) + setOnlyAlertOnce(true) + + clearActions() + addAction( + R.drawable.ic_close_24dp, + context.getString(R.string.action_cancel), + NotificationReceiver.cancelSyncPendingBroadcast(context, Notifications.ID_RESTORE_PROGRESS), + ) + } + + builder.show(Notifications.ID_RESTORE_PROGRESS) + + return builder + } + + fun showSyncError(error: String?) { + context.cancelNotification(Notifications.ID_RESTORE_PROGRESS) + + with(completeNotificationBuilder) { + setContentTitle(context.getString(R.string.sync_error)) + setContentText(error) + + show(Notifications.ID_RESTORE_COMPLETE) + } + } + + fun showSyncSuccess(message: String?) { + context.cancelNotification(Notifications.ID_RESTORE_PROGRESS) + + with(completeNotificationBuilder) { + setContentTitle(context.getString(R.string.sync_complete)) + setContentText(message) + + show(Notifications.ID_RESTORE_COMPLETE) + } + } +}