Use DownloadManager for pages saving
This commit is contained in:
@@ -65,7 +65,7 @@ class KotatsuApp : Application() {
|
||||
trackerModule,
|
||||
settingsModule,
|
||||
readerModule,
|
||||
appWidgetModule
|
||||
appWidgetModule,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,4 +6,5 @@ object CommonHeaders {
|
||||
const val USER_AGENT = "User-Agent"
|
||||
const val ACCEPT = "Accept"
|
||||
const val CONTENT_DISPOSITION = "Content-Disposition"
|
||||
const val COOKIE = "Cookie"
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.utils.CacheUtils
|
||||
import org.koitharu.kotatsu.utils.DownloadManagerHelper
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
val networkModule
|
||||
@@ -28,4 +29,5 @@ val networkModule
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
factory { DownloadManagerHelper(get(), get()) }
|
||||
}
|
||||
@@ -16,9 +16,9 @@ import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||
import org.koitharu.kotatsu.list.ui.model.*
|
||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.utils.MediaStoreCompat
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||
import org.koitharu.kotatsu.utils.ext.resolveName
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
@@ -74,7 +74,7 @@ class LocalListViewModel(
|
||||
launchLoadingJob {
|
||||
val contentResolver = context.contentResolver
|
||||
withContext(Dispatchers.IO) {
|
||||
val name = MediaStoreCompat(contentResolver).getName(uri)
|
||||
val name = contentResolver.resolveName(uri)
|
||||
?: throw IOException("Cannot fetch name from uri: $uri")
|
||||
if (!LocalMangaRepository.isFileSupported(name)) {
|
||||
throw UnsupportedFileException("Unsupported file on $uri")
|
||||
|
||||
@@ -13,6 +13,6 @@ val readerModule
|
||||
single { PagesCache(get()) }
|
||||
|
||||
viewModel { params ->
|
||||
ReaderViewModel(params[0], params[1], get(), get(), get(), get())
|
||||
ReaderViewModel(params[0], params[1], get(), get(), get(), get(), get())
|
||||
}
|
||||
}
|
||||
@@ -196,7 +196,7 @@ class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
|
||||
override fun onActivityResult(result: Boolean) {
|
||||
if (result) {
|
||||
viewModel.saveCurrentState(reader?.getCurrentState())
|
||||
viewModel.saveCurrentPage(contentResolver)
|
||||
viewModel.saveCurrentPage()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.reader.ui
|
||||
import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
import android.util.LongSparseArray
|
||||
import android.webkit.URLUtil
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.*
|
||||
@@ -23,10 +22,9 @@ import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.local.data.PagesCache
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
|
||||
import org.koitharu.kotatsu.utils.MediaStoreCompat
|
||||
import org.koitharu.kotatsu.utils.DownloadManagerHelper
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.ext.IgnoreErrors
|
||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||
@@ -38,7 +36,8 @@ class ReaderViewModel(
|
||||
private val dataRepository: MangaDataRepository,
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val shortcutsRepository: ShortcutsRepository,
|
||||
private val settings: AppSettings
|
||||
private val settings: AppSettings,
|
||||
private val downloadManagerHelper: DownloadManagerHelper,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private var loadingJob: Job? = null
|
||||
@@ -150,7 +149,7 @@ class ReaderViewModel(
|
||||
return pages.filter { it.chapterId == chapterId }.map { it.toMangaPage() }
|
||||
}
|
||||
|
||||
fun saveCurrentPage(resolver: ContentResolver) {
|
||||
fun saveCurrentPage() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
try {
|
||||
val state = currentState.value ?: error("Undefined state")
|
||||
@@ -159,13 +158,8 @@ class ReaderViewModel(
|
||||
}?.toMangaPage() ?: error("Page not found")
|
||||
val repo = MangaRepository(page.source)
|
||||
val pageUrl = repo.getPageUrl(page)
|
||||
val file = get<PagesCache>()[pageUrl] ?: error("Page not found in cache")
|
||||
val uri = file.inputStream().use { input ->
|
||||
val fileName = URLUtil.guessFileName(pageUrl, null, null)
|
||||
MediaStoreCompat(resolver).insertImage(fileName) {
|
||||
input.copyTo(it)
|
||||
}
|
||||
}
|
||||
val downloadId = downloadManagerHelper.downloadPage(page, pageUrl)
|
||||
val uri = downloadManagerHelper.awaitDownload(downloadId)
|
||||
onPageSaved.postCall(uri)
|
||||
} catch (e: CancellationException) {
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
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.model.MangaPage
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
import org.koitharu.kotatsu.utils.ext.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package org.koitharu.kotatsu.utils
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentValues
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.MediaStore
|
||||
import android.provider.OpenableColumns
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.core.database.getStringOrNull
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import java.io.OutputStream
|
||||
|
||||
class MediaStoreCompat(private val contentResolver: ContentResolver) {
|
||||
|
||||
fun insertImage(
|
||||
fileName: String,
|
||||
block: (OutputStream) -> Unit
|
||||
): Uri? {
|
||||
val name = fileName.substringBeforeLast('.')
|
||||
val cv = ContentValues(7)
|
||||
cv.put(MediaStore.Images.Media.DISPLAY_NAME, name)
|
||||
cv.put(MediaStore.Images.Media.TITLE, name)
|
||||
cv.put(
|
||||
MediaStore.Images.Media.MIME_TYPE,
|
||||
MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileName.substringAfterLast('.'))
|
||||
)
|
||||
cv.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis() / 1_000)
|
||||
cv.put(MediaStore.Images.Media.DATE_MODIFIED, System.currentTimeMillis())
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
cv.put(MediaStore.Images.Media.IS_PENDING, 1)
|
||||
}
|
||||
var uri: Uri? = null
|
||||
try {
|
||||
uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, cv)
|
||||
contentResolver.openOutputStream(uri!!)?.use(block)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
cv.clear()
|
||||
cv.put(MediaStore.Images.Media.IS_PENDING, 0)
|
||||
contentResolver.update(uri, cv, null, null)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
uri?.let {
|
||||
contentResolver.delete(it, null, null)
|
||||
}
|
||||
uri = null
|
||||
}
|
||||
return uri
|
||||
}
|
||||
|
||||
fun getName(uri: Uri): String? =
|
||||
(if (uri.scheme == "content") {
|
||||
contentResolver.query(uri, null, null, null, null)?.use {
|
||||
if (it.moveToFirst()) {
|
||||
it.getStringOrNull(it.getColumnIndex(OpenableColumns.DISPLAY_NAME))
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}) ?: uri.path?.substringAfterLast('/')
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
package org.koitharu.kotatsu.utils.ext
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.storage.StorageManager
|
||||
import android.provider.OpenableColumns
|
||||
import androidx.core.database.getStringOrNull
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.R
|
||||
@@ -60,4 +63,19 @@ fun Uri.toFileOrNull() = if (scheme == "file") path?.let(::File) else null
|
||||
|
||||
suspend fun File.deleteAwait() = withContext(Dispatchers.IO) {
|
||||
delete()
|
||||
}
|
||||
|
||||
fun ContentResolver.resolveName(uri: Uri): String? {
|
||||
val fallback = uri.lastPathSegment
|
||||
if (uri.scheme != "content") {
|
||||
return fallback
|
||||
}
|
||||
query(uri, null, null, null, null)?.use {
|
||||
if (it.moveToFirst()) {
|
||||
it.getStringOrNull(it.getColumnIndex(OpenableColumns.DISPLAY_NAME))?.let { name ->
|
||||
return name
|
||||
}
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
Reference in New Issue
Block a user