Added download notifications, resolves #260 (#289)

This commit is contained in:
Bram van de Kerkhof 2016-05-07 23:09:14 +02:00 committed by inorichi
parent 8f144316a6
commit ed77c60283
8 changed files with 262 additions and 34 deletions

View File

@ -0,0 +1,8 @@
package eu.kanade.tachiyomi
object Constants {
const val NOTIFICATION_LIBRARY_ID = 1
const val NOTIFICATION_UPDATER_ID = 2
const val NOTIFICATION_DOWNLOAD_CHAPTER_ID = 3
const val NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID = 4
}

View File

@ -5,6 +5,7 @@ import android.net.Uri
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
@ -14,7 +15,10 @@ import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.source.SourceManager import eu.kanade.tachiyomi.data.source.SourceManager
import eu.kanade.tachiyomi.data.source.base.Source import eu.kanade.tachiyomi.data.source.base.Source
import eu.kanade.tachiyomi.data.source.model.Page import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.util.* import eu.kanade.tachiyomi.util.DiskUtils
import eu.kanade.tachiyomi.util.DynamicConcurrentMergeOperator
import eu.kanade.tachiyomi.util.UrlUtil
import eu.kanade.tachiyomi.util.saveImageTo
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
@ -35,6 +39,8 @@ class DownloadManager(private val context: Context, private val sourceManager: S
val runningSubject = BehaviorSubject.create<Boolean>() val runningSubject = BehaviorSubject.create<Boolean>()
private var downloadsSubscription: Subscription? = null private var downloadsSubscription: Subscription? = null
val downloadNotifier by lazy { DownloadNotifier(context) }
private val threadsSubject = BehaviorSubject.create<Int>() private val threadsSubject = BehaviorSubject.create<Int>()
private var threadsSubscription: Subscription? = null private var threadsSubscription: Subscription? = null
@ -48,10 +54,14 @@ class DownloadManager(private val context: Context, private val sourceManager: S
private set private set
private fun initializeSubscriptions() { private fun initializeSubscriptions() {
downloadsSubscription?.unsubscribe() downloadsSubscription?.unsubscribe()
threadsSubscription = preferences.downloadThreads().asObservable() threadsSubscription = preferences.downloadThreads().asObservable()
.subscribe { threadsSubject.onNext(it) } .subscribe {
threadsSubject.onNext(it)
downloadNotifier.multipleDownloadThreads = it > 1
}
downloadsSubscription = downloadsQueueSubject.flatMap { Observable.from(it) } downloadsSubscription = downloadsQueueSubject.flatMap { Observable.from(it) }
.lift(DynamicConcurrentMergeOperator<Download, Download>({ downloadChapter(it) }, threadsSubject)) .lift(DynamicConcurrentMergeOperator<Download, Download>({ downloadChapter(it) }, threadsSubject))
@ -60,7 +70,9 @@ class DownloadManager(private val context: Context, private val sourceManager: S
.subscribe({ .subscribe({
// Delete successful downloads from queue // Delete successful downloads from queue
if (it.status == Download.DOWNLOADED) { if (it.status == Download.DOWNLOADED) {
// remove downloaded chapter from queue
queue.del(it) queue.del(it)
downloadNotifier.onProgressChange(queue)
} }
if (areAllDownloadsFinished()) { if (areAllDownloadsFinished()) {
DownloadService.stop(context) DownloadService.stop(context)
@ -68,7 +80,7 @@ class DownloadManager(private val context: Context, private val sourceManager: S
}, { e -> }, { e ->
DownloadService.stop(context) DownloadService.stop(context)
Timber.e(e, e.message) Timber.e(e, e.message)
context.toast(e.message) downloadNotifier.onError(e.message)
}) })
if (!isRunning) { if (!isRunning) {
@ -114,6 +126,12 @@ class DownloadManager(private val context: Context, private val sourceManager: S
pending.add(download) pending.add(download)
} }
} }
// Initialize queue size
downloadNotifier.initialQueueSize = queue.size
// Show notification
downloadNotifier.onProgressChange(queue)
if (isRunning) downloadsQueueSubject.onNext(pending) if (isRunning) downloadsQueueSubject.onNext(pending)
} }
@ -164,34 +182,40 @@ class DownloadManager(private val context: Context, private val sourceManager: S
DiskUtils.createDirectory(download.directory) DiskUtils.createDirectory(download.directory)
val pageListObservable = if (download.pages == null) val pageListObservable = if (download.pages == null)
// Pull page list from network and add them to download object // Pull page list from network and add them to download object
download.source.pullPageListFromNetwork(download.chapter.url) download.source.pullPageListFromNetwork(download.chapter.url)
.doOnNext { pages -> .doOnNext { pages ->
download.pages = pages download.pages = pages
savePageList(download) savePageList(download)
} }
else else
// Or if the page list already exists, start from the file // Or if the page list already exists, start from the file
Observable.just(download.pages) Observable.just(download.pages)
return Observable.defer { pageListObservable return Observable.defer {
.doOnNext { pages -> pageListObservable
download.downloadedImages = 0 .doOnNext { pages ->
download.status = Download.DOWNLOADING download.downloadedImages = 0
} download.status = Download.DOWNLOADING
// Get all the URLs to the source images, fetch pages if necessary }
.flatMap { download.source.getAllImageUrlsFromPageList(it) } // Get all the URLs to the source images, fetch pages if necessary
// Start downloading images, consider we can have downloaded images already .flatMap { download.source.getAllImageUrlsFromPageList(it) }
.concatMap { page -> getOrDownloadImage(page, download) } // Start downloading images, consider we can have downloaded images already
// Do after download completes .concatMap { page -> getOrDownloadImage(page, download) }
.doOnCompleted { onDownloadCompleted(download) } // Do when page is downloaded.
.toList() .doOnNext {
.map { pages -> download } downloadNotifier.onProgressChange(download, queue)
// If the page list threw, it will resume here }
.onErrorResumeNext { error -> // Do after download completes
download.status = Download.ERROR .doOnCompleted { onDownloadCompleted(download) }
Observable.just(download) .toList()
} .map { pages -> download }
// If the page list threw, it will resume here
.onErrorResumeNext { error ->
download.status = Download.ERROR
downloadNotifier.onError(error.message, download.chapter.name)
Observable.just(download)
}
}.subscribeOn(Schedulers.io()) }.subscribeOn(Schedulers.io())
} }
@ -297,11 +321,15 @@ class DownloadManager(private val context: Context, private val sourceManager: S
// If any page has an error, the download result will be error // If any page has an error, the download result will be error
for (page in download.pages) { for (page in download.pages) {
actualProgress += page.progress actualProgress += page.progress
if (page.status != Page.READY) status = Download.ERROR if (page.status != Page.READY) {
status = Download.ERROR
downloadNotifier.onError(context.getString(R.string.download_notifier_page_ready_error), download.chapter.name)
}
} }
// Ensure that the chapter folder has all the images // Ensure that the chapter folder has all the images
if (!isChapterDownloaded(download.directory, download.pages)) { if (!isChapterDownloaded(download.directory, download.pages)) {
status = Download.ERROR status = Download.ERROR
downloadNotifier.onError(context.getString(R.string.download_notifier_page_error), download.chapter.name)
} }
download.totalProgress = actualProgress download.totalProgress = actualProgress
download.status = status download.status = status
@ -399,13 +427,19 @@ class DownloadManager(private val context: Context, private val sourceManager: S
return !pending.isEmpty() return !pending.isEmpty()
} }
fun stopDownloads() { fun stopDownloads(error: String = "") {
destroySubscriptions() destroySubscriptions()
for (download in queue) { for (download in queue) {
if (download.status == Download.DOWNLOADING) { if (download.status == Download.DOWNLOADING) {
download.status = Download.ERROR download.status = Download.ERROR
} }
} }
downloadNotifier.onError(error)
}
fun clearQueue() {
queue.clear()
downloadNotifier.onClear()
} }
} }

View File

@ -0,0 +1,180 @@
package eu.kanade.tachiyomi.data.download
import android.content.Context
import android.support.v4.app.NotificationCompat
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.util.notificationManager
/**
* DownloadNotifier is used to show notifications when downloading one or multiple chapters.
* @param context context of application
*/
class DownloadNotifier(private val context: Context) {
/**
* Notification builder.
*/
private val notificationBuilder = NotificationCompat.Builder(context)
/**
* Id of the notification.
*/
private val notificationId = Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ID
/**
* Status of download. Used for correct notification icon.
*/
private var isDownloading = false
/**
* The size of queue on start download.
*/
internal var initialQueueSize = 0
/**
* Simultaneous download setting > 1.
*/
internal var multipleDownloadThreads = false
/**
* Called when download progress changes.
* Note: Only accepted when multi download active.
* @param queue the queue containing downloads.
*/
internal fun onProgressChange(queue: DownloadQueue) {
// If single download mode return.
if (!multipleDownloadThreads)
return
// Update progress.
doOnProgressChange(null, queue)
}
/**
* Called when download progress changes
* Note: Only accepted when single download active
* @param download download object containing download information
* @param queue the queue containing downloads
*/
internal fun onProgressChange(download: Download, queue: DownloadQueue) {
// If multi download mode return.
if (multipleDownloadThreads)
return
// Update progress.
doOnProgressChange(download, queue)
}
/**
* Show notification progress of chapter
* @param download download object containing download information
* @param queue the queue containing downloads
*/
private fun doOnProgressChange(download: Download?, queue: DownloadQueue) {
// Check if download is completed
if (multipleDownloadThreads) {
if (queue.isEmpty()) {
onComplete(null)
return
}
} else {
if (download != null && download.pages.size == download.downloadedImages) {
onComplete(download)
return
}
}
// Create notification
with (notificationBuilder)
{
// Check if icon needs refresh
if (!isDownloading) {
setSmallIcon(android.R.drawable.stat_sys_download)
isDownloading = true
}
if (multipleDownloadThreads) {
setContentTitle(context.getString(R.string.app_name))
setContentText(context.getString(R.string.chapter_downloading_progress)
.format(initialQueueSize - queue.size, initialQueueSize))
setProgress(initialQueueSize, initialQueueSize - queue.size, false)
} else {
download?.let {
if (it.chapter.name.length >= 33)
setContentTitle(it.chapter.name.slice(IntRange(0, 30)).plus("..."))
else
setContentTitle(it.chapter.name)
setContentText(context.getString(R.string.chapter_downloading_progress)
.format(it.downloadedImages, it.pages.size))
setProgress(it.pages.size, it.downloadedImages, false)
}
}
}
// Displays the progress bar on notification
context.notificationManager.notify(notificationId, notificationBuilder.build())
}
/**
* Called when chapter is downloaded
* @param download download object containing download information
*/
private fun onComplete(download: Download?) {
//Create notification.
with(notificationBuilder) {
// Set notification title
if (download != null)
setContentTitle(download.chapter?.name)
else
setContentTitle(context.getString(R.string.app_name))
// Set content information and progress.
setContentText(context.getString(R.string.update_check_notification_download_complete))
setSmallIcon(android.R.drawable.stat_sys_download_done)
setProgress(0, 0, false)
}
// Show notification.
context.notificationManager.notify(notificationId, notificationBuilder.build())
// Reset initial values
isDownloading = false
initialQueueSize = 0
}
/**
* Clears the notification message
*/
internal fun onClear() {
context.notificationManager.cancel(notificationId)
}
/**
* Called on error while downloading chapter
* @param error string containing error information
* @param chapter string containing chapter title
*/
internal fun onError(error: String? = "", chapter: String = "") {
// Create notification
with(notificationBuilder) {
if (chapter.isNullOrEmpty()) {
setContentTitle(context.getString(R.string.download_notifier_title_error))
} else {
setContentTitle(chapter)
}
if (error.isNullOrEmpty())
setContentText(context.getString(R.string.download_notifier_unkown_error))
else
setContentText(error)
setSmallIcon(android.R.drawable.stat_sys_warning)
setProgress(0, 0, false)
}
context.notificationManager.notify(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID, notificationBuilder.build())
isDownloading = false
}
}

View File

@ -82,12 +82,12 @@ class DownloadService : Service() {
stopSelf() stopSelf()
} }
} else if (isRunning) { } else if (isRunning) {
downloadManager.stopDownloads() downloadManager.stopDownloads(baseContext.getString(R.string.download_notifier_text_only_wifi))
} }
} }
else -> { else -> {
if (isRunning) { if (isRunning) {
downloadManager.stopDownloads() downloadManager.stopDownloads(baseContext.getString(R.string.download_notifier_text_only_wifi))
} }
} }
} }

