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.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 ->
|
||||||
runInterruptible(Dispatchers.Default) {
|
val entry = zip.getEntry(uri.fragment)
|
||||||
val image = BitmapFactory.decodeFile(file.absolutePath)
|
context.ensureRamAtLeast(entry.size * 2)
|
||||||
try {
|
zip.getInputStream(zip.getEntry(uri.fragment)).use {
|
||||||
file.outputStream().use { out ->
|
BitmapFactory.decodeStream(it)
|
||||||
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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user