Browse Source

Notification Improvements (#594)

* Download notifier improvements

* Notification improvements

Added a Notification Service.

Added a Notification Activity Handler.

* Removed service. Everything is now managed by single broadcast

* Fixed some flags

* Fixed ReaderActivity call

* Code review

* Added Handler. Removed dismiss onDestroy
Bram van de Kerkhof 8 years ago
parent
commit
c445ea90ba
32 changed files with 969 additions and 370 deletions
  1. 35 34
      app/src/main/AndroidManifest.xml
  2. 11 3
      app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt
  3. 92 17
      app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadNotifier.kt
  4. 37 7
      app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt
  5. 8 24
      app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt
  6. 57 0
      app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationHandler.kt
  7. 277 0
      app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt
  8. 7 1
      app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateCheckerJob.kt
  9. 144 0
      app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderReceiver.kt
  10. 126 76
      app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderService.kt
  11. 0 70
      app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateNotificationReceiver.kt
  12. 32 39
      app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadActivity.kt
  13. 11 4
      app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadPresenter.kt
  14. 2 2
      app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
  15. 2 2
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt
  16. 8 6
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/SaveImageNotifier.kt
  17. 0 84
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/notification/ImageNotificationReceiver.kt
  18. 41 0
      app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt
  19. 33 0
      app/src/main/java/eu/kanade/tachiyomi/util/FileExtensions.kt
  20. BIN
      app/src/main/res/drawable-hdpi/ic_av_pause_grey_24dp_img.png
  21. BIN
      app/src/main/res/drawable-hdpi/ic_av_play_arrow_grey_img.png
  22. BIN
      app/src/main/res/drawable-mdpi/ic_av_pause_grey_24dp_img.png
  23. BIN
      app/src/main/res/drawable-mdpi/ic_av_play_arrow_grey_img.png
  24. BIN
      app/src/main/res/drawable-xhdpi/ic_av_pause_grey_24dp_img.png
  25. BIN
      app/src/main/res/drawable-xhdpi/ic_av_play_arrow_grey_img.png
  26. BIN
      app/src/main/res/drawable-xxhdpi/ic_av_pause_grey_24dp_img.png
  27. BIN
      app/src/main/res/drawable-xxhdpi/ic_av_play_arrow_grey_img.png
  28. BIN
      app/src/main/res/drawable-xxxhdpi/ic_av_pause_grey_24dp_img.png
  29. BIN
      app/src/main/res/drawable-xxxhdpi/ic_av_play_arrow_grey_img.png
  30. 42 0
      app/src/main/res/layout/activity_download_manager.xml
  31. 2 1
      app/src/main/res/menu/menu_navigation.xml
  32. 2 0
      app/src/main/res/values/strings.xml

+ 35 - 34
app/src/main/AndroidManifest.xml

@@ -1,16 +1,17 @@
 <?xml version="1.0" encoding="utf-8"?>
-<manifest
-    xmlns:android="http://schemas.android.com/apk/res/android"
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
     package="eu.kanade.tachiyomi">
 
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
-    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.WAKE_LOCK" />
-    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
-    <uses-permission android:name="android.permission.READ_PHONE_STATE" tools:node="remove" />
+    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+    <uses-permission
+        android:name="android.permission.READ_PHONE_STATE"
+        tools:node="remove" />
     <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
 
     <application
@@ -20,9 +21,8 @@
         android:icon="@mipmap/ic_launcher"
         android:label="@string/app_name"
         android:largeHeap="true"
-        android:theme="@style/Theme.Tachiyomi" >
-        <activity
-            android:name=".ui.main.MainActivity">
+        android:theme="@style/Theme.Tachiyomi">
+        <activity android:name=".ui.main.MainActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
 
@@ -31,40 +31,40 @@
         </activity>
         <activity
             android:name=".ui.manga.MangaActivity"
-            android:parentActivityName=".ui.main.MainActivity"
-            android:exported="true">
-        </activity>
+            android:exported="true"
+            android:parentActivityName=".ui.main.MainActivity" />
         <activity
             android:name=".ui.reader.ReaderActivity"
-            android:theme="@style/Theme.Reader">
-        </activity>
+            android:theme="@style/Theme.Reader" />
         <activity
             android:name=".ui.setting.SettingsActivity"
             android:label="@string/label_settings"
-            android:parentActivityName=".ui.main.MainActivity" >
-        </activity>
+            android:parentActivityName=".ui.main.MainActivity" />
         <activity
             android:name=".ui.category.CategoryActivity"
             android:label="@string/label_categories"
-            android:parentActivityName=".ui.main.MainActivity">
-        </activity>
+            android:parentActivityName=".ui.main.MainActivity" />
         <activity
             android:name=".ui.setting.SettingsDownloadsFragment$CustomLayoutPickerActivity"
             android:label="@string/app_name"
-            android:theme="@style/FilePickerTheme">
-        </activity>
+            android:theme="@style/FilePickerTheme" />
         <activity
             android:name=".ui.setting.AnilistLoginActivity"
             android:label="Anilist">
             <intent-filter>
                 <action android:name="android.intent.action.VIEW" />
+
                 <category android:name="android.intent.category.DEFAULT" />
                 <category android:name="android.intent.category.BROWSABLE" />
+
                 <data
                     android:host="anilist-auth"
                     android:scheme="tachiyomi" />
             </intent-filter>
         </activity>
+        <activity
+            android:name=".ui.download.DownloadActivity"
+            android:launchMode="singleTop" />
 
         <provider
             android:name="android.support.v4.content.FileProvider"
@@ -73,26 +73,27 @@
             android:grantUriPermissions="true">
             <meta-data
                 android:name="android.support.FILE_PROVIDER_PATHS"
-                android:resource="@xml/provider_paths"/>
+                android:resource="@xml/provider_paths" />
         </provider>
 
-        <service android:name=".data.library.LibraryUpdateService"
-            android:exported="false"/>
-
-        <service android:name=".data.download.DownloadService"
-            android:exported="false"/>
-
-        <service android:name=".data.track.TrackUpdateService"
-            android:exported="false"/>
-
-        <service android:name=".data.updater.UpdateDownloaderService"
-            android:exported="false"/>
+        <receiver
+            android:name=".data.notification.NotificationReceiver"
+            android:exported="false" />
 
-        <receiver android:name=".data.updater.UpdateNotificationReceiver"/>
+        <service
+            android:name=".data.library.LibraryUpdateService"
+            android:exported="false" />
 
-        <receiver android:name=".data.library.LibraryUpdateService$CancelUpdateReceiver" />
+        <service
+            android:name=".data.download.DownloadService"
+            android:exported="false" />
 
-        <receiver android:name=".ui.reader.notification.ImageNotificationReceiver" />
+        <service
+            android:name=".data.track.TrackUpdateService"
+            android:exported="false" />
+        <service
+            android:name=".data.updater.UpdateDownloaderService"
+            android:exported="false" />
 
         <meta-data
             android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"

+ 11 - 3
app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt

@@ -59,11 +59,20 @@ class DownloadManager(context: Context) {
         downloader.stop(reason)
     }
 
+    /**
+     * Tells the downloader to pause downloads.
+     */
+    fun pauseDownloads() {
+        downloader.pause()
+    }
+
     /**
      * Empties the download queue.
+     *
+     * @param isNotification value that determines if status is set (needed for view updates)
      */
-    fun clearQueue() {
-        downloader.clearQueue()
+    fun clearQueue(isNotification: Boolean = false) {
+        downloader.clearQueue(isNotification)
     }
 
     /**
@@ -168,5 +177,4 @@ class DownloadManager(context: Context) {
     fun deleteChapter(source: Source, manga: Manga, chapter: Chapter) {
         provider.findChapterDir(source, manga, chapter)?.delete()
     }
-
 }

+ 92 - 17
app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadNotifier.kt

@@ -7,6 +7,8 @@ import eu.kanade.tachiyomi.Constants
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.download.model.Download
 import eu.kanade.tachiyomi.data.download.model.DownloadQueue
+import eu.kanade.tachiyomi.data.notification.NotificationHandler
+import eu.kanade.tachiyomi.data.notification.NotificationReceiver
 import eu.kanade.tachiyomi.util.chop
 import eu.kanade.tachiyomi.util.notificationManager
 
@@ -33,12 +35,34 @@ internal class DownloadNotifier(private val context: Context) {
      * The size of queue on start download.
      */
     var initialQueueSize = 0
+        get() = field
+        set(value) {
+            if (value != 0){
+                isSingleChapter = (value == 1)
+            }
+            field = value
+        }
 
     /**
      * Simultaneous download setting > 1.
      */
     var multipleDownloadThreads = false
 
+    /**
+     * Updated when error is thrown
+     */
+    var errorThrown = false
+
+    /**
+     * Updated when only single page is downloaded
+     */
+    var isSingleChapter = false
+
+    /**
+     * Updated when paused
+     */
+    var paused = false
+
     /**
      * Shows a notification from this builder.
      *
@@ -48,6 +72,14 @@ internal class DownloadNotifier(private val context: Context) {
         context.notificationManager.notify(id, build())
     }
 
+    /**
+     * Clear old actions if they exist.
+     */
+    private fun clearActions() = with(notification) {
+        if (!mActions.isEmpty())
+            mActions.clear()
+    }
+
     /**
      * Dismiss the downloader's notification. Downloader error notifications use a different id, so
      * those can only be dismissed by the user.
@@ -88,24 +120,15 @@ internal class DownloadNotifier(private val context: Context) {
      * @param queue the queue containing downloads.
      */
     private fun doOnProgressChange(download: Download?, queue: DownloadQueue) {
-        // Check if download is completed
-        if (multipleDownloadThreads) {
-            if (queue.isEmpty()) {
-                onChapterCompleted(null)
-                return
-            }
-        } else {
-            if (download != null && download.pages!!.size == download.downloadedImages) {
-                onChapterCompleted(download)
-                return
-            }
-        }
-
         // Create notification
         with(notification) {
-            // Check if icon needs refresh
+            // Check if first call.
             if (!isDownloading) {
                 setSmallIcon(android.R.drawable.stat_sys_download)
+                setAutoCancel(false)
+                clearActions()
+                // Open download manager when clicked
+                setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
                 isDownloading = true
             }
 
@@ -121,7 +144,9 @@ internal class DownloadNotifier(private val context: Context) {
                 setProgress(initialQueueSize, initialQueueSize - queue.size, false)
             } else {
                 download?.let {
-                    setContentTitle(it.chapter.name.chop(30))
+                    val title = it.manga.title.chop(15)
+                    val chapter = download.chapter.name.replaceFirst("$title[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "")
+                    setContentTitle("$title - $chapter".chop(30))
                     setContentText(context.getString(R.string.chapter_downloading_progress)
                             .format(it.downloadedImages, it.pages!!.size))
                     setProgress(it.pages!!.size, it.downloadedImages, false)
@@ -133,17 +158,57 @@ internal class DownloadNotifier(private val context: Context) {
         notification.show()
     }
 
+    /**
+     * Show notification when download is paused.
+     */
+    fun onDownloadPaused() {
+        with(notification) {
+            setContentTitle(context.getString(R.string.chapter_paused))
+            setContentText(context.getString(R.string.download_notifier_download_paused))
+            setSmallIcon(R.drawable.ic_av_pause_grey_24dp_img)
+            setAutoCancel(false)
+            setProgress(0, 0, false)
+            clearActions()
+            // Open download manager when clicked
+            setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
+            // Resume action
+            addAction(R.drawable.ic_av_play_arrow_grey_img,
+                    context.getString(R.string.action_resume),
+                    NotificationReceiver.resumeDownloadsPendingBroadcast(context))
+            //Clear action
+            addAction(R.drawable.ic_clear_grey_24dp_img,
+                    context.getString(R.string.action_clear),
+                    NotificationReceiver.clearDownloadsPendingBroadcast(context))
+        }
+
+        // Show notification.
+        notification.show()
+
+        // Reset initial values
+        isDownloading = false
+        initialQueueSize = 0
+    }
+
     /**
      * Called when chapter is downloaded.
      *
      * @param download download object containing download information.
      */
-    private fun onChapterCompleted(download: Download?) {
+    fun onDownloadCompleted(download: Download, queue: DownloadQueue) {
+        // Check if last download
+        if (!queue.isEmpty()) {
+            return
+        }
         // Create notification.
         with(notification) {
-            setContentTitle(download?.chapter?.name ?: context.getString(R.string.app_name))
+            val title = download.manga.title.chop(15)
+            val chapter = download.chapter.name.replaceFirst("$title[\\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)
         }
 
@@ -165,9 +230,15 @@ internal class DownloadNotifier(private val context: Context) {
             setContentTitle(context.getString(R.string.download_notifier_downloader_title))
             setContentText(reason)
             setSmallIcon(android.R.drawable.stat_sys_warning)
+            setAutoCancel(true)
+            clearActions()
+            setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
             setProgress(0, 0, false)
         }
         notification.show()
+
+        // Reset download information
+        isDownloading = false
     }
 
     /**
@@ -183,11 +254,15 @@ internal class DownloadNotifier(private val context: Context) {
             setContentTitle(chapter ?: context.getString(R.string.download_notifier_downloader_title))
             setContentText(error ?: context.getString(R.string.download_notifier_unkown_error))
             setSmallIcon(android.R.drawable.stat_sys_warning)
+            clearActions()
+            setAutoCancel(false)
+            setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
             setProgress(0, 0, false)
         }
         notification.show(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID)
 
         // Reset download information
+        errorThrown = true
         isDownloading = false
     }
 }

+ 37 - 7
app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt

@@ -133,15 +133,42 @@ class Downloader(private val context: Context, private val provider: DownloadPro
         if (reason != null) {
             notifier.onWarning(reason)
         } else {
-            notifier.dismiss()
+            if (notifier.paused) {
+                notifier.paused = false
+                notifier.onDownloadPaused()
+            } else if (notifier.isSingleChapter && !notifier.errorThrown) {
+                notifier.isSingleChapter = false
+            } else {
+                notifier.dismiss()
+            }
         }
     }
 
+    /**
+     * Pauses the downloader
+     */
+    fun pause() {
+        destroySubscriptions()
+        queue
+                .filter { it.status == Download.DOWNLOADING }
+                .forEach { it.status = Download.QUEUE }
+        notifier.paused = true
+    }
+
     /**
      * Removes everything from the queue.
+     *
+     * @param isNotification value that determines if status is set (needed for view updates)
      */
-    fun clearQueue() {
+    fun clearQueue(isNotification: Boolean = false) {
         destroySubscriptions()
+
+        //Needed to update the chapter view
+        if (isNotification) {
+            queue
+                    .filter { it.status == Download.QUEUE }
+                    .forEach { it.status = Download.NOT_DOWNLOADED }
+        }
         queue.clear()
         notifier.dismiss()
     }
@@ -313,7 +340,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro
         tmpFile?.delete()
 
         // Try to find the image file.
-        val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.")}
+        val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.") }
 
         // If the image is already downloaded, do nothing. Otherwise download from network
         val pageObservable = if (imageFile != null)
@@ -377,10 +404,10 @@ class Downloader(private val context: Context, private val provider: DownloadPro
     private fun getImageExtension(response: Response, file: UniFile): String {
         // Read content type if available.
         val mime = response.body().contentType()?.let { ct -> "${ct.type()}/${ct.subtype()}" }
-        // Else guess from the uri.
-        ?: context.contentResolver.getType(file.uri)
-        // Else read magic numbers.
-        ?: file.openInputStream().buffered().use {
+            // Else guess from the uri.
+            ?: context.contentResolver.getType(file.uri)
+            // Else read magic numbers.
+            ?: file.openInputStream().buffered().use {
             URLConnection.guessContentTypeFromStream(it)
         }
 
@@ -421,6 +448,9 @@ class Downloader(private val context: Context, private val provider: DownloadPro
             notifier.onProgressChange(queue)
         }
         if (areAllDownloadsFinished()) {
+            if (notifier.isSingleChapter && !notifier.errorThrown) {
+                notifier.onDownloadCompleted(download, queue)
+            }
             DownloadService.stop(context)
         }
     }

+ 8 - 24
app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt

@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.data.library
 
 import android.app.PendingIntent
 import android.app.Service
-import android.content.BroadcastReceiver
 import android.content.Context
 import android.content.Intent
 import android.graphics.BitmapFactory
@@ -18,6 +17,7 @@ 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.Companion.start
+import eu.kanade.tachiyomi.data.notification.NotificationReceiver
 import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 import eu.kanade.tachiyomi.data.preference.getOrDefault
 import eu.kanade.tachiyomi.data.source.SourceManager
@@ -69,6 +69,11 @@ class LibraryUpdateService : Service() {
      */
     private var subscription: Subscription? = null
 
+    /**
+     * Pending intent of action that cancels the library update
+     */
+    private val cancelPendingIntent by lazy {NotificationReceiver.cancelLibraryUpdatePendingBroadcast(this)}
+
     /**
      * Id of the library update notification.
      */
@@ -236,13 +241,10 @@ class LibraryUpdateService : Service() {
         val newUpdates = ArrayList<Manga>()
         val failedUpdates = ArrayList<Manga>()
 
-        val cancelIntent = PendingIntent.getBroadcast(this, 0,
-                Intent(this, CancelUpdateReceiver::class.java), 0)
-
         // Emit each manga and update it sequentially.
         return Observable.from(mangaToUpdate)
                 // Notify manga that will update.
-                .doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size, cancelIntent) }
+                .doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size, cancelPendingIntent) }
                 // Update the chapters of the manga.
                 .concatMap { manga ->
                     updateManga(manga)
@@ -316,13 +318,10 @@ class LibraryUpdateService : Service() {
         // Initialize the variables holding the progress of the updates.
         val count = AtomicInteger(0)
 
-        val cancelIntent = PendingIntent.getBroadcast(this, 0,
-                Intent(this, CancelUpdateReceiver::class.java), 0)
-
         // Emit each manga and update it sequentially.
         return Observable.from(mangaToUpdate)
                 // Notify manga that will update.
-                .doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size, cancelIntent) }
+                .doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size, cancelPendingIntent) }
                 // Update the details of the manga.
                 .concatMap { manga ->
                     val source = sourceManager.get(manga.source) as? OnlineSource
@@ -459,19 +458,4 @@ class LibraryUpdateService : Service() {
             intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
             return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
         }
-
-    /**
-     * Class that stops updating the library.
-     */
-    class CancelUpdateReceiver : BroadcastReceiver() {
-        /**
-         * Method called when user wants a library update.
-         * @param context the application context.
-         * @param intent the intent received.
-         */
-        override fun onReceive(context: Context, intent: Intent) {
-            LibraryUpdateService.stop(context)
-            context.notificationManager.cancel(Constants.NOTIFICATION_LIBRARY_ID)
-        }
-    }
 }

+ 57 - 0
app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationHandler.kt

@@ -0,0 +1,57 @@
+package eu.kanade.tachiyomi.data.notification
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.support.v4.content.FileProvider
+import eu.kanade.tachiyomi.BuildConfig
+import eu.kanade.tachiyomi.ui.download.DownloadActivity
+import eu.kanade.tachiyomi.util.getUriCompat
+import java.io.File
+
+/**
+ * Class that manages [PendingIntent] of activity's
+ */
+object NotificationHandler {
+    /**
+     * Returns [PendingIntent] that starts a download activity.
+     *
+     * @param context context of application
+     */
+    internal fun openDownloadManagerPendingActivity(context: Context): PendingIntent {
+        val intent = Intent(context, DownloadActivity::class.java).apply {
+            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
+        }
+        return PendingIntent.getActivity(context, 0, intent, 0)
+    }
+
+    /**
+     * Returns [PendingIntent] that starts a gallery activity
+     *
+     * @param context context of application
+     * @param file file containing image
+     */
+    internal fun openImagePendingActivity(context: Context, file: File): PendingIntent {
+        val intent = Intent(Intent.ACTION_VIEW).apply {
+            val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file)
+            setDataAndType(uri, "image/*")
+            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
+        }
+        return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+    }
+
+    /**
+     * Returns [PendingIntent] that prompts user with apk install intent
+     *
+     * @param context context
+     * @param file file of apk that is installed
+     */
+    fun installApkPendingActivity(context: Context, file: File): PendingIntent {
+        val intent = Intent(Intent.ACTION_VIEW).apply {
+            val uri = file.getUriCompat(context)
+            setDataAndType(uri, "application/vnd.android.package-archive")
+            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
+        }
+        return PendingIntent.getActivity(context, 0, intent, 0)
+    }
+}

+ 277 - 0
app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt

@@ -0,0 +1,277 @@
+package eu.kanade.tachiyomi.data.notification
+
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.os.Handler
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.DatabaseHelper
+import eu.kanade.tachiyomi.data.database.models.Chapter
+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.ui.reader.ReaderActivity
+import eu.kanade.tachiyomi.util.deleteIfExists
+import eu.kanade.tachiyomi.util.getUriCompat
+import eu.kanade.tachiyomi.util.notificationManager
+import eu.kanade.tachiyomi.util.toast
+import uy.kohesive.injekt.injectLazy
+import java.io.File
+import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
+
+/**
+ * Global [BroadcastReceiver] that runs on UI thread
+ * Pending Broadcasts should be made from here.
+ * NOTE: Use local broadcasts if possible.
+ */
+class NotificationReceiver : BroadcastReceiver() {
+    /**
+     * Download manager.
+     */
+    private val downloadManager: DownloadManager by injectLazy()
+
+    override fun onReceive(context: Context, intent: Intent) {
+        when (intent.action) {
+            // Dismiss notification
+            ACTION_DISMISS_NOTIFICATION -> dismissNotification(context, intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
+            // Resume the download service
+            ACTION_RESUME_DOWNLOADS -> DownloadService.start(context)
+            // Clear the download queue
+            ACTION_CLEAR_DOWNLOADS -> downloadManager.clearQueue(true)
+            // Launch share activity and dismiss notification
+            ACTION_SHARE_IMAGE -> shareImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION),
+                    intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
+            // Delete image from path and dismiss notification
+            ACTION_DELETE_IMAGE -> deleteImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION),
+                    intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
+            // Cancel library update and dismiss notification
+            ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context,
+                    intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
+            // Open reader activity
+            ACTION_OPEN_CHAPTER -> {
+                openChapter(context, intent.getLongExtra(EXTRA_MANGA_ID, -1),
+                        intent.getLongExtra(EXTRA_CHAPTER_ID, -1))
+            }
+        }
+    }
+
+    /**
+     * Dismiss the notification
+     *
+     * @param notificationId the id of the notification
+     */
+    private fun dismissNotification(context: Context, notificationId: Int) {
+        context.notificationManager.cancel(notificationId)
+    }
+
+    /**
+     * Called to start share intent to share image
+     *
+     * @param context context of application
+     * @param path path of file
+     * @param notificationId id of notification
+     */
+    private fun shareImage(context: Context, path: String, notificationId: Int) {
+        // Create intent
+        val intent = Intent(Intent.ACTION_SEND).apply {
+            val uri = File(path).getUriCompat(context)
+            putExtra(Intent.EXTRA_STREAM, uri)
+            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
+            type = "image/*"
+        }
+        // Dismiss notification
+        dismissNotification(context, notificationId)
+        // Launch share activity
+        context.startActivity(intent)
+    }
+
+    /**
+     * Starts reader activity
+     *
+     * @param context context of application
+     * @param mangaId id of manga
+     * @param chapterId id of chapter
+     */
+    internal fun openChapter(context: Context, mangaId: Long, chapterId: Long) {
+        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
+            }
+            context.startActivity(intent)
+        } else {
+            context.toast(context.getString(R.string.chapter_error))
+        }
+    }
+
+    /**
+     * Called to delete image
+     *
+     * @param path path of file
+     * @param notificationId id of notification
+     */
+    private fun deleteImage(context: Context, path: String, notificationId: Int) {
+        // Dismiss notification
+        dismissNotification(context, notificationId)
+
+        // Delete file
+        File(path).deleteIfExists()
+    }
+
+    /**
+     * Method called when user wants to stop a library update
+     *
+     * @param context context of application
+     * @param notificationId id of notification
+     */
+    private fun cancelLibraryUpdate(context: Context, notificationId: Int) {
+        LibraryUpdateService.stop(context)
+        Handler().post { dismissNotification(context, notificationId) }
+    }
+
+    companion object {
+        private const val NAME = "NotificationReceiver"
+
+        // Called to launch share intent.
+        private const val ACTION_SHARE_IMAGE = "$ID.$NAME.SHARE_IMAGE"
+
+        // Called to delete image.
+        private const val ACTION_DELETE_IMAGE = "$ID.$NAME.DELETE_IMAGE"
+
+        // Called to cancel library update.
+        private const val ACTION_CANCEL_LIBRARY_UPDATE = "$ID.$NAME.CANCEL_LIBRARY_UPDATE"
+
+        // Called to open chapter
+        private const val ACTION_OPEN_CHAPTER = "$ID.$NAME.ACTION_OPEN_CHAPTER"
+
+        // Value containing file location.
+        private const val EXTRA_FILE_LOCATION = "$ID.$NAME.FILE_LOCATION"
+
+        // Called to resume downloads.
+        private const val ACTION_RESUME_DOWNLOADS = "$ID.$NAME.ACTION_RESUME_DOWNLOADS"
+
+        // Called to clear downloads.
+        private const val ACTION_CLEAR_DOWNLOADS = "$ID.$NAME.ACTION_CLEAR_DOWNLOADS"
+
+        // Called to dismiss notification.
+        private const val ACTION_DISMISS_NOTIFICATION = "$ID.$NAME.ACTION_DISMISS_NOTIFICATION"
+
+        // Value containing notification id.
+        private const val EXTRA_NOTIFICATION_ID = "$ID.$NAME.NOTIFICATION_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"
+
+        /**
+         * Returns a [PendingIntent] that resumes the download of a chapter
+         *
+         * @param context context of application
+         * @return [PendingIntent]
+         */
+        internal fun resumeDownloadsPendingBroadcast(context: Context): PendingIntent {
+            val intent = Intent(context, NotificationReceiver::class.java).apply {
+                action = ACTION_RESUME_DOWNLOADS
+            }
+            return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
+        }
+
+        /**
+         * Returns a [PendingIntent] that clears the download queue
+         *
+         * @param context context of application
+         * @return [PendingIntent]
+         */
+        internal fun clearDownloadsPendingBroadcast(context: Context): PendingIntent {
+            val intent = Intent(context, NotificationReceiver::class.java).apply {
+                action = ACTION_CLEAR_DOWNLOADS
+            }
+            return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
+        }
+
+        /**
+         * Returns [PendingIntent] that starts a service which dismissed the notification
+         *
+         * @param context context of application
+         * @param notificationId id of notification
+         * @return [PendingIntent]
+         */
+        internal fun dismissNotificationPendingBroadcast(context: Context, notificationId: Int): PendingIntent {
+            val intent = Intent(context, NotificationReceiver::class.java).apply {
+                action = ACTION_DISMISS_NOTIFICATION
+                putExtra(EXTRA_NOTIFICATION_ID, notificationId)
+            }
+            return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
+        }
+
+        /**
+         * Returns [PendingIntent] that starts a service which cancels the notification and starts a share activity
+         *
+         * @param context context of application
+         * @param path location path of file
+         * @param notificationId id of notification
+         * @return [PendingIntent]
+         */
+        internal fun shareImagePendingBroadcast(context: Context, path: String, notificationId: Int): PendingIntent {
+            val intent = Intent(context, NotificationReceiver::class.java).apply {
+                action = ACTION_SHARE_IMAGE
+                putExtra(EXTRA_FILE_LOCATION, path)
+                putExtra(EXTRA_NOTIFICATION_ID, notificationId)
+            }
+            return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
+        }
+
+        /**
+         * Returns [PendingIntent] that starts a service which removes an image from disk
+         *
+         * @param context context of application
+         * @param path location path of file
+         * @param notificationId id of notification
+         * @return [PendingIntent]
+         */
+        internal fun deleteImagePendingBroadcast(context: Context, path: String, notificationId: Int): PendingIntent {
+            val intent = Intent(context, NotificationReceiver::class.java).apply {
+                action = ACTION_DELETE_IMAGE
+                putExtra(EXTRA_FILE_LOCATION, path)
+                putExtra(EXTRA_NOTIFICATION_ID, notificationId)
+            }
+            return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
+        }
+
+        /**
+         * Returns [PendingIntent] that start 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
+                putExtra(EXTRA_MANGA_ID, manga.id)
+                putExtra(EXTRA_CHAPTER_ID, chapter.id)
+            }
+            return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
+        }
+
+        /**
+         * Returns [PendingIntent] that starts a service which stops the library update
+         *
+         * @param context context of application
+         * @return [PendingIntent]
+         */
+        internal fun cancelLibraryUpdatePendingBroadcast(context: Context): PendingIntent {
+            val intent = Intent(context, NotificationReceiver::class.java).apply {
+                action = ACTION_CANCEL_LIBRARY_UPDATE
+            }
+            return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
+        }
+    }
+}

+ 7 - 1
app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateCheckerJob.kt

@@ -1,5 +1,7 @@
 package eu.kanade.tachiyomi.data.updater
 
+import android.app.PendingIntent
+import android.content.Intent
 import android.support.v4.app.NotificationCompat
 import com.evernote.android.job.Job
 import com.evernote.android.job.JobManager
@@ -17,6 +19,10 @@ class UpdateCheckerJob : Job() {
                     if (result is GithubUpdateResult.NewUpdate) {
                         val url = result.release.downloadLink
 
+                        val intent = Intent(context, UpdateDownloaderService::class.java).apply {
+                            putExtra(UpdateDownloaderService.EXTRA_DOWNLOAD_URL, url)
+                        }
+
                         NotificationCompat.Builder(context).update {
                             setContentTitle(context.getString(R.string.app_name))
                             setContentText(context.getString(R.string.update_check_notification_update_available))
@@ -24,7 +30,7 @@ class UpdateCheckerJob : Job() {
                             // Download action
                             addAction(android.R.drawable.stat_sys_download_done,
                                     context.getString(R.string.action_download),
-                                    UpdateNotificationReceiver.downloadApkIntent(context, url))
+                                    PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
                         }
                     }
                     Job.Result.SUCCESS

+ 144 - 0
app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderReceiver.kt

@@ -0,0 +1,144 @@
+package eu.kanade.tachiyomi.data.updater
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.support.v4.app.NotificationCompat
+import eu.kanade.tachiyomi.Constants
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.notification.NotificationHandler
+import eu.kanade.tachiyomi.data.notification.NotificationReceiver
+import eu.kanade.tachiyomi.util.notificationManager
+import java.io.File
+import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
+
+/**
+ * Local [BroadcastReceiver] that runs on UI thread
+ * Notification calls from [UpdateDownloaderService] should be made from here.
+ */
+internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceiver() {
+
+    companion object {
+        private const val NAME = "UpdateDownloaderReceiver"
+
+        // Called to show initial notification.
+        internal const val NOTIFICATION_UPDATER_INITIAL = "$ID.$NAME.UPDATER_INITIAL"
+
+        // Called to show progress notification.
+        internal const val NOTIFICATION_UPDATER_PROGRESS = "$ID.$NAME.UPDATER_PROGRESS"
+
+        // Called to show install notification.
+        internal const val NOTIFICATION_UPDATER_INSTALL = "$ID.$NAME.UPDATER_INSTALL"
+
+        // Called to show error notification
+        internal const val NOTIFICATION_UPDATER_ERROR = "$ID.$NAME.UPDATER_ERROR"
+
+        // Value containing action of BroadcastReceiver
+        internal const val EXTRA_ACTION = "$ID.$NAME.ACTION"
+
+        // Value containing progress
+        internal const val EXTRA_PROGRESS = "$ID.$NAME.PROGRESS"
+
+        // Value containing apk path
+        internal const val EXTRA_APK_PATH = "$ID.$NAME.APK_PATH"
+
+        // Value containing apk url
+        internal const val EXTRA_APK_URL = "$ID.$NAME.APK_URL"
+    }
+
+    /**
+     * Notification shown to user
+     */
+    private val notification = NotificationCompat.Builder(context)
+
+    override fun onReceive(context: Context, intent: Intent) {
+        when (intent.getStringExtra(EXTRA_ACTION)) {
+            NOTIFICATION_UPDATER_INITIAL -> basicNotification()
+            NOTIFICATION_UPDATER_PROGRESS -> updateProgress(intent.getIntExtra(EXTRA_PROGRESS, 0))
+            NOTIFICATION_UPDATER_INSTALL -> installNotification(intent.getStringExtra(EXTRA_APK_PATH))
+            NOTIFICATION_UPDATER_ERROR -> errorNotification(intent.getStringExtra(EXTRA_APK_URL))
+        }
+    }
+
+    /**
+     * Called to show basic notification
+     */
+    private fun basicNotification() {
+        // Create notification
+        with(notification) {
+            setContentTitle(context.getString(R.string.app_name))
+            setContentText(context.getString(R.string.update_check_notification_download_in_progress))
+            setSmallIcon(android.R.drawable.stat_sys_download)
+            setOngoing(true)
+        }
+        notification.show()
+    }
+
+    /**
+     * Called to show progress notification
+     *
+     * @param progress progress of download
+     */
+    private fun updateProgress(progress: Int) {
+        with(notification) {
+            setProgress(100, progress, false)
+        }
+        notification.show()
+    }
+
+    /**
+     * Called to show install notification
+     *
+     * @param path path of file
+     */
+    private fun installNotification(path: String) {
+        // Prompt the user to install the new update.
+        with(notification) {
+            setContentText(context.getString(R.string.update_check_notification_download_complete))
+            setSmallIcon(android.R.drawable.stat_sys_download_done)
+            setProgress(0, 0, false)
+            // Install action
+            setContentIntent(NotificationHandler.installApkPendingActivity(context, File(path)))
+            addAction(R.drawable.ic_system_update_grey_24dp_img,
+                    context.getString(R.string.action_install),
+                    NotificationHandler.installApkPendingActivity(context, File(path)))
+            // Cancel action
+            addAction(R.drawable.ic_clear_grey_24dp_img,
+                    context.getString(R.string.action_cancel),
+                    NotificationReceiver.dismissNotificationPendingBroadcast(context, Constants.NOTIFICATION_UPDATER_ID))
+        }
+        notification.show()
+    }
+
+    /**
+     * Called to show error notification
+     *
+     * @param url url of apk
+     */
+    private fun errorNotification(url: String) {
+        // Prompt the user to retry the download.
+        with(notification) {
+            setContentText(context.getString(R.string.update_check_notification_download_error))
+            setSmallIcon(android.R.drawable.stat_sys_warning)
+            setProgress(0, 0, false)
+            // Retry action
+            addAction(R.drawable.ic_refresh_grey_24dp_img,
+                    context.getString(R.string.action_retry),
+                    UpdateDownloaderService.downloadApkPendingService(context, url))
+            // Cancel action
+            addAction(R.drawable.ic_clear_grey_24dp_img,
+                    context.getString(R.string.action_cancel),
+                    NotificationReceiver.dismissNotificationPendingBroadcast(context, Constants.NOTIFICATION_UPDATER_ID))
+        }
+        notification.show()
+    }
+
+    /**
+     * Shows a notification from this builder.
+     *
+     * @param id the id of the notification.
+     */
+    private fun NotificationCompat.Builder.show(id: Int = Constants.NOTIFICATION_UPDATER_ID) {
+        context.notificationManager.notify(id, build())
+    }
+}

+ 126 - 76
app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderService.kt

@@ -1,47 +1,49 @@
 package eu.kanade.tachiyomi.data.updater
 
 import android.app.IntentService
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
 import android.content.Context
 import android.content.Intent
-import android.support.v4.app.NotificationCompat
-import eu.kanade.tachiyomi.Constants.NOTIFICATION_UPDATER_ID
-import eu.kanade.tachiyomi.R
+import android.content.IntentFilter
+import android.os.Build
+import eu.kanade.tachiyomi.BuildConfig
 import eu.kanade.tachiyomi.data.network.GET
 import eu.kanade.tachiyomi.data.network.NetworkHelper
 import eu.kanade.tachiyomi.data.network.ProgressListener
 import eu.kanade.tachiyomi.data.network.newCallWithProgress
-import eu.kanade.tachiyomi.util.notificationManager
+import eu.kanade.tachiyomi.util.registerLocalReceiver
 import eu.kanade.tachiyomi.util.saveTo
+import eu.kanade.tachiyomi.util.sendLocalBroadcastSync
+import eu.kanade.tachiyomi.util.unregisterLocalReceiver
 import timber.log.Timber
 import uy.kohesive.injekt.injectLazy
 import java.io.File
 
 class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.java.name) {
-
-    companion object {
-        /**
-         * Download url.
-         */
-        const val EXTRA_DOWNLOAD_URL = "eu.kanade.APP_DOWNLOAD_URL"
-
-        /**
-         * Downloads a new update and let the user install the new version from a notification.
-         * @param context the application context.
-         * @param url the url to the new update.
-         */
-        fun downloadUpdate(context: Context, url: String) {
-            val intent = Intent(context, UpdateDownloaderService::class.java).apply {
-                putExtra(EXTRA_DOWNLOAD_URL, url)
-            }
-            context.startService(intent)
-        }
-    }
-
     /**
      * Network helper
      */
     private val network: NetworkHelper by injectLazy()
 
+    /**
+     * Local [BroadcastReceiver] that runs on UI thread
+     */
+    private val updaterNotificationReceiver = UpdateDownloaderReceiver(this)
+
+
+    override fun onCreate() {
+        super.onCreate()
+        // Register receiver
+        registerLocalReceiver(updaterNotificationReceiver, IntentFilter(INTENT_FILTER_NAME))
+    }
+
+    override fun onDestroy() {
+        // Unregister receiver
+        unregisterLocalReceiver(updaterNotificationReceiver)
+        super.onDestroy()
+    }
+
     override fun onHandleIntent(intent: Intent?) {
         if (intent == null) return
 
@@ -49,16 +51,14 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
         downloadApk(url)
     }
 
+    /**
+     * Called to start downloading apk of new update
+     *
+     * @param url url location of file
+     */
     fun downloadApk(url: String) {
-        val progressNotification = NotificationCompat.Builder(this)
-
-        progressNotification.update {
-            setContentTitle(getString(R.string.app_name))
-            setContentText(getString(R.string.update_check_notification_download_in_progress))
-            setSmallIcon(android.R.drawable.stat_sys_download)
-            setOngoing(true)
-        }
-
+        // Show notification download starting.
+        sendInitialBroadcast()
         // Progress of the download
         var savedProgress = 0
 
@@ -67,20 +67,16 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
                 val progress = (100 * bytesRead / contentLength).toInt()
                 if (progress > savedProgress) {
                     savedProgress = progress
-
-                    progressNotification.update { setProgress(100, progress, false) }
+                    sendProgressBroadcast(progress)
                 }
             }
         }
 
-        // Reference the context for later usage inside apply blocks.
-        val ctx = this
-
         try {
             // Download the new update.
             val response = network.client.newCallWithProgress(GET(url), progressListener).execute()
 
-            // File where the apk will be saved
+            // File where the apk will be saved.
             val apkFile = File(externalCacheDir, "update.apk")
 
             if (response.isSuccessful) {
@@ -89,48 +85,102 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
                 response.close()
                 throw Exception("Unsuccessful response")
             }
-
-            val installIntent = UpdateNotificationReceiver.installApkIntent(ctx, apkFile)
-
-            // Prompt the user to install the new update.
-            NotificationCompat.Builder(this).update {
-                setContentTitle(getString(R.string.app_name))
-                setContentText(getString(R.string.update_check_notification_download_complete))
-                setSmallIcon(android.R.drawable.stat_sys_download_done)
-                // Install action
-                setContentIntent(installIntent)
-                addAction(R.drawable.ic_system_update_grey_24dp_img,
-                        getString(R.string.action_install),
-                        installIntent)
-                // Cancel action
-                addAction(R.drawable.ic_clear_grey_24dp_img,
-                        getString(R.string.action_cancel),
-                        UpdateNotificationReceiver.cancelNotificationIntent(ctx))
-            }
-
+            sendInstallBroadcast(apkFile.absolutePath)
         } catch (error: Exception) {
             Timber.e(error)
+            sendErrorBroadcast(url)
+        }
+    }
 
-            // Prompt the user to retry the download.
-            NotificationCompat.Builder(this).update {
-                setContentTitle(getString(R.string.app_name))
-                setContentText(getString(R.string.update_check_notification_download_error))
-                setSmallIcon(android.R.drawable.stat_sys_download_done)
-                // Retry action
-                addAction(R.drawable.ic_refresh_grey_24dp_img,
-                        getString(R.string.action_retry),
-                        UpdateNotificationReceiver.downloadApkIntent(ctx, url))
-                // Cancel action
-                addAction(R.drawable.ic_clear_grey_24dp_img,
-                        getString(R.string.action_cancel),
-                        UpdateNotificationReceiver.cancelNotificationIntent(ctx))
-            }
+    /**
+     * Show notification download starting.
+     */
+    private fun sendInitialBroadcast() {
+        val intent = Intent(INTENT_FILTER_NAME).apply {
+            putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_INITIAL)
+        }
+        sendLocalBroadcastSync(intent)
+    }
+
+    /**
+     * Show notification progress changed
+     *
+     * @param progress progress of download
+     */
+    private fun sendProgressBroadcast(progress: Int) {
+        val intent = Intent(INTENT_FILTER_NAME).apply {
+            putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_PROGRESS)
+            putExtra(UpdateDownloaderReceiver.EXTRA_PROGRESS, progress)
+        }
+        // Prevents not showing of install notification TODO weird Android N bug. Find out what goes wrong
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || progress <= 95) {
+            // Show download progress notification.
+            sendLocalBroadcastSync(intent)
         }
     }
 
-    fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) {
-        block()
-        notificationManager.notify(NOTIFICATION_UPDATER_ID, build())
+    /**
+     * Show install notification.
+     *
+     * @param path location of file
+     */
+    private fun sendInstallBroadcast(path: String){
+        val intent = Intent(INTENT_FILTER_NAME).apply {
+            putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_INSTALL)
+            putExtra(UpdateDownloaderReceiver.EXTRA_APK_PATH, path)
+        }
+        sendLocalBroadcastSync(intent)
+    }
+
+    /**
+     * Show error notification.
+     *
+     * @param url url of file
+     */
+    private fun sendErrorBroadcast(url: String){
+        val intent = Intent(INTENT_FILTER_NAME).apply {
+            putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_ERROR)
+            putExtra(UpdateDownloaderReceiver.EXTRA_APK_URL, url)
+        }
+        sendLocalBroadcastSync(intent)
     }
 
