Fix converting bitmaps in local manga
This commit is contained in:
@@ -17,6 +17,7 @@ import android.content.SyncResult
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ResolveInfo
|
||||
import android.database.SQLException
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
@@ -29,6 +30,7 @@ import android.view.Window
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.annotation.IntegerRes
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
@@ -37,6 +39,7 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import androidx.work.CoroutineWorker
|
||||
import com.google.android.material.elevation.ElevationOverlayProvider
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -45,7 +48,9 @@ import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import okio.IOException
|
||||
import okio.use
|
||||
import org.json.JSONException
|
||||
import org.jsoup.internal.StringUtil.StringJoiner
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
@@ -53,6 +58,7 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import org.xmlpull.v1.XmlPullParserException
|
||||
import java.io.File
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
val Context.activityManager: ActivityManager?
|
||||
@@ -230,3 +236,18 @@ fun Context.checkNotificationPermission(): Boolean = if (Build.VERSION.SDK_INT >
|
||||
} else {
|
||||
NotificationManagerCompat.from(this).areNotificationsEnabled()
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
suspend fun Bitmap.compressToPNG(output: File) = runInterruptible(Dispatchers.IO) {
|
||||
output.outputStream().use { os ->
|
||||
if (!compress(Bitmap.CompressFormat.PNG, 100, os)) {
|
||||
throw IOException("Failed to encode bitmap into PNG format")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.ensureRamAtLeast(requiredSize: Long) {
|
||||
if (ramAvailable < requiredSize) {
|
||||
throw IllegalStateException("Not enough free memory")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.koitharu.kotatsu.local.data
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.os.StatFs
|
||||
import com.tomclaw.cache.DiskLruCache
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
@@ -10,15 +11,17 @@ import kotlinx.coroutines.withContext
|
||||
import okio.Source
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import okio.use
|
||||
import org.koitharu.kotatsu.core.util.FileSize
|
||||
import org.koitharu.kotatsu.core.util.ext.compressToPNG
|
||||
import org.koitharu.kotatsu.core.util.ext.longHashCode
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.subdir
|
||||
import org.koitharu.kotatsu.core.util.ext.takeIfReadable
|
||||
import org.koitharu.kotatsu.core.util.ext.takeIfWriteable
|
||||
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
|
||||
import org.koitharu.kotatsu.parsers.util.SuspendLazy
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
@@ -66,6 +69,16 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun put(url: String, bitmap: Bitmap): File = withContext(Dispatchers.IO) {
|
||||
val file = File(cacheDir.get().parentFile, url.longHashCode().toString())
|
||||
try {
|
||||
bitmap.compressToPNG(file)
|
||||
lruCache.get().put(url, file)
|
||||
} finally {
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getAvailableSize(): Long = runCatchingCancellable {
|
||||
val statFs = StatFs(cacheDir.get().absolutePath)
|
||||
statFs.availableBytes
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package org.koitharu.kotatsu.reader.domain
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import androidx.annotation.AnyThread
|
||||
import androidx.collection.LongSparseArray
|
||||
import androidx.collection.set
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import dagger.hilt.android.ActivityRetainedLifecycle
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
@@ -25,6 +25,7 @@ import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okio.use
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
import org.koitharu.kotatsu.core.network.ImageProxyInterceptor
|
||||
import org.koitharu.kotatsu.core.network.MangaHttpClient
|
||||
@@ -34,6 +35,8 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.FileSize
|
||||
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
|
||||
import org.koitharu.kotatsu.core.util.ext.URI_SCHEME_ZIP
|
||||
import org.koitharu.kotatsu.core.util.ext.compressToPNG
|
||||
import org.koitharu.kotatsu.core.util.ext.ensureRamAtLeast
|
||||
import org.koitharu.kotatsu.core.util.ext.ensureSuccess
|
||||
import org.koitharu.kotatsu.core.util.ext.exists
|
||||
import org.koitharu.kotatsu.core.util.ext.getCompletionResultOrNull
|
||||
@@ -48,9 +51,9 @@ import org.koitharu.kotatsu.local.data.isZipUri
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||
import java.io.File
|
||||
import java.util.LinkedList
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.zip.ZipFile
|
||||
import javax.inject.Inject
|
||||
import kotlin.concurrent.Volatile
|
||||
import kotlin.coroutines.AbstractCoroutineContextElement
|
||||
@@ -73,6 +76,7 @@ class PageLoader @Inject constructor(
|
||||
private val semaphore = Semaphore(3)
|
||||
private val convertLock = Mutex()
|
||||
private val prefetchLock = Mutex()
|
||||
|
||||
@Volatile
|
||||
private var repository: MangaRepository? = null
|
||||
private val prefetchQueue = LinkedList<MangaPage>()
|
||||
@@ -122,19 +126,30 @@ class PageLoader @Inject constructor(
|
||||
return loadPageAsync(page, force).await()
|
||||
}
|
||||
|
||||
suspend fun tryConvert(file: File): Boolean = convertLock.withLock {
|
||||
if (context.ramAvailable < file.length() * 2) {
|
||||
return@withLock false
|
||||
}
|
||||
runInterruptible(Dispatchers.Default) {
|
||||
val image = BitmapFactory.decodeFile(file.absolutePath)
|
||||
try {
|
||||
file.outputStream().use { out ->
|
||||
image.compress(Bitmap.CompressFormat.PNG, 100, out)
|
||||
suspend fun convertBimap(uri: Uri): Uri = convertLock.withLock {
|
||||
if (uri.isZipUri()) {
|
||||
val bitmap = runInterruptible(Dispatchers.IO) {
|
||||
ZipFile(uri.schemeSpecificPart).use { zip ->
|
||||
val entry = zip.getEntry(uri.fragment)
|
||||
context.ensureRamAtLeast(entry.size * 2)
|
||||
zip.getInputStream(zip.getEntry(uri.fragment)).use {
|
||||
BitmapFactory.decodeStream(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
cache.put(uri.toString(), bitmap).toUri()
|
||||
} else {
|
||||
val file = uri.toFile()
|
||||
context.ensureRamAtLeast(file.length() * 2)
|
||||
val image = runInterruptible(Dispatchers.IO) {
|
||||
BitmapFactory.decodeFile(file.absolutePath)
|
||||
}
|
||||
try {
|
||||
image.compressToPNG(file)
|
||||
} finally {
|
||||
image.recycle()
|
||||
}
|
||||
uri
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package org.koitharu.kotatsu.reader.ui.pager
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.Observer
|
||||
import com.davemorrissey.labs.subscaleview.DefaultOnImageEventListener
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@@ -139,13 +137,9 @@ class PageHolderDelegate(
|
||||
prevJob?.join()
|
||||
state = State.CONVERTING
|
||||
try {
|
||||
val file = uri.toFile()
|
||||
if (!loader.tryConvert(file)) {
|
||||
state = State.ERROR
|
||||
callback.onError(e)
|
||||
}
|
||||
val newUri = loader.convertBimap(uri)
|
||||
state = State.CONVERTED
|
||||
callback.onImageReady(file.toUri())
|
||||
callback.onImageReady(newUri)
|
||||
} catch (ce: CancellationException) {
|
||||
throw ce
|
||||
} catch (e2: Throwable) {
|
||||
|
||||
Reference in New Issue
Block a user