Переглянути джерело

Queue tracking updates when offline (closes #1497)

Co-authored-by: Jays2Kings <[email protected]>
arkon 3 роки тому
батько
коміт
5ae4621da1

+ 4 - 1
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 {

+ 50 - 0
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<DelayedTrackingItem> {
+        return (preferences.all as Map<String, String>).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,
+    )
+}

+ 75 - 0
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<DatabaseHelper>()
+        val trackManager = Injekt.get<TrackManager>()
+        val delayedTrackingStore = Injekt.get<DelayedTrackingStore>()
+
+        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<DelayedTrackingUpdateJob>()
+                .setConstraints(constraints)
+                .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 20, TimeUnit.SECONDS)
+                .addTag(TAG)
+                .build()
+
+            WorkManager.getInstance(context)
+                .enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request)
+        }
+    }
+}

+ 13 - 3
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<ReaderActivity>() {
 
     /**
@@ -701,6 +705,7 @@ class ReaderPresenter(
         val chapterRead = readerChapter.chapter.chapter_number
 
         val trackManager = Injekt.get<TrackManager>()
+        val context = Injekt.get<Application>()
 
         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 {

+ 12 - 0
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)
+}