diff --git a/app/build.gradle b/app/build.gradle index e876a0a6f..3912d1930 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -75,6 +75,8 @@ android { '-opt-in=kotlin.contracts.ExperimentalContracts', '-opt-in=coil3.annotation.ExperimentalCoilApi', '-opt-in=coil3.annotation.InternalCoilApi', + '-Xjspecify-annotations=strict', + '-Xtype-enhancement-improvements-strict-mode', ] } room { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/image/AvifImageDecoder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/image/AvifImageDecoder.kt index 140fea957..65ea8edd4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/image/AvifImageDecoder.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/image/AvifImageDecoder.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.core.image import android.graphics.Bitmap +import androidx.core.graphics.createBitmap import coil3.ImageLoader import coil3.asImage import coil3.decode.DecodeResult @@ -32,7 +33,7 @@ class AvifImageDecoder( ) } val config = if (info.depth == 8 || info.alphaPresent) Bitmap.Config.ARGB_8888 else Bitmap.Config.RGB_565 - val bitmap = Bitmap.createBitmap(info.width, info.height, config) + val bitmap = createBitmap(info.width, info.height, config) if (!AvifDecoder.decode(bytes, bytes.remaining(), bitmap)) { bitmap.recycle() throw ImageDecodeException(null, "avif") diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/image/BitmapDecoderCompat.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/image/BitmapDecoderCompat.kt index dabb69050..4bedf2f48 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/image/BitmapDecoderCompat.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/image/BitmapDecoderCompat.kt @@ -2,15 +2,19 @@ package org.koitharu.kotatsu.core.image import android.graphics.Bitmap import android.graphics.BitmapFactory +import android.graphics.BitmapRegionDecoder import android.graphics.ImageDecoder import android.os.Build import androidx.annotation.RequiresApi +import androidx.core.graphics.createBitmap import com.davemorrissey.labs.subscaleview.decoder.ImageDecodeException +import okio.IOException import org.aomedia.avif.android.AvifDecoder import org.aomedia.avif.android.AvifDecoder.Info import org.jetbrains.annotations.Blocking import org.koitharu.kotatsu.core.util.MimeTypes import org.koitharu.kotatsu.core.util.ext.MimeType +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.toByteBuffer import org.koitharu.kotatsu.core.util.ext.toMimeTypeOrNull import org.koitharu.kotatsu.parsers.util.runCatchingCancellable @@ -51,6 +55,19 @@ object BitmapDecoderCompat { } } + @Blocking + fun createRegionDecoder(inoutStream: InputStream): BitmapRegionDecoder? = try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + BitmapRegionDecoder.newInstance(inoutStream) + } else { + @Suppress("DEPRECATION") + BitmapRegionDecoder.newInstance(inoutStream, false) + } + } catch (e: IOException) { + e.printStackTraceDebug() + null + } + @Blocking fun probeMimeType(file: File): MimeType? { return MimeTypes.probeMimeType(file) ?: detectBitmapType(file) @@ -62,7 +79,7 @@ object BitmapDecoderCompat { inJustDecodeBounds = true } BitmapFactory.decodeFile(file.path, options)?.recycle() - return options.outMimeType?.toMimeTypeOrNull() + options.outMimeType?.toMimeTypeOrNull() }.getOrNull() private fun checkBitmapNotNull(bitmap: Bitmap?, format: String?): Bitmap = @@ -78,7 +95,7 @@ object BitmapDecoderCompat { ) } val config = if (info.depth == 8 || info.alphaPresent) Bitmap.Config.ARGB_8888 else Bitmap.Config.RGB_565 - val bitmap = Bitmap.createBitmap(info.width, info.height, config) + val bitmap = createBitmap(info.width, info.height, config) if (!AvifDecoder.decode(bytes, bytes.remaining(), bitmap)) { bitmap.recycle() throw ImageDecodeException(null, FORMAT_AVIF) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/image/RegionBitmapDecoder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/image/RegionBitmapDecoder.kt index c397c3b8d..abee08347 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/image/RegionBitmapDecoder.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/image/RegionBitmapDecoder.kt @@ -2,7 +2,6 @@ package org.koitharu.kotatsu.core.image import android.graphics.Bitmap import android.graphics.BitmapFactory -import android.graphics.BitmapRegionDecoder import android.graphics.Rect import android.os.Build import coil3.Extras @@ -11,7 +10,6 @@ import coil3.asImage import coil3.decode.DecodeResult import coil3.decode.DecodeUtils import coil3.decode.Decoder -import coil3.decode.ImageSource import coil3.fetch.SourceFetchResult import coil3.getExtra import coil3.request.Options @@ -25,24 +23,31 @@ import coil3.size.Scale import coil3.size.Size import coil3.size.isOriginal import coil3.size.pxOrElse -import kotlinx.coroutines.runInterruptible import kotlin.math.roundToInt class RegionBitmapDecoder( - private val source: ImageSource, + private val fetchResult: SourceFetchResult, private val options: Options, + private val imageLoader: ImageLoader, ) : Decoder { - override suspend fun decode(): DecodeResult = runInterruptible { - val regionDecoder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - BitmapRegionDecoder.newInstance(source.source().inputStream()) - } else { - @Suppress("DEPRECATION") - BitmapRegionDecoder.newInstance(source.source().inputStream(), false) + override suspend fun decode(): DecodeResult? { + val regionDecoder = BitmapDecoderCompat.createRegionDecoder(fetchResult.source.source().inputStream()) + if (regionDecoder == null) { + val fallbackDecoder = imageLoader.components.newDecoder( + result = fetchResult, + options = options, + imageLoader = imageLoader, + startIndex = 0, + )?.first + return if (fallbackDecoder == null || fallbackDecoder is RegionBitmapDecoder) { + null + } else { + fallbackDecoder.decode() + } } - checkNotNull(regionDecoder) val bitmapOptions = BitmapFactory.Options() - try { + return try { val rect = bitmapOptions.configureScale(regionDecoder.width, regionDecoder.height) bitmapOptions.configureConfig() val bitmap = regionDecoder.decodeRegion(rect, bitmapOptions) @@ -149,7 +154,7 @@ class RegionBitmapDecoder( result: SourceFetchResult, options: Options, imageLoader: ImageLoader - ): Decoder = RegionBitmapDecoder(result.source, options) + ): Decoder = RegionBitmapDecoder(result, options, imageLoader) override fun equals(other: Any?) = other is Factory diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt index 708666d2e..8c37c8316 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt @@ -3,7 +3,9 @@ package org.koitharu.kotatsu.reader.domain import android.content.Context import android.graphics.Rect import android.net.Uri +import android.util.Log import androidx.annotation.AnyThread +import androidx.annotation.CheckResult import androidx.collection.LongSparseArray import androidx.collection.set import androidx.core.net.toFile @@ -30,7 +32,6 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withPermit -import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import okhttp3.Request import okio.use @@ -184,9 +185,10 @@ class PageLoader @Inject constructor( return loadPageAsync(page, force).await() } + @CheckResult suspend fun convertBimap(uri: Uri): Uri = convertLock.withLock { if (uri.isZipUri()) { - val bitmap = runInterruptible(Dispatchers.IO) { + runInterruptible(Dispatchers.IO) { ZipFile(uri.schemeSpecificPart).use { zip -> val entry = zip.getEntry(uri.fragment) context.ensureRamAtLeast(entry.size * 2) @@ -194,8 +196,9 @@ class PageLoader @Inject constructor( BitmapDecoderCompat.decode(it, MimeTypes.getMimeTypeFromExtension(entry.name)) } } + }.use { image -> + cache.put(uri.toString(), image).toUri() } - cache.put(uri.toString(), bitmap).toUri() } else { val file = uri.toFile() runInterruptible(Dispatchers.IO) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/vm/PageViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/vm/PageViewModel.kt index 2ff9cb1b3..c5388c7e6 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/vm/PageViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/vm/PageViewModel.kt @@ -119,7 +119,7 @@ class PageViewModel( } else { null } - state.value = PageState.Loaded(uri.toImageSource(cachedBounds), isConverted = true) + state.value = PageState.Loaded(newUri.toImageSource(cachedBounds), isConverted = true) } catch (ce: CancellationException) { throw ce } catch (e2: Throwable) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 59115d12b..4c3301bb9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ collections = "1.5.0" #noinspection NewerVersionAvailable,GradleDependency - 2.5.3 cause crashes conscrypt = "2.5.2" constraintlayout = "2.2.1" -coreKtx = "1.15.0" +coreKtx = "1.16.0" coroutines = "1.10.2" desugar = "2.1.5" diskLruCache = "1.5"