Fix converting bitmaps in local manga

pull/611/merge
Koitharu 2 years ago
parent 8b63d227a7
commit ec41d36508
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -17,6 +17,7 @@ import android.content.SyncResult
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.ResolveInfo import android.content.pm.ResolveInfo
import android.database.SQLException import android.database.SQLException
import android.graphics.Bitmap
import android.graphics.Color import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
@ -29,6 +30,7 @@ import android.view.Window
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.IntegerRes import androidx.annotation.IntegerRes
import androidx.annotation.WorkerThread
import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -37,6 +39,7 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope import androidx.lifecycle.coroutineScope
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import com.google.android.material.elevation.ElevationOverlayProvider import com.google.android.material.elevation.ElevationOverlayProvider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -45,7 +48,9 @@ import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runInterruptible
import okio.IOException import okio.IOException
import okio.use
import org.json.JSONException import org.json.JSONException
import org.jsoup.internal.StringUtil.StringJoiner import org.jsoup.internal.StringUtil.StringJoiner
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
@ -53,6 +58,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException import org.xmlpull.v1.XmlPullParserException
import java.io.File
import kotlin.math.roundToLong import kotlin.math.roundToLong
val Context.activityManager: ActivityManager? val Context.activityManager: ActivityManager?
@ -230,3 +236,18 @@ fun Context.checkNotificationPermission(): Boolean = if (Build.VERSION.SDK_INT >
} else { } else {
NotificationManagerCompat.from(this).areNotificationsEnabled() 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 package org.koitharu.kotatsu.local.data
import android.content.Context import android.content.Context
import android.graphics.Bitmap
import android.os.StatFs import android.os.StatFs
import com.tomclaw.cache.DiskLruCache import com.tomclaw.cache.DiskLruCache
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
@ -10,15 +11,17 @@ import kotlinx.coroutines.withContext
import okio.Source import okio.Source
import okio.buffer import okio.buffer
import okio.sink import okio.sink
import okio.use
import org.koitharu.kotatsu.core.util.FileSize 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.longHashCode
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.subdir import org.koitharu.kotatsu.core.util.ext.subdir
import org.koitharu.kotatsu.core.util.ext.takeIfReadable import org.koitharu.kotatsu.core.util.ext.takeIfReadable
import org.koitharu.kotatsu.core.util.ext.takeIfWriteable import org.koitharu.kotatsu.core.util.ext.takeIfWriteable
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
import org.koitharu.kotatsu.parsers.util.SuspendLazy import org.koitharu.kotatsu.parsers.util.SuspendLazy
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton 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 { private suspend fun getAvailableSize(): Long = runCatchingCancellable {
val statFs = StatFs(cacheDir.get().absolutePath) val statFs = StatFs(cacheDir.get().absolutePath)
statFs.availableBytes statFs.availableBytes

@ -1,12 +1,12 @@
package org.koitharu.kotatsu.reader.domain package org.koitharu.kotatsu.reader.domain
import android.content.Context import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import androidx.annotation.AnyThread import androidx.annotation.AnyThread
import androidx.collection.LongSparseArray import androidx.collection.LongSparseArray
import androidx.collection.set import androidx.collection.set
import androidx.core.net.toFile
import androidx.core.net.toUri import androidx.core.net.toUri
import dagger.hilt.android.ActivityRetainedLifecycle import dagger.hilt.android.ActivityRetainedLifecycle
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
@ -25,6 +25,7 @@ import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.sync.withPermit
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okio.use
import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.network.ImageProxyInterceptor import org.koitharu.kotatsu.core.network.ImageProxyInterceptor
import org.koitharu.kotatsu.core.network.MangaHttpClient 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.FileSize
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
import org.koitharu.kotatsu.core.util.ext.URI_SCHEME_ZIP 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.ensureSuccess
import org.koitharu.kotatsu.core.util.ext.exists import org.koitharu.kotatsu.core.util.ext.exists
import org.koitharu.kotatsu.core.util.ext.getCompletionResultOrNull 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.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import java.io.File
import java.util.LinkedList import java.util.LinkedList
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import java.util.zip.ZipFile
import javax.inject.Inject import javax.inject.Inject
import kotlin.concurrent.Volatile import kotlin.concurrent.Volatile
import kotlin.coroutines.AbstractCoroutineContextElement import kotlin.coroutines.AbstractCoroutineContextElement
@ -73,6 +76,7 @@ class PageLoader @Inject constructor(
private val semaphore = Semaphore(3) private val semaphore = Semaphore(3)
private val convertLock = Mutex() private val convertLock = Mutex()
private val prefetchLock = Mutex() private val prefetchLock = Mutex()
@Volatile @Volatile
private var repository: MangaRepository? = null private var repository: MangaRepository? = null
private val prefetchQueue = LinkedList<MangaPage>() private val prefetchQueue = LinkedList<MangaPage>()
@ -122,19 +126,30 @@ class PageLoader @Inject constructor(
return loadPageAsync(page, force).await() return loadPageAsync(page, force).await()
} }
suspend fun tryConvert(file: File): Boolean = convertLock.withLock { suspend fun convertBimap(uri: Uri): Uri = convertLock.withLock {
if (context.ramAvailable < file.length() * 2) { if (uri.isZipUri()) {
return@withLock false 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)
} }
runInterruptible(Dispatchers.Default) {
val image = BitmapFactory.decodeFile(file.absolutePath)
try {
file.outputStream().use { out ->
image.compress(Bitmap.CompressFormat.PNG, 100, out)
} }
}
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 { } finally {
image.recycle() image.recycle()
} }
uri
} }
} }

@ -1,8 +1,6 @@
package org.koitharu.kotatsu.reader.ui.pager package org.koitharu.kotatsu.reader.ui.pager
import android.net.Uri import android.net.Uri
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.davemorrissey.labs.subscaleview.DefaultOnImageEventListener import com.davemorrissey.labs.subscaleview.DefaultOnImageEventListener
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
@ -139,13 +137,9 @@ class PageHolderDelegate(
prevJob?.join() prevJob?.join()
state = State.CONVERTING state = State.CONVERTING
try { try {
val file = uri.toFile() val newUri = loader.convertBimap(uri)
if (!loader.tryConvert(file)) {
state = State.ERROR
callback.onError(e)
}
state = State.CONVERTED state = State.CONVERTED
callback.onImageReady(file.toUri()) callback.onImageReady(newUri)
} catch (ce: CancellationException) { } catch (ce: CancellationException) {
throw ce throw ce
} catch (e2: Throwable) { } catch (e2: Throwable) {

Loading…
Cancel
Save