Move tracker logic into own class

This commit is contained in:
Koitharu
2022-06-15 13:35:24 +03:00
parent 30c0fd600f
commit 3edfd0892a
16 changed files with 254 additions and 163 deletions

View File

@@ -67,19 +67,19 @@ android {
afterEvaluate {
compileDebugKotlin {
kotlinOptions {
freeCompilerArgs += ['-opt-in=kotlin.RequiresOptIn']
freeCompilerArgs += ['-opt-in=org.koitharu.kotatsu.parsers.InternalParsersApi']
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
implementation('com.github.nv95:kotatsu-parsers:ab87a50e9b') {
implementation('com.github.nv95:kotatsu-parsers:0ed35a4b21') {
exclude group: 'org.json', module: 'json'
}
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.2'
implementation 'androidx.core:core-ktx:1.8.0-rc02'
implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.activity:activity-ktx:1.5.0-rc01'
implementation 'androidx.fragment:fragment-ktx:1.5.0-rc01'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0-rc01'

View File

@@ -1,16 +1,14 @@
package org.koitharu.kotatsu.core.parser
import java.util.*
import org.koitharu.kotatsu.parsers.InternalParsersApi
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import java.util.*
/**
* This parser is just for parser development, it should not be used in releases
*/
@OptIn(InternalParsersApi::class)
class DummyParser(override val context: MangaLoaderContext) : MangaParser(MangaSource.DUMMY) {
override val configKeyDomain: ConfigKey.Domain

View File

@@ -21,7 +21,7 @@ interface TrackLogsDao {
suspend fun removeAll(mangaId: Long)
@Query("DELETE FROM track_logs WHERE manga_id NOT IN (SELECT manga_id FROM tracks)")
suspend fun cleanup()
suspend fun gc()
@Query("SELECT COUNT(*) FROM track_logs")
suspend fun count(): Int

View File

@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.core.db.dao
import androidx.room.*
import org.koitharu.kotatsu.core.db.entity.TrackEntity
@Dao
abstract class TracksDao {
@@ -32,7 +31,7 @@ abstract class TracksDao {
abstract suspend fun delete(mangaId: Long)
@Query("DELETE FROM tracks WHERE manga_id NOT IN (SELECT manga_id FROM history UNION SELECT manga_id FROM favourites)")
abstract suspend fun cleanup()
abstract suspend fun gc()
@Transaction
open suspend fun upsert(entity: TrackEntity) {

View File

@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.core.exceptions
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
class CompositeException(val errors: Collection<Throwable>) : Exception(
message = errors.mapNotNullToSet { it.message }.joinToString()
)
class CompositeException(val errors: Collection<Throwable>) : Exception() {
override val message: String = errors.mapNotNullToSet { it.message }.joinToString()
}

View File

@@ -1,14 +1,41 @@
package org.koitharu.kotatsu.core.model
import android.os.Parcelable
import java.util.*
import kotlinx.parcelize.Parcelize
import org.koitharu.kotatsu.parsers.model.Manga
data class MangaTracking(
class MangaTracking(
val manga: Manga,
val knownChaptersCount: Int,
val lastChapterId: Long,
val lastNotifiedChapterId: Long,
val lastCheck: Date?
)
val lastCheck: Date?,
) {
fun isEmpty(): Boolean {
return knownChaptersCount <= 0 || lastChapterId == 0L
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MangaTracking
if (manga != other.manga) return false
if (knownChaptersCount != other.knownChaptersCount) return false
if (lastChapterId != other.lastChapterId) return false
if (lastNotifiedChapterId != other.lastNotifiedChapterId) return false
if (lastCheck != other.lastCheck) return false
return true
}
override fun hashCode(): Int {
var result = manga.hashCode()
result = 31 * result + knownChaptersCount
result = 31 * result + lastChapterId.hashCode()
result = 31 * result + lastNotifiedChapterId.hashCode()
result = 31 * result + (lastCheck?.hashCode() ?: 0)
return result
}
}

View File

@@ -1,11 +1,11 @@
package org.koitharu.kotatsu.core.parser
import java.lang.ref.WeakReference
import java.util.*
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.*
import java.lang.ref.WeakReference
import java.util.*
interface MangaRepository {
@@ -13,7 +13,7 @@ interface MangaRepository {
val sortOrders: Set<SortOrder>
suspend fun getList(offset: Int, query: String?): List<Manga>
suspend fun getList(offset: Int, query: String): List<Manga>
suspend fun getList(offset: Int, tags: Set<MangaTag>?, sortOrder: SortOrder?): List<Manga>

View File

@@ -20,7 +20,7 @@ class RemoteMangaRepository(private val parser: MangaParser) : MangaRepository {
getConfig().defaultSortOrder = value
}
override suspend fun getList(offset: Int, query: String?): List<Manga> {
override suspend fun getList(offset: Int, query: String): List<Manga> {
return parser.getList(offset, query)
}

View File

@@ -7,6 +7,12 @@ import androidx.annotation.WorkerThread
import androidx.collection.ArraySet
import androidx.core.net.toFile
import androidx.core.net.toUri
import java.io.File
import java.io.IOException
import java.util.*
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.*
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
import org.koitharu.kotatsu.core.parser.MangaRepository
@@ -22,12 +28,6 @@ import org.koitharu.kotatsu.utils.ext.deleteAwait
import org.koitharu.kotatsu.utils.ext.longHashCode
import org.koitharu.kotatsu.utils.ext.readText
import org.koitharu.kotatsu.utils.ext.resolveName
import java.io.File
import java.io.IOException
import java.util.*
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import kotlin.coroutines.CoroutineContext
private const val MAX_PARALLELISM = 4
@@ -37,12 +37,12 @@ class LocalMangaRepository(private val storageManager: LocalStorageManager) : Ma
private val filenameFilter = CbzFilter()
private val locks = CompositeMutex<Long>()
override suspend fun getList(offset: Int, query: String?): List<Manga> {
override suspend fun getList(offset: Int, query: String): List<Manga> {
if (offset > 0) {
return emptyList()
}
val list = getRawList()
if (!query.isNullOrEmpty()) {
if (query.isNotEmpty()) {
list.retainAll { x ->
x.title.contains(query, ignoreCase = true) ||
x.altTitle?.contains(query, ignoreCase = true) == true

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.tracker
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
import org.koitharu.kotatsu.tracker.domain.Tracker
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.tracker.ui.FeedViewModel
import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels
@@ -13,5 +14,7 @@ val trackerModule
factory { TrackingRepository(get()) }
factory { TrackerNotificationChannels(androidContext(), get()) }
factory { Tracker(get()) }
viewModel { FeedViewModel(get()) }
}

View File

@@ -0,0 +1,132 @@
package org.koitharu.kotatsu.tracker.domain
import org.koitharu.kotatsu.core.model.MangaTracking
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates
class Tracker(
private val repository: TrackingRepository,
) {
suspend fun fetchUpdates(track: MangaTracking, commit: Boolean): MangaUpdates {
val repo = MangaRepository(track.manga.source)
val details = repo.getDetails(track.manga)
val chapters = details.chapters.orEmpty()
if (track.isEmpty()) {
// first check or manga was empty on last check
if (commit) {
repository.storeTrackResult(
mangaId = track.manga.id,
knownChaptersCount = chapters.size,
lastChapterId = chapters.lastOrNull()?.id ?: 0L,
previousTrackChapterId = 0L,
newChapters = emptyList(),
saveTrackLog = false,
)
}
return MangaUpdates(
manga = details,
newChapters = emptyList(),
)
}
val newChapters = details.getNewChapters(track.lastChapterId)
if (newChapters.isEmpty()) {
if (commit) {
repository.storeTrackResult(
mangaId = track.manga.id,
knownChaptersCount = chapters.size,
lastChapterId = chapters.lastOrNull()?.id ?: 0L,
previousTrackChapterId = 0L,
newChapters = emptyList(),
saveTrackLog = false,
)
}
return MangaUpdates(
manga = details,
newChapters = emptyList(),
)
}
return when {
// the same chapters count
chapters.size == track.knownChaptersCount -> {
if (chapters.lastOrNull()?.id == track.lastChapterId) {
// manga was not updated. skip
MangaUpdates(
manga = details,
newChapters = emptyList(),
)
} else {
// number of chapters still the same, bu last chapter changed.
// maybe some chapters are removed. we need to find last known chapter
val knownChapter = chapters.indexOfLast { it.id == track.lastChapterId }
if (knownChapter == -1) {
// confuse. reset anything
if (commit) {
repository.storeTrackResult(
mangaId = track.manga.id,
knownChaptersCount = chapters.size,
lastChapterId = chapters.lastOrNull()?.id ?: 0L,
previousTrackChapterId = 0L,
newChapters = emptyList(),
saveTrackLog = false,
)
}
MangaUpdates(
manga = details,
newChapters = emptyList(),
)
} else {
val newChapters = chapters.takeLast(chapters.size - knownChapter + 1)
if (commit) {
repository.storeTrackResult(
mangaId = track.manga.id,
knownChaptersCount = knownChapter + 1,
lastChapterId = track.lastChapterId,
previousTrackChapterId = track.lastNotifiedChapterId,
newChapters = newChapters,
saveTrackLog = true,
)
}
MangaUpdates(
manga = details,
newChapters = details.getNewChapters(track.lastNotifiedChapterId),
)
}
}
}
else -> {
val newChapters = chapters.takeLast(chapters.size - track.knownChaptersCount)
if (commit) {
repository.storeTrackResult(
mangaId = track.manga.id,
knownChaptersCount = track.knownChaptersCount,
lastChapterId = track.lastChapterId,
previousTrackChapterId = track.lastNotifiedChapterId,
newChapters = newChapters,
saveTrackLog = true,
)
}
MangaUpdates(
manga = details,
newChapters = details.getNewChapters(track.lastNotifiedChapterId),
)
}
}
}
private fun Manga.getNewChapters(lastChapterId: Long): List<MangaChapter> {
val chapters = chapters ?: return emptyList()
if (lastChapterId == 0L) {
return emptyList()
}
val raw = chapters.takeLastWhile { x -> x.id != lastChapterId }
return if (raw.isEmpty() || raw.size == chapters.size) {
emptyList()
} else {
raw
}
}
}

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.tracker.domain
import androidx.room.withTransaction
import java.util.*
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.*
import org.koitharu.kotatsu.core.model.FavouriteCategory
@@ -11,7 +12,6 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapToSet
import java.util.*
class TrackingRepository(
private val db: MangaDatabase,
@@ -28,7 +28,8 @@ class TrackingRepository(
suspend fun getFavouritesManga(): Map<FavouriteCategory, List<Manga>> {
val categories = db.favouriteCategoriesDao.findAll()
return categories.associateTo(LinkedHashMap(categories.size)) { categoryEntity ->
categoryEntity.toFavouriteCategory() to db.favouritesDao.findAllManga(categoryEntity.categoryId).toMangaList()
categoryEntity.toFavouriteCategory() to db.favouritesDao.findAllManga(categoryEntity.categoryId)
.toMangaList()
}
}
@@ -68,10 +69,10 @@ class TrackingRepository(
suspend fun clearLogs() = db.trackLogsDao.clear()
suspend fun cleanup() {
suspend fun gc() {
db.withTransaction {
db.tracksDao.cleanup()
db.trackLogsDao.cleanup()
db.tracksDao.gc()
db.trackLogsDao.gc()
}
}

View File

@@ -0,0 +1,9 @@
package org.koitharu.kotatsu.tracker.domain.model
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
class MangaUpdates(
val manga: Manga,
val newChapters: List<MangaChapter>,
)

View File

@@ -14,35 +14,38 @@ import androidx.lifecycle.map
import androidx.work.*
import coil.ImageLoader
import coil.request.ImageRequest
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.tracker.domain.Tracker
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.PendingIntentCompat
import org.koitharu.kotatsu.utils.ext.referer
import org.koitharu.kotatsu.utils.ext.toBitmapOrNull
import org.koitharu.kotatsu.utils.ext.trySetForeground
import org.koitharu.kotatsu.utils.progress.Progress
import java.util.concurrent.TimeUnit
class TrackWorker(context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams), KoinComponent {
CoroutineWorker(context, workerParams),
KoinComponent {
private val notificationManager by lazy {
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
}
private val coil by inject<ImageLoader>()
private val repository by inject<TrackingRepository>()
private val settings by inject<AppSettings>()
private val channels by inject<TrackerNotificationChannels>()
private val tracker by inject<Tracker>()
override suspend fun doWork(): Result {
if (!settings.isTrackerEnabled) {
@@ -54,84 +57,25 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
val tracks = getAllTracks()
var success = 0
val workData = Data.Builder()
.putInt(DATA_TOTAL, tracks.size)
val workData = Data.Builder().putInt(DATA_TOTAL, tracks.size)
for ((index, item) in tracks.withIndex()) {
val (track, channelId) = item
val details = runCatching {
MangaRepository(track.manga.source).getDetails(track.manga)
val updates = runCatching {
tracker.fetchUpdates(track, commit = true)
}.onSuccess {
success++
}.getOrNull()
workData.putInt(DATA_PROGRESS, index)
setProgress(workData.build())
val chapters = details?.chapters ?: continue
when {
// first check or manga was empty on last check
track.knownChaptersCount <= 0 || track.lastChapterId == 0L -> {
repository.storeTrackResult(
mangaId = track.manga.id,
knownChaptersCount = chapters.size,
lastChapterId = chapters.lastOrNull()?.id ?: 0L,
previousTrackChapterId = 0L,
newChapters = emptyList(),
saveTrackLog = false,
)
}
// the same chapters count
chapters.size == track.knownChaptersCount -> {
if (chapters.lastOrNull()?.id == track.lastChapterId) {
// manga was not updated. skip
} else {
// number of chapters still the same, bu last chapter changed.
// maybe some chapters are removed. we need to find last known chapter
val knownChapter = chapters.indexOfLast { it.id == track.lastChapterId }
if (knownChapter == -1) {
// confuse. reset anything
repository.storeTrackResult(
mangaId = track.manga.id,
knownChaptersCount = chapters.size,
lastChapterId = chapters.lastOrNull()?.id ?: 0L,
previousTrackChapterId = 0L,
newChapters = emptyList(),
saveTrackLog = false,
)
} else {
val newChapters = chapters.takeLast(chapters.size - knownChapter + 1)
repository.storeTrackResult(
mangaId = track.manga.id,
knownChaptersCount = knownChapter + 1,
lastChapterId = track.lastChapterId,
previousTrackChapterId = track.lastNotifiedChapterId,
newChapters = newChapters,
saveTrackLog = true,
)
showNotification(
details,
channelId,
newChapters.takeLastWhile { x -> x.id != track.lastNotifiedChapterId },
)
}
}
}
else -> {
val newChapters = chapters.takeLast(chapters.size - track.knownChaptersCount)
repository.storeTrackResult(
mangaId = track.manga.id,
knownChaptersCount = track.knownChaptersCount,
lastChapterId = track.lastChapterId,
previousTrackChapterId = track.lastNotifiedChapterId,
newChapters = newChapters,
saveTrackLog = true,
)
showNotification(
manga = track.manga,
channelId = channelId,
newChapters = newChapters.takeLastWhile { x -> x.id != track.lastNotifiedChapterId },
)
}
if (updates != null && updates.newChapters.isNotEmpty()) {
showNotification(
manga = updates.manga,
channelId = channelId,
newChapters = updates.newChapters,
)
}
success++
}
repository.cleanup()
repository.gc()
return if (success == 0) {
Result.retry()
} else {
@@ -194,8 +138,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
val colorPrimary = ContextCompat.getColor(applicationContext, R.color.blue_primary)
val builder = NotificationCompat.Builder(applicationContext, channelId)
val summary = applicationContext.resources.getQuantityString(
R.plurals.new_chapters,
newChapters.size, newChapters.size
R.plurals.new_chapters, newChapters.size, newChapters.size
)
with(builder) {
setContentText(summary)
@@ -203,10 +146,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
setNumber(newChapters.size)
setLargeIcon(
coil.execute(
ImageRequest.Builder(applicationContext)
.data(manga.coverUrl)
.referer(manga.publicUrl)
.build()
ImageRequest.Builder(applicationContext).data(manga.coverUrl).referer(manga.publicUrl).build()
).toBitmapOrNull()
)
setSmallIcon(R.drawable.ic_stat_book_plus)
@@ -220,8 +160,10 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
val intent = DetailsActivity.newIntent(applicationContext, manga)
setContentIntent(
PendingIntent.getActivity(
applicationContext, id,
intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
applicationContext,
id,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
)
)
setAutoCancel(true)
@@ -251,9 +193,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
val title = applicationContext.getString(R.string.check_for_new_chapters)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
WORKER_CHANNEL_ID,
title,
NotificationManager.IMPORTANCE_LOW
WORKER_CHANNEL_ID, title, NotificationManager.IMPORTANCE_LOW
)
channel.setShowBadge(false)
channel.enableVibration(false)
@@ -262,17 +202,11 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
notificationManager.createNotificationChannel(channel)
}
val notification = NotificationCompat.Builder(applicationContext, WORKER_CHANNEL_ID)
.setContentTitle(title)
.setPriority(NotificationCompat.PRIORITY_MIN)
.setDefaults(0)
.setColor(ContextCompat.getColor(applicationContext, R.color.blue_primary_dark))
.setSilent(true)
.setProgress(0, 0, true)
.setSmallIcon(android.R.drawable.stat_notify_sync)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_DEFERRED)
.setOngoing(true)
.build()
val notification = NotificationCompat.Builder(applicationContext, WORKER_CHANNEL_ID).setContentTitle(title)
.setPriority(NotificationCompat.PRIORITY_MIN).setDefaults(0)
.setColor(ContextCompat.getColor(applicationContext, R.color.blue_primary_dark)).setSilent(true)
.setProgress(0, 0, true).setSmallIcon(android.R.drawable.stat_notify_sync)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_DEFERRED).setOngoing(true).build()
return ForegroundInfo(WORKER_NOTIFICATION_ID, notification)
}
@@ -287,44 +221,31 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
private const val TAG_ONESHOT = "tracking_oneshot"
fun setup(context: Context) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val request = PeriodicWorkRequestBuilder<TrackWorker>(4, TimeUnit.HOURS)
.setConstraints(constraints)
.addTag(TAG)
.setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES)
.build()
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.KEEP, request)
val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
val request =
PeriodicWorkRequestBuilder<TrackWorker>(4, TimeUnit.HOURS).setConstraints(constraints).addTag(TAG)
.setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES).build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.KEEP, request)
}
fun startNow(context: Context) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val request = OneTimeWorkRequestBuilder<TrackWorker>()
.setConstraints(constraints)
.addTag(TAG_ONESHOT)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()
WorkManager.getInstance(context)
.enqueue(request)
val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
val request = OneTimeWorkRequestBuilder<TrackWorker>().setConstraints(constraints).addTag(TAG_ONESHOT)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST).build()
WorkManager.getInstance(context).enqueue(request)
}
fun getProgressLiveData(context: Context): LiveData<Progress?> {
return WorkManager.getInstance(context)
.getWorkInfosByTagLiveData(TAG)
.map { list ->
list.find { work ->
work.state == WorkInfo.State.RUNNING
}?.let { workInfo ->
Progress(
value = workInfo.progress.getInt(DATA_PROGRESS, 0),
total = workInfo.progress.getInt(DATA_TOTAL, -1)
).takeUnless { it.isIndeterminate }
}
return WorkManager.getInstance(context).getWorkInfosByTagLiveData(TAG).map { list ->
list.find { work ->
work.state == WorkInfo.State.RUNNING
}?.let { workInfo ->
Progress(
value = workInfo.progress.getInt(DATA_PROGRESS, 0),
total = workInfo.progress.getInt(DATA_TOTAL, -1)
).takeUnless { it.isIndeterminate }
}
}
}
}
}