Tracking manga updates
This commit is contained in:
@@ -1,12 +1,19 @@
|
|||||||
package org.koitharu.kotatsu.domain
|
package org.koitharu.kotatsu.domain
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
|
import android.media.ThumbnailUtils
|
||||||
import android.util.Size
|
import android.util.Size
|
||||||
|
import androidx.annotation.Px
|
||||||
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
|
import coil.Coil
|
||||||
|
import coil.api.get
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import org.koin.core.KoinComponent
|
import org.koin.core.KoinComponent
|
||||||
import org.koin.core.get
|
import org.koin.core.get
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
|
import org.koitharu.kotatsu.core.model.Manga
|
||||||
import org.koitharu.kotatsu.core.model.MangaPage
|
import org.koitharu.kotatsu.core.model.MangaPage
|
||||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||||
import org.koitharu.kotatsu.utils.ext.await
|
import org.koitharu.kotatsu.utils.ext.await
|
||||||
@@ -54,4 +61,23 @@ object MangaUtils : KoinComponent {
|
|||||||
check(imageHeight > 0 && imageWidth > 0)
|
check(imageHeight > 0 && imageWidth > 0)
|
||||||
return Size(imageWidth, imageHeight)
|
return Size(imageWidth, imageHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getMangaIcon(manga: Manga, @Px width: Int, @Px height: Int): Bitmap? {
|
||||||
|
try {
|
||||||
|
val bmp = Coil.loader().get(manga.coverUrl) {
|
||||||
|
size(width, height)
|
||||||
|
}.toBitmap()
|
||||||
|
return ThumbnailUtils.extractThumbnail(
|
||||||
|
bmp,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
ThumbnailUtils.OPTIONS_RECYCLE_INPUT
|
||||||
|
)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,10 +5,10 @@ import android.app.NotificationChannel
|
|||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.drawable.BitmapDrawable
|
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.model.Manga
|
import org.koitharu.kotatsu.core.model.Manga
|
||||||
import org.koitharu.kotatsu.ui.details.MangaDetailsActivity
|
import org.koitharu.kotatsu.ui.details.MangaDetailsActivity
|
||||||
@@ -73,7 +73,7 @@ class DownloadNotification(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setLargeIcon(icon: Drawable?) {
|
fun setLargeIcon(icon: Drawable?) {
|
||||||
builder.setLargeIcon((icon as? BitmapDrawable)?.bitmap)
|
builder.setLargeIcon(icon?.toBitmap())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setProgress(chaptersTotal: Int, pagesTotal: Int, chapter: Int, page: Int) {
|
fun setProgress(chaptersTotal: Int, pagesTotal: Int, chapter: Int, page: Int) {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.os.WorkSource
|
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
@@ -14,7 +13,7 @@ import kotlinx.coroutines.*
|
|||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import org.koin.core.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.local.PagesCache
|
import org.koitharu.kotatsu.core.local.PagesCache
|
||||||
|
|||||||
@@ -2,15 +2,19 @@ package org.koitharu.kotatsu.ui.tracker
|
|||||||
|
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.app.job.JobInfo
|
import android.app.job.JobInfo
|
||||||
import android.app.job.JobParameters
|
import android.app.job.JobParameters
|
||||||
import android.app.job.JobScheduler
|
import android.app.job.JobScheduler
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.annotation.MainThread
|
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
|
import coil.Coil
|
||||||
|
import coil.api.get
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
@@ -19,20 +23,19 @@ import org.koitharu.kotatsu.core.model.MangaChapter
|
|||||||
import org.koitharu.kotatsu.domain.MangaProviderFactory
|
import org.koitharu.kotatsu.domain.MangaProviderFactory
|
||||||
import org.koitharu.kotatsu.domain.tracking.TrackingRepository
|
import org.koitharu.kotatsu.domain.tracking.TrackingRepository
|
||||||
import org.koitharu.kotatsu.ui.common.BaseJobService
|
import org.koitharu.kotatsu.ui.common.BaseJobService
|
||||||
|
import org.koitharu.kotatsu.ui.details.MangaDetailsActivity
|
||||||
import org.koitharu.kotatsu.utils.ext.safe
|
import org.koitharu.kotatsu.utils.ext.safe
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class TrackerJobService : BaseJobService() {
|
class TrackerJobService : BaseJobService() {
|
||||||
|
|
||||||
private lateinit var repo: TrackingRepository
|
private val notificationManager by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
|
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
repo = TrackingRepository()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun doWork(params: JobParameters) {
|
override suspend fun doWork(params: JobParameters) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
val repo = TrackingRepository()
|
||||||
val tracks = repo.getAllTracks()
|
val tracks = repo.getAllTracks()
|
||||||
if (tracks.isEmpty()) {
|
if (tracks.isEmpty()) {
|
||||||
return@withContext
|
return@withContext
|
||||||
@@ -53,14 +56,16 @@ class TrackerJobService : BaseJobService() {
|
|||||||
newChapters = 0
|
newChapters = 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
track.knownChaptersCount == 0 && track.lastChapterId == 0L -> { //manga was empty on last check
|
track.knownChaptersCount == 0 && track.lastChapterId == 0L -> { //manga was empty on last check
|
||||||
repo.storeTrackResult(
|
repo.storeTrackResult(
|
||||||
mangaId = track.manga.id,
|
mangaId = track.manga.id,
|
||||||
knownChaptersCount = track.knownChaptersCount,
|
knownChaptersCount = track.knownChaptersCount,
|
||||||
lastChapterId = 0L,
|
lastChapterId = 0L,
|
||||||
newChapters = chapters.size
|
newChapters = chapters.size
|
||||||
)
|
)
|
||||||
//TODO notify
|
if (chapters.isNotEmpty()) {
|
||||||
|
showNotification(track.manga, chapters)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
chapters.size == track.knownChaptersCount -> {
|
chapters.size == track.knownChaptersCount -> {
|
||||||
if (chapters.lastOrNull()?.id == track.lastChapterId) {
|
if (chapters.lastOrNull()?.id == track.lastChapterId) {
|
||||||
@@ -78,24 +83,30 @@ class TrackerJobService : BaseJobService() {
|
|||||||
newChapters = 0
|
newChapters = 0
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
val newChapters = chapters.size - knownChapter + 1
|
||||||
repo.storeTrackResult(
|
repo.storeTrackResult(
|
||||||
mangaId = track.manga.id,
|
mangaId = track.manga.id,
|
||||||
knownChaptersCount = knownChapter + 1,
|
knownChaptersCount = knownChapter + 1,
|
||||||
lastChapterId = track.lastChapterId,
|
lastChapterId = track.lastChapterId,
|
||||||
newChapters = chapters.size - knownChapter + 1
|
newChapters = newChapters
|
||||||
)
|
)
|
||||||
//TODO notify
|
if (newChapters != 0) {
|
||||||
|
showNotification(track.manga, chapters.takeLast(newChapters))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
|
val newChapters = chapters.size - track.knownChaptersCount
|
||||||
repo.storeTrackResult(
|
repo.storeTrackResult(
|
||||||
mangaId = track.manga.id,
|
mangaId = track.manga.id,
|
||||||
knownChaptersCount = track.knownChaptersCount,
|
knownChaptersCount = track.knownChaptersCount,
|
||||||
lastChapterId = track.lastChapterId,
|
lastChapterId = track.lastChapterId,
|
||||||
newChapters = chapters.size - track.knownChaptersCount
|
newChapters = newChapters
|
||||||
)
|
)
|
||||||
//TODO notify
|
if (newChapters != 0) {
|
||||||
|
showNotification(track.manga, chapters.takeLast(newChapters))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
success++
|
success++
|
||||||
@@ -106,24 +117,55 @@ class TrackerJobService : BaseJobService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainThread
|
private suspend fun showNotification(manga: Manga, newChapters: List<MangaChapter>) {
|
||||||
private fun showNotification(manga: Manga, newChapters: List<MangaChapter>) {
|
val id = manga.url.hashCode()
|
||||||
//TODO
|
val colorPrimary = ContextCompat.getColor(this@TrackerJobService, R.color.blue_primary)
|
||||||
|
val builder = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||||
|
with(builder) {
|
||||||
|
setContentText(resources.getQuantityString(R.plurals.new_chapters,
|
||||||
|
newChapters.size, newChapters.size))
|
||||||
|
setContentText(manga.title)
|
||||||
|
setNumber(newChapters.size)
|
||||||
|
setLargeIcon(safe {
|
||||||
|
Coil.loader().get(manga.coverUrl).toBitmap()
|
||||||
|
})
|
||||||
|
setSmallIcon(R.drawable.ic_stat_book_plus)
|
||||||
|
val style = NotificationCompat.InboxStyle(this)
|
||||||
|
for (chapter in newChapters) {
|
||||||
|
style.addLine(chapter.name)
|
||||||
|
}
|
||||||
|
style.setSummaryText(manga.title)
|
||||||
|
setStyle(style)
|
||||||
|
val intent = MangaDetailsActivity.newIntent(this@TrackerJobService, manga)
|
||||||
|
setContentIntent(PendingIntent.getActivity(this@TrackerJobService, id,
|
||||||
|
intent, PendingIntent.FLAG_UPDATE_CURRENT))
|
||||||
|
setAutoCancel(true)
|
||||||
|
color = colorPrimary
|
||||||
|
setLights(colorPrimary, 1000, 5000)
|
||||||
|
setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
notificationManager.notify(TAG, id, builder.build())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val JOB_ID = 7
|
private const val JOB_ID = 7
|
||||||
private const val CHANNEL_ID = "tracking"
|
private const val CHANNEL_ID = "tracking"
|
||||||
|
private const val TAG = "tracking"
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
private fun createNotificationChannel(context: Context) {
|
private fun createNotificationChannel(context: Context) {
|
||||||
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
val manager =
|
||||||
|
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
if (manager.getNotificationChannel(CHANNEL_ID) == null) {
|
if (manager.getNotificationChannel(CHANNEL_ID) == null) {
|
||||||
val channel = NotificationChannel(CHANNEL_ID,
|
val channel = NotificationChannel(CHANNEL_ID,
|
||||||
context.getString(R.string.new_chapters), NotificationManager.IMPORTANCE_DEFAULT)
|
context.getString(R.string.new_chapters),
|
||||||
|
NotificationManager.IMPORTANCE_DEFAULT)
|
||||||
channel.setShowBadge(true)
|
channel.setShowBadge(true)
|
||||||
channel.lightColor = ContextCompat.getColor(context, R.color.blue_primary)
|
channel.lightColor = ContextCompat.getColor(context, R.color.blue_primary)
|
||||||
|
channel.enableLights(true)
|
||||||
manager.createNotificationChannel(channel)
|
manager.createNotificationChannel(channel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,7 +178,8 @@ class TrackerJobService : BaseJobService() {
|
|||||||
// if (scheduler.allPendingJobs != null) {
|
// if (scheduler.allPendingJobs != null) {
|
||||||
// return
|
// return
|
||||||
// }
|
// }
|
||||||
val jobInfo = JobInfo.Builder(JOB_ID, ComponentName(context, TrackerJobService::class.java))
|
val jobInfo =
|
||||||
|
JobInfo.Builder(JOB_ID, ComponentName(context, TrackerJobService::class.java))
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
jobInfo.setRequiredNetworkType(JobInfo.NETWORK_TYPE_NOT_ROAMING)
|
jobInfo.setRequiredNetworkType(JobInfo.NETWORK_TYPE_NOT_ROAMING)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
17
app/src/main/res/drawable-anydpi-v24/ic_stat_book_plus.xml
Normal file
17
app/src/main/res/drawable-anydpi-v24/ic_stat_book_plus.xml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<vector
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:tint="#FFFFFF"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<group
|
||||||
|
android:scaleX="0.92"
|
||||||
|
android:scaleY="0.92"
|
||||||
|
android:translateX="0.96"
|
||||||
|
android:translateY="0.96">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M18,22H6A2,2 0,0 1,4 20V4C4,2.89 4.9,2 6,2H7V9L9.5,7.5L12,9V2H18A2,2 0,0 1,20 4V20A2,2 0,0 1,18 22M14,20H16V18H18V16H16V14H14V16H12V18H14V20Z" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
BIN
app/src/main/res/drawable-hdpi/ic_stat_book_plus.png
Normal file
BIN
app/src/main/res/drawable-hdpi/ic_stat_book_plus.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 336 B |
BIN
app/src/main/res/drawable-mdpi/ic_stat_book_plus.png
Normal file
BIN
app/src/main/res/drawable-mdpi/ic_stat_book_plus.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 289 B |
BIN
app/src/main/res/drawable-xhdpi/ic_stat_book_plus.png
Normal file
BIN
app/src/main/res/drawable-xhdpi/ic_stat_book_plus.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 480 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_stat_book_plus.png
Normal file
BIN
app/src/main/res/drawable-xxhdpi/ic_stat_book_plus.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 629 B |
BIN
app/src/main/res/drawable-xxxhdpi/ic_stat_book_plus.png
Normal file
BIN
app/src/main/res/drawable-xxxhdpi/ic_stat_book_plus.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 914 B |
@@ -10,4 +10,9 @@
|
|||||||
<item quantity="few">%1$d элемента</item>
|
<item quantity="few">%1$d элемента</item>
|
||||||
<item quantity="many">%1$d элементов</item>
|
<item quantity="many">%1$d элементов</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<plurals name="new_chapters">
|
||||||
|
<item quantity="one">%1$d новая глава</item>
|
||||||
|
<item quantity="few">%1$d новых главы</item>
|
||||||
|
<item quantity="many">%1$d новых глав</item>
|
||||||
|
</plurals>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -8,4 +8,8 @@
|
|||||||
<item quantity="one">%1$d item</item>
|
<item quantity="one">%1$d item</item>
|
||||||
<item quantity="other">%1$d items</item>
|
<item quantity="other">%1$d items</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<plurals name="new_chapters">
|
||||||
|
<item quantity="one">%1$d new chapter</item>
|
||||||
|
<item quantity="other">%1$d new chapters</item>
|
||||||
|
</plurals>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user