View File

@ -12,6 +12,7 @@ import android.util.Pair
import com.github.pwittchen.reactivenetwork.library.ConnectivityStatus import com.github.pwittchen.reactivenetwork.library.ConnectivityStatus
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork
import eu.kanade.tachiyomi.App import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.Constants
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
@ -67,7 +68,7 @@ class LibraryUpdateService : Service() {
/** /**
* Id of the library update notification. * Id of the library update notification.
*/ */
const val UPDATE_NOTIFICATION_ID = 1 const val UPDATE_NOTIFICATION_ID = Constants.NOTIFICATION_LIBRARY_ID
/** /**
* Key for manual library update. * Key for manual library update.
@ -206,8 +207,8 @@ class LibraryUpdateService : Service() {
showNotification(getString(R.string.notification_update_error), "") showNotification(getString(R.string.notification_update_error), "")
stopSelf(startId) stopSelf(startId)
}, { }, {
stopSelf(startId) stopSelf(startId)
}) })
return Service.START_STICKY return Service.START_STICKY
} }
@ -451,7 +452,6 @@ class LibraryUpdateService : Service() {
class CancelUpdateReceiver : BroadcastReceiver() { class CancelUpdateReceiver : BroadcastReceiver() {
/** /**
* Method called when user wants a library update. * Method called when user wants a library update.
*
* @param context the application context. * @param context the application context.
* @param intent the intent received. * @param intent the intent received.
*/ */
@ -460,5 +460,4 @@ class LibraryUpdateService : Service() {
context.notificationManager.cancel(UPDATE_NOTIFICATION_ID) context.notificationManager.cancel(UPDATE_NOTIFICATION_ID)
} }
} }
} }

