diff --git a/app/src/main/java/eu/kanade/tachiyomi/Constants.kt b/app/src/main/java/eu/kanade/tachiyomi/Constants.kt new file mode 100644 index 000000000..0af2b11b7 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/Constants.kt @@ -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 +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt index 2a9aae5c1..dfc0d6458 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt @@ -5,6 +5,7 @@ import android.net.Uri import com.google.gson.Gson import com.google.gson.reflect.TypeToken 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.Manga 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.base.Source 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.Subscription import rx.android.schedulers.AndroidSchedulers @@ -35,6 +39,8 @@ class DownloadManager(private val context: Context, private val sourceManager: S val runningSubject = BehaviorSubject.create() private var downloadsSubscription: Subscription? = null + val downloadNotifier by lazy { DownloadNotifier(context) } + private val threadsSubject = BehaviorSubject.create() private var threadsSubscription: Subscription? = null @@ -48,10 +54,14 @@ class DownloadManager(private val context: Context, private val sourceManager: S private set private fun initializeSubscriptions() { + downloadsSubscription?.unsubscribe() threadsSubscription = preferences.downloadThreads().asObservable() - .subscribe { threadsSubject.onNext(it) } + .subscribe { + threadsSubject.onNext(it) + downloadNotifier.multipleDownloadThreads = it > 1 + } downloadsSubscription = downloadsQueueSubject.flatMap { Observable.from(it) } .lift(DynamicConcurrentMergeOperator({ downloadChapter(it) }, threadsSubject)) @@ -60,7 +70,9 @@ class DownloadManager(private val context: Context, private val sourceManager: S .subscribe({ // Delete successful downloads from queue if (it.status == Download.DOWNLOADED) { + // remove downloaded chapter from queue queue.del(it) + downloadNotifier.onProgressChange(queue) } if (areAllDownloadsFinished()) { DownloadService.stop(context) @@ -68,7 +80,7 @@ class DownloadManager(private val context: Context, private val sourceManager: S }, { e -> DownloadService.stop(context) Timber.e(e, e.message) - context.toast(e.message) + downloadNotifier.onError(e.message) }) if (!isRunning) { @@ -114,6 +126,12 @@ class DownloadManager(private val context: Context, private val sourceManager: S pending.add(download) } } + + // Initialize queue size + downloadNotifier.initialQueueSize = queue.size + // Show notification + downloadNotifier.onProgressChange(queue) + if (isRunning) downloadsQueueSubject.onNext(pending) } @@ -164,34 +182,40 @@ class DownloadManager(private val context: Context, private val sourceManager: S DiskUtils.createDirectory(download.directory) 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) .doOnNext { pages -> download.pages = pages savePageList(download) } 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) - return Observable.defer { pageListObservable - .doOnNext { pages -> - download.downloadedImages = 0 - download.status = Download.DOWNLOADING - } - // Get all the URLs to the source images, fetch pages if necessary - .flatMap { download.source.getAllImageUrlsFromPageList(it) } - // Start downloading images, consider we can have downloaded images already - .concatMap { page -> getOrDownloadImage(page, download) } - // Do after download completes - .doOnCompleted { onDownloadCompleted(download) } - .toList() - .map { pages -> download } - // If the page list threw, it will resume here - .onErrorResumeNext { error -> - download.status = Download.ERROR - Observable.just(download) - } + return Observable.defer { + pageListObservable + .doOnNext { pages -> + download.downloadedImages = 0 + download.status = Download.DOWNLOADING + } + // Get all the URLs to the source images, fetch pages if necessary + .flatMap { download.source.getAllImageUrlsFromPageList(it) } + // Start downloading images, consider we can have downloaded images already + .concatMap { page -> getOrDownloadImage(page, download) } + // Do when page is downloaded. + .doOnNext { + downloadNotifier.onProgressChange(download, queue) + } + // Do after download completes + .doOnCompleted { onDownloadCompleted(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()) } @@ -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 for (page in download.pages) { 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 if (!isChapterDownloaded(download.directory, download.pages)) { status = Download.ERROR + downloadNotifier.onError(context.getString(R.string.download_notifier_page_error), download.chapter.name) } download.totalProgress = actualProgress download.status = status @@ -399,13 +427,19 @@ class DownloadManager(private val context: Context, private val sourceManager: S return !pending.isEmpty() } - fun stopDownloads() { + fun stopDownloads(error: String = "") { destroySubscriptions() for (download in queue) { if (download.status == Download.DOWNLOADING) { download.status = Download.ERROR } } + downloadNotifier.onError(error) + } + + fun clearQueue() { + queue.clear() + downloadNotifier.onClear() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadNotifier.kt new file mode 100644 index 000000000..ee37f7df4 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadNotifier.kt @@ -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 + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadService.kt index 597374c9d..8407be3da 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadService.kt @@ -82,12 +82,12 @@ class DownloadService : Service() { stopSelf() } } else if (isRunning) { - downloadManager.stopDownloads() + downloadManager.stopDownloads(baseContext.getString(R.string.download_notifier_text_only_wifi)) } } else -> { if (isRunning) { - downloadManager.stopDownloads() + downloadManager.stopDownloads(baseContext.getString(R.string.download_notifier_text_only_wifi)) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt index 2f45206e2..995718cf0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt @@ -12,6 +12,7 @@ import android.util.Pair import com.github.pwittchen.reactivenetwork.library.ConnectivityStatus import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork import eu.kanade.tachiyomi.App +import eu.kanade.tachiyomi.Constants import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Category @@ -67,7 +68,7 @@ class LibraryUpdateService : Service() { /** * 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. @@ -206,8 +207,8 @@ class LibraryUpdateService : Service() { showNotification(getString(R.string.notification_update_error), "") stopSelf(startId) }, { - stopSelf(startId) - }) + stopSelf(startId) + }) return Service.START_STICKY } @@ -451,7 +452,6 @@ class LibraryUpdateService : Service() { class CancelUpdateReceiver : BroadcastReceiver() { /** * Method called when user wants a library update. - * * @param context the application context. * @param intent the intent received. */ @@ -460,5 +460,4 @@ class LibraryUpdateService : Service() { context.notificationManager.cancel(UPDATE_NOTIFICATION_ID) } } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloader.kt index 145c3bdee..517bb5bfc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloader.kt @@ -9,6 +9,7 @@ import android.net.Uri import android.os.AsyncTask import android.support.v4.app.NotificationCompat import eu.kanade.tachiyomi.App +import eu.kanade.tachiyomi.Constants import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.network.NetworkHelper import eu.kanade.tachiyomi.data.network.ProgressListener @@ -181,7 +182,7 @@ class UpdateDownloader(private val context: Context) : val FILE_LOCATION = "file_location" // Id of the notification - val notificationId = 2 + val notificationId = Constants.NOTIFICATION_UPDATER_ID } override fun onReceive(context: Context, intent: Intent) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadPresenter.kt index 46ca86bda..a575a8924 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadPresenter.kt @@ -59,7 +59,7 @@ class DownloadPresenter : BasePresenter() { * Clears the download queue. */ fun clearQueue() { - downloadQueue.clear() + downloadManager.clearQueue() start(GET_DOWNLOAD_QUEUE) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d47172de9..a050264dd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -303,4 +303,10 @@ No recent chapters Empty library + + Error + An unexpected error occurred while downloading chapter + A page is missing in directory + A page is not loaded + No wifi connection available