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:
inorichi
2016-11-20 11:20:57 +01:00
committed by GitHub
parent 59c626b4a8
commit 6f297161de
34 changed files with 1325 additions and 855 deletions
@@ -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) {