View File

@ -9,6 +9,7 @@ import android.net.Uri
import android.os.AsyncTask import android.os.AsyncTask
import android.support.v4.app.NotificationCompat import android.support.v4.app.NotificationCompat
import eu.kanade.tachiyomi.App import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.Constants
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.network.NetworkHelper import eu.kanade.tachiyomi.data.network.NetworkHelper
import eu.kanade.tachiyomi.data.network.ProgressListener import eu.kanade.tachiyomi.data.network.ProgressListener
@ -181,7 +182,7 @@ class UpdateDownloader(private val context: Context) :
val FILE_LOCATION = "file_location" val FILE_LOCATION = "file_location"
// Id of the notification // Id of the notification
val notificationId = 2 val notificationId = Constants.NOTIFICATION_UPDATER_ID
} }
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {

View File

@ -59,7 +59,7 @@ class DownloadPresenter : BasePresenter<DownloadFragment>() {
* Clears the download queue. * Clears the download queue.
*/ */
fun clearQueue() { fun clearQueue() {
downloadQueue.clear() downloadManager.clearQueue()
start(GET_DOWNLOAD_QUEUE) start(GET_DOWNLOAD_QUEUE)
} }

View File

@ -303,4 +303,10 @@
<string name="information_no_recent">No recent chapters</string> <string name="information_no_recent">No recent chapters</string>
<string name="information_empty_library">Empty library</string> <string name="information_empty_library">Empty library</string>
<!-- Download Notification -->
<string name="download_notifier_title_error">Error</string>
<string name="download_notifier_unkown_error">An unexpected error occurred while downloading chapter</string>
<string name="download_notifier_page_error">A page is missing in directory</string>
<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>
</resources> </resources>