-}
+    companion object {
+        /**
+         * Name of Local BroadCastReceiver.
+         */
+        private val INTENT_FILTER_NAME = UpdateDownloaderService::class.java.name
+
+        /**
+         * Download url.
+         */
+        internal const val EXTRA_DOWNLOAD_URL = "${BuildConfig.APPLICATION_ID}.UpdateDownloaderService.DOWNLOAD_URL"
+
+        /**
+         * Downloads a new update and let the user install the new version from a notification.
+         * @param context the application context.
+         * @param url the url to the new update.
+         */
+        fun downloadUpdate(context: Context, url: String) {
+            val intent = Intent(context, UpdateDownloaderService::class.java).apply {
+                putExtra(EXTRA_DOWNLOAD_URL, url)
+            }
+            context.startService(intent)
+        }
+
+        /**
+         * Returns [PendingIntent] that starts a service which downloads the apk specified in url.
+         *
+         * @param url the url to the new update.
+         * @return [PendingIntent]
+         */
+        internal fun downloadApkPendingService(context: Context, url: String): PendingIntent {
+            val intent = Intent(context, UpdateDownloaderService::class.java).apply {
+                putExtra(EXTRA_DOWNLOAD_URL, url)
+            }
+            return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+        }
+    }
+}
+
+

+ 0 - 70
app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateNotificationReceiver.kt

