From 5ae4621da10710acb5a966f6570e1603d0d3ca3f Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 4 Sep 2021 16:37:35 -0400 Subject: [PATCH] Queue tracking updates when offline (closes #1497) Co-authored-by: Jays2Kings --- .../java/eu/kanade/tachiyomi/AppModule.kt | 5 +- .../data/track/job/DelayedTrackingStore.kt | 50 +++++++++++++ .../track/job/DelayedTrackingUpdateJob.kt | 75 +++++++++++++++++++ .../tachiyomi/ui/reader/ReaderPresenter.kt | 16 +++- .../util/system/ContextExtensions.kt | 12 +++ 5 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/job/DelayedTrackingStore.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/job/DelayedTrackingUpdateJob.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt b/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt index 25b8d69877..053d4c333b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt @@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.job.DelayedTrackingStore import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.source.SourceManager @@ -23,6 +24,8 @@ class AppModule(val app: Application) : InjektModule { override fun InjektRegistrar.registerInjectables() { addSingleton(app) + addSingletonFactory { Json { ignoreUnknownKeys = true } } + addSingletonFactory { PreferencesHelper(app) } addSingletonFactory { DatabaseHelper(app) } @@ -41,7 +44,7 @@ class AppModule(val app: Application) : InjektModule { addSingletonFactory { TrackManager(app) } - addSingletonFactory { Json { ignoreUnknownKeys = true } } + addSingletonFactory { DelayedTrackingStore(app) } // Asynchronously init expensive components for a faster cold start ContextCompat.getMainExecutor(app).execute { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/job/DelayedTrackingStore.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/job/DelayedTrackingStore.kt new file mode 100644 index 0000000000..f4aa4d94db --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/job/DelayedTrackingStore.kt @@ -0,0 +1,50 @@ +package eu.kanade.tachiyomi.data.track.job + +import android.content.Context +import androidx.core.content.edit +import eu.kanade.tachiyomi.data.database.models.Track +import timber.log.Timber + +class DelayedTrackingStore(context: Context) { + + /** + * Preference file where queued tracking updates are stored. + */ + private val preferences = context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE) + + fun addItem(track: Track) { + val trackId = track.id.toString() + val (_, lastChapterRead) = preferences.getString(trackId, "0:0.0")!!.split(":") + if (track.last_chapter_read > lastChapterRead.toFloat()) { + val value = "${track.manga_id}:${track.last_chapter_read}" + Timber.i("Queuing track item: $trackId, $value") + preferences.edit { + putString(trackId, value) + } + } + } + + fun clear() { + preferences.edit { + clear() + } + } + + fun getItems(): List { + return (preferences.all as Map).entries + .map { + val (mangaId, lastChapterRead) = it.value.split(":") + DelayedTrackingItem( + trackId = it.key.toLong(), + mangaId = mangaId.toLong(), + lastChapterRead = lastChapterRead.toFloat(), + ) + } + } + + data class DelayedTrackingItem( + val trackId: Long, + val mangaId: Long, + val lastChapterRead: Float, + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/job/DelayedTrackingUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/job/DelayedTrackingUpdateJob.kt new file mode 100644 index 0000000000..2a10743b00 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/job/DelayedTrackingUpdateJob.kt @@ -0,0 +1,75 @@ +package eu.kanade.tachiyomi.data.track.job + +import android.content.Context +import androidx.work.BackoffPolicy +import androidx.work.Constraints +import androidx.work.CoroutineWorker +import androidx.work.ExistingWorkPolicy +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.track.TrackManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import timber.log.Timber +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.util.concurrent.TimeUnit + +class DelayedTrackingUpdateJob(context: Context, workerParams: WorkerParameters) : + CoroutineWorker(context, workerParams) { + + override suspend fun doWork(): Result { + val db = Injekt.get() + val trackManager = Injekt.get() + val delayedTrackingStore = Injekt.get() + + withContext(Dispatchers.IO) { + val tracks = delayedTrackingStore.getItems().mapNotNull { + val manga = db.getManga(it.mangaId).executeAsBlocking() ?: return@withContext + db.getTracks(manga).executeAsBlocking() + .find { track -> track.id == it.trackId } + ?.also { track -> + track.last_chapter_read = it.lastChapterRead + } + } + + tracks.forEach { track -> + try { + val service = trackManager.getService(track.sync_id) + if (service != null && service.isLogged) { + service.update(track, true) + db.insertTrack(track).executeAsBlocking() + } + } catch (e: Exception) { + Timber.e(e) + } + } + + delayedTrackingStore.clear() + } + + return Result.success() + } + + companion object { + private const val TAG = "DelayedTrackingUpdate" + + fun setupTask(context: Context) { + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + val request = OneTimeWorkRequestBuilder() + .setConstraints(constraints) + .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 20, TimeUnit.SECONDS) + .addTag(TAG) + .build() + + WorkManager.getInstance(context) + .enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt index 26355daa4d..91f1c673c2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt @@ -11,6 +11,8 @@ import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.job.DelayedTrackingStore +import eu.kanade.tachiyomi.data.track.job.DelayedTrackingUpdateJob import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.Page @@ -31,6 +33,7 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.getPicturesDir import eu.kanade.tachiyomi.util.storage.getTempShareDir import eu.kanade.tachiyomi.util.system.ImageUtil +import eu.kanade.tachiyomi.util.system.isOnline import eu.kanade.tachiyomi.util.updateCoverLastModified import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -53,7 +56,8 @@ class ReaderPresenter( private val sourceManager: SourceManager = Injekt.get(), private val downloadManager: DownloadManager = Injekt.get(), private val coverCache: CoverCache = Injekt.get(), - private val preferences: PreferencesHelper = Injekt.get() + private val preferences: PreferencesHelper = Injekt.get(), + private val delayedTrackingStore: DelayedTrackingStore = Injekt.get(), ) : BasePresenter() { /** @@ -701,6 +705,7 @@ class ReaderPresenter( val chapterRead = readerChapter.chapter.chapter_number val trackManager = Injekt.get() + val context = Injekt.get() launchIO { db.getTracks(manga).executeAsBlocking() @@ -713,8 +718,13 @@ class ReaderPresenter( // for a while. The view can still be garbage collected. async { runCatching { - service.update(track, true) - db.insertTrack(track).executeAsBlocking() + if (context.isOnline()) { + service.update(track, true) + db.insertTrack(track).executeAsBlocking() + } else { + delayedTrackingStore.addItem(track) + DelayedTrackingUpdateJob.setupTask(context) + } } } } else { diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt index 84787a21d3..40dc9116ee 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt @@ -15,6 +15,7 @@ import android.content.res.Configuration import android.content.res.Resources import android.graphics.Color import android.net.ConnectivityManager +import android.net.NetworkCapabilities import android.net.Uri import android.net.wifi.WifiManager import android.os.Build @@ -365,3 +366,14 @@ fun Context.createReaderThemeContext(): Context { } return this } + +fun Context.isOnline(): Boolean { + val networkCapabilities = connectivityManager.activeNetwork ?: return false + val actNw = connectivityManager.getNetworkCapabilities(networkCapabilities) ?: return false + val maxTransport = when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 -> NetworkCapabilities.TRANSPORT_LOWPAN + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> NetworkCapabilities.TRANSPORT_WIFI_AWARE + else -> NetworkCapabilities.TRANSPORT_VPN + } + return (NetworkCapabilities.TRANSPORT_CELLULAR..maxTransport).any(actNw::hasTransport) +}