Fix page saving

This commit is contained in:
Koitharu
2022-04-03 10:05:05 +03:00
parent db3183c6e2
commit a21297d209
9 changed files with 91 additions and 135 deletions

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.core.network
import java.util.concurrent.TimeUnit
import okhttp3.CookieJar
import okhttp3.OkHttpClient
import org.koin.dsl.bind
@@ -8,8 +9,6 @@ import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.utils.DownloadManagerHelper
import java.util.concurrent.TimeUnit
val networkModule
get() = module {
@@ -28,6 +27,5 @@ val networkModule
}
}.build()
}
factory { DownloadManagerHelper(get(), get()) }
single<MangaLoaderContext> { MangaLoaderContextImpl(get(), get(), get()) }
}
}

View File

@@ -6,6 +6,7 @@ import org.koin.dsl.module
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.local.ui.LocalListViewModel
import org.koitharu.kotatsu.utils.ExternalStorageHelper
val localModule
get() = module {
@@ -13,5 +14,7 @@ val localModule
single { LocalStorageManager(androidContext(), get()) }
single { LocalMangaRepository(get()) }
factory { ExternalStorageHelper(androidContext()) }
viewModel { LocalListViewModel(get(), get(), get(), get()) }
}

View File

@@ -21,7 +21,7 @@ val readerModule
historyRepository = get(),
shortcutsRepository = get(),
settings = get(),
downloadManagerHelper = get(),
externalStorageHelper = get(),
)
}
}

View File