@@ -1,70 +0,0 @@
-package eu.kanade.tachiyomi.data.updater
-
-import android.app.PendingIntent
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import android.net.Uri
-import android.os.Build
-import android.support.v4.content.FileProvider
-import eu.kanade.tachiyomi.BuildConfig
-import eu.kanade.tachiyomi.Constants.NOTIFICATION_UPDATER_ID
-import eu.kanade.tachiyomi.util.notificationManager
-import java.io.File
-
-class UpdateNotificationReceiver : BroadcastReceiver() {
-
-    override fun onReceive(context: Context, intent: Intent) {
-        when (intent.action) {
-            ACTION_CANCEL_NOTIFICATION -> cancelNotification(context)
-        }
-    }
-
-    companion object {
-        // Cancel notification action
-        const val ACTION_CANCEL_NOTIFICATION = "eu.kanade.CANCEL_NOTIFICATION"
-
-        fun cancelNotificationIntent(context: Context): PendingIntent {
-            val intent = Intent(context, UpdateNotificationReceiver::class.java).apply {
-                action = ACTION_CANCEL_NOTIFICATION
-            }
-            return PendingIntent.getBroadcast(context, 0, intent, 0)
-        }
-
-        /**
-         * Prompt user with apk install intent
-         *
-         * @param context context
-         * @param file file of apk that is installed
-         */
-        fun installApkIntent(context: Context, file: File): PendingIntent {
-            val intent = Intent(Intent.ACTION_VIEW).apply {
-                val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
-                    FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file)
-                else Uri.fromFile(file)
-                setDataAndType(uri, "application/vnd.android.package-archive")
-                flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
-            }
-            cancelNotification(context)
-            return PendingIntent.getActivity(context, 0, intent, 0)
-        }
-
-        /**
-         * Downloads a new update and let the user install the new version from a notification.
-         *
-         * @param context the application context.
-         * @param url the url to the new update.
-         */
-        fun downloadApkIntent(context: Context, url: String): PendingIntent {
-            val intent = Intent(context, UpdateDownloaderService::class.java).apply {
-                putExtra(UpdateDownloaderService.EXTRA_DOWNLOAD_URL, url)
-            }
-            return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
-        }
-
-        fun cancelNotification(context: Context) {
-            context.notificationManager.cancel(NOTIFICATION_UPDATER_ID)
-        }
-    }
-
-}

