Skip to content

Commit

Permalink
Merge pull request #11 from jordond/coroutine-safety
Browse files Browse the repository at this point in the history
Feature: Safer coroutines & less leaky Context
  • Loading branch information
lincollincol authored Jun 13, 2022
2 parents 4434922 + 9cda6b0 commit 5168eef
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 100 deletions.
2 changes: 1 addition & 1 deletion heifconverter/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ dependencies {

// Coroutines
def coroutines_version = "1.6.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"

// Testing
Expand Down
180 changes: 96 additions & 84 deletions heifconverter/src/main/java/linc/com/heifconverter/HeifConverter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ import android.graphics.BitmapFactory
import android.os.Build
import android.os.Environment
import androidx.core.content.ContextCompat.getExternalFilesDirs
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import linc.com.heifconverter.HeifConverter.Format.JPEG
import linc.com.heifconverter.HeifConverter.Format.PNG
Expand All @@ -21,12 +24,8 @@ import java.net.HttpURLConnection
import java.net.URL
import java.util.*
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine


object HeifConverter{

private lateinit var context: Context
class HeifConverter internal constructor(private val context: Context) {

private var pathToHeicFile: String? = null
private var url: String? = null
Expand All @@ -42,11 +41,10 @@ object HeifConverter{

private var fromDataType = InputDataType.NONE

fun useContext(context: Context) : HeifConverter {
this.context = context
HeifReader.initialize(HeifConverter.context)
private val heifReader = HeifReader(context)

init {
initDefaultValues()
return this
}

fun fromFile(pathToFile: String) : HeifConverter {
Expand Down Expand Up @@ -131,94 +129,102 @@ object HeifConverter{
* convert using coroutines to get result synchronously
* @return map of [Key] to values
*/
suspend fun convertBlocking(): Map<String, Any?> =
suspendCoroutine { cont ->
convert { result -> cont.resume(result) }
}
suspend fun convertBlocking(): Map<String, Any?> {
var bitmap: Bitmap? = null

fun convert(block: (result: Map<String, Any?>) -> Unit) {
// Android versions below Q
CoroutineScope(Dispatchers.Main).launch {
var bitmap: Bitmap? = null

// Handle Android Q version in every case
withContext(Dispatchers.IO) {
bitmap = when (fromDataType) {
InputDataType.FILE -> {
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> BitmapFactory.decodeFile(pathToHeicFile)
else -> HeifReader.decodeFile(pathToHeicFile)
}
// Handle Android Q version in every case
withContext(Dispatchers.IO) {
bitmap = when (fromDataType) {
InputDataType.FILE -> {
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> BitmapFactory.decodeFile(pathToHeicFile)
else -> heifReader.decodeFile(pathToHeicFile)
}
InputDataType.URL -> {
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> {
// Download image
val url = URL(url)
val connection = url
.openConnection() as HttpURLConnection
connection.doInput = true
connection.connect()
val input: InputStream = connection.inputStream
BitmapFactory.decodeStream(input)
}
else -> HeifReader.decodeUrl(url!!)
}
InputDataType.URL -> {
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> {
// Download image
val url = URL(url)
val connection = url
.openConnection() as HttpURLConnection
connection.doInput = true
connection.connect()
val input: InputStream = connection.inputStream
BitmapFactory.decodeStream(input)
}
else -> heifReader.decodeUrl(url!!)
}
InputDataType.RESOURCES -> {
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> BitmapFactory.decodeResource(context.resources, resId!!)
else -> HeifReader.decodeResource(context.resources, resId!!)
}
}
InputDataType.RESOURCES -> {
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> BitmapFactory.decodeResource(context.resources, resId!!)
else -> heifReader.decodeResource(context.resources, resId!!)
}
InputDataType.INPUT_STREAM -> {
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> BitmapFactory.decodeStream(inputStream!!)
else -> HeifReader.decodeStream(inputStream!!)
}
}
InputDataType.INPUT_STREAM -> {
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> BitmapFactory.decodeStream(inputStream!!)
else -> heifReader.decodeStream(inputStream!!)
}
InputDataType.BYTE_ARRAY -> {
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> BitmapFactory.decodeByteArray(
byteArray!!,
0,
byteArray!!.size,
BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
)
else -> HeifReader.decodeByteArray(byteArray!!)
}
}
InputDataType.BYTE_ARRAY -> {
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> BitmapFactory.decodeByteArray(
byteArray!!,
0,
byteArray!!.size,
BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
)
else -> heifReader.decodeByteArray(byteArray!!)
}
else -> throw IllegalStateException("You forget to pass input type: File, Url etc. Use such functions: fromFile(. . .) etc.")
}
else -> throw IllegalStateException("You forget to pass input type: File, Url etc. Use such functions: fromFile(. . .) etc.")
}
}

val directoryToSave = File(pathToSaveDirectory)
var dest: File? = File(directoryToSave, "$convertedFileName$outputFormat")
val directoryToSave = File(pathToSaveDirectory)
var dest: File? = File(directoryToSave, "$convertedFileName$outputFormat")

withContext(Dispatchers.IO) {
val out = FileOutputStream(dest!!)
try {
bitmap?.compress(useFormat(outputFormat), outputQuality, out)
if(!saveResultImage) {
dest!!.delete()
dest = null
}
withContext(Dispatchers.Main) {
block(mapOf(
Key.BITMAP to bitmap,
Key.IMAGE_PATH to (dest?.path ?: "You set saveResultImage(false). If you want to save file - pass true"))
)
}
} catch (e : Exception) {
e.printStackTrace()
}finally {
out.flush()
out.close()
val result: MutableMap<String, Any?> = mutableMapOf(Key.BITMAP to bitmap)
withContext(Dispatchers.IO) {
val out = FileOutputStream(dest!!)
try {
bitmap?.compress(useFormat(outputFormat), outputQuality, out)
if (!saveResultImage) {
dest!!.delete()
dest = null
}
result[Key.IMAGE_PATH] = dest?.path
?: "You set saveResultImage(false). If you want to save file - pass true"
} catch (cancellation: CancellationException) {
throw cancellation
} catch (e : Exception) {
e.printStackTrace()
} finally {
out.flush()
out.close()
}
}

return result.toMap()
}

/**
* convert asynchronously using [block] to receive the results.
*
* @param[coroutineScope] [CoroutineScope] to launch the coroutine in.
* @param[block] lambda for retrieving the result.
* @return A reference to the launched coroutine as a [Job], cancel via [Job.cancel].
*/
fun convert(
coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Main),
block: (result: Map<String, Any?>) -> Unit,
): Job = coroutineScope.launch {
val result = convertBlocking()
block(result)
}

private fun initDefaultValues() {
Expand All @@ -228,7 +234,9 @@ object HeifConverter{
}

private fun useFormat(format: String) = when(format) {
WEBP -> Bitmap.CompressFormat.WEBP
WEBP ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) Bitmap.CompressFormat.WEBP_LOSSY
else @Suppress("DEPRECATION") Bitmap.CompressFormat.WEBP
PNG -> Bitmap.CompressFormat.PNG
else -> Bitmap.CompressFormat.JPEG
}
Expand All @@ -249,4 +257,8 @@ object HeifConverter{
BYTE_ARRAY, NONE
}

companion object {

fun useContext(context: Context) = HeifConverter(context)
}
}
29 changes: 14 additions & 15 deletions heifconverter/src/main/java/linc/com/heifconverter/HeifReader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,25 +35,14 @@ import kotlin.coroutines.CoroutineContext
*
* Create Bitmap object from HEIF file, byte-array, stream, etc.
*/
internal object HeifReader {
private const val TAG = "HeifReader"
internal class HeifReader(context: Context) {

/**
* input data size limitation for safety.
*/
private const val LIMIT_FILESIZE = 20 * 1024 * 1024 // 20[MB]
.toLong()
private var mRenderScript: RenderScript? = null
private var mCacheDir: File? = null
private var mDecoderName: String? = null
private var mDecoderSupportedSize: Size? = null

/**
* Initialize HeifReader module.
*
* @param context Context.
*/
fun initialize(context: Context) {
init {
mRenderScript = RenderScript.create(context)
mCacheDir = context.cacheDir

Expand Down Expand Up @@ -556,6 +545,16 @@ internal object HeifReader {
var length = 0
}

private class FormatFallbackException internal constructor(ex: Throwable?) :
Exception(ex)
private class FormatFallbackException internal constructor(ex: Throwable?) : Exception(ex)

companion object {

private const val TAG = "HeifReader"

/**
* input data size limitation for safety.
*/
private const val LIMIT_FILESIZE = 20 * 1024 * 1024 // 20[MB]
.toLong()
}
}

0 comments on commit 5168eef

Please sign in to comment.