diff --git a/apng_library/build.gradle b/apng_library/build.gradle
index 038ac45..655d275 100644
--- a/apng_library/build.gradle
+++ b/apng_library/build.gradle
@@ -1,6 +1,5 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
-apply plugin: 'kotlin-android-extensions'
android {
@@ -29,8 +28,7 @@ android {
}
dependencies {
- implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3'
- implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0'
implementation 'androidx.appcompat:appcompat:1.3.0'
diff --git a/apng_library/src/main/AndroidManifest.xml b/apng_library/src/main/AndroidManifest.xml
index a49ba4d..e7377fa 100644
--- a/apng_library/src/main/AndroidManifest.xml
+++ b/apng_library/src/main/AndroidManifest.xml
@@ -1,2 +1,3 @@
+
diff --git a/apng_library/src/main/java/oupson/apng/decoder/ApngDecoder.kt b/apng_library/src/main/java/oupson/apng/decoder/ApngDecoder.kt
index bf08a56..ef8dad0 100644
--- a/apng_library/src/main/java/oupson/apng/decoder/ApngDecoder.kt
+++ b/apng_library/src/main/java/oupson/apng/decoder/ApngDecoder.kt
@@ -3,23 +3,19 @@ package oupson.apng.decoder
import android.content.Context
import android.graphics.*
import android.graphics.drawable.AnimatedImageDrawable
-import android.graphics.drawable.AnimationDrawable
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.util.Log
-import android.widget.ImageView
import androidx.annotation.RawRes
import kotlinx.coroutines.*
import oupson.apng.BuildConfig
-import oupson.apng.decoder.ApngDecoder.Companion.decodeApng
import oupson.apng.drawable.ApngDrawable
import oupson.apng.exceptions.BadApngException
import oupson.apng.exceptions.BadCRCException
import oupson.apng.utils.Loader
import oupson.apng.utils.Utils
-import oupson.apng.utils.Utils.Companion.isPng
import java.io.*
import java.net.URL
import java.nio.ByteBuffer
@@ -29,21 +25,7 @@ import java.util.zip.CRC32
* An APNG Decoder.
* Call [decodeApng]
*/
-class ApngDecoder {
- interface Callback {
- /**
- * Function called when the file was successfully decoded.
- * @param drawable Can be an [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif. If it is not an animated image, it is a [Drawable].
- */
- fun onSuccess(drawable: Drawable)
-
- /**
- * Function called when something gone wrong.
- * @param error The problem.
- */
- fun onError(error: Exception)
- }
-
+class ApngDecoder(input: InputStream, val config: Config) {
class Config(
internal var speed: Float = 1f,
internal var bitmapConfig: Bitmap.Config = Bitmap.Config.ARGB_8888,
@@ -71,6 +53,580 @@ class ApngDecoder {
}
}
+ private var inputStream: InputStream? = input
+ private var result: Result? = null
+
+ /**
+ * Decode Apng and return a Drawable who can be an [ApngDrawable] if it end successfully. Can also be an [android.graphics.drawable.AnimatedImageDrawable].
+ * @param context Context needed for the animation drawable
+ * @param inStream Input Stream to decode. Will be closed at the end.
+ * @param config Decoder configuration
+ * @return [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif. If it is not an animated image, it is a [Drawable].
+ */
+ suspend fun decodeApng(
+ context: Context
+ ): Result =
+ kotlin.runCatching {
+ withContext(Dispatchers.Default) {
+ val inputStream = BufferedInputStream(inputStream)
+ val bytes = ByteArray(8)
+ inputStream.mark(8)
+
+ withContext(Dispatchers.IO) {
+ inputStream.read(bytes)
+ }
+
+ if (Utils.isPng(bytes)) {
+ var png: ByteArrayOutputStream? = null
+ var cover: ByteArrayOutputStream? = null
+ var delay = -1f
+ var yOffset = -1
+ var xOffset = -1
+ var plte: ByteArray? = null
+ var tnrs: ByteArray? = null
+ var maxWidth = 0
+ var maxHeight = 0
+ var blendOp: Utils.Companion.BlendOp =
+ Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE
+ var disposeOp: Utils.Companion.DisposeOp =
+ Utils.Companion.DisposeOp.APNG_DISPOSE_OP_NONE
+
+ var ihdrOfApng = ByteArray(0)
+
+ var isApng = false
+
+ val drawable = ApngDrawable().apply {
+ isOneShot = false
+ }
+
+ var buffer: Bitmap? = null
+
+ var byteRead: Int
+ val lengthChunk = ByteArray(4)
+ do {
+ val length: Int
+ val chunk: ByteArray
+ if (withContext(Dispatchers.IO) {
+ byteRead = inputStream.read(lengthChunk)
+
+ if (byteRead != -1) {
+ length = Utils.uIntFromBytesBigEndian(lengthChunk)
+
+ chunk = ByteArray(length + 8)
+ byteRead = inputStream.read(chunk)
+ false
+ } else {
+ chunk = ByteArray(0)
+ true
+ }
+ }) {
+ break
+ }
+
+ val byteArray = lengthChunk.plus(chunk)
+ val chunkCRC = Utils.uIntFromBytesBigEndian(byteArray, byteArray.size - 4)
+ val crc = CRC32()
+ crc.update(byteArray, 4, byteArray.size - 8)
+ if (chunkCRC == crc.value.toInt()) {
+ val name = byteArray.copyOfRange(4, 8)
+ when {
+ name.contentEquals(Utils.fcTL) -> {
+ if (png == null) {
+ if (config.decodeCoverFrame) {
+ drawable.coverFrame = cover?.let {
+ it.write(zeroLength)
+ // Generate crc for IEND
+ val crC32 = CRC32()
+ crC32.update(Utils.IEND, 0, Utils.IEND.size)
+ it.write(Utils.IEND)
+ it.write(Utils.uIntToByteArray(crC32.value.toInt()))
+
+ val pngBytes = it.toByteArray()
+ BitmapFactory.decodeByteArray(
+ pngBytes,
+ 0,
+ pngBytes.size
+ )
+ }
+ }
+ cover?.close()
+ cover = null
+ } else {
+ // Add IEND body length : 0
+ png.write(zeroLength)
+ // Add IEND
+ // Generate crc for IEND
+ val crC32 = CRC32()
+ crC32.update(Utils.IEND, 0, Utils.IEND.size)
+ png.write(Utils.IEND)
+ png.write(Utils.uIntToByteArray(crC32.value.toInt()))
+
+ val btm = Bitmap.createBitmap(
+ maxWidth,
+ maxHeight,
+ Bitmap.Config.ARGB_8888
+ )
+
+ val pngBytes = png.toByteArray()
+ val decoded = BitmapFactory.decodeByteArray(
+ pngBytes,
+ 0,
+ pngBytes.size
+ )
+ val canvas = Canvas(btm)
+ canvas.drawBitmap(buffer!!, 0f, 0f, null)
+
+ if (blendOp == Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE) {
+ canvas.drawRect(
+ xOffset.toFloat(),
+ yOffset.toFloat(),
+ xOffset + decoded.width.toFloat(),
+ yOffset + decoded.height.toFloat(),
+ clearPaint
+ )
+ }
+
+ canvas.drawBitmap(
+ decoded,
+ xOffset.toFloat(),
+ yOffset.toFloat(),
+ null
+ )
+
+ drawable.addFrame(
+ BitmapDrawable(
+ context.resources,
+ if (btm.config != config.bitmapConfig) {
+ if (BuildConfig.DEBUG)
+ Log.v(
+ TAG,
+ "Bitmap Config : ${btm.config}, Config : $config"
+ )
+ btm.copy(config.bitmapConfig, btm.isMutable)
+ } else {
+ btm
+ }
+ ),
+ (delay / config.speed).toInt()
+ )
+
+ when (disposeOp) {
+ Utils.Companion.DisposeOp.APNG_DISPOSE_OP_PREVIOUS -> {
+ //Do nothings
+ }
+ // Add current frame to bitmap buffer
+ // APNG_DISPOSE_OP_BACKGROUND: the frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.
+ Utils.Companion.DisposeOp.APNG_DISPOSE_OP_BACKGROUND -> {
+ val res = Bitmap.createBitmap(
+ maxWidth,
+ maxHeight,
+ Bitmap.Config.ARGB_8888
+ )
+ val can = Canvas(res)
+ can.drawBitmap(btm, 0f, 0f, null)
+ can.drawRect(
+ xOffset.toFloat(),
+ yOffset.toFloat(),
+ xOffset + decoded.width.toFloat(),
+ yOffset + decoded.height.toFloat(),
+ clearPaint
+ )
+ buffer = res
+ }
+ else -> buffer = btm
+ }
+ }
+
+ png?.close()
+ png = ByteArrayOutputStream(4096)
+
+ // Parse Frame ConTroL chunk
+ // Get the width of the png
+ val width = Utils.uIntFromBytesBigEndian(
+ byteArray, 12
+ )
+ // Get the height of the png
+ val height = Utils.uIntFromBytesBigEndian(
+ byteArray, 16
+ )
+
+ /*
+ * The `delay_num` and `delay_den` parameters together specify a fraction indicating the time to display the current frame, in seconds.
+ * If the the value of the numerator is 0 the decoder should render the next frame as quickly as possible, though viewers may impose a reasonable lower bound.
+ */
+ // Get delay numerator
+ val delayNum = Utils.uShortFromBytesBigEndian(
+ byteArray, 28
+ ).toFloat()
+ // Get delay denominator
+ var delayDen = Utils.uShortFromBytesBigEndian(
+ byteArray, 30
+ ).toFloat()
+
+ // If the denominator is 0, it is to be treated as if it were 100 (that is, `delay_num` then specifies 1/100ths of a second).
+ if (delayDen == 0f) {
+ delayDen = 100f
+ }
+
+ delay = (delayNum / delayDen * 1000)
+
+ // Get x and y offsets
+ xOffset = Utils.uIntFromBytesBigEndian(
+ byteArray, 20
+ )
+ yOffset = Utils.uIntFromBytesBigEndian(
+ byteArray, 24
+ )
+ blendOp = Utils.decodeBlendOp(byteArray[33].toInt())
+ disposeOp = Utils.decodeDisposeOp(byteArray[32].toInt())
+
+ if (xOffset + width > maxWidth) {
+ throw BadApngException("`xOffset` + `width` must be <= `IHDR` width")
+ } else if (yOffset + height > maxHeight) {
+ throw BadApngException("`yOffset` + `height` must be <= `IHDR` height")
+ }
+
+ png.write(Utils.pngSignature)
+ png.write(
+ generateIhdr(
+ ihdrOfApng,
+ width,
+ height
+ )
+ )
+ plte?.let {
+ png.write(it)
+ }
+ tnrs?.let {
+ png.write(it)
+ }
+
+ }
+ name.contentEquals(Utils.IEND) -> {
+ if (isApng && png != null) {
+ png.write(zeroLength)
+ // Add IEND
+ // Generate crc for IEND
+ val crC32 = CRC32()
+ crC32.update(Utils.IEND, 0, Utils.IEND.size)
+ png.write(Utils.IEND)
+ png.write(Utils.uIntToByteArray(crC32.value.toInt()))
+
+ val btm = Bitmap.createBitmap(
+ maxWidth,
+ maxHeight,
+ Bitmap.Config.ARGB_8888
+ )
+
+ val pngBytes = png.toByteArray()
+ png.close()
+ val decoded = BitmapFactory.decodeByteArray(
+ pngBytes,
+ 0,
+ pngBytes.size
+ )
+ val canvas = Canvas(btm)
+ canvas.drawBitmap(buffer!!, 0f, 0f, null)
+
+ if (blendOp == Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE) {
+ canvas.drawRect(
+ xOffset.toFloat(),
+ yOffset.toFloat(),
+ xOffset + decoded.width.toFloat(),
+ yOffset + decoded.height.toFloat(),
+ clearPaint
+ )
+ }
+
+ canvas.drawBitmap(
+ decoded,
+ xOffset.toFloat(),
+ yOffset.toFloat(),
+ null
+ )
+ drawable.addFrame(
+ BitmapDrawable(
+ context.resources,
+ if (btm.config != config.bitmapConfig) {
+ if (BuildConfig.DEBUG)
+ Log.v(
+ TAG,
+ "Bitmap Config : ${btm.config}, Config : $config"
+ )
+ btm.copy(config.bitmapConfig, btm.isMutable)
+ } else {
+ btm
+ }
+ ),
+ (delay / config.speed).toInt()
+ )
+
+ when (disposeOp) {
+ Utils.Companion.DisposeOp.APNG_DISPOSE_OP_PREVIOUS -> {
+ //Do nothings
+ }
+ // Add current frame to bitmap buffer
+ // APNG_DISPOSE_OP_BACKGROUND: the frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.
+ Utils.Companion.DisposeOp.APNG_DISPOSE_OP_BACKGROUND -> {
+ val res = Bitmap.createBitmap(
+ maxWidth,
+ maxHeight,
+ Bitmap.Config.ARGB_8888
+ )
+ val can = Canvas(res)
+ can.drawBitmap(btm, 0f, 0f, null)
+ can.drawRect(
+ xOffset.toFloat(),
+ yOffset.toFloat(),
+ xOffset + decoded.width.toFloat(),
+ yOffset + decoded.height.toFloat(),
+ clearPaint
+ )
+ buffer = res
+ }
+ else -> buffer = btm
+ }
+ } else {
+ cover?.let {
+ it.write(zeroLength)
+ // Add IEND
+ // Generate crc for IEND
+ val crC32 = CRC32()
+ crC32.update(Utils.IEND, 0, Utils.IEND.size)
+ it.write(Utils.IEND)
+ it.write(Utils.uIntToByteArray(crC32.value.toInt()))
+
+ withContext(Dispatchers.IO) {
+ inputStream.close()
+ }
+
+ val pngBytes = it.toByteArray()
+ it.close()
+
+ return@withContext BitmapDrawable(
+ context.resources,
+ BitmapFactory.decodeByteArray(
+ pngBytes,
+ 0,
+ pngBytes.size
+ )
+ )
+ }
+ }
+ }
+ name.contentEquals(Utils.IDAT) -> {
+ val w = if (png == null) {
+ if (isApng && !config.decodeCoverFrame) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Ignoring cover frame")
+ continue
+ }
+ if (cover == null) {
+ cover = ByteArrayOutputStream()
+ cover.write(Utils.pngSignature)
+ cover.write(
+ generateIhdr(
+ ihdrOfApng,
+ maxWidth,
+ maxHeight
+ )
+ )
+ }
+ cover
+ } else {
+ png
+ }
+
+ // Find the chunk length
+ val bodySize =
+ Utils.uIntFromBytesBigEndian(
+ byteArray, 0
+ )
+ w.write(byteArray.copyOfRange(0, 4))
+
+ val body = ByteArray(4 + bodySize)
+
+ System.arraycopy(Utils.IDAT, 0, body, 0, 4)
+
+ // Get image bytes
+ System.arraycopy(byteArray, 8, body, 4, bodySize)
+
+ val crC32 = CRC32()
+ crC32.update(body, 0, body.size)
+ w.write(body)
+ w.write(Utils.uIntToByteArray(crC32.value.toInt()))
+ }
+ name.contentEquals(Utils.fdAT) -> {
+ // Find the chunk length
+ val bodySize = Utils.uIntFromBytesBigEndian(byteArray, 0)
+ png?.write(Utils.uIntToByteArray(bodySize - 4))
+
+ val body = ByteArray(bodySize)
+ System.arraycopy(Utils.IDAT, 0, body, 0, 4)
+
+ // Get image bytes
+ System.arraycopy(byteArray, 12, body, 4, bodySize - 4)
+
+ val crC32 = CRC32()
+ crC32.update(body, 0, body.size)
+ png?.write(body)
+ png?.write(Utils.uIntToByteArray(crC32.value.toInt()))
+ }
+ name.contentEquals(Utils.plte) -> {
+ plte = byteArray
+ }
+ name.contentEquals(Utils.tnrs) -> {
+ tnrs = byteArray
+ }
+ name.contentEquals(Utils.IHDR) -> {
+ // Get length of the body of the chunk
+ val bodySize = Utils.uIntFromBytesBigEndian(byteArray, 0)
+ // Get the width of the png
+ maxWidth = Utils.uIntFromBytesBigEndian(byteArray, 8)
+ // Get the height of the png
+ maxHeight = Utils.uIntFromBytesBigEndian(byteArray, 12)
+ ihdrOfApng = byteArray.copyOfRange(4 + 4, 4 + bodySize + 4)
+
+ buffer = Bitmap.createBitmap(
+ maxWidth,
+ maxHeight,
+ Bitmap.Config.ARGB_8888
+ )
+ }
+ name.contentEquals(Utils.acTL) -> { // TODO GET NBR REPETITIONS
+ isApng = true
+ }
+ }
+ } else throw BadCRCException()
+ } while (byteRead != -1 && isActive)
+ withContext(Dispatchers.IO) {
+ inputStream.close()
+ }
+ drawable
+ } else {
+ if (BuildConfig.DEBUG)
+ Log.i(TAG, "Decoding non APNG stream")
+ inputStream.reset()
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ val bytesRead: ByteArray
+ withContext(Dispatchers.IO) {
+ bytesRead = inputStream.readBytes()
+ inputStream.close()
+ }
+ val buf = ByteBuffer.wrap(bytesRead)
+ val source = ImageDecoder.createSource(buf)
+ withContext(Dispatchers.IO) {
+ ImageDecoder.decodeDrawable(source)
+ }
+ } else {
+ val drawable = Drawable.createFromStream(
+ inputStream,
+ null
+ )
+ withContext(Dispatchers.IO) {
+ inputStream.close()
+ }
+ drawable!!
+ }
+ }
+ }
+ }
+
+ suspend fun getDecoded(context: Context): Result {
+ if (result == null) {
+ result = decodeApng(context)
+
+ kotlin.runCatching {
+ withContext(Dispatchers.IO) {
+ inputStream?.close()
+ }
+ }.onFailure {
+ this.result = Result.failure(it)
+ }
+
+ inputStream = null
+ }
+
+ return result ?: Result.failure(NullPointerException("result is null"))
+ }
+
+ /**
+ * Decode Apng and return a Drawable who can be an [ApngDrawable] if it end successfully. Can also be an [android.graphics.drawable.AnimatedImageDrawable].
+ * @param context Context needed for animation drawable.
+ * @param file File to decode.
+ * @param config Decoder configuration
+ * @return [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif. If it is not an animated image, it is a [Drawable].
+ */
+ // TODO DOC
+ constructor(file: File, config: Config = Config()) : this(FileInputStream(file), config)
+
+ /**
+ * Decode Apng and return a Drawable who can be an [ApngDrawable] if it end successfully. Can also be an [android.graphics.drawable.AnimatedImageDrawable].
+ * @param context Context is needed for contentResolver and animation drawable.
+ * @param uri Uri to open.
+ * @param config Decoder configuration
+ * @return [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif.
+ */
+ // TODO DOC + better
+ constructor(
+ context: Context,
+ uri: Uri,
+ config: Config = Config()
+ ) : this(context.contentResolver.openInputStream(uri)!!, config)
+
+ /**
+ * Decode Apng and return a Drawable who can be an [ApngDrawable] if it end successfully. Can also be an [android.graphics.drawable.AnimatedImageDrawable].
+ * @param context Context is needed for contentResolver and animation drawable.
+ * @param res Resource to decode.
+ * @param config Decoder configuration
+ * @return [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif.
+ */
+ // TODO DOC
+ constructor(
+ context: Context,
+ @RawRes res: Int,
+ config: Config = Config()
+ ) : this(context.resources.openRawResource(res), config)
+
+
+ /**
+ * Generate a correct IHDR from the IHDR chunk of the APNG.
+ * @param ihdrOfApng The IHDR of the APNG.
+ * @param width The width of the frame.
+ * @param height The height of the frame.
+ * @return [ByteArray] The generated IHDR.
+ */
+ private fun generateIhdr(ihdrOfApng: ByteArray, width: Int, height: Int): ByteArray {
+ val ihdr =
+ ByteArray(0xD + 4 + 4 + 4) // 0xD (IHDR body length) + 4 (0x0, 0x0, 0x0, 0xD : the chunk length) + 4 : IHDR + 4 : CRC
+
+ // Add chunk body length
+ System.arraycopy(Utils.uIntToByteArray(0xD), 0, ihdr, 0, 4)
+
+ // We need a body var to know body length and generate crc
+ val ihdrBody = ByteArray(0xD + 4) // 0xD (IHDR body length) + 4 : IHDR
+
+ // Add IHDR
+ System.arraycopy(Utils.IHDR, 0, ihdrBody, 0, 4)
+
+ // Add the max width and height
+ System.arraycopy(Utils.uIntToByteArray(width), 0, ihdrBody, 4, 4)
+ System.arraycopy(Utils.uIntToByteArray(height), 0, ihdrBody, 8, 4)
+
+ // Add complicated stuff like depth color ...
+ // If you want correct png you need same parameters.
+ System.arraycopy(ihdrOfApng, 8, ihdrBody, 12, 5)
+
+ // Generate CRC
+ val crC32 = CRC32()
+ crC32.update(ihdrBody, 0, 0xD + 4)
+
+ System.arraycopy(ihdrBody, 0, ihdr, 4, 0xD + 4)
+ System.arraycopy(Utils.uIntToByteArray(crC32.value.toInt()), 0, ihdr, 0xD + 4 + 4, 4)
+ return ihdr
+ }
+
companion object {
private const val TAG = "ApngDecoder"
private val zeroLength = byteArrayOf(0x00, 0x00, 0x00, 0x00)
@@ -84,544 +640,6 @@ class ApngDecoder {
}
}
- /**
- * Decode Apng and return a Drawable who can be an [ApngDrawable] if it end successfully. Can also be an [android.graphics.drawable.AnimatedImageDrawable].
- * @param context Context needed for the animation drawable
- * @param inStream Input Stream to decode. Will be closed at the end.
- * @param config Decoder configuration
- * @return [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif. If it is not an animated image, it is a [Drawable].
- */
- // TODO DOCUMENT CONFIG
- @Suppress(
- "MemberVisibilityCanBePrivate",
- "BlockingMethodInNonBlockingContext"
- ) // BlockingMethodInNonBlockingContext is a warning generated by java @Throws
- @JvmStatic
- @JvmOverloads
- suspend fun decodeApng(
- context: Context,
- inStream: InputStream,
- config: Config = Config()
- ): Drawable = withContext(Dispatchers.Default) {
- val inputStream = BufferedInputStream(inStream)
- val bytes = ByteArray(8)
- inputStream.mark(8)
- withContext(Dispatchers.IO) {
- inputStream.read(bytes)
- }
-
- if (isPng(bytes)) {
- var png: ByteArrayOutputStream? = null
- var cover: ByteArrayOutputStream? = null
- var delay = -1f
- var yOffset = -1
- var xOffset = -1
- var plte: ByteArray? = null
- var tnrs: ByteArray? = null
- var maxWidth = 0
- var maxHeight = 0
- var blendOp: Utils.Companion.BlendOp = Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE
- var disposeOp: Utils.Companion.DisposeOp =
- Utils.Companion.DisposeOp.APNG_DISPOSE_OP_NONE
-
- var ihdrOfApng = ByteArray(0)
-
- var isApng = false
-
- val drawable = ApngDrawable().apply {
- isOneShot = false
- }
-
- var buffer: Bitmap? = null
-
- var byteRead: Int
- val lengthChunk = ByteArray(4)
- do {
- val length: Int
- val chunk: ByteArray
- if (withContext(Dispatchers.IO) {
- byteRead = inputStream.read(lengthChunk)
-
-
- if (byteRead != -1) {
- length = Utils.uIntFromBytesBigEndian(lengthChunk)
-
- chunk = ByteArray(length + 8)
- byteRead = inputStream.read(chunk)
- false
- } else {
- chunk = ByteArray(0)
- true
- }
- }) {
- break
- }
-
- val byteArray = lengthChunk.plus(chunk)
- val chunkCRC = Utils.uIntFromBytesBigEndian(byteArray, byteArray.size - 4)
- val crc = CRC32()
- crc.update(byteArray, 4, byteArray.size - 8)
- if (chunkCRC == crc.value.toInt()) {
- val name = byteArray.copyOfRange(4, 8)
- when {
- name.contentEquals(Utils.fcTL) -> {
- if (png == null) {
- if (config.decodeCoverFrame) {
- drawable.coverFrame = cover?.let {
- it.write(zeroLength)
- // Generate crc for IEND
- val crC32 = CRC32()
- crC32.update(Utils.IEND, 0, Utils.IEND.size)
- it.write(Utils.IEND)
- it.write(Utils.uIntToByteArray(crC32.value.toInt()))
-
- val pngBytes = it.toByteArray()
- BitmapFactory.decodeByteArray(
- pngBytes,
- 0,
- pngBytes.size
- )
- }
- }
- cover = null
- } else {
- // Add IEND body length : 0
- png.write(zeroLength)
- // Add IEND
- // Generate crc for IEND
- val crC32 = CRC32()
- crC32.update(Utils.IEND, 0, Utils.IEND.size)
- png.write(Utils.IEND)
- png.write(Utils.uIntToByteArray(crC32.value.toInt()))
-
- val btm = Bitmap.createBitmap(
- maxWidth,
- maxHeight,
- Bitmap.Config.ARGB_8888
- )
-
- val pngBytes = png.toByteArray()
- val decoded = BitmapFactory.decodeByteArray(
- pngBytes,
- 0,
- pngBytes.size
- )
- val canvas = Canvas(btm)
- canvas.drawBitmap(buffer!!, 0f, 0f, null)
-
- if (blendOp == Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE) {
- canvas.drawRect(
- xOffset.toFloat(),
- yOffset.toFloat(),
- xOffset + decoded.width.toFloat(),
- yOffset + decoded.height.toFloat(),
- clearPaint
- )
- }
-
- canvas.drawBitmap(
- decoded,
- xOffset.toFloat(),
- yOffset.toFloat(),
- null
- )
-
- drawable.addFrame(
- BitmapDrawable(
- context.resources,
- if (btm.config != config.bitmapConfig) {
- if (BuildConfig.DEBUG)
- Log.v(
- TAG,
- "Bitmap Config : ${btm.config}, Config : $config"
- )
- btm.copy(config.bitmapConfig, btm.isMutable)
- } else {
- btm
- }
- ),
- (delay / config.speed).toInt()
- )
-
- when (disposeOp) {
- Utils.Companion.DisposeOp.APNG_DISPOSE_OP_PREVIOUS -> {
- //Do nothings
- }
- // Add current frame to bitmap buffer
- // APNG_DISPOSE_OP_BACKGROUND: the frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.
- Utils.Companion.DisposeOp.APNG_DISPOSE_OP_BACKGROUND -> {
- val res = Bitmap.createBitmap(
- maxWidth,
- maxHeight,
- Bitmap.Config.ARGB_8888
- )
- val can = Canvas(res)
- can.drawBitmap(btm, 0f, 0f, null)
- can.drawRect(
- xOffset.toFloat(),
- yOffset.toFloat(),
- xOffset + decoded.width.toFloat(),
- yOffset + decoded.height.toFloat(),
- clearPaint
- )
- buffer = res
- }
- else -> buffer = btm
- }
-
- }
-
- png = ByteArrayOutputStream(4096)
-
- // Parse Frame ConTroL chunk
- // Get the width of the png
- val width = Utils.uIntFromBytesBigEndian(
- byteArray, 12
- )
- // Get the height of the png
- val height = Utils.uIntFromBytesBigEndian(
- byteArray, 16
- )
-
- /*
- * The `delay_num` and `delay_den` parameters together specify a fraction indicating the time to display the current frame, in seconds.
- * If the the value of the numerator is 0 the decoder should render the next frame as quickly as possible, though viewers may impose a reasonable lower bound.
- */
- // Get delay numerator
- val delayNum = Utils.uShortFromBytesBigEndian(
- byteArray, 28
- ).toFloat()
- // Get delay denominator
- var delayDen = Utils.uShortFromBytesBigEndian(
- byteArray, 30
- ).toFloat()
-
- // If the denominator is 0, it is to be treated as if it were 100 (that is, `delay_num` then specifies 1/100ths of a second).
- if (delayDen == 0f) {
- delayDen = 100f
- }
-
- delay = (delayNum / delayDen * 1000)
-
- // Get x and y offsets
- xOffset = Utils.uIntFromBytesBigEndian(
- byteArray, 20
- )
- yOffset = Utils.uIntFromBytesBigEndian(
- byteArray, 24
- )
- blendOp = Utils.decodeBlendOp(byteArray[33].toInt())
- disposeOp = Utils.decodeDisposeOp(byteArray[32].toInt())
-
- if (xOffset + width > maxWidth) {
- throw BadApngException("`xOffset` + `width` must be <= `IHDR` width")
- } else if (yOffset + height > maxHeight) {
- throw BadApngException("`yOffset` + `height` must be <= `IHDR` height")
- }
-
- png.write(Utils.pngSignature)
- png.write(
- generateIhdr(
- ihdrOfApng,
- width,
- height
- )
- )
- plte?.let {
- png.write(it)
- }
- tnrs?.let {
- png.write(it)
- }
-
- }
- name.contentEquals(Utils.IEND) -> {
- if (isApng && png != null) {
- png.write(zeroLength)
- // Add IEND
- // Generate crc for IEND
- val crC32 = CRC32()
- crC32.update(Utils.IEND, 0, Utils.IEND.size)
- png.write(Utils.IEND)
- png.write(Utils.uIntToByteArray(crC32.value.toInt()))
-
- val btm = Bitmap.createBitmap(
- maxWidth,
- maxHeight,
- Bitmap.Config.ARGB_8888
- )
-
- val pngBytes = png.toByteArray()
- val decoded = BitmapFactory.decodeByteArray(
- pngBytes,
- 0,
- pngBytes.size
- )
- val canvas = Canvas(btm)
- canvas.drawBitmap(buffer!!, 0f, 0f, null)
-
- if (blendOp == Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE) {
- canvas.drawRect(
- xOffset.toFloat(),
- yOffset.toFloat(),
- xOffset + decoded.width.toFloat(),
- yOffset + decoded.height.toFloat(),
- clearPaint
- )
- }
-
- canvas.drawBitmap(
- decoded,
- xOffset.toFloat(),
- yOffset.toFloat(),
- null
- )
- drawable.addFrame(
- BitmapDrawable(
- context.resources,
- if (btm.config != config.bitmapConfig) {
- if (BuildConfig.DEBUG)
- Log.v(
- TAG,
- "Bitmap Config : ${btm.config}, Config : $config"
- )
- btm.copy(config.bitmapConfig, btm.isMutable)
- } else {
- btm
- }
- ),
- (delay / config.speed).toInt()
- )
-
- when (disposeOp) {
- Utils.Companion.DisposeOp.APNG_DISPOSE_OP_PREVIOUS -> {
- //Do nothings
- }
- // Add current frame to bitmap buffer
- // APNG_DISPOSE_OP_BACKGROUND: the frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.
- Utils.Companion.DisposeOp.APNG_DISPOSE_OP_BACKGROUND -> {
- val res = Bitmap.createBitmap(
- maxWidth,
- maxHeight,
- Bitmap.Config.ARGB_8888
- )
- val can = Canvas(res)
- can.drawBitmap(btm, 0f, 0f, null)
- can.drawRect(
- xOffset.toFloat(),
- yOffset.toFloat(),
- xOffset + decoded.width.toFloat(),
- yOffset + decoded.height.toFloat(),
- clearPaint
- )
- buffer = res
- }
- else -> buffer = btm
- }
- } else {
- cover?.let {
- it.write(zeroLength)
- // Add IEND
- // Generate crc for IEND
- val crC32 = CRC32()
- crC32.update(Utils.IEND, 0, Utils.IEND.size)
- it.write(Utils.IEND)
- it.write(Utils.uIntToByteArray(crC32.value.toInt()))
- withContext(Dispatchers.IO) {
- inputStream.close()
- }
-
- val pngBytes = it.toByteArray()
- return@withContext BitmapDrawable(
- context.resources,
- BitmapFactory.decodeByteArray(
- pngBytes,
- 0,
- pngBytes.size
- )
- )
- }
- }
- }
- name.contentEquals(Utils.IDAT) -> {
- val w = if (png == null) {
- if (isApng && !config.decodeCoverFrame) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Ignoring cover frame")
- continue
- }
- if (cover == null) {
- cover = ByteArrayOutputStream()
- cover.write(Utils.pngSignature)
- cover.write(
- generateIhdr(
- ihdrOfApng,
- maxWidth,
- maxHeight
- )
- )
- }
- cover
- } else {
- png
- }
-
- // Find the chunk length
- val bodySize =
- Utils.uIntFromBytesBigEndian(
- byteArray, 0
- )
- w.write(byteArray.copyOfRange(0, 4))
-
- val body = ByteArray(4 + bodySize)
-
- System.arraycopy(Utils.IDAT, 0, body, 0, 4)
-
- // Get image bytes
- System.arraycopy(byteArray, 8, body, 4, bodySize)
-
- val crC32 = CRC32()
- crC32.update(body, 0, body.size)
- w.write(body)
- w.write(Utils.uIntToByteArray(crC32.value.toInt()))
- }
- name.contentEquals(Utils.fdAT) -> {
- // Find the chunk length
- val bodySize = Utils.uIntFromBytesBigEndian(byteArray, 0)
- png?.write(Utils.uIntToByteArray(bodySize - 4))
-
- val body = ByteArray(bodySize)
- System.arraycopy(Utils.IDAT, 0, body, 0, 4)
-
- // Get image bytes
- System.arraycopy(byteArray, 12, body, 4, bodySize - 4)
-
- val crC32 = CRC32()
- crC32.update(body, 0, body.size)
- png?.write(body)
- png?.write(Utils.uIntToByteArray(crC32.value.toInt()))
- }
- name.contentEquals(Utils.plte) -> {
- plte = byteArray
- }
- name.contentEquals(Utils.tnrs) -> {
- tnrs = byteArray
- }
- name.contentEquals(Utils.IHDR) -> {
- // Get length of the body of the chunk
- val bodySize = Utils.uIntFromBytesBigEndian(byteArray, 0)
- // Get the width of the png
- maxWidth = Utils.uIntFromBytesBigEndian(byteArray, 8)
- // Get the height of the png
- maxHeight = Utils.uIntFromBytesBigEndian(byteArray, 12)
- ihdrOfApng = byteArray.copyOfRange(4 + 4, 4 + bodySize + 4)
-
- buffer = Bitmap.createBitmap(
- maxWidth,
- maxHeight,
- Bitmap.Config.ARGB_8888
- )
- }
- name.contentEquals(Utils.acTL) -> { // TODO GET NBR REPETITIONS
- isApng = true
- }
- }
- } else throw BadCRCException()
- } while (byteRead != -1 && isActive)
- withContext(Dispatchers.IO) {
- inputStream.close()
- }
- return@withContext drawable
- } else {
- if (BuildConfig.DEBUG)
- Log.i(TAG, "Decoding non APNG stream")
- inputStream.reset()
-
- return@withContext if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- val bytesRead: ByteArray
- withContext(Dispatchers.IO) {
- bytesRead = inputStream.readBytes()
- inputStream.close()
- }
- val buf = ByteBuffer.wrap(bytesRead)
- val source = ImageDecoder.createSource(buf)
- withContext(Dispatchers.IO) {
- ImageDecoder.decodeDrawable(source)
- }
- } else {
- val drawable = Drawable.createFromStream(
- inputStream,
- null
- )
- withContext(Dispatchers.IO) {
- inputStream.close()
- }
- drawable
- }
- }
- }
-
- /**
- * Decode Apng and return a Drawable who can be an [ApngDrawable] if it end successfully. Can also be an [android.graphics.drawable.AnimatedImageDrawable].
- * @param context Context needed for animation drawable.
- * @param file File to decode.
- * @param config Decoder configuration
- * @return [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif. If it is not an animated image, it is a [Drawable].
- */
- @Suppress("unused", "BlockingMethodInNonBlockingContext")
- @JvmStatic
- // TODO DOCUMENT
- suspend fun decodeApng(
- context: Context,
- file: File,
- config: Config = Config()
- ): Drawable =
- decodeApng(
- context,
- withContext(Dispatchers.IO) { FileInputStream(file) }, config
- )
-
- /**
- * Decode Apng and return a Drawable who can be an [ApngDrawable] if it end successfully. Can also be an [android.graphics.drawable.AnimatedImageDrawable].
- * @param context Context is needed for contentResolver and animation drawable.
- * @param uri Uri to open.
- * @param config Decoder configuration
- * @return [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif.
- */
- @Suppress("unused")
- @JvmStatic
- suspend fun decodeApng(
- context: Context,
- uri: Uri,
- config: Config = Config()
- ): Drawable {
- val inputStream = context.contentResolver.openInputStream(uri)!!
- return decodeApng(
- context,
- inputStream,
- config
- )
- }
-
- /**
- * Decode Apng and return a Drawable who can be an [ApngDrawable] if it end successfully. Can also be an [android.graphics.drawable.AnimatedImageDrawable].
- * @param context Context is needed for contentResolver and animation drawable.
- * @param res Resource to decode.
- * @param config Decoder configuration
- * @return [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif.
- */
- @Suppress("unused")
- @JvmStatic
- suspend fun decodeApng(
- context: Context,
- @RawRes res: Int,
- config: Config = Config()
- ): Drawable =
- decodeApng(
- context,
- context.resources.openRawResource(res),
- config
- )
/**
* Decode Apng and return a Drawable who can be an [ApngDrawable] if it end successfully. Can also be an [android.graphics.drawable.AnimatedImageDrawable].
@@ -630,303 +648,19 @@ class ApngDecoder {
* @param config Decoder configuration
* @return [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif.
*/
- @Suppress("unused", "BlockingMethodInNonBlockingContext")
+ @Suppress("unused")
@JvmStatic
- suspend fun decodeApng(
- context: Context,
+ suspend fun constructFromUrl(
url: URL,
config: Config = Config()
- ) =
- withContext(Dispatchers.IO) {
- decodeApng(
- context,
- ByteArrayInputStream(Loader.load(url)),
- config
- )
- }
-
- /**
- * Load Apng into an imageView, asynchronously.
- * @param context Context needed for animation drawable.
- * @param file File to decode.
- * @param imageView Image View.
- * @param callback [ApngDecoder.Callback] to handle success and error.
- * @param config Decoder configuration
- */
- @Suppress("unused", "BlockingMethodInNonBlockingContext")
- @JvmStatic
- @JvmOverloads
- fun decodeApngAsyncInto(
- context: Context,
- file: File,
- imageView: ImageView,
- callback: Callback? = null,
- config: Config = Config(),
- scope: CoroutineScope = GlobalScope
- ) {
- scope.launch(Dispatchers.Default) {
- try {
- val drawable =
- decodeApng(
- context,
- withContext(Dispatchers.IO) {
- FileInputStream(file)
- },
- config
- )
- withContext(Dispatchers.Main) {
- imageView.setImageDrawable(drawable)
- (drawable as? AnimationDrawable)?.start()
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- (drawable as? AnimatedImageDrawable)?.start()
- }
- callback?.onSuccess(drawable)
- }
- } catch (e: Exception) {
- withContext(Dispatchers.Main) {
- callback?.onError(e)
- }
- }
- }
- }
-
- /**
- * Load Apng into an imageView, asynchronously.
- * @param context Context needed for animation drawable and content resolver.
- * @param uri Uri to load.
- * @param imageView Image View.
- * @param callback [ApngDecoder.Callback] to handle success and error.
- * @param config Decoder configuration
- */
- @Suppress("unused")
- @JvmStatic
- @JvmOverloads
- fun decodeApngAsyncInto(
- context: Context,
- uri: Uri,
- imageView: ImageView,
- callback: Callback? = null,
- config: Config = Config(),
- scope: CoroutineScope = GlobalScope
- ) {
- val inputStream = context.contentResolver.openInputStream(uri)!!
- scope.launch(Dispatchers.Default) {
- try {
- val drawable =
- decodeApng(
- context,
- inputStream,
- config
- )
- withContext(Dispatchers.Main) {
- imageView.setImageDrawable(drawable)
- (drawable as? AnimationDrawable)?.start()
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- (drawable as? AnimatedImageDrawable)?.start()
- }
- callback?.onSuccess(drawable)
- }
- } catch (e: Exception) {
- withContext(Dispatchers.Main) {
- callback?.onError(e)
- }
- }
- }
- }
-
- /**
- * Load Apng into an imageView, asynchronously.
- * @param context Context needed to decode the resource and for the animation drawable.
- * @param res Raw resource to load.
- * @param imageView Image View.
- * @param callback [ApngDecoder.Callback] to handle success and error.
- * @param config Decoder configuration
- */
- @Suppress("unused")
- @JvmStatic
- @JvmOverloads
- fun decodeApngAsyncInto(
- context: Context, @RawRes res: Int,
- imageView: ImageView,
- callback: Callback? = null,
- config: Config = Config(),
- scope: CoroutineScope = GlobalScope
- ) {
- scope.launch(Dispatchers.Default) {
- try {
- val drawable =
- decodeApng(
- context,
- context.resources.openRawResource(res),
- config
- )
- withContext(Dispatchers.Main) {
- imageView.setImageDrawable(drawable)
- (drawable as? AnimationDrawable)?.start()
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- (drawable as? AnimatedImageDrawable)?.start()
- }
- callback?.onSuccess(drawable)
- }
- } catch (e: Exception) {
- withContext(Dispatchers.Main) {
- callback?.onError(e)
- }
- }
- }
-
- }
-
- /**
- * Load Apng into an imageView, asynchronously.
- * @param context Context needed for the animation drawable.
- * @param url URL to load.
- * @param imageView Image View.
- * @param callback [ApngDecoder.Callback] to handle success and error.
- * @param config Decoder configuration
- */
- @Suppress("unused", "BlockingMethodInNonBlockingContext")
- @JvmStatic
- @JvmOverloads
- fun decodeApngAsyncInto(
- context: Context,
- url: URL,
- imageView: ImageView,
- callback: Callback? = null,
- config: Config = Config(),
- scope: CoroutineScope = GlobalScope
- ) {
- scope.launch(Dispatchers.Default) {
- try {
- val drawable = decodeApng(
- context,
- ByteArrayInputStream(
- Loader.load(
- url
- )
- ),
+ ): Result =
+ kotlin.runCatching {
+ withContext(Dispatchers.IO) {
+ ApngDecoder(
+ ByteArrayInputStream(Loader.load(url)),
config
)
- withContext(Dispatchers.Main) {
- imageView.setImageDrawable(drawable)
- (drawable as? AnimationDrawable)?.start()
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- (drawable as? AnimatedImageDrawable)?.start()
- }
- callback?.onSuccess(drawable)
- }
- } catch (e: Exception) {
- withContext(Dispatchers.Main) {
- callback?.onError(e)
- }
}
}
- }
-
- /**
- * Load Apng into an imageView, asynchronously.
- * @param context Context needed for decoding the image and creating the animation drawable.
- * @param string URL to load
- * @param imageView Image View.
- * @param callback [ApngDecoder.Callback] to handle success and error.
- * @param config Decoder configuration
- */
- @Suppress("unused")
- @JvmStatic
- @JvmOverloads
- fun decodeApngAsyncInto(
- context: Context,
- string: String,
- imageView: ImageView,
- callback: Callback? = null,
- config: Config = Config(),
- scope: CoroutineScope = GlobalScope
- ) {
- scope.launch(Dispatchers.Default) {
- try {
- if (string.startsWith("http://") || string.startsWith("https://")) {
- decodeApngAsyncInto(
- context,
- URL(string),
- imageView,
- callback,
- config
- )
- } else if (File(string).exists()) {
- var pathToLoad =
- if (string.startsWith("content://")) string else "file://$string"
- pathToLoad = pathToLoad.replace("%", "%25").replace("#", "%23")
- decodeApngAsyncInto(
- context,
- Uri.parse(pathToLoad),
- imageView,
- callback,
- config
- )
- } else if (string.startsWith("file://android_asset/")) {
- val drawable =
- decodeApng(
- context,
- context.assets.open(string.replace("file:///android_asset/", "")),
-
- config
- )
- withContext(Dispatchers.Main) {
- imageView.setImageDrawable(drawable)
- (drawable as? AnimationDrawable)?.start()
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- (drawable as? AnimatedImageDrawable)?.start()
- }
- callback?.onSuccess(drawable)
- }
- } else {
- withContext(Dispatchers.Main) {
- callback?.onError(Exception("Cannot open string"))
- }
- }
- } catch (e: Exception) {
- withContext(Dispatchers.Main) {
- callback?.onError(e)
- }
- }
- }
- }
-
- /**
- * Generate a correct IHDR from the IHDR chunk of the APNG.
- * @param ihdrOfApng The IHDR of the APNG.
- * @param width The width of the frame.
- * @param height The height of the frame.
- * @return [ByteArray] The generated IHDR.
- */
- private fun generateIhdr(ihdrOfApng: ByteArray, width: Int, height: Int): ByteArray {
- val ihdr =
- ByteArray(0xD + 4 + 4 + 4) // 0xD (IHDR body length) + 4 (0x0, 0x0, 0x0, 0xD : the chunk length) + 4 : IHDR + 4 : CRC
-
- // Add chunk body length
- System.arraycopy(Utils.uIntToByteArray(0xD), 0, ihdr, 0, 4)
-
- // We need a body var to know body length and generate crc
- val ihdrBody = ByteArray(0xD + 4) // 0xD (IHDR body length) + 4 : IHDR
-
- // Add IHDR
- System.arraycopy(Utils.IHDR, 0, ihdrBody, 0, 4)
-
- // Add the max width and height
- System.arraycopy(Utils.uIntToByteArray(width), 0, ihdrBody, 4, 4)
- System.arraycopy(Utils.uIntToByteArray(height), 0, ihdrBody, 8, 4)
-
- // Add complicated stuff like depth color ...
- // If you want correct png you need same parameters.
- System.arraycopy(ihdrOfApng, 8, ihdrBody, 12, 5)
-
- // Generate CRC
- val crC32 = CRC32()
- crC32.update(ihdrBody, 0, 0xD + 4)
-
- System.arraycopy(ihdrBody, 0, ihdr, 4, 0xD + 4)
- System.arraycopy(Utils.uIntToByteArray(crC32.value.toInt()), 0, ihdr, 0xD + 4 + 4, 4)
- return ihdr
- }
}
}
\ No newline at end of file
diff --git a/apng_library/src/main/java/oupson/apng/decoder/ApngLoader.kt b/apng_library/src/main/java/oupson/apng/decoder/ApngLoader.kt
new file mode 100644
index 0000000..3c46fb4
--- /dev/null
+++ b/apng_library/src/main/java/oupson/apng/decoder/ApngLoader.kt
@@ -0,0 +1,347 @@
+package oupson.apng.decoder
+
+import android.content.Context
+import android.graphics.drawable.AnimatedImageDrawable
+import android.graphics.drawable.AnimationDrawable
+import android.graphics.drawable.Drawable
+import android.net.Uri
+import android.os.Build
+import android.widget.ImageView
+import androidx.annotation.RawRes
+import kotlinx.coroutines.*
+import oupson.apng.drawable.ApngDrawable
+import oupson.apng.utils.Utils.Companion.mapResult
+import java.io.File
+import java.io.FileInputStream
+import java.net.URL
+
+class ApngLoader(parent: Job? = null) {
+ interface Callback {
+ /**
+ * Function called when the file was successfully decoded.
+ * @param drawable Can be an [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif. If it is not an animated image, it is a [Drawable].
+ */
+ fun onSuccess(drawable: Drawable)
+
+ /**
+ * Function called when something gone wrong.
+ * @param error The problem.
+ */
+ fun onError(error: Throwable)
+ }
+
+ private val job = SupervisorJob(parent)
+ private val coroutineScope: CoroutineScope = CoroutineScope(job)
+
+ fun cancelAll() {
+ coroutineScope.cancel(CancellationException("Loading was canceled"))
+ }
+
+
+ /**
+ * Load Apng into an imageView.
+ * @param context Context needed for animation drawable.
+ * @param file File to decode.
+ * @param imageView Image View.
+ * @param config Decoder configuration
+ */
+ suspend fun decodeApngInto(
+ context: Context,
+ file: File,
+ imageView: ImageView,
+ config: ApngDecoder.Config = ApngDecoder.Config()
+ ): Result =
+ kotlin.runCatching {
+ withContext(Dispatchers.IO) {
+ FileInputStream(file)
+ }
+ }.mapResult { input ->
+ ApngDecoder(
+ input,
+ config
+ ).getDecoded(context)
+ }.onSuccess { drawable ->
+ withContext(Dispatchers.Main) {
+ imageView.setImageDrawable(drawable)
+ (drawable as? AnimationDrawable)?.start()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ (drawable as? AnimatedImageDrawable)?.start()
+ }
+ }
+ }
+
+ /**
+ * Load Apng into an imageView.
+ * @param context Context needed for animation drawable and content resolver.
+ * @param uri Uri to load.
+ * @param imageView Image View.
+ * @param config Decoder configuration
+ */
+ suspend fun decodeApngInto(
+ context: Context,
+ uri: Uri,
+ imageView: ImageView,
+ config: ApngDecoder.Config = ApngDecoder.Config()
+ ): Result =
+ kotlin.runCatching {
+ withContext(Dispatchers.IO) { context.contentResolver.openInputStream(uri) }!!
+ }.mapResult { inputStream ->
+ ApngDecoder(
+ inputStream,
+ config
+ ).getDecoded(context)
+ }.onSuccess { drawable ->
+ withContext(Dispatchers.Main) {
+ imageView.setImageDrawable(drawable)
+ (drawable as? AnimationDrawable)?.start()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ (drawable as? AnimatedImageDrawable)?.start()
+ }
+ }
+ }
+
+
+ /**
+ * Load Apng into an imageView.
+ * @param context Context needed to decode the resource and for the animation drawable.
+ * @param res Raw resource to load.
+ * @param imageView Image View.
+ * @param config Decoder configuration
+ */
+ suspend fun decodeApngInto(
+ context: Context, @RawRes res: Int,
+ imageView: ImageView,
+ config: ApngDecoder.Config = ApngDecoder.Config()
+ ): Result =
+ ApngDecoder(
+ withContext(Dispatchers.IO) {
+ context.resources.openRawResource(res)
+ },
+ config
+ ).getDecoded(context)
+ .onSuccess { drawable ->
+ withContext(Dispatchers.Main) {
+ imageView.setImageDrawable(drawable)
+ (drawable as? AnimationDrawable)?.start()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ (drawable as? AnimatedImageDrawable)?.start()
+ }
+ }
+ }
+
+
+ /**
+ * Load Apng into an imageView, asynchronously.
+ * @param context Context needed for the animation drawable.
+ * @param url URL to load.
+ * @param imageView Image View.
+ * @param config Decoder configuration
+ */
+ suspend fun decodeApngInto(
+ context: Context,
+ url: URL,
+ imageView: ImageView,
+ config: ApngDecoder.Config = ApngDecoder.Config()
+ ): Result =
+ ApngDecoder.constructFromUrl(url, config).getOrElse { return Result.failure(it) }
+ .getDecoded(context)
+ .onSuccess { drawable ->
+ withContext(Dispatchers.Main) {
+ imageView.setImageDrawable(drawable)
+ (drawable as? AnimationDrawable)?.start()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ (drawable as? AnimatedImageDrawable)?.start()
+ }
+ }
+ }
+
+ /**
+ * Load Apng into an imageView, asynchronously.
+ * @param context Context needed for decoding the image and creating the animation drawable.
+ * @param string URL to load
+ * @param imageView Image View.
+ * @param config Decoder configuration
+ */
+ @Suppress("unused")
+ suspend fun decodeApngInto(
+ context: Context,
+ string: String,
+ imageView: ImageView,
+ config: ApngDecoder.Config = ApngDecoder.Config()
+ ): Result {
+ return if (string.startsWith("http://") || string.startsWith("https://")) {
+ kotlin.runCatching { URL(string) }
+ .mapResult { url ->
+ decodeApngInto(
+ context,
+ url,
+ imageView,
+ config
+ )
+ }
+
+ } else if (File(string).exists()) {
+ var pathToLoad =
+ if (string.startsWith("content://")) string else "file://$string"
+ pathToLoad = pathToLoad.replace("%", "%25").replace("#", "%23")
+ decodeApngInto(
+ context,
+ Uri.parse(pathToLoad),
+ imageView,
+ config
+ )
+ } else if (string.startsWith("file://android_asset/")) {
+ kotlin.runCatching {
+ withContext(Dispatchers.IO) {
+ context.assets.open(string.replace("file:///android_asset/", ""))
+ }
+ }
+ .mapResult { inputStream -> ApngDecoder(inputStream, config).getDecoded(context) }
+ .onSuccess { drawable ->
+ withContext(Dispatchers.Main) {
+ imageView.setImageDrawable(drawable)
+ (drawable as? AnimationDrawable)?.start()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ (drawable as? AnimatedImageDrawable)?.start()
+ }
+ }
+ }
+ } else {
+ throw Exception("Cannot open string")
+ }
+ }
+
+
+// region with callback
+ /**
+ * Load Apng into an imageView, asynchronously.
+ * @param context Context needed for animation drawable.
+ * @param file File to decode.
+ * @param imageView Image View.
+ * @param callback [ApngLoader.Callback] to handle success and error.
+ * @param config Decoder configuration
+ */
+ @Suppress("unused")
+ @JvmOverloads
+ fun decodeApngAsyncInto(
+ context: Context,
+ file: File,
+ imageView: ImageView,
+ callback: Callback? = null,
+ config: ApngDecoder.Config = ApngDecoder.Config()
+ ) =
+ coroutineScope.launch(Dispatchers.Default) {
+ val drawable = decodeApngInto(context, file, imageView, config)
+ withContext(Dispatchers.Main) {
+ drawable
+ .onSuccess { callback?.onSuccess(it) }
+ .onFailure { callback?.onError(it) }
+ }
+ }
+
+
+ /**
+ * Load Apng into an imageView, asynchronously.
+ * @param context Context needed for animation drawable and content resolver.
+ * @param uri Uri to load.
+ * @param imageView Image View.
+ * @param callback [ApngLoader.Callback] to handle success and error.
+ * @param config Decoder configuration
+ */
+ @Suppress("unused")
+ @JvmOverloads
+ fun decodeApngAsyncInto(
+ context: Context,
+ uri: Uri,
+ imageView: ImageView,
+ callback: Callback? = null,
+ config: ApngDecoder.Config = ApngDecoder.Config()
+ ) = coroutineScope.launch(Dispatchers.Default) {
+ val drawable = decodeApngInto(context, uri, imageView, config)
+ withContext(Dispatchers.Main) {
+ drawable
+ .onSuccess { callback?.onSuccess(it) }
+ .onFailure { callback?.onError(it) }
+ }
+ }
+
+ /**
+ * Load Apng into an imageView, asynchronously.
+ * @param context Context needed to decode the resource and for the animation drawable.
+ * @param res Raw resource to load.
+ * @param imageView Image View.
+ * @param callback [ApngLoader.Callback] to handle success and error.
+ * @param config Decoder configuration
+ */
+ @Suppress("unused")
+ @JvmOverloads
+ fun decodeApngAsyncInto(
+ context: Context, @RawRes res: Int,
+ imageView: ImageView,
+ callback: Callback? = null,
+ config: ApngDecoder.Config = ApngDecoder.Config()
+ ) = coroutineScope.launch(Dispatchers.Default) {
+ val drawable = decodeApngInto(context, res, imageView, config)
+ withContext(Dispatchers.Main) {
+ drawable
+ .onSuccess { callback?.onSuccess(it) }
+ .onFailure { callback?.onError(it) }
+ }
+ }
+
+ /**
+ * Load Apng into an imageView, asynchronously.
+ * @param context Context needed for the animation drawable.
+ * @param url URL to load.
+ * @param imageView Image View.
+ * @param callback [ApngLoader.Callback] to handle success and error.
+ * @param config Decoder configuration
+ */
+ @Suppress("unused")
+ @JvmOverloads
+ fun decodeApngAsyncInto(
+ context: Context,
+ url: URL,
+ imageView: ImageView,
+ callback: Callback? = null,
+ config: ApngDecoder.Config = ApngDecoder.Config()
+ ) = coroutineScope.launch(Dispatchers.Default) {
+ val drawable = decodeApngInto(context, url, imageView, config)
+ withContext(Dispatchers.Main) {
+ drawable
+ .onSuccess { callback?.onSuccess(it) }
+ .onFailure { callback?.onError(it) }
+ }
+ }
+
+
+ /**
+ * Load Apng into an imageView, asynchronously.
+ * @param context Context needed for decoding the image and creating the animation drawable.
+ * @param string URL to load
+ * @param imageView Image View.
+ * @param callback [ApngLoader.Callback] to handle success and error.
+ * @param config Decoder configuration
+ */
+ @Suppress("unused")
+ @JvmOverloads
+ fun decodeApngAsyncInto(
+ context: Context,
+ string: String,
+ imageView: ImageView,
+ callback: Callback? = null,
+ config: ApngDecoder.Config = ApngDecoder.Config()
+ ) =
+ coroutineScope.launch(Dispatchers.Default) {
+ val drawable = decodeApngInto(context, string, imageView, config)
+ withContext(Dispatchers.Main) {
+ if (drawable.isSuccess) {
+ callback?.onSuccess(drawable.getOrNull()!!)
+ } else {
+ callback?.onError(drawable.exceptionOrNull()!!)
+ }
+ }
+ }
+
+// endregion with callback
+}
\ No newline at end of file
diff --git a/apng_library/src/main/java/oupson/apng/encoder/ApngEncoder.kt b/apng_library/src/main/java/oupson/apng/encoder/ApngEncoder.kt
index 113de2b..66c9d0a 100644
--- a/apng_library/src/main/java/oupson/apng/encoder/ApngEncoder.kt
+++ b/apng_library/src/main/java/oupson/apng/encoder/ApngEncoder.kt
@@ -291,7 +291,7 @@ class ApngEncoder(
) {
if (currentFrame == 0) {
if (btm.width != width || btm.height != height)
- throw InvalidFrameSizeException(
+ throw InvalidFrameSizeException( // TODO
btm.width,
btm.height,
width,
diff --git a/apng_library/src/main/java/oupson/apng/utils/Loader.kt b/apng_library/src/main/java/oupson/apng/utils/Loader.kt
index 775008b..ddfe8af 100644
--- a/apng_library/src/main/java/oupson/apng/utils/Loader.kt
+++ b/apng_library/src/main/java/oupson/apng/utils/Loader.kt
@@ -24,8 +24,11 @@ class Loader {
val connection = url.openConnection() as HttpURLConnection
connection.useCaches = true
connection.connect()
+
+ val inputStream = connection.inputStream
+
if (connection.responseCode == 200) {
- val input = BufferedInputStream(connection.inputStream)
+ val input = BufferedInputStream(inputStream)
val output = ByteArrayOutputStream()
var bytesRead: Int
val buffer = ByteArray(4096)
@@ -36,9 +39,13 @@ class Loader {
} while (bytesRead != -1)
input.close()
output.close()
+
+ inputStream.close()
connection.disconnect()
+
output.toByteArray()
} else {
+ inputStream.close()
connection.disconnect()
throw Exception("Error when downloading file : ${connection.responseCode}")
}
diff --git a/apng_library/src/main/java/oupson/apng/utils/Utils.kt b/apng_library/src/main/java/oupson/apng/utils/Utils.kt
index 10d861f..d8de186 100644
--- a/apng_library/src/main/java/oupson/apng/utils/Utils.kt
+++ b/apng_library/src/main/java/oupson/apng/utils/Utils.kt
@@ -208,7 +208,7 @@ class Utils {
(bytes[1] and 0xFF))
// TODO DOCUMENT AND TEST
- fun uShortFromBytesBigEndian(bytes: ByteArray, offset : Int = 0): Int =
+ fun uShortFromBytesBigEndian(bytes: ByteArray, offset: Int = 0): Int =
(((bytes[offset].toInt() and 0xFF) shl 8) or
(bytes[offset + 1].toInt() and 0xFF))
@@ -330,5 +330,14 @@ class Utils {
}
return result
}
+
+ suspend fun Result.mapResult(block: suspend (T) -> Result): Result {
+ return this.fold(
+ onSuccess = {
+ block.invoke(it)
+ },
+ onFailure = { Result.failure(it) }
+ )
+ }
}
}
\ No newline at end of file
diff --git a/app-test/build.gradle b/app-test/build.gradle
index ce736ef..0beeee8 100644
--- a/app-test/build.gradle
+++ b/app-test/build.gradle
@@ -1,6 +1,5 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
-apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 30
@@ -24,29 +23,35 @@ android {
buildTypes {
release {
- minifyEnabled false
+ minifyEnabled true
+ shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
productFlavors {
}
+
+ buildFeatures {
+ viewBinding = true
+ }
+
}
dependencies {
- implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
- implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3'
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0'
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'com.google.android.material:material:1.3.0'
+ implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
- implementation 'com.squareup.picasso:picasso:2.71828'
+ implementation("io.coil-kt:coil:1.2.2")
implementation project(":apng_library")
// implementation fileTree(include: ['*.aar'], dir: 'libs')
diff --git a/app-test/src/main/java/oupson/apngcreator/activities/CreatorActivity.kt b/app-test/src/main/java/oupson/apngcreator/activities/CreatorActivity.kt
index 50427ea..8b6dde6 100644
--- a/app-test/src/main/java/oupson/apngcreator/activities/CreatorActivity.kt
+++ b/app-test/src/main/java/oupson/apngcreator/activities/CreatorActivity.kt
@@ -14,18 +14,18 @@ import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.FileProvider
+import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
-import kotlinx.android.synthetic.main.activity_creator.*
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import oupson.apng.encoder.ApngEncoder
@@ -33,6 +33,7 @@ import oupson.apng.utils.Utils
import oupson.apngcreator.BuildConfig
import oupson.apngcreator.R
import oupson.apngcreator.adapter.ImageAdapter
+import oupson.apngcreator.databinding.ActivityCreatorBinding
import oupson.apngcreator.dialogs.DelayInputDialog
import java.io.File
import java.io.FileOutputStream
@@ -42,8 +43,6 @@ import kotlin.collections.ArrayList
class CreatorActivity : AppCompatActivity() {
companion object {
- private const val PICK_IMAGE = 1
- private const val WRITE_REQUEST_CODE = 2
private const val TAG = "CreatorActivity"
private const val CREATION_CHANNEL_ID =
@@ -57,35 +56,97 @@ class CreatorActivity : AppCompatActivity() {
private var nextImageId: Long = 0
+ private var binding: ActivityCreatorBinding? = null
+
+ private val pickLauncher =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+ if (result.resultCode == Activity.RESULT_OK) {
+ val data: Intent? = result.data
+
+ if (data?.clipData != null) {
+ for (i in 0 until data.clipData!!.itemCount) {
+ items.add(Triple(data.clipData!!.getItemAt(i).uri, 1000, nextImageId++))
+ }
+ adapter?.notifyDataSetChanged()
+ } else if (data?.data != null) {
+ items.add(Triple(data.data!!, 1000, nextImageId++))
+ adapter?.notifyDataSetChanged()
+ }
+ }
+ }
+
+ private val writeLauncher =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+ if (result.resultCode == Activity.RESULT_OK) {
+ val data: Intent? = result.data
+
+ if (data?.data != null) {
+ if (BuildConfig.DEBUG)
+ Log.i(TAG, "Intent data : ${data.data}")
+
+ val builder = NotificationCompat.Builder(this, CREATION_CHANNEL_ID).apply {
+ setContentTitle(getString(R.string.create_notification_title))
+ setContentText(
+ this@CreatorActivity.resources.getQuantityString(
+ R.plurals.create_notification_description,
+ 0,
+ 0,
+ items.size
+ )
+ )
+ setSmallIcon(R.drawable.ic_create_white_24dp)
+ priority = NotificationCompat.PRIORITY_LOW
+ }
+ lifecycleScope.launch(Dispatchers.IO) {
+ val out = contentResolver.openOutputStream(data.data!!) ?: return@launch
+ saveToOutputStream(
+ items.map { Pair(it.first, it.second) },
+ out,
+ builder = builder
+ )
+ out.close()
+
+ if (binding != null) {
+ withContext(Dispatchers.Main) {
+ Snackbar.make(
+ binding!!.imageRecyclerView,
+ R.string.done,
+ Snackbar.LENGTH_SHORT
+ ).show()
+ }
+ }
+ }
+ }
+ }
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ binding = ActivityCreatorBinding.inflate(layoutInflater)
- setContentView(R.layout.activity_creator)
+ setContentView(binding?.root)
- fabAddImage.setOnClickListener {
+ binding?.fabAddImage?.setOnClickListener {
val getIntent = Intent(Intent.ACTION_GET_CONTENT)
getIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
getIntent.type = "image/*"
- startActivityForResult(
- getIntent,
- PICK_IMAGE
- )
+ pickLauncher.launch(getIntent)
}
- adapter = ImageAdapter(this, items)
+ adapter = ImageAdapter(this, items, lifecycleScope)
adapter?.setHasStableIds(true)
- imageRecyclerView.layoutManager = LinearLayoutManager(this)
- imageRecyclerView.setHasFixedSize(true)
- imageRecyclerView.itemAnimator = object : DefaultItemAnimator() {
+ binding?.imageRecyclerView?.layoutManager = LinearLayoutManager(this)
+ binding?.imageRecyclerView?.setHasFixedSize(true)
+ binding?.imageRecyclerView?.itemAnimator = object : DefaultItemAnimator() {
override fun canReuseUpdatedViewHolder(viewHolder: RecyclerView.ViewHolder): Boolean {
return true
}
}
- imageRecyclerView.setItemViewCacheSize(20)
+ binding?.imageRecyclerView?.setItemViewCacheSize(20)
if (adapter != null)
- ItemTouchHelper(SwipeToDeleteCallback(adapter!!)).attachToRecyclerView(imageRecyclerView)
+ ItemTouchHelper(SwipeToDeleteCallback(adapter!!)).attachToRecyclerView(binding?.imageRecyclerView)
adapter?.clickListener = { position ->
DelayInputDialog(object : DelayInputDialog.InputSenderDialogListener {
@@ -104,8 +165,8 @@ class CreatorActivity : AppCompatActivity() {
}, items[position].second).show(supportFragmentManager, null)
}
- setSupportActionBar(creatorBottomAppBar)
- imageRecyclerView.adapter = adapter
+ setSupportActionBar(binding?.creatorBottomAppBar)
+ binding?.imageRecyclerView?.adapter = adapter
supportActionBar?.setDisplayHomeAsUpEnabled(true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -148,7 +209,7 @@ class CreatorActivity : AppCompatActivity() {
priority = NotificationCompat.PRIORITY_LOW
}
- GlobalScope.launch(Dispatchers.IO) {
+ lifecycleScope.launch(Dispatchers.IO) {
val randomFileName = UUID.randomUUID().toString()
val f = File(filesDir, "images/$randomFileName.png").apply {
if (!exists()) {
@@ -205,7 +266,7 @@ class CreatorActivity : AppCompatActivity() {
setSmallIcon(R.drawable.ic_create_white_24dp)
priority = NotificationCompat.PRIORITY_LOW
}
- GlobalScope.launch(Dispatchers.IO) {
+ lifecycleScope.launch(Dispatchers.IO) {
val randomFileName = UUID.randomUUID().toString()
val f = File(filesDir, "images/$randomFileName.png").apply {
if (!exists()) {
@@ -272,7 +333,10 @@ class CreatorActivity : AppCompatActivity() {
type = "image/png"
putExtra(Intent.EXTRA_TITLE, "${items[0].first.lastPathSegment}.png")
}
- startActivityForResult(intent, WRITE_REQUEST_CODE)
+
+ writeLauncher.launch(
+ intent
+ )
}
true
}
@@ -409,7 +473,9 @@ class CreatorActivity : AppCompatActivity() {
}
}
- encoder.writeEnd()
+ withContext(Dispatchers.IO) {
+ encoder.writeEnd()
+ }
if (builder != null) {
@@ -423,71 +489,14 @@ class CreatorActivity : AppCompatActivity() {
}
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- super.onActivityResult(requestCode, resultCode, data)
- when (requestCode) {
- PICK_IMAGE -> {
- if (resultCode == Activity.RESULT_OK) {
- if (data?.clipData != null) {
- for (i in 0 until data.clipData!!.itemCount) {
- items.add(Triple(data.clipData!!.getItemAt(i).uri, 1000, nextImageId++))
- }
- adapter?.notifyDataSetChanged()
- } else if (data?.data != null) {
- items.add(Triple(data.data!!, 1000, nextImageId++))
- adapter?.notifyDataSetChanged()
- }
- }
- }
- WRITE_REQUEST_CODE -> {
- if (resultCode == Activity.RESULT_OK) {
- if (data?.data != null) {
- if (BuildConfig.DEBUG)
- Log.i(TAG, "Intent data : ${data.data}")
-
- val builder = NotificationCompat.Builder(this, CREATION_CHANNEL_ID).apply {
- setContentTitle(getString(R.string.create_notification_title))
- setContentText(
- this@CreatorActivity.resources.getQuantityString(
- R.plurals.create_notification_description,
- 0,
- 0,
- items.size
- )
- )
- setSmallIcon(R.drawable.ic_create_white_24dp)
- priority = NotificationCompat.PRIORITY_LOW
- }
- GlobalScope.launch(Dispatchers.IO) {
-
- val out = contentResolver.openOutputStream(data.data!!) ?: return@launch
- saveToOutputStream(
- items.map { Pair(it.first, it.second) },
- out,
- builder = builder
- )
- out.close()
-
- withContext(Dispatchers.Main) {
- Snackbar.make(
- imageRecyclerView,
- R.string.done,
- Snackbar.LENGTH_SHORT
- ).show()
- }
- }
- }
- }
- }
- }
- }
-
override fun onDestroy() {
super.onDestroy()
- val deleteResult = File(filesDir, "images").deleteRecursively()
- if (BuildConfig.DEBUG)
- Log.v(TAG, "Deleted images dir : $deleteResult")
+ lifecycleScope.launch(Dispatchers.IO) {
+ val deleteResult = File(filesDir, "images").deleteRecursively()
+ if (BuildConfig.DEBUG)
+ Log.v(TAG, "Deleted images dir : $deleteResult")
+ }
}
inner class SwipeToDeleteCallback(private val adapter: ImageAdapter) :
diff --git a/app-test/src/main/java/oupson/apngcreator/activities/MainActivity.kt b/app-test/src/main/java/oupson/apngcreator/activities/MainActivity.kt
index 7b49170..d7ac924 100644
--- a/app-test/src/main/java/oupson/apngcreator/activities/MainActivity.kt
+++ b/app-test/src/main/java/oupson/apngcreator/activities/MainActivity.kt
@@ -4,19 +4,25 @@ import android.annotation.SuppressLint
import android.content.Intent
import android.net.http.HttpResponseCache
import android.os.Bundle
+import android.os.StrictMode
+import android.os.StrictMode.ThreadPolicy
+import android.os.StrictMode.VmPolicy
import android.util.Log
import android.view.MenuItem
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.GravityCompat
+import androidx.lifecycle.lifecycleScope
import com.google.android.material.bottomappbar.BottomAppBarTopEdgeTreatment
import com.google.android.material.shape.CutCornerTreatment
import com.google.android.material.shape.MaterialShapeDrawable
import com.google.android.material.shape.ShapeAppearanceModel
import com.google.android.material.shape.ShapePath
-import kotlinx.android.synthetic.main.activity_main.*
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
import oupson.apngcreator.BuildConfig
import oupson.apngcreator.R
+import oupson.apngcreator.databinding.ActivityMainBinding
import oupson.apngcreator.fragments.ApngDecoderFragment
import oupson.apngcreator.fragments.JavaFragment
import oupson.apngcreator.fragments.KotlinFragment
@@ -28,38 +34,67 @@ class MainActivity : AppCompatActivity() {
private const val TAG = "MainActivity"
}
+ private var binding: ActivityMainBinding? = null
+
override fun onCreate(savedInstanceState: Bundle?) {
+ if (BuildConfig.DEBUG) {
+ StrictMode.setThreadPolicy(
+ ThreadPolicy.Builder()
+ .detectDiskReads()
+ .detectDiskWrites()
+ .detectAll()
+ .penaltyLog()
+ .build()
+ )
+ StrictMode.setVmPolicy(
+ VmPolicy.Builder()
+ .detectLeakedSqlLiteObjects()
+ .detectLeakedClosableObjects()
+ .penaltyLog()
+
+ .build()
+ )
+ }
super.onCreate(savedInstanceState)
if (BuildConfig.DEBUG)
- Log.v(TAG, "supportFragmentManager.fragments.size : ${supportFragmentManager.fragments.size}")
+ Log.v(
+ TAG,
+ "supportFragmentManager.fragments.size : ${supportFragmentManager.fragments.size}"
+ )
- setContentView(R.layout.activity_main)
+ binding = ActivityMainBinding.inflate(layoutInflater)
- setSupportActionBar(bottomAppBar)
+
+ setContentView(binding?.root)
+
+ setSupportActionBar(binding?.bottomAppBar)
setUpBottomAppBarShapeAppearance()
val httpCacheSize = 10 * 1024 * 1024.toLong() // 10 MiB
- val httpCacheDir = File(cacheDir, "http")
- HttpResponseCache.install(httpCacheDir, httpCacheSize)
+ lifecycleScope.launch(Dispatchers.IO) {
+ val httpCacheDir = File(cacheDir, "http")
+ HttpResponseCache.install(httpCacheDir, httpCacheSize)
+ }
- fabCreate.setOnClickListener {
+ binding?.fabCreate?.setOnClickListener {
startActivity(Intent(this, CreatorActivity::class.java))
}
- val drawerToggle = ActionBarDrawerToggle(this, drawer_layout, bottomAppBar,
+ val drawerToggle = ActionBarDrawerToggle(
+ this, binding?.drawerLayout, binding?.bottomAppBar,
R.string.open,
R.string.close
)
- drawer_layout.addDrawerListener(drawerToggle)
+ binding?.drawerLayout?.addDrawerListener(drawerToggle)
drawerToggle.syncState()
var selected = 0
- navigationView.setNavigationItemSelectedListener { menuItem : MenuItem ->
- when(menuItem.itemId) {
+ binding?.navigationView?.setNavigationItemSelectedListener { menuItem: MenuItem ->
+ when (menuItem.itemId) {
R.id.menu_kotlin_fragment -> {
if (selected != 0) {
supportFragmentManager.beginTransaction().apply {
@@ -98,20 +133,21 @@ class MainActivity : AppCompatActivity() {
}
}
- drawer_layout.closeDrawer(GravityCompat.START)
+ binding?.drawerLayout?.closeDrawer(GravityCompat.START)
return@setNavigationItemSelectedListener true
}
if (intent.hasExtra("fragment") && supportFragmentManager.fragments.size == 0) {
- when(intent.getStringExtra("fragment")) {
+ when (intent.getStringExtra("fragment")) {
"kotlin" -> {
supportFragmentManager.beginTransaction().apply {
add(
R.id.fragment_container,
- KotlinFragment.newInstance(), "KotlinFragment")
+ KotlinFragment.newInstance(), "KotlinFragment"
+ )
}.commit()
- navigationView.setCheckedItem(R.id.menu_kotlin_fragment)
+ binding?.navigationView?.setCheckedItem(R.id.menu_kotlin_fragment)
selected = 0
}
"java" -> {
@@ -121,7 +157,7 @@ class MainActivity : AppCompatActivity() {
JavaFragment()
)
}.commit()
- navigationView.setCheckedItem(R.id.menu_java_fragment)
+ binding?.navigationView?.setCheckedItem(R.id.menu_java_fragment)
selected = 1
}
"apng_decoder" -> {
@@ -131,7 +167,7 @@ class MainActivity : AppCompatActivity() {
ApngDecoderFragment.newInstance()
)
}.commit()
- navigationView.setCheckedItem(R.id.menu_apng_decoder_fragment)
+ binding?.navigationView?.setCheckedItem(R.id.menu_apng_decoder_fragment)
selected = 2
}
}
@@ -139,35 +175,41 @@ class MainActivity : AppCompatActivity() {
supportFragmentManager.beginTransaction().apply {
add(
R.id.fragment_container,
- KotlinFragment.newInstance(), "KotlinFragment")
+ KotlinFragment.newInstance(), "KotlinFragment"
+ )
}.commit()
- navigationView.setCheckedItem(R.id.menu_kotlin_fragment)
+ binding?.navigationView?.setCheckedItem(R.id.menu_kotlin_fragment)
}
}
override fun onStop() {
super.onStop()
- HttpResponseCache.getInstalled()?.flush()
+ lifecycleScope.launch(Dispatchers.IO) {
+ HttpResponseCache.getInstalled()?.flush()
+ }
}
private fun setUpBottomAppBarShapeAppearance() {
- val fabShapeAppearanceModel: ShapeAppearanceModel = fabCreate.shapeAppearanceModel
- val cutCornersFab =
- (fabShapeAppearanceModel.bottomLeftCorner is CutCornerTreatment
- && fabShapeAppearanceModel.bottomRightCorner is CutCornerTreatment)
- val topEdge =
- if (cutCornersFab) BottomAppBarCutCornersTopEdge(
- bottomAppBar.fabCradleMargin,
- bottomAppBar.fabCradleRoundedCornerRadius,
- bottomAppBar.cradleVerticalOffset
- ) else BottomAppBarTopEdgeTreatment(
- bottomAppBar.fabCradleMargin,
- bottomAppBar.fabCradleRoundedCornerRadius,
- bottomAppBar.cradleVerticalOffset
- )
- val babBackground = bottomAppBar.background as MaterialShapeDrawable
- babBackground.shapeAppearanceModel =
- babBackground.shapeAppearanceModel.toBuilder().setTopEdge(topEdge).build()
+ if (binding != null) {
+ val fabShapeAppearanceModel: ShapeAppearanceModel =
+ binding!!.fabCreate.shapeAppearanceModel
+ val cutCornersFab =
+ (fabShapeAppearanceModel.bottomLeftCorner is CutCornerTreatment
+ && fabShapeAppearanceModel.bottomRightCorner is CutCornerTreatment)
+ val topEdge =
+ if (cutCornersFab) BottomAppBarCutCornersTopEdge(
+ binding!!.bottomAppBar.fabCradleMargin,
+ binding!!.bottomAppBar.fabCradleRoundedCornerRadius,
+ binding!!.bottomAppBar.cradleVerticalOffset
+ ) else BottomAppBarTopEdgeTreatment(
+ binding!!.bottomAppBar.fabCradleMargin,
+ binding!!.bottomAppBar.fabCradleRoundedCornerRadius,
+ binding!!.bottomAppBar.cradleVerticalOffset
+ )
+ val babBackground = binding!!.bottomAppBar.background as MaterialShapeDrawable
+ babBackground.shapeAppearanceModel =
+ babBackground.shapeAppearanceModel.toBuilder().setTopEdge(topEdge).build()
+ }
}
diff --git a/app-test/src/main/java/oupson/apngcreator/activities/ViewerActivity.kt b/app-test/src/main/java/oupson/apngcreator/activities/ViewerActivity.kt
index 36a9cc7..aaad23d 100644
--- a/app-test/src/main/java/oupson/apngcreator/activities/ViewerActivity.kt
+++ b/app-test/src/main/java/oupson/apngcreator/activities/ViewerActivity.kt
@@ -9,14 +9,21 @@ import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
-import kotlinx.android.synthetic.main.activity_viewer.*
import oupson.apng.decoder.ApngDecoder
-import oupson.apngcreator.R
+import oupson.apng.decoder.ApngLoader
+import oupson.apngcreator.databinding.ActivityViewerBinding
class ViewerActivity : AppCompatActivity() {
+ private var apngLoader: ApngLoader? = null
+ private var binding: ActivityViewerBinding? = null
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_viewer)
+ binding = ActivityViewerBinding.inflate(layoutInflater)
+
+ setContentView(binding?.root)
+
+ this.apngLoader = ApngLoader()
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
@@ -50,16 +57,26 @@ class ViewerActivity : AppCompatActivity() {
private fun load() {
val uri = intent.data ?: return
- ApngDecoder.decodeApngAsyncInto(this, uri, viewerImageView, callback = object : ApngDecoder.Callback {
- override fun onSuccess(drawable: Drawable) {}
- override fun onError(error: Exception) {
- Log.e("ViewerActivity", "Error when loading file", error)
- }
- }, ApngDecoder.Config(decodeCoverFrame = false))
+
+ if (binding != null)
+ apngLoader?.decodeApngAsyncInto(
+ this,
+ uri,
+ binding!!.viewerImageView,
+ callback = object : ApngLoader.Callback {
+ override fun onSuccess(drawable: Drawable) {}
+ override fun onError(error: Throwable) {
+ Log.e("ViewerActivity", "Error when loading file", error)
+ }
+ },
+ ApngDecoder.Config(decodeCoverFrame = false)
+ )
}
- override fun onRequestPermissionsResult(requestCode: Int,
- permissions: Array, grantResults: IntArray) {
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array, grantResults: IntArray
+ ) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
2 -> {
diff --git a/app-test/src/main/java/oupson/apngcreator/adapter/ImageAdapter.kt b/app-test/src/main/java/oupson/apngcreator/adapter/ImageAdapter.kt
index 7acf900..e0cb56e 100644
--- a/app-test/src/main/java/oupson/apngcreator/adapter/ImageAdapter.kt
+++ b/app-test/src/main/java/oupson/apngcreator/adapter/ImageAdapter.kt
@@ -10,21 +10,25 @@ import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import oupson.apngcreator.R
-class ImageAdapter(private val context : Context, private val list : List>) : RecyclerView.Adapter() {
- inner class ImageHolder(view : View) : RecyclerView.ViewHolder(view) {
- val imageView : ImageView? = view.findViewById(R.id.listImageView)
- val textDelay : TextView? = view.findViewById(R.id.textDelay)
- val positionTextView : TextView? = view.findViewById(R.id.position_textView)
- val nameTextView : TextView? = view.findViewById(R.id.name_textView)
+class ImageAdapter(
+ private val context: Context,
+ private val list: List>,
+ private val scope: CoroutineScope
+) : RecyclerView.Adapter() {
+ inner class ImageHolder(view: View) : RecyclerView.ViewHolder(view) {
+ val imageView: ImageView? = view.findViewById(R.id.listImageView)
+ val textDelay: TextView? = view.findViewById(R.id.textDelay)
+ val positionTextView: TextView? = view.findViewById(R.id.position_textView)
+ val nameTextView: TextView? = view.findViewById(R.id.name_textView)
}
- var clickListener : ((position : Int) -> Unit)? = null
+ var clickListener: ((position: Int) -> Unit)? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageHolder {
val inflater = LayoutInflater.from(parent.context)
@@ -36,7 +40,7 @@ class ImageAdapter(private val context : Context, private val list : List
@@ -150,13 +157,13 @@ class KotlinFragment : Fragment() {
}
}
- override fun onError(error: Exception) {
+ override fun onError(error: Throwable) {
Log.e(TAG, "Error when decoding apng", error)
}
})
}
- Picasso.get().load(imageUrls[selected]).into(normalImageView)
+ normalImageView?.load(imageUrls[selected])
}
override fun onPause() {
diff --git a/app-test/src/main/res/layout/dialog_delay.xml b/app-test/src/main/res/layout/dialog_delay.xml
index 042fe51..c337f95 100644
--- a/app-test/src/main/res/layout/dialog_delay.xml
+++ b/app-test/src/main/res/layout/dialog_delay.xml
@@ -1,8 +1,10 @@
-
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:viewBindingIgnore="true">
+ tools:context=".fragments.ApngDecoderFragment"
+ tools:viewBindingIgnore="true">
+ tools:context=".fragments.JavaFragment"
+ tools:viewBindingIgnore="true">
+ android:layout_height="match_parent"
+ tools:viewBindingIgnore="true">
+ android:orientation="horizontal"
+ tools:viewBindingIgnore="true">