+ 32 - 39
app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadFragment.kt → app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadActivity.kt

@@ -2,15 +2,17 @@ package eu.kanade.tachiyomi.ui.download
 
 import android.os.Bundle
 import android.support.v7.widget.LinearLayoutManager
-import android.view.*
+import android.view.Menu
+import android.view.MenuItem
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.download.DownloadService
 import eu.kanade.tachiyomi.data.download.model.Download
 import eu.kanade.tachiyomi.data.source.model.Page
-import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
-import eu.kanade.tachiyomi.ui.main.MainActivity
+import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
 import eu.kanade.tachiyomi.util.plusAssign
+import kotlinx.android.synthetic.main.activity_main.*
 import kotlinx.android.synthetic.main.fragment_download_queue.*
+import kotlinx.android.synthetic.main.toolbar.*
 import nucleus.factory.RequiresPresenter
 import rx.Observable
 import rx.Subscription
@@ -20,19 +22,18 @@ import java.util.*
 import java.util.concurrent.TimeUnit
 
 /**
- * Fragment that shows the currently active downloads.
+ * Activity that shows the currently active downloads.
  * Uses R.layout.fragment_download_queue.
  */
 @RequiresPresenter(DownloadPresenter::class)
-class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
-
+class DownloadActivity : BaseRxActivity<DownloadPresenter>() {
     /**
      * Adapter containing the active downloads.
      */
     private lateinit var adapter: DownloadAdapter
 
     /**
-     * Subscription list to be cleared during [onDestroyView].
+     * Subscription list to be cleared during [onDestroy].
      */
     private val subscriptions by lazy { CompositeSubscription() }
 
@@ -46,38 +47,22 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
      */
     private var isRunning: Boolean = false
 
-    companion object {
-        /**
-         * Creates a new instance of this fragment.
-         *
-         * @return a new instance of [DownloadFragment].
-         */
-        fun newInstance(): DownloadFragment {
-            return DownloadFragment()
-        }
-    }
-
     override fun onCreate(savedState: Bundle?) {
+        setAppTheme()
         super.onCreate(savedState)
-        setHasOptionsMenu(true)
-    }
-
-    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View {
-        return inflater.inflate(R.layout.fragment_download_queue, container, false)
-    }
-
-    override fun onViewCreated(view: View, savedState: Bundle?) {
+        setContentView(R.layout.activity_download_manager)
+        setupToolbar(toolbar)
         setToolbarTitle(R.string.label_download_queue)
 
         // Check if download queue is empty and update information accordingly.
         setInformationView()
 
         // Initialize adapter.
-        adapter = DownloadAdapter(activity)
+        adapter = DownloadAdapter(this)
         recycler.adapter = adapter
 
         // Set the layout manager for the recycler and fixed size.
-        recycler.layoutManager = LinearLayoutManager(activity)
+        recycler.layoutManager = LinearLayoutManager(this)
         recycler.setHasFixedSize(true)
 
         // Suscribe to changes
@@ -94,20 +79,21 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
                 .subscribe { onUpdateDownloadedPages(it) }
     }
 
-    override fun onDestroyView() {
+    override fun onDestroy() {
         for (subscription in progressSubscriptions.values) {
             subscription.unsubscribe()
         }
         progressSubscriptions.clear()
         subscriptions.clear()
-        super.onDestroyView()
+        super.onDestroy()
     }
 
-    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
-        inflater.inflate(R.menu.download_queue, menu)
+    override fun onCreateOptionsMenu(menu: Menu): Boolean {
+        menuInflater.inflate(R.menu.download_queue, menu)
+        return true
     }
 
-    override fun onPrepareOptionsMenu(menu: Menu) {
+    override fun onPrepareOptionsMenu(menu: Menu): Boolean {
         // Set start button visibility.
         menu.findItem(R.id.start_queue).isVisible = !isRunning && !presenter.downloadQueue.isEmpty()
 
@@ -116,14 +102,18 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
 
         // Set clear button visibility.
         menu.findItem(R.id.clear_queue).isVisible = !presenter.downloadQueue.isEmpty()
+        return true
     }
 
     override fun onOptionsItemSelected(item: MenuItem): Boolean {
         when (item.itemId) {
-            R.id.start_queue -> DownloadService.start(activity)
-            R.id.pause_queue -> DownloadService.stop(activity)
+            R.id.start_queue -> DownloadService.start(this)
+            R.id.pause_queue -> {
+                DownloadService.stop(this)
+                presenter.pauseDownloads()
+            }
             R.id.clear_queue -> {
-                DownloadService.stop(activity)
+                DownloadService.stop(this)
                 presenter.clearQueue()
             }
             else -> return super.onOptionsItemSelected(item)
@@ -198,7 +188,7 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
      */
     private fun onQueueStatusChange(running: Boolean) {
         isRunning = running
-        activity.supportInvalidateOptionsMenu()
+        supportInvalidateOptionsMenu()
 
         // Check if download queue is empty and update information accordingly.
         setInformationView()
@@ -210,7 +200,7 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
      * @param downloads the downloads from the queue.
      */
     fun onNextDownloads(downloads: List<Download>) {
-        activity.supportInvalidateOptionsMenu()
+        supportInvalidateOptionsMenu()
         setInformationView()
         adapter.setItems(downloads)
     }
@@ -247,8 +237,11 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
      * Set information view when queue is empty
      */
     private fun setInformationView() {
-        (activity as MainActivity).updateEmptyView(presenter.downloadQueue.isEmpty(),
+        updateEmptyView(presenter.downloadQueue.isEmpty(),
                 R.string.information_no_downloads, R.drawable.ic_file_download_black_128dp)
     }
 
+    fun updateEmptyView(show: Boolean, textResource: Int, drawable: Int) {
+        if (show) empty_view.show(drawable, textResource) else empty_view.hide()
+    }
 }

+ 11 - 4
app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadPresenter.kt

@@ -12,9 +12,9 @@ import uy.kohesive.injekt.injectLazy
 import java.util.*
 
 /**
- * Presenter of [DownloadFragment].
+ * Presenter of [DownloadActivity].
  */
-class DownloadPresenter : BasePresenter<DownloadFragment>() {
+class DownloadPresenter : BasePresenter<DownloadActivity>() {
 
     /**
      * Download manager.
@@ -33,7 +33,7 @@ class DownloadPresenter : BasePresenter<DownloadFragment>() {
         downloadQueue.getUpdatedObservable()
                 .observeOn(AndroidSchedulers.mainThread())
                 .map { ArrayList(it) }
-                .subscribeLatestCache(DownloadFragment::onNextDownloads, { view, error ->
+                .subscribeLatestCache(DownloadActivity::onNextDownloads, { view, error ->
                     Timber.e(error)
                 })
     }
@@ -48,6 +48,13 @@ class DownloadPresenter : BasePresenter<DownloadFragment>() {
                 .onBackpressureBuffer()
     }
 
+    /**
+     * Pauses the download queue.
+     */
+    fun pauseDownloads() {
+        downloadManager.pauseDownloads()
+    }
+
     /**
      * Clears the download queue.
      */
@@ -55,4 +62,4 @@ class DownloadPresenter : BasePresenter<DownloadFragment>() {
         downloadManager.clearQueue()
     }
 
-}
+}

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

@@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 import eu.kanade.tachiyomi.ui.backup.BackupFragment
 import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
 import eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment
-import eu.kanade.tachiyomi.ui.download.DownloadFragment
+import eu.kanade.tachiyomi.ui.download.DownloadActivity
 import eu.kanade.tachiyomi.ui.latest_updates.LatestUpdatesFragment
 import eu.kanade.tachiyomi.ui.library.LibraryFragment
 import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersFragment
@@ -63,7 +63,7 @@ class MainActivity : BaseActivity() {
                 R.id.nav_drawer_recently_read -> setFragment(RecentlyReadFragment.newInstance(), id)
                 R.id.nav_drawer_catalogues -> setFragment(CatalogueFragment.newInstance(), id)
                 R.id.nav_drawer_latest_updates -> setFragment(LatestUpdatesFragment.newInstance(), id)
-                R.id.nav_drawer_downloads -> setFragment(DownloadFragment.newInstance(), id)
+                R.id.nav_drawer_downloads -> startActivity(Intent(this, DownloadActivity::class.java))
                 R.id.nav_drawer_settings -> {
                     val intent = Intent(this, SettingsActivity::class.java)
                     startActivityForResult(intent, REQUEST_OPEN_SETTINGS)

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

@@ -19,7 +19,7 @@ import eu.kanade.tachiyomi.data.source.online.OnlineSource
 import eu.kanade.tachiyomi.data.track.TrackManager
 import eu.kanade.tachiyomi.data.track.TrackUpdateService
 import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
-import eu.kanade.tachiyomi.ui.reader.notification.ImageNotifier
+import eu.kanade.tachiyomi.ui.reader.SaveImageNotifier
 import eu.kanade.tachiyomi.util.DiskUtil
 import eu.kanade.tachiyomi.util.RetryWithDelay
 import eu.kanade.tachiyomi.util.SharedData
@@ -562,7 +562,7 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
             return
 
         // Used to show image notification.
-        val imageNotifier = ImageNotifier(context)
+        val imageNotifier = SaveImageNotifier(context)
 
         // Remove the notification if it already exists (user feedback).
         imageNotifier.onClear()

+ 8 - 6
app/src/main/java/eu/kanade/tachiyomi/ui/reader/notification/ImageNotifier.kt → app/src/main/java/eu/kanade/tachiyomi/ui/reader/SaveImageNotifier.kt

@@ -1,4 +1,4 @@
-package eu.kanade.tachiyomi.ui.reader.notification
+package eu.kanade.tachiyomi.ui.reader
 
 import android.content.Context
 import android.graphics.Bitmap
@@ -7,13 +7,15 @@ import com.bumptech.glide.Glide
 import com.bumptech.glide.load.engine.DiskCacheStrategy
 import eu.kanade.tachiyomi.Constants
 import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.notification.NotificationHandler
+import eu.kanade.tachiyomi.data.notification.NotificationReceiver
 import eu.kanade.tachiyomi.util.notificationManager
 import java.io.File
 
 /**
  * Class used to show BigPictureStyle notifications
  */
-class ImageNotifier(private val context: Context) {
+class SaveImageNotifier(private val context: Context) {
     /**
      * Notification builder.
      */
@@ -58,15 +60,15 @@ class ImageNotifier(private val context: Context) {
             if (!mActions.isEmpty())
                 mActions.clear()
 
-            setContentIntent(ImageNotificationReceiver.showImageIntent(context, file))
+            setContentIntent(NotificationHandler.openImagePendingActivity(context, file))
             // Share action
             addAction(R.drawable.ic_share_grey_24dp,
-                      context.getString(R.string.action_share),
-                      ImageNotificationReceiver.shareImageIntent(context, file))
+                    context.getString(R.string.action_share),
+                    NotificationReceiver.shareImagePendingBroadcast(context, file.absolutePath, notificationId))
             // Delete action
             addAction(R.drawable.ic_delete_grey_24dp,
                     context.getString(R.string.action_delete),
-                    ImageNotificationReceiver.deleteImageIntent(context, file.absolutePath, notificationId))
+                    NotificationReceiver.deleteImagePendingBroadcast(context, file.absolutePath, notificationId))
             updateNotification()
 
         }

+ 0 - 84
app/src/main/java/eu/kanade/tachiyomi/ui/reader/notification/ImageNotificationReceiver.kt

@@ -1,84 +0,0 @@
-package eu.kanade.tachiyomi.ui.reader.notification
-
-import android.app.PendingIntent
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import android.support.v4.content.FileProvider
-import eu.kanade.tachiyomi.BuildConfig
-import eu.kanade.tachiyomi.util.notificationManager
-import java.io.File
-import eu.kanade.tachiyomi.Constants.NOTIFICATION_DOWNLOAD_IMAGE_ID as defaultNotification
-
-/**
- * The BroadcastReceiver of [ImageNotifier]
- * Intent calls should be made from this class.
- */
-class ImageNotificationReceiver : BroadcastReceiver() {
-    override fun onReceive(context: Context, intent: Intent) {
-        when (intent.action) {
-            ACTION_DELETE_IMAGE -> {
-                deleteImage(intent.getStringExtra(EXTRA_FILE_LOCATION))
-                context.notificationManager.cancel(intent.getIntExtra(NOTIFICATION_ID, defaultNotification))
-            }
-        }
-    }
-
-    /**
-     * Called to delete image
-     *
-     * @param path path of file
-     */
-    private fun deleteImage(path: String) {
-        val file = File(path)
-        if (file.exists()) file.delete()
-    }
-
-    companion object {
-        private const val ACTION_DELETE_IMAGE = "eu.kanade.DELETE_IMAGE"
-
-        private const val EXTRA_FILE_LOCATION = "file_location"
-
-        private const val NOTIFICATION_ID = "notification_id"
-
-        /**
-         * Called to start share intent to share image
-         *
-         * @param context context of application
-         * @param file file that contains image
-         */
-        internal fun shareImageIntent(context: Context, file: File): PendingIntent {
-            val intent = Intent(Intent.ACTION_SEND).apply {
-                val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file)
-                putExtra(Intent.EXTRA_STREAM, uri)
-                type = "image/*"
-                flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
-            }
-            return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
-        }
-
-        /**
-         * Called to show image in gallery application
-         *
-         * @param context context of application
-         * @param file file that contains image
-         */
-        internal fun showImageIntent(context: Context, file: File): PendingIntent {
-            val intent = Intent(Intent.ACTION_VIEW).apply {
-                val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file)
-                setDataAndType(uri, "image/*")
-                flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
-            }
-            return PendingIntent.getActivity(context, 0, intent, 0)
-        }
-
-        internal fun deleteImageIntent(context: Context, path: String, notificationId: Int): PendingIntent {
-            val intent = Intent(context, ImageNotificationReceiver::class.java).apply {
-                action = ACTION_DELETE_IMAGE
-                putExtra(EXTRA_FILE_LOCATION, path)
-                putExtra(NOTIFICATION_ID, notificationId)
-            }
-            return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
-        }
-    }
-}

+ 41 - 0
app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt

@@ -2,7 +2,10 @@ package eu.kanade.tachiyomi.util
 
 import android.app.Notification
 import android.app.NotificationManager
+import android.content.BroadcastReceiver
 import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
 import android.content.pm.PackageManager
 import android.content.res.Resources
 import android.net.ConnectivityManager
@@ -10,6 +13,7 @@ import android.os.PowerManager
 import android.support.annotation.StringRes
 import android.support.v4.app.NotificationCompat
 import android.support.v4.content.ContextCompat
+import android.support.v4.content.LocalBroadcastManager
 import android.widget.Toast
 
 /**
@@ -95,3 +99,40 @@ val Context.connectivityManager: ConnectivityManager
 val Context.powerManager: PowerManager
     get() = getSystemService(Context.POWER_SERVICE) as PowerManager
 
+/**
+ * Function used to send a local broadcast asynchronous
+ *
+ * @param intent intent that contains broadcast information
+ */
+fun Context.sendLocalBroadcast(intent:Intent){
+    LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
+}
+
+/**
+ * Function used to send a local broadcast synchronous
+ *
+ * @param intent intent that contains broadcast information
+ */
+fun Context.sendLocalBroadcastSync(intent: Intent) {
+    LocalBroadcastManager.getInstance(this).sendBroadcastSync(intent)
+}
+
+/**
+ * Function used to register local broadcast
+ *
+ * @param receiver receiver that gets registered.
+ */
+fun Context.registerLocalReceiver(receiver: BroadcastReceiver, filter: IntentFilter ){
+    LocalBroadcastManager.getInstance(this).registerReceiver(receiver, filter)
+}
+
+/**
+ * Function used to unregister local broadcast
+ *
+ * @param receiver receiver that gets unregistered.
+ */
+fun Context.unregisterLocalReceiver(receiver: BroadcastReceiver){
+    LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver)
+}
+
+

+ 33 - 0
app/src/main/java/eu/kanade/tachiyomi/util/FileExtensions.kt

@@ -0,0 +1,33 @@
+package eu.kanade.tachiyomi.util
+
+import android.content.Context
+import android.net.Uri
+import android.os.Build
+import android.support.v4.content.FileProvider
+import eu.kanade.tachiyomi.BuildConfig
+import java.io.File
+
+/**
+ * Returns the uri of a file
+ *
+ * @param context context of application
+ */
+fun File.getUriCompat(context: Context): Uri {
+    val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
+        FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", this)
+    else Uri.fromFile(this)
+    return uri
+}
+
+/**
+ * Deletes file if exists
+ *
+ * @return success of file deletion
+ */
+fun File.deleteIfExists(): Boolean {
+    if (this.exists()) {
+        this.delete()
+        return true
+    }
+    return false
+}

BIN
app/src/main/res/drawable-hdpi/ic_av_pause_grey_24dp_img.png


BIN
app/src/main/res/drawable-hdpi/ic_av_play_arrow_grey_img.png


BIN
app/src/main/res/drawable-mdpi/ic_av_pause_grey_24dp_img.png


BIN
app/src/main/res/drawable-mdpi/ic_av_play_arrow_grey_img.png


BIN
app/src/main/res/drawable-xhdpi/ic_av_pause_grey_24dp_img.png


BIN
app/src/main/res/drawable-xhdpi/ic_av_play_arrow_grey_img.png


BIN
app/src/main/res/drawable-xxhdpi/ic_av_pause_grey_24dp_img.png


BIN
app/src/main/res/drawable-xxhdpi/ic_av_play_arrow_grey_img.png


BIN
app/src/main/res/drawable-xxxhdpi/ic_av_pause_grey_24dp_img.png


BIN
app/src/main/res/drawable-xxxhdpi/ic_av_play_arrow_grey_img.png


+ 42 - 0
app/src/main/res/layout/activity_download_manager.xml

@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:fitsSystemWindows="true">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+
+        <android.support.design.widget.AppBarLayout
+            android:id="@+id/appbar"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+
+            <include layout="@layout/toolbar"/>
+
+        </android.support.design.widget.AppBarLayout>
+
+        <FrameLayout
+            android:id="@+id/frame_container"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent">
+
+            <android.support.v7.widget.RecyclerView
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:id="@+id/recycler"
+                tools:listitem="@layout/item_download"/>
+
+            <eu.kanade.tachiyomi.widget.EmptyView
+                android:id="@+id/empty_view"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:visibility="gone"/>
+        </FrameLayout>
+    </LinearLayout>
+
+</android.support.design.widget.CoordinatorLayout>

+ 2 - 1
app/src/main/res/menu/menu_navigation.xml

@@ -26,7 +26,8 @@
         <item
             android:id="@+id/nav_drawer_downloads"
             android:icon="@drawable/ic_file_download_black_24dp"
-            android:title="@string/label_download_queue" />
+            android:title="@string/label_download_queue"
+            android:checkable="false" />
     </group>
     <group android:id="@+id/group_settings"
            android:checkableBehavior="single">

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

@@ -260,6 +260,7 @@
     <string name="chapter_downloading">Downloading</string>
     <string name="chapter_downloading_progress">Downloading (%1$d/%2$d)</string>
     <string name="chapter_error">Error</string>
+    <string name="chapter_paused">Paused</string>
     <string name="fetch_chapters_error">Error while fetching chapters</string>
     <string name="show_title">Show title</string>
     <string name="show_chapter_number">Show chapter number</string>
@@ -383,5 +384,6 @@
     <string name="download_notifier_page_ready_error">A page is not loaded</string>
     <string name="download_notifier_text_only_wifi">No wifi connection available</string>
     <string name="download_notifier_no_network">No network connection available</string>
+    <string name="download_notifier_download_paused">Download paused</string>
 
 </resources>