123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311 |
- package eu.kanade.tachiyomi.data.library
- import android.app.Notification
- import android.app.PendingIntent
- import android.content.Context
- import android.content.Intent
- import android.graphics.Bitmap
- import android.graphics.BitmapFactory
- import android.net.Uri
- import androidx.core.app.NotificationCompat
- import androidx.core.app.NotificationManagerCompat
- import com.bumptech.glide.Glide
- import eu.kanade.tachiyomi.R
- import eu.kanade.tachiyomi.data.database.models.Chapter
- import eu.kanade.tachiyomi.data.database.models.Manga
- import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
- import eu.kanade.tachiyomi.data.notification.NotificationReceiver
- import eu.kanade.tachiyomi.data.notification.Notifications
- import eu.kanade.tachiyomi.data.preference.PreferencesHelper
- import eu.kanade.tachiyomi.ui.main.MainActivity
- import eu.kanade.tachiyomi.util.lang.chop
- import eu.kanade.tachiyomi.util.system.notification
- import eu.kanade.tachiyomi.util.system.notificationBuilder
- import eu.kanade.tachiyomi.util.system.notificationManager
- import uy.kohesive.injekt.injectLazy
- import java.text.DecimalFormat
- import java.text.DecimalFormatSymbols
- class LibraryUpdateNotifier(private val context: Context) {
- private val preferences: PreferencesHelper by injectLazy()
- /**
- * Pending intent of action that cancels the library update
- */
- private val cancelIntent by lazy {
- NotificationReceiver.cancelLibraryUpdatePendingBroadcast(context)
- }
- /**
- * Bitmap of the app for notifications.
- */
- private val notificationBitmap by lazy {
- BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)
- }
- /**
- * Cached progress notification to avoid creating a lot.
- */
- val progressNotificationBuilder by lazy {
- context.notificationBuilder(Notifications.CHANNEL_LIBRARY) {
- setContentTitle(context.getString(R.string.app_name))
- setSmallIcon(R.drawable.ic_refresh_24dp)
- setLargeIcon(notificationBitmap)
- setOngoing(true)
- setOnlyAlertOnce(true)
- addAction(R.drawable.ic_close_24dp, context.getString(android.R.string.cancel), cancelIntent)
- }
- }
- /**
- * Shows the notification containing the currently updating manga and the progress.
- *
- * @param manga the manga that's being updated.
- * @param current the current progress.
- * @param total the total progress.
- */
- fun showProgressNotification(manga: Manga, current: Int, total: Int) {
- val title = if (preferences.hideNotificationContent()) {
- context.getString(R.string.notification_check_updates)
- } else {
- manga.title
- }
- context.notificationManager.notify(
- Notifications.ID_LIBRARY_PROGRESS,
- progressNotificationBuilder
- .setContentTitle(title)
- .setProgress(total, current, false)
- .build()
- )
- }
- /**
- * Shows notification containing update entries that failed with action to open full log.
- *
- * @param errors List of entry titles that failed to update.
- * @param uri Uri for error log file containing all titles that failed.
- */
- fun showUpdateErrorNotification(errors: List<String>, uri: Uri) {
- if (errors.isEmpty()) {
- return
- }
- context.notificationManager.notify(
- Notifications.ID_LIBRARY_ERROR,
- context.notificationBuilder(Notifications.CHANNEL_LIBRARY) {
- setContentTitle(context.resources.getQuantityString(R.plurals.notification_update_error, errors.size, errors.size))
- setStyle(
- NotificationCompat.BigTextStyle().bigText(
- errors.joinToString("\n") {
- it.chop(NOTIF_TITLE_MAX_LEN)
- }
- )
- )
- setSmallIcon(R.drawable.ic_tachi)
- val errorLogIntent = NotificationReceiver.openErrorLogPendingActivity(context, uri)
- setContentIntent(errorLogIntent)
- addAction(
- R.drawable.nnf_ic_file_folder,
- context.getString(R.string.action_open_log),
- errorLogIntent
- )
- }
- .build()
- )
- }
- /**
- * Shows the notification containing the result of the update done by the service.
- *
- * @param updates a list of manga with new updates.
- */
- fun showUpdateNotifications(updates: List<Pair<Manga, Array<Chapter>>>) {
- if (updates.isEmpty()) {
- return
- }
- NotificationManagerCompat.from(context).apply {
- // Parent group notification
- notify(
- Notifications.ID_NEW_CHAPTERS,
- context.notification(Notifications.CHANNEL_NEW_CHAPTERS) {
- setContentTitle(context.getString(R.string.notification_new_chapters))
- if (updates.size == 1 && !preferences.hideNotificationContent()) {
- setContentText(updates.first().first.title.chop(NOTIF_TITLE_MAX_LEN))
- } else {
- setContentText(context.resources.getQuantityString(R.plurals.notification_new_chapters_summary, updates.size, updates.size))
- if (!preferences.hideNotificationContent()) {
- setStyle(
- NotificationCompat.BigTextStyle().bigText(
- updates.joinToString("\n") {
- it.first.title.chop(NOTIF_TITLE_MAX_LEN)
- }
- )
- )
- }
- }
- setSmallIcon(R.drawable.ic_tachi)
- setLargeIcon(notificationBitmap)
- setGroup(Notifications.GROUP_NEW_CHAPTERS)
- setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
- setGroupSummary(true)
- priority = NotificationCompat.PRIORITY_HIGH
- setContentIntent(getNotificationIntent())
- setAutoCancel(true)
- }
- )
- // Per-manga notification
- if (!preferences.hideNotificationContent()) {
- updates.forEach {
- val (manga, chapters) = it
- notify(manga.id.hashCode(), createNewChaptersNotification(manga, chapters))
- }
- }
- }
- }
- private fun createNewChaptersNotification(manga: Manga, chapters: Array<Chapter>): Notification {
- return context.notification(Notifications.CHANNEL_NEW_CHAPTERS) {
- setContentTitle(manga.title)
- val description = getNewChaptersDescription(chapters)
- setContentText(description)
- setStyle(NotificationCompat.BigTextStyle().bigText(description))
- setSmallIcon(R.drawable.ic_tachi)
- val icon = getMangaIcon(manga)
- if (icon != null) {
- setLargeIcon(icon)
- }
- setGroup(Notifications.GROUP_NEW_CHAPTERS)
- setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
- priority = NotificationCompat.PRIORITY_HIGH
- // Open first chapter on tap
- setContentIntent(NotificationReceiver.openChapterPendingActivity(context, manga, chapters.first()))
- setAutoCancel(true)
- // Mark chapters as read action
- addAction(
- R.drawable.ic_glasses_black_24dp,
- context.getString(R.string.action_mark_as_read),
- NotificationReceiver.markAsReadPendingBroadcast(
- context,
- manga,
- chapters,
- Notifications.ID_NEW_CHAPTERS
- )
- )
- // View chapters action
- addAction(
- R.drawable.ic_book_24dp,
- context.getString(R.string.action_view_chapters),
- NotificationReceiver.openChapterPendingActivity(
- context,
- manga,
- Notifications.ID_NEW_CHAPTERS
- )
- )
- }
- }
- /**
- * Cancels the progress notification.
- */
- fun cancelProgressNotification() {
- context.notificationManager.cancel(Notifications.ID_LIBRARY_PROGRESS)
- }
- private fun getMangaIcon(manga: Manga): Bitmap? {
- return try {
- Glide.with(context)
- .asBitmap()
- .load(manga.toMangaThumbnail())
- .dontTransform()
- .centerCrop()
- .circleCrop()
- .override(
- NOTIF_ICON_SIZE,
- NOTIF_ICON_SIZE
- )
- .submit()
- .get()
- } catch (e: Exception) {
- null
- }
- }
- private fun getNewChaptersDescription(chapters: Array<Chapter>): String {
- val formatter = DecimalFormat(
- "#.###",
- DecimalFormatSymbols()
- .apply { decimalSeparator = '.' }
- )
- val displayableChapterNumbers = chapters
- .filter { it.isRecognizedNumber }
- .sortedBy { it.chapter_number }
- .map { formatter.format(it.chapter_number) }
- .toSet()
- return when (displayableChapterNumbers.size) {
- // No sensible chapter numbers to show (i.e. no chapters have parsed chapter number)
- 0 -> {
- // "1 new chapter" or "5 new chapters"
- context.resources.getQuantityString(R.plurals.notification_chapters_generic, chapters.size, chapters.size)
- }
- // Only 1 chapter has a parsed chapter number
- 1 -> {
- val remaining = chapters.size - displayableChapterNumbers.size
- if (remaining == 0) {
- // "Chapter 2.5"
- context.resources.getString(R.string.notification_chapters_single, displayableChapterNumbers.first())
- } else {
- // "Chapter 2.5 and 10 more"
- context.resources.getString(R.string.notification_chapters_single_and_more, displayableChapterNumbers.first(), remaining)
- }
- }
- // Everything else (i.e. multiple parsed chapter numbers)
- else -> {
- val shouldTruncate = displayableChapterNumbers.size > NOTIF_MAX_CHAPTERS
- if (shouldTruncate) {
- // "Chapters 1, 2.5, 3, 4, 5 and 10 more"
- val remaining = displayableChapterNumbers.size - NOTIF_MAX_CHAPTERS
- val joinedChapterNumbers = displayableChapterNumbers.take(NOTIF_MAX_CHAPTERS).joinToString(", ")
- context.resources.getQuantityString(R.plurals.notification_chapters_multiple_and_more, remaining, joinedChapterNumbers, remaining)
- } else {
- // "Chapters 1, 2.5, 3"
- context.resources.getString(R.string.notification_chapters_multiple, displayableChapterNumbers.joinToString(", "))
- }
- }
- }
- }
- /**
- * Returns an intent to open the main activity.
- */
- private fun getNotificationIntent(): PendingIntent {
- val intent = Intent(context, MainActivity::class.java).apply {
- flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
- action = MainActivity.SHORTCUT_RECENTLY_UPDATED
- }
- return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
- }
- companion object {
- private const val NOTIF_MAX_CHAPTERS = 5
- private const val NOTIF_TITLE_MAX_LEN = 45
- private const val NOTIF_ICON_SIZE = 192
- }
- }
|