Group tracker notifications
This commit is contained in:
@@ -21,13 +21,11 @@ import org.koitharu.kotatsu.favourites.data.toMangaList
|
||||
import org.koitharu.kotatsu.favourites.domain.model.Cover
|
||||
import org.koitharu.kotatsu.list.domain.ListSortOrder
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels
|
||||
import javax.inject.Inject
|
||||
|
||||
@Reusable
|
||||
class FavouritesRepository @Inject constructor(
|
||||
private val db: MangaDatabase,
|
||||
private val channels: TrackerNotificationChannels,
|
||||
) {
|
||||
|
||||
suspend fun getAllManga(): List<Manga> {
|
||||
@@ -145,7 +143,6 @@ class FavouritesRepository @Inject constructor(
|
||||
)
|
||||
val id = db.getFavouriteCategoriesDao().insert(entity)
|
||||
val category = entity.toFavouriteCategory(id)
|
||||
channels.createChannel(category)
|
||||
return category
|
||||
}
|
||||
|
||||
@@ -174,10 +171,6 @@ class FavouritesRepository @Inject constructor(
|
||||
db.getFavouriteCategoriesDao().delete(id)
|
||||
}
|
||||
}
|
||||
// run after transaction success
|
||||
for (id in ids) {
|
||||
channels.deleteChannel(id)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setCategoryOrder(id: Long, order: ListSortOrder) {
|
||||
|
||||
@@ -21,7 +21,7 @@ import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.settings.tracker.categories.TrackerCategoriesConfigSheet
|
||||
import org.koitharu.kotatsu.settings.utils.DozeHelper
|
||||
import org.koitharu.kotatsu.settings.utils.MultiSummaryProvider
|
||||
import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels
|
||||
import org.koitharu.kotatsu.tracker.work.TrackerNotificationHelper
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -33,7 +33,7 @@ class TrackerSettingsFragment :
|
||||
private val dozeHelper = DozeHelper(this)
|
||||
|
||||
@Inject
|
||||
lateinit var channels: TrackerNotificationChannels
|
||||
lateinit var notificationHelper: TrackerNotificationHelper
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_tracker)
|
||||
@@ -88,7 +88,7 @@ class TrackerSettingsFragment :
|
||||
true
|
||||
}
|
||||
|
||||
channels.areNotificationsDisabled -> {
|
||||
!notificationHelper.getAreNotificationsEnabled() -> {
|
||||
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
.setData(Uri.fromParts("package", requireContext().packageName, null))
|
||||
startActivitySafe(intent)
|
||||
@@ -116,8 +116,7 @@ class TrackerSettingsFragment :
|
||||
val pref = findPreference<Preference>(AppSettings.KEY_NOTIFICATIONS_SETTINGS) ?: return
|
||||
pref.setSummary(
|
||||
when {
|
||||
channels.areNotificationsDisabled -> R.string.disabled
|
||||
channels.isNotificationGroupEnabled() -> R.string.show_notification_new_chapters_on
|
||||
notificationHelper.getAreNotificationsEnabled() -> R.string.show_notification_new_chapters_on
|
||||
else -> R.string.show_notification_new_chapters_off
|
||||
},
|
||||
)
|
||||
|
||||
@@ -15,8 +15,6 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.tracker.domain.model.MangaTracking
|
||||
import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates
|
||||
import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels
|
||||
import org.koitharu.kotatsu.tracker.work.TrackingItem
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
import kotlin.contracts.InvocationKind
|
||||
@@ -28,28 +26,12 @@ class Tracker @Inject constructor(
|
||||
private val repository: TrackingRepository,
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val favouritesRepository: FavouritesRepository,
|
||||
private val channels: TrackerNotificationChannels,
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
) {
|
||||
|
||||
suspend fun getTracks(limit: Int): List<TrackingItem> {
|
||||
suspend fun getTracks(limit: Int): List<MangaTracking> {
|
||||
repository.updateTracks()
|
||||
return repository.getTracks(offset = 0, limit = limit).map {
|
||||
val categoryId = repository.getCategoryId(it.manga.id)
|
||||
TrackingItem(
|
||||
tracking = it,
|
||||
channelId = if (categoryId == NO_ID) {
|
||||
channels.getHistoryChannelId()
|
||||
} else {
|
||||
channels.getFavouritesChannelId(categoryId)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateNotificationsChannels() {
|
||||
val categories = favouritesRepository.getCategories()
|
||||
channels.updateChannels(categories)
|
||||
return repository.getTracks(offset = 0, limit = limit)
|
||||
}
|
||||
|
||||
suspend fun gc() {
|
||||
@@ -131,7 +113,7 @@ class Tracker @Inject constructor(
|
||||
private fun compare(track: MangaTracking, manga: Manga, branch: String?): MangaUpdates.Success {
|
||||
if (track.isEmpty()) {
|
||||
// first check or manga was empty on last check
|
||||
return MangaUpdates.Success(manga, emptyList(), isValid = false, channelId = null)
|
||||
return MangaUpdates.Success(manga, emptyList(), isValid = false)
|
||||
}
|
||||
val chapters = requireNotNull(manga.getChapters(branch))
|
||||
val newChapters = chapters.takeLastWhile { x -> x.id != track.lastChapterId }
|
||||
@@ -141,16 +123,15 @@ class Tracker @Inject constructor(
|
||||
manga = manga,
|
||||
newChapters = emptyList(),
|
||||
isValid = chapters.lastOrNull()?.id == track.lastChapterId,
|
||||
channelId = null,
|
||||
)
|
||||
}
|
||||
|
||||
newChapters.size == chapters.size -> {
|
||||
MangaUpdates.Success(manga, emptyList(), isValid = false, channelId = null)
|
||||
MangaUpdates.Success(manga, emptyList(), isValid = false)
|
||||
}
|
||||
|
||||
else -> {
|
||||
MangaUpdates.Success(manga, newChapters, isValid = true, channelId = null)
|
||||
MangaUpdates.Success(manga, newChapters, isValid = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ sealed interface MangaUpdates {
|
||||
override val manga: Manga,
|
||||
val newChapters: List<MangaChapter>,
|
||||
val isValid: Boolean,
|
||||
val channelId: String?,
|
||||
) : MangaUpdates {
|
||||
|
||||
fun isNotEmpty() = newChapters.isNotEmpty()
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
package org.koitharu.kotatsu.tracker.work
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import androidx.annotation.CheckResult
|
||||
import androidx.core.app.NotificationChannelCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationCompat.VISIBILITY_PUBLIC
|
||||
import androidx.core.app.NotificationCompat.VISIBILITY_SECRET
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.PendingIntentCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.Constraints
|
||||
@@ -27,8 +24,6 @@ import androidx.work.WorkManager
|
||||
import androidx.work.WorkQuery
|
||||
import androidx.work.WorkerParameters
|
||||
import androidx.work.await
|
||||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import dagger.Reusable
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
@@ -37,6 +32,7 @@ import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
@@ -53,17 +49,15 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.ext.awaitUniqueWorkInfoByName
|
||||
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
|
||||
import org.koitharu.kotatsu.core.util.ext.onEachIndexed
|
||||
import org.koitharu.kotatsu.core.util.ext.toBitmapOrNull
|
||||
import org.koitharu.kotatsu.core.util.ext.trySetForeground
|
||||
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.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.parsers.util.toIntUp
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
import org.koitharu.kotatsu.settings.work.PeriodicWorkScheduler
|
||||
import org.koitharu.kotatsu.tracker.domain.Tracker
|
||||
import org.koitharu.kotatsu.tracker.domain.model.MangaTracking
|
||||
import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates
|
||||
import org.koitharu.kotatsu.tracker.work.TrackerNotificationHelper.NotificationInfo
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
@@ -74,7 +68,7 @@ import com.google.android.material.R as materialR
|
||||
class TrackWorker @AssistedInject constructor(
|
||||
@Assisted context: Context,
|
||||
@Assisted workerParams: WorkerParameters,
|
||||
private val coil: ImageLoader,
|
||||
private val notificationHelper: TrackerNotificationHelper,
|
||||
private val settings: AppSettings,
|
||||
private val tracker: Tracker,
|
||||
private val workManager: WorkManager,
|
||||
@@ -84,6 +78,7 @@ class TrackWorker @AssistedInject constructor(
|
||||
private val notificationManager by lazy { NotificationManagerCompat.from(applicationContext) }
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
notificationHelper.updateChannels()
|
||||
val isForeground = trySetForeground()
|
||||
logger.log("doWork(): attempt $runAttemptCount")
|
||||
return try {
|
||||
@@ -105,32 +100,33 @@ class TrackWorker @AssistedInject constructor(
|
||||
if (!settings.isTrackerEnabled) {
|
||||
return Result.success(workDataOf(0, 0))
|
||||
}
|
||||
tracker.updateNotificationsChannels()
|
||||
val tracks = tracker.getTracks(if (isFullRun) Int.MAX_VALUE else BATCH_SIZE)
|
||||
logger.log("Total ${tracks.size} tracks")
|
||||
if (tracks.isEmpty()) {
|
||||
return Result.success(workDataOf(0, 0))
|
||||
}
|
||||
|
||||
checkUpdatesAsync(tracks)
|
||||
val notifications = checkUpdatesAsync(tracks)
|
||||
if (notifications.isNotEmpty() && applicationContext.checkNotificationPermission(null)) {
|
||||
val groupNotification = notificationHelper.createGroupNotification(notifications)
|
||||
notifications.forEach { notificationManager.notify(it.tag, it.id, it.notification) }
|
||||
if (groupNotification != null) {
|
||||
notificationManager.notify(TAG, TrackerNotificationHelper.GROUP_NOTIFICATION_ID, groupNotification)
|
||||
}
|
||||
}
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
private suspend fun checkUpdatesAsync(tracks: List<TrackingItem>): List<MangaUpdates> {
|
||||
@CheckResult
|
||||
private suspend fun checkUpdatesAsync(tracks: List<MangaTracking>): List<NotificationInfo> {
|
||||
val semaphore = Semaphore(MAX_PARALLELISM)
|
||||
return channelFlow {
|
||||
for ((track, channelId) in tracks) {
|
||||
for (track in tracks) {
|
||||
launch {
|
||||
semaphore.withPermit {
|
||||
send(
|
||||
runCatchingCancellable {
|
||||
tracker.fetchUpdates(track, commit = true).let {
|
||||
if (it is MangaUpdates.Success) {
|
||||
it.copy(channelId = channelId)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
tracker.fetchUpdates(track, commit = true)
|
||||
}.getOrElse { error ->
|
||||
MangaUpdates.Failure(
|
||||
manga = track.manga,
|
||||
@@ -145,94 +141,26 @@ class TrackWorker @AssistedInject constructor(
|
||||
if (applicationContext.checkNotificationPermission(WORKER_CHANNEL_ID)) {
|
||||
notificationManager.notify(WORKER_NOTIFICATION_ID, createWorkerNotification(tracks.size, index + 1))
|
||||
}
|
||||
if (it is MangaUpdates.Failure) {
|
||||
val e = it.error
|
||||
logger.log("checkUpdatesAsync", e)
|
||||
if (e is CloudFlareProtectedException) {
|
||||
CaptchaNotifier(applicationContext).notify(e)
|
||||
}
|
||||
}
|
||||
}.mapNotNull {
|
||||
when (it) {
|
||||
is MangaUpdates.Failure -> {
|
||||
val e = it.error
|
||||
logger.log("checkUpdatesAsync", e)
|
||||
if (e is CloudFlareProtectedException) {
|
||||
CaptchaNotifier(applicationContext).notify(e)
|
||||
}
|
||||
}
|
||||
|
||||
is MangaUpdates.Success -> {
|
||||
if (it.isValid && it.isNotEmpty()) {
|
||||
showNotification(
|
||||
manga = it.manga,
|
||||
channelId = it.channelId,
|
||||
newChapters = it.newChapters,
|
||||
)
|
||||
}
|
||||
is MangaUpdates.Failure -> null
|
||||
is MangaUpdates.Success -> if (it.isValid && it.isNotEmpty()) {
|
||||
notificationHelper.createNotification(
|
||||
manga = it.manga,
|
||||
newChapters = it.newChapters,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}.toList(ArrayList(tracks.size))
|
||||
}
|
||||
|
||||
private suspend fun showNotification(
|
||||
manga: Manga,
|
||||
channelId: String?,
|
||||
newChapters: List<MangaChapter>,
|
||||
) {
|
||||
if (newChapters.isEmpty() || channelId == null || !applicationContext.checkNotificationPermission(channelId)) {
|
||||
return
|
||||
}
|
||||
val id = manga.url.hashCode()
|
||||
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,
|
||||
)
|
||||
with(builder) {
|
||||
setContentText(summary)
|
||||
setContentTitle(manga.title)
|
||||
setNumber(newChapters.size)
|
||||
setLargeIcon(
|
||||
coil.execute(
|
||||
ImageRequest.Builder(applicationContext)
|
||||
.data(manga.coverUrl)
|
||||
.tag(manga.source)
|
||||
.build(),
|
||||
).toBitmapOrNull(),
|
||||
)
|
||||
setSmallIcon(R.drawable.ic_stat_book_plus)
|
||||
setGroup(GROUP_NEW_CHAPTERS)
|
||||
val style = NotificationCompat.InboxStyle(this)
|
||||
for (chapter in newChapters) {
|
||||
style.addLine(chapter.name)
|
||||
}
|
||||
style.setSummaryText(manga.title)
|
||||
style.setBigContentTitle(summary)
|
||||
setStyle(style)
|
||||
val intent = DetailsActivity.newIntent(applicationContext, manga)
|
||||
setContentIntent(
|
||||
PendingIntentCompat.getActivity(
|
||||
applicationContext,
|
||||
id,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT,
|
||||
false,
|
||||
),
|
||||
)
|
||||
setAutoCancel(true)
|
||||
setCategory(NotificationCompat.CATEGORY_PROMO)
|
||||
setVisibility(if (manga.isNsfw) VISIBILITY_SECRET else VISIBILITY_PUBLIC)
|
||||
setShortcutId(manga.id.toString())
|
||||
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
builder.setSound(settings.notificationSound)
|
||||
var defaults = if (settings.notificationLight) {
|
||||
setLights(colorPrimary, 1000, 5000)
|
||||
NotificationCompat.DEFAULT_LIGHTS
|
||||
} else 0
|
||||
if (settings.notificationVibrate) {
|
||||
builder.setVibrate(longArrayOf(500, 500, 500, 500))
|
||||
defaults = defaults or NotificationCompat.DEFAULT_VIBRATE
|
||||
}
|
||||
builder.setDefaults(defaults)
|
||||
}
|
||||
}
|
||||
notificationManager.notify(TAG, id, builder.build())
|
||||
}.toList()
|
||||
}
|
||||
|
||||
override suspend fun getForegroundInfo(): ForegroundInfo {
|
||||
@@ -250,11 +178,7 @@ class TrackWorker @AssistedInject constructor(
|
||||
|
||||
val notification = createWorkerNotification(0, 0)
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ForegroundInfo(
|
||||
WORKER_NOTIFICATION_ID,
|
||||
notification,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
|
||||
)
|
||||
ForegroundInfo(WORKER_NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
||||
} else {
|
||||
ForegroundInfo(WORKER_NOTIFICATION_ID, notification)
|
||||
}
|
||||
@@ -367,7 +291,6 @@ class TrackWorker @AssistedInject constructor(
|
||||
|
||||
const val WORKER_CHANNEL_ID = "track_worker"
|
||||
const val WORKER_NOTIFICATION_ID = 35
|
||||
const val GROUP_NEW_CHAPTERS = "org.koitharu.kotatsu.NEW_CHAPTERS"
|
||||
const val TAG = "tracking"
|
||||
const val TAG_ONESHOT = "tracking_oneshot"
|
||||
const val MAX_PARALLELISM = 6
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
package org.koitharu.kotatsu.tracker.work
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationChannelCompat
|
||||
import androidx.core.app.NotificationChannelGroupCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
|
||||
class TrackerNotificationChannels @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val settings: AppSettings,
|
||||
) {
|
||||
|
||||
private val manager = NotificationManagerCompat.from(context)
|
||||
|
||||
val areNotificationsDisabled: Boolean
|
||||
get() = !manager.areNotificationsEnabled()
|
||||
|
||||
fun updateChannels(categories: Collection<FavouriteCategory>) {
|
||||
manager.deleteNotificationChannel(OLD_CHANNEL_ID)
|
||||
val group = createGroup()
|
||||
val existingChannels = group.channels.associateByTo(HashMap()) { it.id }
|
||||
for (category in categories) {
|
||||
val id = getFavouritesChannelId(category.id)
|
||||
if (existingChannels.remove(id)?.name == category.title) {
|
||||
continue
|
||||
}
|
||||
val channel = NotificationChannelCompat.Builder(id, NotificationManagerCompat.IMPORTANCE_DEFAULT)
|
||||
.setName(category.title)
|
||||
.setGroup(GROUP_ID)
|
||||
.build()
|
||||
manager.createNotificationChannel(channel)
|
||||
}
|
||||
existingChannels.remove(CHANNEL_ID_HISTORY)
|
||||
createHistoryChannel()
|
||||
for (id in existingChannels.keys) {
|
||||
manager.deleteNotificationChannel(id)
|
||||
}
|
||||
}
|
||||
|
||||
fun createChannel(category: FavouriteCategory) {
|
||||
val id = getFavouritesChannelId(category.id)
|
||||
val channel = NotificationChannelCompat.Builder(id, NotificationManagerCompat.IMPORTANCE_DEFAULT)
|
||||
.setName(category.title)
|
||||
.setGroup(createGroup().id)
|
||||
.build()
|
||||
manager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
fun deleteChannel(categoryId: Long) {
|
||||
manager.deleteNotificationChannel(getFavouritesChannelId(categoryId))
|
||||
}
|
||||
|
||||
fun isFavouriteNotificationsEnabled(category: FavouriteCategory): Boolean {
|
||||
if (!manager.areNotificationsEnabled()) {
|
||||
return false
|
||||
}
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = manager.getNotificationChannel(getFavouritesChannelId(category.id))
|
||||
channel != null && channel.importance != NotificationManager.IMPORTANCE_NONE
|
||||
} else {
|
||||
// fallback
|
||||
settings.isTrackerNotificationsEnabled
|
||||
}
|
||||
}
|
||||
|
||||
fun isHistoryNotificationsEnabled(): Boolean {
|
||||
if (!manager.areNotificationsEnabled()) {
|
||||
return false
|
||||
}
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = manager.getNotificationChannel(getHistoryChannelId())
|
||||
channel != null && channel.importance != NotificationManager.IMPORTANCE_NONE
|
||||
} else {
|
||||
// fallback
|
||||
settings.isTrackerNotificationsEnabled
|
||||
}
|
||||
}
|
||||
|
||||
fun isNotificationGroupEnabled(): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
return settings.isTrackerNotificationsEnabled
|
||||
}
|
||||
val group = manager.getNotificationChannelGroupCompat(GROUP_ID) ?: return true
|
||||
return !group.isBlocked && group.channels.any { it.importance != NotificationManagerCompat.IMPORTANCE_NONE }
|
||||
}
|
||||
|
||||
fun getFavouritesChannelId(categoryId: Long): String {
|
||||
return CHANNEL_ID_PREFIX + categoryId
|
||||
}
|
||||
|
||||
fun getHistoryChannelId(): String {
|
||||
return CHANNEL_ID_HISTORY
|
||||
}
|
||||
|
||||
private fun createGroup(): NotificationChannelGroupCompat {
|
||||
return manager.getNotificationChannelGroupCompat(GROUP_ID) ?: run {
|
||||
val group = NotificationChannelGroupCompat.Builder(GROUP_ID)
|
||||
.setName(context.getString(R.string.new_chapters))
|
||||
.build()
|
||||
manager.createNotificationChannelGroup(group)
|
||||
group
|
||||
}
|
||||
}
|
||||
|
||||
private fun createHistoryChannel() {
|
||||
val channel = NotificationChannelCompat.Builder(CHANNEL_ID_HISTORY, NotificationManagerCompat.IMPORTANCE_DEFAULT)
|
||||
.setName(context.getString(R.string.history))
|
||||
.setGroup(GROUP_ID)
|
||||
.build()
|
||||
manager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val GROUP_ID = "trackers"
|
||||
private const val CHANNEL_ID_PREFIX = "track_fav_"
|
||||
private const val CHANNEL_ID_HISTORY = "track_history"
|
||||
private const val OLD_CHANNEL_ID = "tracking"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
package org.koitharu.kotatsu.tracker.work
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationChannelCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationCompat.VISIBILITY_PUBLIC
|
||||
import androidx.core.app.NotificationCompat.VISIBILITY_SECRET
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.PendingIntentCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
|
||||
import org.koitharu.kotatsu.core.util.ext.toBitmapOrNull
|
||||
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.ui.updates.UpdatesActivity
|
||||
import javax.inject.Inject
|
||||
|
||||
class TrackerNotificationHelper @Inject constructor(
|
||||
@ApplicationContext private val applicationContext: Context,
|
||||
private val settings: AppSettings,
|
||||
private val coil: ImageLoader,
|
||||
) {
|
||||
|
||||
fun getAreNotificationsEnabled(): Boolean {
|
||||
val manager = NotificationManagerCompat.from(applicationContext)
|
||||
if (!manager.areNotificationsEnabled()) {
|
||||
return false
|
||||
}
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = manager.getNotificationChannel(CHANNEL_ID)
|
||||
channel != null && channel.importance != NotificationManager.IMPORTANCE_NONE
|
||||
} else {
|
||||
// fallback
|
||||
settings.isTrackerNotificationsEnabled
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun createNotification(manga: Manga, newChapters: List<MangaChapter>): NotificationInfo? {
|
||||
if (newChapters.isEmpty() || !applicationContext.checkNotificationPermission(CHANNEL_ID)) {
|
||||
return null
|
||||
}
|
||||
val id = manga.url.hashCode()
|
||||
val builder = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
|
||||
val summary = applicationContext.resources.getQuantityString(
|
||||
R.plurals.new_chapters,
|
||||
newChapters.size,
|
||||
newChapters.size,
|
||||
)
|
||||
with(builder) {
|
||||
setContentText(summary)
|
||||
setContentTitle(manga.title)
|
||||
setNumber(newChapters.size)
|
||||
setLargeIcon(
|
||||
coil.execute(
|
||||
ImageRequest.Builder(applicationContext)
|
||||
.data(manga.coverUrl)
|
||||
.tag(manga.source)
|
||||
.build(),
|
||||
).toBitmapOrNull(),
|
||||
)
|
||||
setSmallIcon(R.drawable.ic_stat_book_plus)
|
||||
setGroup(GROUP_NEW_CHAPTERS)
|
||||
val style = NotificationCompat.InboxStyle(this)
|
||||
for (chapter in newChapters) {
|
||||
style.addLine(chapter.name)
|
||||
}
|
||||
style.setSummaryText(manga.title)
|
||||
style.setBigContentTitle(summary)
|
||||
setStyle(style)
|
||||
val intent = DetailsActivity.newIntent(applicationContext, manga)
|
||||
setContentIntent(
|
||||
PendingIntentCompat.getActivity(
|
||||
applicationContext,
|
||||
id,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT,
|
||||
false,
|
||||
),
|
||||
)
|
||||
setVisibility(if (manga.isNsfw) VISIBILITY_SECRET else VISIBILITY_PUBLIC)
|
||||
setShortcutId(manga.id.toString())
|
||||
applyCommonSettings(this)
|
||||
}
|
||||
return NotificationInfo(id, TAG, builder.build(), manga, newChapters.size)
|
||||
}
|
||||
|
||||
fun createGroupNotification(
|
||||
notifications: List<NotificationInfo>
|
||||
): Notification? {
|
||||
if (notifications.size <= 1) {
|
||||
return null
|
||||
}
|
||||
val newChaptersCount = notifications.sumOf { it.newChapters }
|
||||
val builder = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
|
||||
with(builder) {
|
||||
val title = applicationContext.resources.getQuantityString(
|
||||
R.plurals.new_chapters,
|
||||
newChaptersCount,
|
||||
newChaptersCount,
|
||||
)
|
||||
setContentTitle(title)
|
||||
setContentText(notifications.joinToString { it.manga.title })
|
||||
setSmallIcon(R.drawable.ic_stat_book_plus)
|
||||
val style = NotificationCompat.InboxStyle(this)
|
||||
for (item in notifications) {
|
||||
style.addLine(
|
||||
applicationContext.getString(R.string.new_chapters_pattern, item.manga.title, item.newChapters),
|
||||
)
|
||||
}
|
||||
style.setBigContentTitle(title)
|
||||
setStyle(style)
|
||||
setNumber(newChaptersCount)
|
||||
setGroup(GROUP_NEW_CHAPTERS)
|
||||
setGroupSummary(true)
|
||||
val intent = UpdatesActivity.newIntent(applicationContext)
|
||||
setContentIntent(
|
||||
PendingIntentCompat.getActivity(
|
||||
applicationContext,
|
||||
GROUP_NOTIFICATION_ID,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT,
|
||||
false,
|
||||
),
|
||||
)
|
||||
applyCommonSettings(this)
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
fun updateChannels() {
|
||||
val manager = NotificationManagerCompat.from(applicationContext)
|
||||
manager.deleteNotificationChannel(LEGACY_CHANNEL_ID)
|
||||
manager.deleteNotificationChannel(LEGACY_CHANNEL_ID_HISTORY)
|
||||
manager.deleteNotificationChannelGroup(LEGACY_CHANNELS_GROUP_ID)
|
||||
|
||||
val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_DEFAULT)
|
||||
.setName(applicationContext.getString(R.string.new_chapters))
|
||||
.setDescription(applicationContext.getString(R.string.show_notification_new_chapters_on))
|
||||
.setShowBadge(true)
|
||||
.setLightColor(ContextCompat.getColor(applicationContext, R.color.blue_primary))
|
||||
.build()
|
||||
manager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
private fun applyCommonSettings(builder: NotificationCompat.Builder) {
|
||||
builder.setAutoCancel(true)
|
||||
builder.setCategory(NotificationCompat.CATEGORY_SOCIAL)
|
||||
builder.priority = NotificationCompat.PRIORITY_DEFAULT
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
builder.setSound(settings.notificationSound)
|
||||
var defaults = if (settings.notificationLight) {
|
||||
builder.setLights(ContextCompat.getColor(applicationContext, R.color.blue_primary), 1000, 5000)
|
||||
NotificationCompat.DEFAULT_LIGHTS
|
||||
} else 0
|
||||
if (settings.notificationVibrate) {
|
||||
builder.setVibrate(longArrayOf(500, 500, 500, 500))
|
||||
defaults = defaults or NotificationCompat.DEFAULT_VIBRATE
|
||||
}
|
||||
builder.setDefaults(defaults)
|
||||
}
|
||||
}
|
||||
|
||||
class NotificationInfo(
|
||||
val id: Int,
|
||||
val tag: String,
|
||||
val notification: Notification,
|
||||
val manga: Manga,
|
||||
val newChapters: Int,
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
||||
const val CHANNEL_ID = "tracker_chapters"
|
||||
const val GROUP_NOTIFICATION_ID = 0
|
||||
const val GROUP_NEW_CHAPTERS = "org.koitharu.kotatsu.NEW_CHAPTERS"
|
||||
const val TAG = "tracker"
|
||||
|
||||
private const val LEGACY_CHANNELS_GROUP_ID = "trackers"
|
||||
private const val LEGACY_CHANNEL_ID_PREFIX = "track_fav_"
|
||||
private const val LEGACY_CHANNEL_ID_HISTORY = "track_history"
|
||||
private const val LEGACY_CHANNEL_ID = "tracking"
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package org.koitharu.kotatsu.tracker.work
|
||||
|
||||
import org.koitharu.kotatsu.tracker.domain.model.MangaTracking
|
||||
|
||||
data class TrackingItem(
|
||||
val tracking: MangaTracking,
|
||||
val channelId: String?,
|
||||
)
|
||||
@@ -633,4 +633,5 @@
|
||||
<string name="less_frequently">Less frequently</string>
|
||||
<string name="more_frequently">More frequently</string>
|
||||
<string name="frequency_of_check">Frequency of check</string>
|
||||
<string name="new_chapters_pattern">%1$s: %2$d</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user