Browse Source

Group notifcations for Library updates (#2582)

Jays2Kings 5 years ago
parent
commit
7f115f2e83

+ 8 - 0
app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt

@@ -52,6 +52,14 @@ interface ChapterQueries : DbProvider {
                     .build())
             .prepare()
 
+    fun getChapter(url: String, mangaId: Long) = db.get()
+        .`object`(Chapter::class.java)
+        .withQuery(Query.builder()
+            .table(ChapterTable.TABLE)
+            .where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?")
+            .whereArgs(url, mangaId)
+            .build())
+        .prepare()
 
     fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare()
 

+ 0 - 49
app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadNotifier.kt

@@ -30,27 +30,11 @@ internal class DownloadNotifier(private val context: Context) {
      */
     private var isDownloading = false
 
-    /**
-     * The size of queue on start download.
-     */
-    var initialQueueSize = 0
-        set(value) {
-            if (value != 0) {
-                isSingleChapter = (value == 1)
-            }
-            field = value
-        }
-
     /**
      * Updated when error is thrown
      */
     var errorThrown = false
 
-    /**
-     * Updated when only single page is downloaded
-     */
-    var isSingleChapter = false
-
     /**
      * Updated when paused
      */
@@ -144,39 +128,6 @@ internal class DownloadNotifier(private val context: Context) {
 
         // Reset initial values
         isDownloading = false
-        initialQueueSize = 0
-    }
-
-    /**
-     * Called when chapter is downloaded.
-     *
-     * @param download download object containing download information.
-     */
-    fun onDownloadCompleted(download: Download, queue: DownloadQueue) {
-        // Check if last download
-        if (!queue.isEmpty()) {
-            return
-        }
-        // Create notification.
-        with(notificationBuilder) {
-            val title = download.manga.title.chop(15)
-            val quotedTitle = Pattern.quote(title)
-            val chapter = download.chapter.name.replaceFirst("$quotedTitle[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "")
-            setContentTitle("$title - $chapter".chop(30))
-            setContentText(context.getString(R.string.update_check_notification_download_complete))
-            setSmallIcon(android.R.drawable.stat_sys_download_done)
-            setAutoCancel(true)
-            clearActions()
-            setContentIntent(NotificationReceiver.openChapterPendingBroadcast(context, download.manga, download.chapter))
-            setProgress(0, 0, false)
-        }
-
-        // Show notification.
-        notificationBuilder.show()
-
-        // Reset initial values
-        isDownloading = false
-        initialQueueSize = 0
     }
 
     /**

+ 0 - 8
app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt

@@ -126,8 +126,6 @@ class Downloader(
             if (notifier.paused) {
                 notifier.paused = false
                 notifier.onDownloadPaused()
-            } else if (notifier.isSingleChapter && !notifier.errorThrown) {
-                notifier.isSingleChapter = false
             } else {
                 notifier.dismiss()
             }
@@ -229,9 +227,6 @@ class Downloader(
         if (chaptersToQueue.isNotEmpty()) {
             queue.addAll(chaptersToQueue)
 
-            // Initialize queue size.
-            notifier.initialQueueSize = queue.size
-
             if (isRunning) {
                 // Send the list of downloads to the downloader.
                 downloadsRelay.call(chaptersToQueue)
@@ -428,9 +423,6 @@ class Downloader(
             queue.remove(download)
         }
         if (areAllDownloadsFinished()) {
-            if (notifier.isSingleChapter && !notifier.errorThrown) {
-                notifier.onDownloadCompleted(download, queue)
-            }
             DownloadService.stop(context)
         }
     }

+ 76 - 33
app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt

@@ -10,6 +10,10 @@ import android.os.Build
 import android.os.IBinder
 import android.os.PowerManager
 import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationCompat.GROUP_ALERT_SUMMARY
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.ContextCompat
+import com.bumptech.glide.Glide
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.database.DatabaseHelper
 import eu.kanade.tachiyomi.data.database.models.Category
@@ -202,9 +206,9 @@ class LibraryUpdateService(
      * @return the start value of the command.
      */
     override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
-        if (intent == null) return Service.START_NOT_STICKY
+        if (intent == null) return START_NOT_STICKY
         val target = intent.getSerializableExtra(KEY_TARGET) as? Target
-                ?: return Service.START_NOT_STICKY
+                ?: return START_NOT_STICKY
 
         // Unsubscribe from any previous subscription if needed.
         subscription?.unsubscribe()
@@ -276,7 +280,7 @@ class LibraryUpdateService(
         // Initialize the variables holding the progress of the updates.
         val count = AtomicInteger(0)
         // List containing new updates
-        val newUpdates = ArrayList<Manga>()
+        val newUpdates = ArrayList<Pair<LibraryManga, Array<Chapter>>>()
         // list containing failed updates
         val failedUpdates = ArrayList<Manga>()
         // List containing categories that get included in downloads.
@@ -309,7 +313,8 @@ class LibraryUpdateService(
                                 }
                             }
                             // Convert to the manga that contains new chapters.
-                            .map { manga }
+                            .map { Pair(manga, (it.first.sortedByDescending { ch -> ch
+                                .source_order }.toTypedArray())) }
                 }
                 // Add manga with new chapters to the list.
                 .doOnNext { manga ->
@@ -331,6 +336,7 @@ class LibraryUpdateService(
 
                     cancelProgressNotification()
                 }
+                .map { manga -> manga.first }
     }
 
     fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
@@ -444,39 +450,76 @@ class LibraryUpdateService(
      *
      * @param updates a list of manga with new updates.
      */
-    private fun showResultNotification(updates: List<Manga>) {
-        val newUpdates = updates.map { it.title.chop(45) }.toMutableSet()
-
-        // Append new chapters from a previous, existing notification
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            val previousNotification = notificationManager.activeNotifications
-                    .find { it.id == Notifications.ID_LIBRARY_RESULT }
-
-            if (previousNotification != null) {
-                val oldUpdates = previousNotification.notification.extras
-                        .getString(Notification.EXTRA_BIG_TEXT)
-
-                if (!oldUpdates.isNullOrEmpty()) {
-                    newUpdates += oldUpdates.split("\n")
+    private fun showResultNotification(updates: List<Pair<Manga, Array<Chapter>>>) {
+        val notifications = ArrayList<Pair<Notification, Int>>()
+        updates.forEach {
+            val manga = it.first
+            val chapters = it.second
+            val chapterNames = chapters.map { chapter -> chapter.name }.toSet()
+            notifications.add(Pair(notification(Notifications.CHANNEL_NEW_CHAPTERS) {
+                setSmallIcon(R.drawable.ic_tachi)
+                try {
+                    val icon = Glide.with(this@LibraryUpdateService)
+                        .asBitmap().load(manga).dontTransform().centerCrop().circleCrop()
+                        .override(256, 256).submit().get()
+                    setLargeIcon(icon)
                 }
-            }
+                catch (e: Exception) { }
+                setGroupAlertBehavior(GROUP_ALERT_SUMMARY)
+                setContentTitle(manga.title)
+                val chaptersNames = if (chapterNames.size > 5) {
+                    "${chapterNames.take(4).joinToString(", ")}, " +
+                        resources.getQuantityString(R.plurals.notification_and_n_more,
+                            (chapterNames.size - 4), (chapterNames.size - 4))
+                } else chapterNames.joinToString(", ")
+                setContentText(chaptersNames)
+                setStyle(NotificationCompat.BigTextStyle().bigText(chaptersNames))
+                priority = NotificationCompat.PRIORITY_HIGH
+                setGroup(Notifications.GROUP_NEW_CHAPTERS)
+                setContentIntent(
+                    NotificationReceiver.openChapterPendingActivity(
+                        this@LibraryUpdateService, manga, chapters.first()
+                    )
+                )
+                addAction(R.drawable.ic_glasses_black_24dp, getString(R.string.action_mark_as_read),
+                    NotificationReceiver.markAsReadPendingBroadcast(this@LibraryUpdateService,
+                        manga, chapters, Notifications.ID_NEW_CHAPTERS))
+                addAction(R.drawable.ic_book_white_24dp, getString(R.string.action_view_chapters),
+                    NotificationReceiver.openChapterPendingActivity(this@LibraryUpdateService,
+                        manga, Notifications.ID_NEW_CHAPTERS))
+                setAutoCancel(true)
+            }, manga.id.hashCode()))
         }
 
-        notificationManager.notify(Notifications.ID_LIBRARY_RESULT, notification(Notifications.CHANNEL_LIBRARY) {
-            setSmallIcon(R.drawable.ic_book_white_24dp)
-            setLargeIcon(notificationBitmap)
-            setContentTitle(getString(R.string.notification_new_chapters))
-            if (newUpdates.size > 1) {
-                setContentText(getString(R.string.notification_new_chapters_text, newUpdates.size))
-                setStyle(NotificationCompat.BigTextStyle().bigText(newUpdates.joinToString("\n")))
-                setNumber(newUpdates.size)
-            } else {
-                setContentText(newUpdates.first())
+        NotificationManagerCompat.from(this).apply {
+
+            notify(Notifications.ID_NEW_CHAPTERS, notification(Notifications.CHANNEL_NEW_CHAPTERS) {
+                setSmallIcon(R.drawable.ic_tachi)
+                setLargeIcon(notificationBitmap)
+                setContentTitle(getString(R.string.notification_new_chapters))
+                if (updates.size > 1) {
+                    setContentText(resources.getQuantityString(R.plurals
+                        .notification_new_chapters_text,
+                        updates.size, updates.size))
+                    setStyle(NotificationCompat.BigTextStyle().bigText(updates.joinToString("\n") {
+                        it.first.title.chop(45)
+                    }))
+                }
+                else {
+                    setContentText(updates.first().first.title.chop(45))
+                }
+                priority = NotificationCompat.PRIORITY_HIGH
+                setGroup(Notifications.GROUP_NEW_CHAPTERS)
+                setGroupAlertBehavior(GROUP_ALERT_SUMMARY)
+                setGroupSummary(true)
+                setContentIntent(getNotificationIntent())
+                setAutoCancel(true)
+            })
+
+            notifications.forEach {
+                notify(it.second, it.first)
             }
-            priority = NotificationCompat.PRIORITY_HIGH
-            setContentIntent(getNotificationIntent())
-            setAutoCancel(true)
-        })
+        }
     }
 
     /**

+ 115 - 7
app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt

@@ -4,6 +4,7 @@ import android.app.PendingIntent
 import android.content.BroadcastReceiver
 import android.content.Context
 import android.content.Intent
+import android.os.Build
 import android.os.Handler
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.database.DatabaseHelper
@@ -12,11 +13,17 @@ import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.data.download.DownloadManager
 import eu.kanade.tachiyomi.data.download.DownloadService
 import eu.kanade.tachiyomi.data.library.LibraryUpdateService
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.source.SourceManager
+import eu.kanade.tachiyomi.ui.main.MainActivity
+import eu.kanade.tachiyomi.ui.manga.MangaController
 import eu.kanade.tachiyomi.ui.reader.ReaderActivity
 import eu.kanade.tachiyomi.util.storage.DiskUtil
 import eu.kanade.tachiyomi.util.storage.getUriCompat
 import eu.kanade.tachiyomi.util.system.notificationManager
 import eu.kanade.tachiyomi.util.system.toast
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
 import uy.kohesive.injekt.injectLazy
 import java.io.File
 import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
@@ -60,6 +67,15 @@ class NotificationReceiver : BroadcastReceiver() {
                 openChapter(context, intent.getLongExtra(EXTRA_MANGA_ID, -1),
                         intent.getLongExtra(EXTRA_CHAPTER_ID, -1))
             }
+            ACTION_MARK_AS_READ -> {
+                val notificationId = intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
+                if (notificationId > -1) dismissNotification(
+                    context, notificationId, intent.getIntExtra(EXTRA_GROUP_ID, 0)
+                )
+                val urls = intent.getStringArrayExtra(EXTRA_CHAPTER_URL) ?: return
+                val mangaId = intent.getLongExtra(EXTRA_MANGA_ID, -1)
+                markAsRead(urls, mangaId)
+            }
         }
     }
 
@@ -104,7 +120,6 @@ class NotificationReceiver : BroadcastReceiver() {
         val db = DatabaseHelper(context)
         val manga = db.getManga(mangaId).executeAsBlocking()
         val chapter = db.getChapter(chapterId).executeAsBlocking()
-
         if (manga != null && chapter != null) {
             val intent = ReaderActivity.newIntent(context, manga, chapter).apply {
                 flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
@@ -143,6 +158,28 @@ class NotificationReceiver : BroadcastReceiver() {
         Handler().post { dismissNotification(context, notificationId) }
     }
 
+    /**
+     * Method called when user wants to mark as read
+     *
+     * @param context context of application
+     * @param notificationId id of notification
+     */
+    private fun markAsRead(chapterUrls: Array<String>, mangaId: Long) {
+        val db: DatabaseHelper = Injekt.get()
+        chapterUrls.forEach {
+            val chapter = db.getChapter(it, mangaId).executeAsBlocking() ?: return
+            chapter.read = true
+            db.updateChapterProgress(chapter).executeAsBlocking()
+            val preferences: PreferencesHelper = Injekt.get()
+            if (preferences.removeAfterMarkedAsRead()) {
+                val manga = db.getManga(mangaId).executeAsBlocking() ?: return
+                val sourceManager: SourceManager = Injekt.get()
+                val source = sourceManager.get(manga.source) ?: return
+                downloadManager.deleteChapters(listOf(chapter), manga, source)
+            }
+        }
+    }
+
     companion object {
         private const val NAME = "NotificationReceiver"
 
@@ -155,6 +192,9 @@ class NotificationReceiver : BroadcastReceiver() {
         // Called to cancel library update.
         private const val ACTION_CANCEL_LIBRARY_UPDATE = "$ID.$NAME.CANCEL_LIBRARY_UPDATE"
 
+        // Called to mark as read
+        private const val ACTION_MARK_AS_READ = "$ID.$NAME.MARK_AS_READ"
+
         // Called to open chapter
         private const val ACTION_OPEN_CHAPTER = "$ID.$NAME.ACTION_OPEN_CHAPTER"
 
@@ -179,12 +219,18 @@ class NotificationReceiver : BroadcastReceiver() {
         // Value containing notification id.
         private const val EXTRA_NOTIFICATION_ID = "$ID.$NAME.NOTIFICATION_ID"
 
+        // Value containing group id.
+        private const val EXTRA_GROUP_ID = "$ID.$NAME.EXTRA_GROUP_ID"
+
         // Value containing manga id.
         private const val EXTRA_MANGA_ID = "$ID.$NAME.EXTRA_MANGA_ID"
 
         // Value containing chapter id.
         private const val EXTRA_CHAPTER_ID = "$ID.$NAME.EXTRA_CHAPTER_ID"
 
+        // Value containing chapter url.
+        private const val EXTRA_CHAPTER_URL = "$ID.$NAME.EXTRA_CHAPTER_URL"
+
         /**
          * Returns a [PendingIntent] that resumes the download of a chapter
          *
@@ -246,6 +292,32 @@ class NotificationReceiver : BroadcastReceiver() {
             return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
         }
 
+        /**
+         * Returns [PendingIntent] that starts a service which dismissed the notification
+         *
+         * @param context context of application
+         * @param notificationId id of notification
+         * @return [PendingIntent]
+         */
+        internal fun dismissNotification(context: Context, notificationId: Int, groupId: Int? =
+            null) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+                val groupKey = context.notificationManager.activeNotifications.find {
+                    it.id == notificationId
+                }?.groupKey
+                if (groupId != null && groupId != 0 && groupKey != null && groupKey.isNotEmpty()) {
+                    val notifications = context.notificationManager.activeNotifications.filter {
+                        it.groupKey == groupKey
+                    }
+                    if (notifications.size == 2) {
+                        context.notificationManager.cancel(groupId)
+                        return
+                    }
+                }
+            }
+            context.notificationManager.cancel(notificationId)
+        }
+
         /**
          * Returns [PendingIntent] that starts a service which cancels the notification and starts a share activity
          *
@@ -281,19 +353,55 @@ class NotificationReceiver : BroadcastReceiver() {
         }
 
         /**
-         * Returns [PendingIntent] that start a reader activity containing chapter.
+         * Returns [PendingIntent] that starts a reader activity containing chapter.
          *
          * @param context context of application
          * @param manga manga of chapter
          * @param chapter chapter that needs to be opened
          */
-        internal fun openChapterPendingBroadcast(context: Context, manga: Manga, chapter: Chapter): PendingIntent {
-            val intent = Intent(context, NotificationReceiver::class.java).apply {
-                action = ACTION_OPEN_CHAPTER
+        internal fun openChapterPendingActivity(context: Context, manga: Manga, chapter:
+        Chapter): PendingIntent {
+            val newIntent = ReaderActivity.newIntent(context, manga, chapter)
+            return PendingIntent.getActivity(context, manga.id.hashCode(), newIntent, PendingIntent
+                .FLAG_UPDATE_CURRENT)
+        }
+
+        /**
+         * Returns [PendingIntent] that opens the manga info controller.
+         *
+         * @param context context of application
+         * @param manga manga of chapter
+         */
+        internal fun openChapterPendingActivity(context: Context, manga: Manga, groupId: Int):
+            PendingIntent {
+            val newIntent =
+                Intent(context, MainActivity::class.java).setAction(MainActivity.SHORTCUT_MANGA)
+                    .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+                    .putExtra(MangaController.MANGA_EXTRA, manga.id)
+                    .putExtra("notificationId", manga.id.hashCode())
+                    .putExtra("groupId", groupId)
+            return PendingIntent.getActivity(
+                context, manga.id.hashCode(), newIntent, PendingIntent.FLAG_UPDATE_CURRENT
+            )
+        }
+
+        /**
+         * Returns [PendingIntent] that marks a chapter as read and deletes it if preferred
+         *
+         * @param context context of application
+         * @param manga manga of chapter
+         */
+        internal fun markAsReadPendingBroadcast(context: Context, manga: Manga, chapters:
+            Array<Chapter>, groupId: Int):
+            PendingIntent {
+            val newIntent = Intent(context, NotificationReceiver::class.java).apply {
+                action = ACTION_MARK_AS_READ
+                putExtra(EXTRA_CHAPTER_URL, chapters.map { it.url }.toTypedArray())
                 putExtra(EXTRA_MANGA_ID, manga.id)
-                putExtra(EXTRA_CHAPTER_ID, chapter.id)
+                putExtra(EXTRA_NOTIFICATION_ID, manga.id.hashCode())
+                putExtra(EXTRA_GROUP_ID, groupId)
             }
-            return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+            return PendingIntent.getBroadcast(context, manga.id.hashCode(), newIntent, PendingIntent.FLAG_UPDATE_CURRENT)
         }
 
         /**

+ 18 - 6
app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt

@@ -23,15 +23,21 @@ object Notifications {
      * Notification channel and ids used by the library updater.
      */
     const val CHANNEL_LIBRARY = "library_channel"
-    const val ID_LIBRARY_PROGRESS = 101
-    const val ID_LIBRARY_RESULT = 102
+    const val ID_LIBRARY_PROGRESS = -101
 
     /**
      * Notification channel and ids used by the downloader.
      */
     const val CHANNEL_DOWNLOADER = "downloader_channel"
-    const val ID_DOWNLOAD_CHAPTER = 201
-    const val ID_DOWNLOAD_CHAPTER_ERROR = 202
+    const val ID_DOWNLOAD_CHAPTER = -201
+    const val ID_DOWNLOAD_CHAPTER_ERROR = -202
+
+    /**
+     * Notification channel and ids used by the library updater.
+     */
+    const val CHANNEL_NEW_CHAPTERS = "new_chapters_channel"
+    const val ID_NEW_CHAPTERS = -301
+    const val GROUP_NEW_CHAPTERS = "eu.kanade.tachiyomi.NEW_CHAPTERS"
 
     /**
      * Creates the notification channels introduced in Android Oreo.
@@ -45,9 +51,15 @@ object Notifications {
                 NotificationChannel(CHANNEL_COMMON, context.getString(R.string.channel_common),
                         NotificationManager.IMPORTANCE_LOW),
                 NotificationChannel(CHANNEL_LIBRARY, context.getString(R.string.channel_library),
-                        NotificationManager.IMPORTANCE_LOW),
+                        NotificationManager.IMPORTANCE_LOW).apply {
+                    setShowBadge(false)
+                },
                 NotificationChannel(CHANNEL_DOWNLOADER, context.getString(R.string.channel_downloader),
-                        NotificationManager.IMPORTANCE_LOW)
+                        NotificationManager.IMPORTANCE_LOW).apply {
+                    setShowBadge(false)
+                },
+                NotificationChannel(CHANNEL_NEW_CHAPTERS, context.getString(R.string.channel_new_chapters),
+                        NotificationManager.IMPORTANCE_DEFAULT)
         )
         context.notificationManager.createNotificationChannels(channels)
     }

+ 5 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt

@@ -12,6 +12,7 @@ import androidx.drawerlayout.widget.DrawerLayout
 import com.bluelinelabs.conductor.*
 import eu.kanade.tachiyomi.Migrations
 import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.notification.NotificationReceiver
 import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
 import eu.kanade.tachiyomi.ui.base.controller.*
 import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
@@ -136,6 +137,10 @@ class MainActivity : BaseActivity() {
     }
 
     private fun handleIntentAction(intent: Intent): Boolean {
+        val notificationId = intent.getIntExtra("notificationId", -1)
+        if (notificationId > -1) NotificationReceiver.dismissNotification(
+            applicationContext, notificationId, intent.getIntExtra("groupId", 0)
+        )
         when (intent.action) {
             SHORTCUT_LIBRARY -> setSelectedDrawerItem(R.id.nav_drawer_library)
             SHORTCUT_RECENTLY_UPDATED -> setSelectedDrawerItem(R.id.nav_drawer_recent_updates)

+ 12 - 5
app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt

@@ -19,6 +19,8 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
 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.notification.NotificationReceiver
+import eu.kanade.tachiyomi.data.notification.Notifications
 import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 import eu.kanade.tachiyomi.data.preference.getOrDefault
 import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
@@ -104,10 +106,14 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
         const val VERTICAL = 3
         const val WEBTOON = 4
 
-        fun newIntent(context: Context, manga: Manga, chapter: Chapter): Intent {
+        fun newIntent(context: Context, manga: Manga, chapter: Chapter):
+            Intent {
             val intent = Intent(context, ReaderActivity::class.java)
             intent.putExtra("manga", manga.id)
             intent.putExtra("chapter", chapter.id)
+            // chapters just added from library updates don't have an id yet
+            intent.putExtra("chapterUrl", chapter.url)
+            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
             return intent
         }
     }
@@ -126,13 +132,14 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
         if (presenter.needsInit()) {
             val manga = intent.extras!!.getLong("manga", -1)
             val chapter = intent.extras!!.getLong("chapter", -1)
-
-            if (manga == -1L || chapter == -1L) {
+            val chapterUrl = intent.extras!!.getString("chapterUrl", "")
+            if (manga == -1L || chapterUrl == "" && chapter == -1L) {
                 finish()
                 return
             }
-
-            presenter.init(manga, chapter)
+            NotificationReceiver.dismissNotification(this, manga.hashCode(), Notifications.ID_NEW_CHAPTERS)
+            if (chapter > -1) presenter.init(manga, chapter)
+            else presenter.init(manga, chapterUrl)
         }
 
         if (savedState != null) {

+ 13 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt

@@ -185,6 +185,19 @@ class ReaderPresenter(
                 }, ReaderActivity::setInitialChapterError)
     }
 
+    /**
+     * Initializes this presenter with the given [mangaId] and [chapterUrl]. This method will
+     * fetch the manga from the database and initialize the initial chapter.
+     */
+    fun init(mangaId: Long, chapterUrl: String) {
+        if (!needsInit()) return
+        val context = Injekt.get<Application>()
+        val db = DatabaseHelper(context)
+        val chapterId = db.getChapter(chapterUrl, mangaId).executeAsBlocking()?.id
+        if (chapterId != null)
+            init(mangaId, chapterId)
+    }
+
     /**
      * Initializes this presenter with the given [manga] and [initialChapterId]. This method will
      * set the chapter loader, view subscriptions and trigger an initial load.

+ 3 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersController.kt

@@ -13,12 +13,14 @@ import eu.davidea.flexibleadapter.items.IFlexible
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.download.model.Download
 import eu.kanade.tachiyomi.data.library.LibraryUpdateService
+import eu.kanade.tachiyomi.data.notification.Notifications
 import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
 import eu.kanade.tachiyomi.ui.base.controller.NucleusController
 import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag
 import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
 import eu.kanade.tachiyomi.ui.manga.MangaController
 import eu.kanade.tachiyomi.ui.reader.ReaderActivity
+import eu.kanade.tachiyomi.util.system.notificationManager
 import eu.kanade.tachiyomi.util.system.toast
 import kotlinx.android.synthetic.main.recent_chapters_controller.empty_view
 import kotlinx.android.synthetic.main.recent_chapters_controller.recycler
@@ -68,7 +70,7 @@ class RecentChaptersController : NucleusController<RecentChaptersPresenter>(),
      */
     override fun onViewCreated(view: View) {
         super.onViewCreated(view)
-
+        view.context.notificationManager.cancel(Notifications.ID_NEW_CHAPTERS)
         // Init RecyclerView and adapter
         val layoutManager = LinearLayoutManager(view.context)
         recycler.layoutManager = layoutManager

+ 19 - 0
app/src/main/res/drawable/ic_tachi.xml

@@ -0,0 +1,19 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="256dp"
+    android:height="256dp"
+    android:tint="#FFFFFF"
+    android:viewportWidth="256"
+    android:viewportHeight="256">
+  <path
+      android:pathData="M102.6,19.2c0.3,5.7 0.6,13.1 0.8,16.6l0.4,6.3 -42.7,-0.3c-23.4,-0.2 -43.4,-0.7 -44.3,-1.2 -1.7,-0.8 -1.8,0.6 -1.8,20.4l0,21.2 2.3,-0.6c6.6,-1.9 33.3,-2.6 108.7,-2.6 75.4,-0 102.1,0.7 108.8,2.6l2.2,0.6 0,-21.2c0,-19.8 -0.1,-21.2 -1.7,-20.4 -1,0.5 -21,1 -44.4,1.2l-42.7,0.3 0.4,-6.3c0.2,-3.5 0.5,-10.9 0.8,-16.6l0.4,-10.2 -23.8,-0 -23.8,-0 0.4,10.2z"
+      android:fillColor="#000000"
+      android:strokeColor="#00000000"/>
+  <path
+      android:pathData="M58.8,93.2c-10.4,3.9 -18.8,7.7 -18.8,8.3 0,0.7 1.4,4.3 3.1,8.1 8,17.7 20.6,61.5 24.1,83.6 0.6,4.3 1.6,7.8 2.2,7.8 0.6,-0 10.4,-3.2 21.9,-7.1 14.9,-5.2 20.7,-7.6 20.7,-8.7 0,-3.2 -17.8,-61 -26.2,-85 -3.8,-10.6 -5.4,-14.2 -6.7,-14.1 -0.9,-0 -10,3.2 -20.3,7.1z"
+      android:fillColor="#000000"
+      android:strokeColor="#00000000"/>
+  <path
+      android:pathData="M167.2,93.7c-3.3,21 -15.6,61.6 -28.8,95l-6.9,17.3 -62.7,-0 -62.8,-0 0,20 0,20 121.5,-0 121.5,-0 0,-20 0,-20 -37.1,-0c-29.3,-0 -37,-0.3 -36.6,-1.3 0.3,-0.6 2.7,-5.9 5.3,-11.7 2.5,-5.8 7.5,-18.3 11,-27.9 6.7,-18.4 21.4,-64.3 21.4,-67 0,-1.3 -4.6,-2.8 -21.2,-6.9 -11.7,-2.9 -21.8,-5.2 -22.4,-5.2 -0.6,-0 -1.6,3.5 -2.2,7.7z"
+      android:fillColor="#000000"
+      android:strokeColor="#00000000"/>
+</vector>

+ 11 - 2
app/src/main/res/values/strings.xml

@@ -61,6 +61,7 @@
     <string name="action_sort_down">Sort down</string>
     <string name="action_show_downloaded">Downloaded</string>
     <string name="action_next_unread">Next unread</string>
+    <string name="action_view_chapters">View chapters</string>
     <string name="action_start">Start</string>
     <string name="action_stop">Stop</string>
     <string name="action_pause">Pause</string>
@@ -484,7 +485,14 @@
     <!-- Library update service notifications -->
     <string name="notification_update_progress">Update progress: %1$d/%2$d</string>
     <string name="notification_new_chapters">New chapters found</string>
-    <string name="notification_new_chapters_text">For %1$d titles</string>
+    <plurals name="notification_new_chapters_text">
+        <item quantity="one">For %d title</item>
+        <item quantity="other">For %d titles</item>
+    </plurals>
+    <plurals name="notification_and_n_more">
+        <item quantity="one">and %1$d more chapter.</item>
+        <item quantity="other">and %1$d more chapters.</item>
+    </plurals>
     <string name="notification_cover_update_failed">Failed to update cover</string>
     <string name="notification_first_add_to_library">Please add the manga to your library before doing this</string>
     <string name="notification_not_connected_to_ac_title">Sync canceled</string>
@@ -537,7 +545,8 @@
 
     <!-- Notification channels -->
     <string name="channel_common">Common</string>
-    <string name="channel_library">Library</string>
+    <string name="channel_library">Updating Library</string>
     <string name="channel_downloader">Downloader</string>
+    <string name="channel_new_chapters">New Chapters</string>
 
 </resources>