Download manager rewrite (#535)
* Saving to SD working * Rename imagePath to uri * Handle android < 21 * Minor changes * Separate downloader from the manager. Optimize folder lookups * Persist downloads across restarts * Fix for #511 * Updated ReactiveNetwork. Add some documentation * More documentation and minor fixes * Handle persistent notifications. Other minor changes * Improve downloader and add documentation * Rename pageNumber to index in Page class * Remove unused methods * Use chop method * Make sure dest dir is created * Reset downloads dir preference * Use invalidate options menu in download fragment and fix wrong condition * Fix empty download queue after application restart * Use addAll method in download queue to avoid too many notifications * Inform download manager changes
This commit is contained in:
@@ -70,14 +70,15 @@ class ChapterLoader(
|
||||
private fun retrievePageList(chapter: ReaderChapter) = Observable.just(chapter)
|
||||
.flatMap {
|
||||
// Check if the chapter is downloaded.
|
||||
chapter.isDownloaded = downloadManager.isChapterDownloaded(source, manga, chapter)
|
||||
chapter.isDownloaded = downloadManager.findChapterDir(source, manga, chapter) != null
|
||||
|
||||
// Fetch the page list from disk.
|
||||
if (chapter.isDownloaded)
|
||||
Observable.just(downloadManager.getSavedPageList(source, manga, chapter)!!)
|
||||
// Fetch the page list from cache or fallback to network
|
||||
else
|
||||
if (chapter.isDownloaded) {
|
||||
// Fetch the page list from disk.
|
||||
downloadManager.buildPageList(source, manga, chapter)
|
||||
} else {
|
||||
// Fetch the page list from cache or fallback to network
|
||||
source.fetchPageList(chapter)
|
||||
}
|
||||
}
|
||||
.doOnNext { pages ->
|
||||
chapter.pages = pages
|
||||
@@ -85,21 +86,11 @@ class ChapterLoader(
|
||||
}
|
||||
|
||||
private fun loadPages(chapter: ReaderChapter) {
|
||||
if (chapter.isDownloaded) {
|
||||
loadDownloadedPages(chapter)
|
||||
} else {
|
||||
if (!chapter.isDownloaded) {
|
||||
loadOnlinePages(chapter)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadDownloadedPages(chapter: ReaderChapter) {
|
||||
val chapterDir = downloadManager.getAbsoluteChapterDirectory(source, manga, chapter)
|
||||
subscriptions += Observable.from(chapter.pages!!)
|
||||
.flatMap { downloadManager.getDownloadedImage(it, chapterDir) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
private fun loadOnlinePages(chapter: ReaderChapter) {
|
||||
chapter.pages?.let { pages ->
|
||||
val startPage = chapter.requestedPage
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION_CODES.KITKAT
|
||||
import android.os.Bundle
|
||||
@@ -265,7 +264,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
val activePage = pages.getOrElse(chapter.requestedPage) { pages.first() }
|
||||
|
||||
viewer?.onPageListReady(chapter, activePage)
|
||||
setActiveChapter(chapter, activePage.pageNumber)
|
||||
setActiveChapter(chapter, activePage.index)
|
||||
}
|
||||
|
||||
fun onEnterChapter(chapter: ReaderChapter, currentPage: Int) {
|
||||
@@ -332,7 +331,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
fun onPageChanged(page: Page) {
|
||||
presenter.onPageChanged(page)
|
||||
|
||||
val pageNumber = page.pageNumber + 1
|
||||
val pageNumber = page.index + 1
|
||||
val pageCount = page.chapter.pages!!.size
|
||||
page_number.text = "$pageNumber/$pageCount"
|
||||
if (page_seekbar.rotation != 180f) {
|
||||
@@ -340,7 +339,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
} else {
|
||||
right_page_text.text = "$pageNumber"
|
||||
}
|
||||
page_seekbar.progress = page.pageNumber
|
||||
page_seekbar.progress = page.index
|
||||
}
|
||||
|
||||
fun gotoPageInCurrentChapter(pageIndex: Int) {
|
||||
@@ -481,7 +480,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
|
||||
val shareIntent = Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
putExtra(Intent.EXTRA_STREAM, Uri.parse(page.imagePath))
|
||||
putExtra(Intent.EXTRA_STREAM, page.uri)
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
type = "image/jpeg"
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ import rx.schedulers.Schedulers
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
@@ -98,15 +97,6 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
||||
*/
|
||||
private val source by lazy { sourceManager.get(manga.source)!! }
|
||||
|
||||
/**
|
||||
* Directory of pictures
|
||||
*/
|
||||
private val pictureDirectory: String by lazy {
|
||||
Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
||||
Environment.DIRECTORY_PICTURES + File.separator +
|
||||
context.getString(R.string.app_name) + File.separator
|
||||
}
|
||||
|
||||
/**
|
||||
* Chapter list for the active manga. It's retrieved lazily and should be accessed for the first
|
||||
* time in a background thread to avoid blocking the UI.
|
||||
@@ -351,9 +341,9 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
||||
fun retryPage(page: Page?) {
|
||||
if (page != null && source is OnlineSource) {
|
||||
page.status = Page.QUEUE
|
||||
val path = page.imagePath
|
||||
if (!path.isNullOrEmpty() && !page.chapter.isDownloaded) {
|
||||
chapterCache.removeFileFromCache(File(path).name)
|
||||
val uri = page.uri
|
||||
if (uri != null && !page.chapter.isDownloaded) {
|
||||
chapterCache.removeFileFromCache(uri.encodedPath.substringAfterLast('/'))
|
||||
}
|
||||
loader.retryPage(page)
|
||||
}
|
||||
@@ -370,27 +360,27 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
||||
val pages = chapter.pages ?: return
|
||||
|
||||
Observable.fromCallable {
|
||||
// Chapters with 1 page don't trigger page changes, so mark them as read.
|
||||
if (pages.size == 1) {
|
||||
chapter.read = true
|
||||
}
|
||||
|
||||
// Cache current page list progress for online chapters to allow a faster reopen
|
||||
if (!chapter.isDownloaded) {
|
||||
source.let { if (it is OnlineSource) it.savePageList(chapter, pages) }
|
||||
}
|
||||
|
||||
if (chapter.read) {
|
||||
val removeAfterReadSlots = prefs.removeAfterReadSlots()
|
||||
when (removeAfterReadSlots) {
|
||||
// Setting disabled
|
||||
-1 -> { /**Empty function**/ }
|
||||
// Remove current read chapter
|
||||
0 -> deleteChapter(chapter, manga)
|
||||
// Remove previous chapter specified by user in settings.
|
||||
else -> getAdjacentChaptersStrategy(chapter, removeAfterReadSlots)
|
||||
.first?.let { deleteChapter(it, manga) }
|
||||
try {
|
||||
if (chapter.read) {
|
||||
val removeAfterReadSlots = prefs.removeAfterReadSlots()
|
||||
when (removeAfterReadSlots) {
|
||||
// Setting disabled
|
||||
-1 -> { /* Empty function */ }
|
||||
// Remove current read chapter
|
||||
0 -> deleteChapter(chapter, manga)
|
||||
// Remove previous chapter specified by user in settings.
|
||||
else -> getAdjacentChaptersStrategy(chapter, removeAfterReadSlots)
|
||||
.first?.let { deleteChapter(it, manga) }
|
||||
}
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
// TODO find out why it crashes
|
||||
Timber.e(error)
|
||||
}
|
||||
|
||||
db.updateChapterProgress(chapter).executeAsBlocking()
|
||||
@@ -414,7 +404,7 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
||||
*/
|
||||
fun onPageChanged(page: Page) {
|
||||
val chapter = page.chapter
|
||||
chapter.last_page_read = page.pageNumber
|
||||
chapter.last_page_read = page.index
|
||||
if (chapter.pages!!.last() === page) {
|
||||
chapter.read = true
|
||||
}
|
||||
@@ -537,7 +527,8 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
||||
try {
|
||||
if (manga.favorite) {
|
||||
if (manga.thumbnail_url != null) {
|
||||
coverCache.copyToCache(manga.thumbnail_url!!, File(page.imagePath).inputStream())
|
||||
val input = context.contentResolver.openInputStream(page.uri)
|
||||
coverCache.copyToCache(manga.thumbnail_url!!, input)
|
||||
context.toast(R.string.cover_updated)
|
||||
} else {
|
||||
throw Exception("Image url not found")
|
||||
@@ -552,40 +543,47 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Save page to local storage
|
||||
* @throws IOException
|
||||
* Save page to local storage.
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
internal fun savePage(page: Page) {
|
||||
if (page.status != Page.READY)
|
||||
return
|
||||
|
||||
// Used to show image notification
|
||||
// Used to show image notification.
|
||||
val imageNotifier = ImageNotifier(context)
|
||||
|
||||
// Location of image file.
|
||||
val inputFile = File(page.imagePath)
|
||||
|
||||
// File where the image will be saved.
|
||||
val destFile = File(pictureDirectory, manga.title + " - " + chapter.name +
|
||||
" - " + downloadManager.getImageFilename(page))
|
||||
|
||||
//Remove the notification if already exist (user feedback)
|
||||
// Remove the notification if it already exists (user feedback).
|
||||
imageNotifier.onClear()
|
||||
if (inputFile.exists()) {
|
||||
// Copy file
|
||||
Observable.fromCallable { inputFile.copyTo(destFile, true) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
// Show notification
|
||||
imageNotifier.onComplete(it)
|
||||
},
|
||||
{ error ->
|
||||
Timber.e(error)
|
||||
imageNotifier.onError(error.message)
|
||||
})
|
||||
}
|
||||
|
||||
// Pictures directory.
|
||||
val pictureDirectory = Environment.getExternalStorageDirectory().absolutePath +
|
||||
File.separator + Environment.DIRECTORY_PICTURES +
|
||||
File.separator + context.getString(R.string.app_name)
|
||||
|
||||
// Copy file in background.
|
||||
Observable
|
||||
.fromCallable {
|
||||
// File where the image will be saved.
|
||||
val destDir = File(pictureDirectory)
|
||||
destDir.mkdirs()
|
||||
|
||||
val destFile = File(destDir, manga.title + " - " + chapter.name +
|
||||
" - " + (page.index + 1))
|
||||
|
||||
// Location of image file.
|
||||
context.contentResolver.openInputStream(page.uri).use { input ->
|
||||
destFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
|
||||
imageNotifier.onComplete(destFile)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe({},
|
||||
{ error ->
|
||||
Timber.e(error)
|
||||
imageNotifier.onError(error.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,9 @@ package eu.kanade.tachiyomi.ui.reader.notification
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.media.Image
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.request.animation.GlideAnimation
|
||||
import com.bumptech.glide.request.target.SimpleTarget
|
||||
import eu.kanade.tachiyomi.Constants
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.notificationManager
|
||||
@@ -29,24 +26,25 @@ class ImageNotifier(private val context: Context) {
|
||||
get() = Constants.NOTIFICATION_DOWNLOAD_IMAGE_ID
|
||||
|
||||
/**
|
||||
* Called when image download/copy is complete
|
||||
* @param file image file containing downloaded page image
|
||||
* Called when image download/copy is complete. This method must be called in a background
|
||||
* thread.
|
||||
*
|
||||
* @param file image file containing downloaded page image.
|
||||
*/
|
||||
fun onComplete(file: File) {
|
||||
val bitmap = Glide.with(context)
|
||||
.load(file)
|
||||
.asBitmap()
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
.into(720, 1280)
|
||||
.get()
|
||||
|
||||
Glide.with(context).load(file).asBitmap().diskCacheStrategy(DiskCacheStrategy.NONE).skipMemoryCache(true).into(object : SimpleTarget<Bitmap>(720, 1280) {
|
||||
/**
|
||||
* The method that will be called when the resource load has finished.
|
||||
* @param resource the loaded resource.
|
||||
*/
|
||||
override fun onResourceReady(resource: Bitmap?, glideAnimation: GlideAnimation<in Bitmap>?) {
|
||||
if (resource!= null){
|
||||
showCompleteNotification(file, resource)
|
||||
}else{
|
||||
onError(null)
|
||||
}
|
||||
}
|
||||
})
|
||||
if (bitmap != null) {
|
||||
showCompleteNotification(file, bitmap)
|
||||
} else {
|
||||
onError(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showCompleteNotification(file: File, image: Bitmap) {
|
||||
@@ -75,7 +73,7 @@ class ImageNotifier(private val context: Context) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the notification message
|
||||
* Clears the notification message.
|
||||
*/
|
||||
fun onClear() {
|
||||
context.notificationManager.cancel(notificationId)
|
||||
@@ -88,8 +86,8 @@ class ImageNotifier(private val context: Context) {
|
||||
|
||||
|
||||
/**
|
||||
* Called on error while downloading image
|
||||
* @param error string containing error information
|
||||
* Called on error while downloading image.
|
||||
* @param error string containing error information.
|
||||
*/
|
||||
fun onError(error: String?) {
|
||||
// Create notification
|
||||
|
||||
@@ -95,7 +95,7 @@ abstract class BaseReader : BaseFragment() {
|
||||
|
||||
// Active chapter has changed.
|
||||
if (oldChapter.id != newChapter.id) {
|
||||
readerActivity.onEnterChapter(newPage.chapter, newPage.pageNumber)
|
||||
readerActivity.onEnterChapter(newPage.chapter, newPage.index)
|
||||
}
|
||||
// Request next chapter only when the conditions are met.
|
||||
if (pages.size - position < 5 && chapters.last().id == newChapter.id
|
||||
@@ -125,7 +125,7 @@ abstract class BaseReader : BaseFragment() {
|
||||
*/
|
||||
fun getPageIndex(search: Page): Int {
|
||||
for ((index, page) in pages.withIndex()) {
|
||||
if (page.pageNumber == search.pageNumber && page.chapter.id == search.chapter.id) {
|
||||
if (page.index == search.index && page.chapter.id == search.chapter.id) {
|
||||
return index
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.PointF
|
||||
import android.os.Build
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
@@ -208,13 +210,25 @@ class PageView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
|
||||
* Called when the page is ready.
|
||||
*/
|
||||
private fun setImage() {
|
||||
val path = page.imagePath
|
||||
if (path != null && File(path).exists()) {
|
||||
progress_text.visibility = View.INVISIBLE
|
||||
image_view.setImage(ImageSource.uri(path))
|
||||
} else {
|
||||
val uri = page.uri
|
||||
if (uri == null) {
|
||||
page.status = Page.ERROR
|
||||
return
|
||||
}
|
||||
|
||||
val file = if (Build.VERSION.SDK_INT < 21 || UniFile.isFileUri(uri)) {
|
||||
UniFile.fromFile(File(uri.path))
|
||||
} else {
|
||||
// Tree uri returns the root folder
|
||||
UniFile.fromSingleUri(context, uri)
|
||||
}!!
|
||||
if (!file.exists()) {
|
||||
page.status = Page.ERROR
|
||||
return
|
||||
}
|
||||
|
||||
progress_text.visibility = View.INVISIBLE
|
||||
image_view.setImage(ImageSource.uri(file.uri))
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
|
||||
|
||||
import android.os.Build
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
@@ -242,14 +244,26 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
|
||||
* Called when the page is ready.
|
||||
*/
|
||||
private fun setImage() = with(view) {
|
||||
val path = page?.imagePath
|
||||
if (path != null && File(path).exists()) {
|
||||
progress_text.visibility = View.INVISIBLE
|
||||
image_view.visibility = View.VISIBLE
|
||||
image_view.setImage(ImageSource.uri(path))
|
||||
} else {
|
||||
val uri = page?.uri
|
||||
if (uri == null) {
|
||||
page?.status = Page.ERROR
|
||||
return
|
||||
}
|
||||
|
||||
val file = if (Build.VERSION.SDK_INT < 21 || UniFile.isFileUri(uri)) {
|
||||
UniFile.fromFile(File(uri.path))
|
||||
} else {
|
||||
// Tree uri returns the root folder
|
||||
UniFile.fromSingleUri(context, uri)
|
||||
}!!
|
||||
if (!file.exists()) {
|
||||
page?.status = Page.ERROR
|
||||
return
|
||||
}
|
||||
|
||||
progress_text.visibility = View.INVISIBLE
|
||||
image_view.visibility = View.VISIBLE
|
||||
image_view.setImage(ImageSource.uri(file.uri))
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -116,7 +116,7 @@ class WebtoonReader : BaseReader() {
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
val savedPosition = pages.getOrNull(layoutManager.findFirstVisibleItemPosition())?.pageNumber ?: 0
|
||||
val savedPosition = pages.getOrNull(layoutManager.findFirstVisibleItemPosition())?.index ?: 0
|
||||
outState.putInt(SAVED_POSITION, savedPosition)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
@@ -163,7 +163,7 @@ class WebtoonReader : BaseReader() {
|
||||
* @param currentPage the initial page to display.
|
||||
*/
|
||||
override fun onChapterSet(chapter: ReaderChapter, currentPage: Page) {
|
||||
this.currentPage = currentPage.pageNumber
|
||||
this.currentPage = currentPage.index
|
||||
|
||||
// Make sure the view is already initialized.
|
||||
if (view != null) {
|
||||
|
||||
Reference in New Issue
Block a user