Skip to content
This repository has been archived by the owner on Mar 8, 2024. It is now read-only.

Commit

Permalink
Merge pull request #23 from brightec/fix/Coroutines
Browse files Browse the repository at this point in the history
fix/Coroutines
  • Loading branch information
NickHolcombe authored Oct 21, 2019
2 parents 80cbf70 + a3071c4 commit 6f5ddd9
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 69 deletions.
3 changes: 2 additions & 1 deletion kbarcode/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ android {

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.1.1'
implementation 'com.jakewharton.timber:timber:4.7.1'
implementation 'com.google.firebase:firebase-ml-vision:21.0.0'
implementation 'androidx.lifecycle:lifecycle-livedata:2.1.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import com.google.firebase.ml.vision.common.FirebaseVisionImage
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.tasks.await
import kotlinx.coroutines.withContext
import uk.co.brightec.kbarcode.camera.FrameMetadata
import java.nio.ByteBuffer
Expand All @@ -19,11 +19,12 @@ internal abstract class VisionImageProcessorSingleBase<T> :
VisionImageProcessorSingle {

@VisibleForTesting
internal var processingImage: Pair<Image, FrameMetadata>? = null
internal var processingImage: Image? = null
var onImageProcessed: ((Image) -> Unit)? = null

private var currentJob: Job? = null
@VisibleForTesting
internal var scope = CoroutineScope(Job())
internal var scope = CoroutineScope(Dispatchers.Main)

override fun process(data: ByteBuffer, frameMetadata: FrameMetadata) {
throw NotImplementedError("This could be implemented similar to below")
Expand All @@ -38,15 +39,21 @@ internal abstract class VisionImageProcessorSingleBase<T> :
frameMetadata: FrameMetadata
) {
if (!isProcessing()) {
scope.launch {
processingImage = image
currentJob = scope.launch {
startDetection(image, frameMetadata)
processingImage = null
}
}
}

@CallSuper
override fun stop() {
scope.cancel()
currentJob?.cancel()
processingImage?.let {
onImageProcessed?.invoke(it)
processingImage = null
}
}

fun isProcessing() = processingImage != null
Expand All @@ -56,23 +63,20 @@ internal abstract class VisionImageProcessorSingleBase<T> :
image: Image,
metadata: FrameMetadata
) {
processingImage = image to metadata
val fbImage = convertToVisionImage(image, metadata)
detectInImage(fbImage)
.addOnCompleteListener { task ->
@Suppress("UnsafeCallOnNullableType")
onImageProcessed?.invoke(processingImage!!.first)
processingImage = null
if (task.isSuccessful && task.result != null) {
@Suppress("UnsafeCallOnNullableType")
onSuccess(task.result!!, metadata)
} else {
onFailure(task.exception ?: Exception("Unknown"))
}
}
@Suppress("TooGenericExceptionCaught") // As specific as we can be with Firebase
try {
val fbImage = convertToVisionImage(image, metadata)
val result = detectInImage(fbImage).await()
onSuccess(result, metadata)
} catch (e: Exception) {
onFailure(e)
} finally {
onImageProcessed?.invoke(image)
}
}

@VisibleForTesting
@Throws(IllegalStateException::class)
internal suspend fun convertToVisionImage(image: Image, frameMetadata: FrameMetadata) =
withContext(Dispatchers.Default) {
FirebaseVisionImage.fromMediaImage(image, frameMetadata.rotation)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ package uk.co.brightec.kbarcode.processor.base

import android.media.Image
import androidx.test.filters.SmallTest
import com.google.android.gms.tasks.OnCompleteListener
import com.google.android.gms.tasks.Task
import com.google.firebase.ml.vision.common.FirebaseVisionImage
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.doAnswer
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.never
Expand All @@ -17,9 +15,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.runBlockingTest
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
Expand Down Expand Up @@ -58,6 +54,7 @@ internal class VisionImageProcessorSingleBaseTest {

@Test
fun image__process__callsDetect() = runBlockingTest {
// STUB
doReturn(Unit).whenever(processor).startDetection(any(), any())

// GIVEN
Expand All @@ -74,7 +71,7 @@ internal class VisionImageProcessorSingleBaseTest {
@Test
fun isProcessing__isProcessing__true() {
// GIVEN
processor.processingImage = mock<Image>() to mock()
processor.processingImage = mock()

// WHEN
val result = processor.isProcessing()
Expand All @@ -96,7 +93,10 @@ internal class VisionImageProcessorSingleBaseTest {
}

@Test
fun image__startDetection__setsVar_runsDetection() = runBlockingTest {
fun image__startDetection__runsDetection() = runBlockingTest {
// STUB
processor.mockSuccess(mock())

// GIVEN
val image = mock<Image>()
val frameMetadata = mock<FrameMetadata>()
Expand All @@ -107,78 +107,57 @@ internal class VisionImageProcessorSingleBaseTest {
processor.startDetection(image, frameMetadata)

// THEN
assertEquals(image to frameMetadata, processor.processingImage)
verify(processor).detectInImage(visionImage)
verify(processor.task).addOnCompleteListener(any())
}

@Test
fun image_completeSuccess_listener__startDetection__callsListener_setsVar_onSuccess() =
fun image_completeSuccess_listener__startDetection__callsListener_onSuccess() =
runBlockingTest {
// GIVEN
val image = mock<Image>()
val frameMetadata = mock<FrameMetadata>()
val visionImage = mock<FirebaseVisionImage>()
doReturn(visionImage).whenever(processor).convertToVisionImage(any(), any())
val foo = mock<Foo>()
val result = mock<Task<Foo>> {
on { isSuccessful } doReturn true
on { result } doReturn foo
}
val task = processor.task
doAnswer {
val listener = it.getArgument<OnCompleteListener<Foo>>(0)
listener.onComplete(result)
return@doAnswer task
}.whenever(task).addOnCompleteListener(any())
processor.mockSuccess(foo)
val listener = mock<((Image) -> Unit)>()
processor.onImageProcessed = listener

// WHEN
processor.startDetection(image, frameMetadata)

// THEN
verify(listener).invoke(image)
assertNull(processor.processingImage)
verify(processor).onSuccess(foo, frameMetadata)
verify(listener).invoke(image)
}

@Test
fun image_completeError_listener__startDetection__callsListener_setsVar_onFailure() =
runBlockingTest {
// GIVEN
val image = mock<Image>()
val frameMetadata = mock<FrameMetadata>()
val visionImage = mock<FirebaseVisionImage>()
doReturn(visionImage).whenever(processor).convertToVisionImage(any(), any())
val error = mock<Exception>()
val result = mock<Task<Foo>> {
on { isSuccessful } doReturn false
on { exception } doReturn error
}
val task = processor.task
doAnswer {
val listener = it.getArgument<OnCompleteListener<Foo>>(0)
listener.onComplete(result)
return@doAnswer task
}.whenever(task).addOnCompleteListener(any())
val listener = mock<((Image) -> Unit)>()
processor.onImageProcessed = listener
fun image_completeError_listener__startDetection__callsListener_onFailure() = runBlockingTest {
// GIVEN
val image = mock<Image>()
val frameMetadata = mock<FrameMetadata>()
val visionImage = mock<FirebaseVisionImage>()
doReturn(visionImage).whenever(processor).convertToVisionImage(any(), any())
val error = mock<Exception>()
processor.mockError(error)
val listener = mock<((Image) -> Unit)>()
processor.onImageProcessed = listener

// WHEN
processor.startDetection(image, frameMetadata)
// WHEN
processor.startDetection(image, frameMetadata)

// THEN
verify(listener).invoke(image)
assertNull(processor.processingImage)
verify(processor).onFailure(error)
}
// THEN
verify(processor).onFailure(error)
verify(listener).invoke(image)
}

class Foo

class VisionImageProcessorSingleBaseImpl : VisionImageProcessorSingleBase<Foo>() {

internal val task = mock<Task<Foo>>()
internal val task = mock<Task<Foo>> {
on { isCanceled } doReturn false
}

override fun detectInImage(image: FirebaseVisionImage): Task<Foo> {
return task
Expand All @@ -187,5 +166,17 @@ internal class VisionImageProcessorSingleBaseTest {
override fun onSuccess(results: Foo, frameMetadata: FrameMetadata) {}

override fun onFailure(e: Exception) {}

internal fun mockSuccess(foo: Foo) {
whenever(task.isComplete).doReturn(true)
whenever(task.result).doReturn(foo)
whenever(task.exception).doReturn(null)
}

internal fun mockError(error: Exception) {
whenever(task.isComplete).doReturn(true)
whenever(task.result).doReturn(null)
whenever(task.exception).doReturn(error)
}
}
}

0 comments on commit 6f5ddd9

Please sign in to comment.