@@ -6,6 +6,10 @@ import android.graphics.BitmapFactory
import android.net.Uri
import androidx.collection.LongSparseArray
import androidx.collection.set
import java.io.File
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
import java.util.zip.ZipFile
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -27,10 +31,6 @@ import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.utils.ext.connectivityManager
import org.koitharu.kotatsu.utils.progress.ProgressDeferred
import java.io.File
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
import java.util.zip.ZipFile
private const val PROGRESS_UNDEFINED = -1f
private const val PREFETCH_LIMIT_DEFAULT = 10
@@ -77,7 +77,7 @@ class PageLoader : KoinComponent, Closeable {
}
}
fun loadPageAsync(page: MangaPage, force: Boolean) : ProgressDeferred<File, Float> {
fun loadPageAsync(page: MangaPage, force: Boolean): ProgressDeferred<File, Float> {
if (!force) {
cache[page.url]?.let {
return getCompletedTask(it)

View File

@@ -0,0 +1,26 @@
package org.koitharu.kotatsu.reader.ui
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Environment
import android.provider.DocumentsContract
import android.webkit.MimeTypeMap
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.net.toUri
class PageSaveContract : ActivityResultContracts.CreateDocument() {
override fun createIntent(context: Context, input: String): Intent {
val intent = super.createIntent(context, input)
intent.type = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(input.substringAfterLast('.')) ?: "image/*"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
intent.putExtra(
DocumentsContract.EXTRA_INITIAL_URI,
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).toUri(),
)
}
return intent
}
}

View File

@@ -1,18 +1,15 @@
package org.koitharu.kotatsu.reader.ui
import android.Manifest
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.view.*
import android.widget.Toast
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets
import androidx.core.net.toUri
import androidx.core.view.*
import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
@@ -60,7 +57,7 @@ class ReaderActivity :
GridTouchHelper.OnGridTouchListener,
OnPageSelectListener,
ReaderConfigDialog.Callback,
ActivityResultCallback<Boolean>,
ActivityResultCallback<Uri?>,
ReaderControlDelegate.OnInteractionListener,
OnApplyWindowInsetsListener {
@@ -75,10 +72,7 @@ class ReaderActivity :
private lateinit var touchHelper: GridTouchHelper
private lateinit var orientationHelper: ScreenOrientationHelper
private lateinit var controlDelegate: ReaderControlDelegate
private val permissionsRequest = registerForActivityResult(
ActivityResultContracts.RequestPermission(),
this
)
private val savePageRequest = registerForActivityResult(PageSaveContract(), this)
private var gestureInsets: Insets = Insets.NONE
private val reader
@@ -190,29 +184,20 @@ class ReaderActivity :
}
}
R.id.action_save_page -> {
if (!viewModel.content.value?.pages.isNullOrEmpty()) {
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
) {
onActivityResult(true)
} else {
permissionsRequest.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
} else {
showWaitWhileLoading()
}
viewModel.getCurrentPage()?.also { page ->
viewModel.saveCurrentState(reader?.getCurrentState())
val name = page.url.toUri().lastPathSegment
savePageRequest.launch(name)
} ?: showWaitWhileLoading()
}
else -> return super.onOptionsItemSelected(item)
}
return true
}
override fun onActivityResult(result: Boolean) {
if (result) {
viewModel.saveCurrentState(reader?.getCurrentState())
viewModel.saveCurrentPage()
override fun onActivityResult(uri: Uri?) {
if (uri != null) {
viewModel.saveCurrentPage(uri)
}
}

View File

@@ -8,6 +8,7 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.base.domain.MangaUtils
@@ -25,7 +26,7 @@ import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
import org.koitharu.kotatsu.utils.DownloadManagerHelper
import org.koitharu.kotatsu.utils.ExternalStorageHelper
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.IgnoreErrors
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
@@ -39,7 +40,7 @@ class ReaderViewModel(
private val historyRepository: HistoryRepository,
private val shortcutsRepository: ShortcutsRepository,
private val settings: AppSettings,
private val downloadManagerHelper: DownloadManagerHelper,
private val externalStorageHelper: ExternalStorageHelper,
) : BaseViewModel() {
private var loadingJob: Job? = null
@@ -136,25 +137,29 @@ class ReaderViewModel(
return pages.filter { it.chapterId == chapterId }.map { it.toMangaPage() }
}
fun saveCurrentPage() {
fun saveCurrentPage(destination: Uri) {
launchJob(Dispatchers.Default) {
try {
val state = currentState.value ?: error("Undefined state")
val page = content.value?.pages?.find {
it.chapterId == state.chapterId && it.index == state.page
}?.toMangaPage() ?: error("Page not found")
val repo = MangaRepository(page.source)
val pageUrl = repo.getPageUrl(page)
val downloadId = downloadManagerHelper.downloadPage(page, pageUrl)
val uri = downloadManagerHelper.awaitDownload(downloadId)
onPageSaved.postCall(uri)
val page = getCurrentPage() ?: error("Page not found")
externalStorageHelper.savePage(page, destination)
onPageSaved.postCall(destination)
} catch (_: CancellationException) {
} catch (e: Exception) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
onPageSaved.postCall(null)
}
}
}
fun getCurrentPage(): MangaPage? {
val state = currentState.value ?: return null
return content.value?.pages?.find {
it.chapterId == state.chapterId && it.index == state.page
}?.toMangaPage()
}
fun switchChapter(id: Long) {
val prevJob = loadingJob
loadingJob = launchLoadingJob(Dispatchers.Default) {

View File

@@ -1,87 +0,0 @@
package org.koitharu.kotatsu.utils
import android.app.DownloadManager
import android.app.DownloadManager.Request.*
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import android.os.Build
import android.os.Environment
import androidx.core.net.toUri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.toFileNameSafe
import java.io.File
import kotlin.coroutines.resume
class DownloadManagerHelper(
private val context: Context,
private val cookieJar: CookieJar,
) {
private val manager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
private val subDir = context.getString(R.string.app_name).toFileNameSafe()
fun downloadPage(page: MangaPage, fullUrl: String): Long {
val uri = fullUrl.toUri()
val cookies = cookieJar.loadForRequest(fullUrl.toHttpUrl())
val dest = subDir + File.separator + uri.lastPathSegment
val request = DownloadManager.Request(uri)
.addRequestHeader(CommonHeaders.REFERER, page.referer)
.addRequestHeader(CommonHeaders.COOKIE, cookieHeader(cookies))
.setAllowedOverMetered(true)
.setAllowedNetworkTypes(NETWORK_WIFI or NETWORK_MOBILE)
.setNotificationVisibility(VISIBILITY_VISIBLE)
.setDestinationInExternalPublicDir(Environment.DIRECTORY_PICTURES, dest)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
@Suppress("DEPRECATION")
request.allowScanningByMediaScanner()
}
return manager.enqueue(request)
}
suspend fun awaitDownload(id: Long): Uri {
getUriForDownloadedFile(id)?.let { return it } // fast path
suspendCancellableCoroutine<Unit> { cont ->
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
if (
intent?.action == DownloadManager.ACTION_DOWNLOAD_COMPLETE &&
intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0L) == id
) {
context.unregisterReceiver(this)
cont.resume(Unit)
}
}
}
context.registerReceiver(
receiver,
IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
)
cont.invokeOnCancellation {
context.unregisterReceiver(receiver)
}
}
return checkNotNull(getUriForDownloadedFile(id))
}
private suspend fun getUriForDownloadedFile(id: Long) = withContext(Dispatchers.IO) {
manager.getUriForDownloadedFile(id)
}
private fun cookieHeader(cookies: List<Cookie>): String = buildString {
cookies.forEachIndexed { index, cookie ->
if (index > 0) append("; ")
append(cookie.name).append('=').append(cookie.value)
}
}
}

View File

@@ -0,0 +1,26 @@
package org.koitharu.kotatsu.utils
import android.content.Context
import android.net.Uri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import okio.IOException
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.domain.PageLoader
class ExternalStorageHelper(context: Context) {
private val contentResolver = context.contentResolver
suspend fun savePage(page: MangaPage, destination: Uri) {
val pageLoader = PageLoader()
val pageFile = pageLoader.loadPage(page, force = false)
runInterruptible(Dispatchers.IO) {
contentResolver.openOutputStream(destination)?.use { output ->
pageFile.inputStream().use { input ->
input.copyTo(output)
}
} ?: throw IOException("Output stream is null")
}
}
}