From 80c8344f8d21b2d89082892cb17b275739edf552 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 29 Mar 2020 13:32:25 +0300 Subject: [PATCH] Manga tracking job service --- .idea/codeStyles/Project.xml | 1 + app/src/main/AndroidManifest.xml | 5 + .../java/org/koitharu/kotatsu/KotatsuApp.kt | 2 + .../koitharu/kotatsu/core/db/FavouritesDao.kt | 4 + .../koitharu/kotatsu/core/db/HistoryDao.kt | 11 +- .../org/koitharu/kotatsu/core/db/TracksDao.kt | 3 + .../kotatsu/core/model/MangaTracking.kt | 13 ++ .../domain/history/HistoryRepository.kt | 7 +- .../domain/tracking/TrackingRepository.kt | 63 +++++++ .../kotatsu/ui/common/BaseJobService.kt | 39 +++++ .../koitharu/kotatsu/ui/common/BaseService.kt | 3 +- .../kotatsu/ui/tracker/TrackerJobService.kt | 154 ++++++++++++++++++ app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 14 files changed, 300 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/model/MangaTracking.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/domain/tracking/TrackingRepository.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/ui/common/BaseJobService.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/ui/tracker/TrackerJobService.kt diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 87bd3a768..bda8001d6 100755 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -23,6 +23,7 @@ + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 338d25c9f..20cdafacd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,6 +9,7 @@ + + + + @Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM favourites)") + abstract suspend fun findAllManga(): List + @Transaction @Query("SELECT * FROM favourites WHERE manga_id = :id GROUP BY manga_id") abstract suspend fun find(id: Long): FavouriteManga? diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/HistoryDao.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/HistoryDao.kt index 52931b071..23c006949 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/HistoryDao.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/HistoryDao.kt @@ -3,6 +3,7 @@ package org.koitharu.kotatsu.core.db import androidx.room.* import org.koitharu.kotatsu.core.db.entity.HistoryEntity import org.koitharu.kotatsu.core.db.entity.HistoryWithManga +import org.koitharu.kotatsu.core.db.entity.MangaEntity @Dao @@ -15,6 +16,9 @@ abstract class HistoryDao { @Query("SELECT * FROM history ORDER BY updated_at DESC LIMIT :limit OFFSET :offset") abstract suspend fun findAll(offset: Int, limit: Int): List + @Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM history)") + abstract suspend fun findAllManga(): List + @Query("SELECT * FROM history WHERE manga_id = :id") abstract suspend fun find(id: Long): HistoryEntity? @@ -33,10 +37,11 @@ abstract class HistoryDao { suspend fun update(entity: HistoryEntity) = update(entity.mangaId, entity.page, entity.chapterId, entity.scroll, entity.updatedAt) @Transaction - open suspend fun upsert(entity: HistoryEntity) { - if (update(entity) == 0) { + open suspend fun upsert(entity: HistoryEntity): Boolean { + return if (update(entity) == 0) { insert(entity) - } + true + } else false } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/TracksDao.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/TracksDao.kt index 5eb087dde..ae72b2c11 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/TracksDao.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/TracksDao.kt @@ -10,6 +10,9 @@ abstract class TracksDao { @Query("SELECT * FROM tracks") abstract suspend fun findAll(): List + @Query("SELECT * FROM tracks WHERE manga_id = :mangaId") + abstract suspend fun find(mangaId: Long): TrackEntity? + @Query("DELETE FROM tracks") abstract suspend fun clear() diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaTracking.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaTracking.kt new file mode 100644 index 000000000..e9a0629c9 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaTracking.kt @@ -0,0 +1,13 @@ +package org.koitharu.kotatsu.core.model + +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize +import java.util.* + +@Parcelize +data class MangaTracking ( + val manga: Manga, + val knownChaptersCount: Int, + val lastChapterId: Long, + val lastCheck: Date? +): Parcelable \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/domain/history/HistoryRepository.kt b/app/src/main/java/org/koitharu/kotatsu/domain/history/HistoryRepository.kt index fc7df11ce..0346aa5bd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/domain/history/HistoryRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/domain/history/HistoryRepository.kt @@ -10,6 +10,7 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity import org.koitharu.kotatsu.core.db.entity.TagEntity import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.MangaHistory +import org.koitharu.kotatsu.domain.tracking.TrackingRepository import java.util.* class HistoryRepository : KoinComponent { @@ -25,7 +26,7 @@ class HistoryRepository : KoinComponent { val tags = manga.tags.map(TagEntity.Companion::fromMangaTag) db.tagsDao.upsert(tags) db.mangaDao.upsert(MangaEntity.from(manga), tags) - db.historyDao.upsert( + if (db.historyDao.upsert( HistoryEntity( mangaId = manga.id, createdAt = System.currentTimeMillis(), @@ -34,7 +35,9 @@ class HistoryRepository : KoinComponent { page = page, scroll = scroll ) - ) + )) { + TrackingRepository().insertOrNothing(manga) + } notifyHistoryChanged() } diff --git a/app/src/main/java/org/koitharu/kotatsu/domain/tracking/TrackingRepository.kt b/app/src/main/java/org/koitharu/kotatsu/domain/tracking/TrackingRepository.kt new file mode 100644 index 000000000..95ca283dc --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/domain/tracking/TrackingRepository.kt @@ -0,0 +1,63 @@ +package org.koitharu.kotatsu.domain.tracking + +import org.koin.core.KoinComponent +import org.koin.core.inject +import org.koitharu.kotatsu.core.db.MangaDatabase +import org.koitharu.kotatsu.core.db.entity.TrackEntity +import org.koitharu.kotatsu.core.model.Manga +import org.koitharu.kotatsu.core.model.MangaTracking +import java.util.* + +class TrackingRepository : KoinComponent { + + private val db: MangaDatabase by inject() + + suspend fun getNewChaptersCount(mangaId: Long): Int { + val entity = db.tracksDao.find(mangaId) ?: return 0 + return entity.newChapters + } + + suspend fun getAllTracks(): List { + val favourites = db.favouritesDao.findAllManga() + val history = db.historyDao.findAllManga() + val manga = (favourites + history).distinctBy { it.id } + val tracks = db.tracksDao.findAll().groupBy { it.mangaId } + return manga.map { m -> + val track = tracks[m.id]?.singleOrNull() + MangaTracking( + manga = m.toManga(), + knownChaptersCount = track?.totalChapters ?: -1, + lastChapterId = track?.lastChapterId ?: 0, + lastCheck = track?.lastCheck?.takeUnless { it == 0L }?.let(::Date) + ) + } + } + + suspend fun storeTrackResult( + mangaId: Long, + knownChaptersCount: Int, + lastChapterId: Long, + newChapters: Int + ) { + val entity = TrackEntity( + mangaId = mangaId, + newChapters = newChapters, + lastCheck = System.currentTimeMillis(), + lastChapterId = lastChapterId, + totalChapters = knownChaptersCount + ) + db.tracksDao.upsert(entity) + } + + suspend fun insertOrNothing(manga: Manga) { + val chapters = manga.chapters ?: return + val entity = TrackEntity( + mangaId = manga.id, + totalChapters = chapters.size, + lastChapterId = chapters.lastOrNull()?.id ?: 0L, + newChapters = 0, + lastCheck = System.currentTimeMillis() + ) + db.tracksDao.insert(entity) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/common/BaseJobService.kt b/app/src/main/java/org/koitharu/kotatsu/ui/common/BaseJobService.kt new file mode 100644 index 000000000..3be358a6e --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/common/BaseJobService.kt @@ -0,0 +1,39 @@ +package org.koitharu.kotatsu.ui.common + +import android.app.job.JobParameters +import android.app.job.JobService +import android.util.SparseArray +import androidx.annotation.CallSuper +import androidx.core.util.set +import kotlinx.coroutines.* + +abstract class BaseJobService : JobService() { + + private val jobServiceScope = object : CoroutineScope { + override val coroutineContext = Dispatchers.Main + SupervisorJob() + } + private val jobs = SparseArray(2) + + @CallSuper + override fun onStartJob(params: JobParameters): Boolean { + jobs[params.jobId] = jobServiceScope.launch { + val isSuccess = try { + doWork(params) + true + } catch (_: Throwable) { + false + } + jobFinished(params, !isSuccess) + } + return true + } + + @CallSuper + override fun onStopJob(params: JobParameters): Boolean { + val job = jobs[params.jobId] ?: return false + return !job.isCompleted + } + + @Throws(Throwable::class) + protected abstract suspend fun doWork(params: JobParameters) +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/common/BaseService.kt b/app/src/main/java/org/koitharu/kotatsu/ui/common/BaseService.kt index 68959b7fa..6d9c39a74 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/common/BaseService.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/common/BaseService.kt @@ -7,10 +7,9 @@ import androidx.annotation.CallSuper import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob -import org.koin.core.KoinComponent import kotlin.coroutines.CoroutineContext -abstract class BaseService : Service(), KoinComponent, CoroutineScope { +abstract class BaseService : Service(), CoroutineScope { private val job = SupervisorJob() diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/tracker/TrackerJobService.kt b/app/src/main/java/org/koitharu/kotatsu/ui/tracker/TrackerJobService.kt new file mode 100644 index 000000000..cdff3d53d --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/tracker/TrackerJobService.kt @@ -0,0 +1,154 @@ +package org.koitharu.kotatsu.ui.tracker + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.job.JobInfo +import android.app.job.JobParameters +import android.app.job.JobScheduler +import android.content.ComponentName +import android.content.Context +import android.os.Build +import androidx.annotation.MainThread +import androidx.annotation.RequiresApi +import androidx.core.content.ContextCompat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.model.Manga +import org.koitharu.kotatsu.core.model.MangaChapter +import org.koitharu.kotatsu.domain.MangaProviderFactory +import org.koitharu.kotatsu.domain.tracking.TrackingRepository +import org.koitharu.kotatsu.ui.common.BaseJobService +import org.koitharu.kotatsu.utils.ext.safe +import java.util.concurrent.TimeUnit + +class TrackerJobService : BaseJobService() { + + private lateinit var repo: TrackingRepository + + override fun onCreate() { + super.onCreate() + repo = TrackingRepository() + } + + override suspend fun doWork(params: JobParameters) { + withContext(Dispatchers.IO) { + val tracks = repo.getAllTracks() + if (tracks.isEmpty()) { + return@withContext + } + var success = 0 + for (track in tracks) { + val details = safe { + MangaProviderFactory.create(track.manga.source) + .getDetails(track.manga) + } + val chapters = details?.chapters ?: continue + when { + track.knownChaptersCount == -1 -> { //first check + repo.storeTrackResult( + mangaId = track.manga.id, + knownChaptersCount = chapters.size, + lastChapterId = chapters.lastOrNull()?.id ?: 0L, + newChapters = 0 + ) + } + track.knownChaptersCount == 0 && track.lastChapterId == 0L -> { //manga was empty on last check + repo.storeTrackResult( + mangaId = track.manga.id, + knownChaptersCount = track.knownChaptersCount, + lastChapterId = 0L, + newChapters = chapters.size + ) + //TODO notify + } + 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 + repo.storeTrackResult( + mangaId = track.manga.id, + knownChaptersCount = chapters.size, + lastChapterId = chapters.lastOrNull()?.id ?: 0L, + newChapters = 0 + ) + } else { + repo.storeTrackResult( + mangaId = track.manga.id, + knownChaptersCount = knownChapter + 1, + lastChapterId = track.lastChapterId, + newChapters = chapters.size - knownChapter + 1 + ) + //TODO notify + } + } + } + else -> { + repo.storeTrackResult( + mangaId = track.manga.id, + knownChaptersCount = track.knownChaptersCount, + lastChapterId = track.lastChapterId, + newChapters = chapters.size - track.knownChaptersCount + ) + //TODO notify + } + } + success++ + } + if (success == 0) { + throw RuntimeException("Cannot check any manga updates") + } + } + } + + @MainThread + private fun showNotification(manga: Manga, newChapters: List) { + //TODO + } + + companion object { + + private const val JOB_ID = 7 + private const val CHANNEL_ID = "tracking" + + @RequiresApi(Build.VERSION_CODES.O) + private fun createNotificationChannel(context: Context) { + val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + if (manager.getNotificationChannel(CHANNEL_ID) == null) { + val channel = NotificationChannel(CHANNEL_ID, + context.getString(R.string.new_chapters), NotificationManager.IMPORTANCE_DEFAULT) + channel.setShowBadge(true) + channel.lightColor = ContextCompat.getColor(context, R.color.blue_primary) + manager.createNotificationChannel(channel) + } + } + + fun setup(context: Context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createNotificationChannel(context) + } + val scheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler + // if (scheduler.allPendingJobs != null) { + // return + // } + val jobInfo = JobInfo.Builder(JOB_ID, ComponentName(context, TrackerJobService::class.java)) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + jobInfo.setRequiredNetworkType(JobInfo.NETWORK_TYPE_NOT_ROAMING) + } else { + jobInfo.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + jobInfo.setRequiresBatteryNotLow(true) + } + jobInfo.setRequiresDeviceIdle(true) + jobInfo.setPersisted(true) + jobInfo.setPeriodic(TimeUnit.HOURS.toMillis(6)) + scheduler.schedule(jobInfo.build()) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 241763581..c5775d26d 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -113,4 +113,5 @@ Сохранить мангу Уведомления Включено %1$d из %2$d + Новые главы \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4ea2d4002..d35cd0b47 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -114,4 +114,5 @@ Save manga Notifications Enabled %1$d from %2$d + New chapters \ No newline at end of file